Coverage for cli / _image_reference.py: 100.00%
71 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-09-14 22:07 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-09-14 22:07 +0000
1"""Linear-time validation helpers for immutable container image references."""
3from __future__ import annotations
5_LOWER_HEX = frozenset("0123456789abcdef")
6_LOWER_ALNUM = frozenset("abcdefghijklmnopqrstuvwxyz0123456789")
7_HOST_ALNUM = _LOWER_ALNUM | frozenset("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
8_TAG_FIRST_CHARACTERS = _HOST_ALNUM | frozenset("_")
9_TAG_CHARACTERS = _TAG_FIRST_CHARACTERS | frozenset(".-")
10_DIGEST_SEPARATOR = "@sha256:"
13def _valid_host_label(label: str) -> bool:
14 return (
15 bool(label)
16 and label[0] in _HOST_ALNUM
17 and label[-1] in _HOST_ALNUM
18 and all(character in _HOST_ALNUM or character == "-" for character in label)
19 )
22def _valid_registry(segment: str) -> bool:
23 """Validate a DNS-style registry host with an optional numeric TCP port."""
24 if segment.count(":") > 1:
25 return False
26 host = segment
27 if ":" in segment:
28 host, port_text = segment.rsplit(":", 1)
29 if not port_text.isascii() or not port_text.isdigit() or len(port_text) > 5:
30 return False
31 port = int(port_text)
32 if not 1 <= port <= 65535:
33 return False
34 return bool(host) and all(_valid_host_label(label) for label in host.split("."))
37def _valid_repository_component(component: str) -> bool:
38 """Validate one lowercase distribution repository-name component."""
39 if not component or component[0] not in _LOWER_ALNUM:
40 return False
41 index = 0
42 length = len(component)
43 while True:
44 while index < length and component[index] in _LOWER_ALNUM:
45 index += 1
46 if index == length:
47 return True
48 if component[index] == ".":
49 index += 1
50 elif component[index] == "_":
51 index += 1
52 if index < length and component[index] == "_":
53 index += 1
54 elif component[index] == "-":
55 while index < length and component[index] == "-":
56 index += 1
57 else:
58 return False
59 if index == length or component[index] not in _LOWER_ALNUM:
60 return False
63def immutable_sha256_digest(value: object) -> str | None:
64 """Return a lowercase SHA-256 digest for one strict image reference.
66 Parsing is explicit and linear-time: registry, lowercase repository path,
67 optional Docker tag, and digest are bounded independently. Uppercase tag
68 characters remain valid, while malformed hosts, ports, and separators are
69 rejected before a deployment can reach the image-pull phase.
70 """
71 if not isinstance(value, str) or value.count(_DIGEST_SEPARATOR) != 1:
72 return None
73 name, digest = value.split(_DIGEST_SEPARATOR, 1)
74 if (
75 not name
76 or "@" in name
77 or len(digest) != 64
78 or any(character not in _LOWER_HEX for character in digest)
79 ):
80 return None
82 last_slash = name.rfind("/")
83 last_colon = name.rfind(":")
84 tag: str | None = None
85 repository = name
86 if last_colon > last_slash:
87 repository = name[:last_colon]
88 tag = name[last_colon + 1 :]
89 if tag is not None and (
90 not tag
91 or len(tag) > 128
92 or tag[0] not in _TAG_FIRST_CHARACTERS
93 or any(character not in _TAG_CHARACTERS for character in tag[1:])
94 ):
95 return None
97 segments = repository.split("/")
98 if any(not segment for segment in segments):
99 return None
100 first = segments[0]
101 has_registry = len(segments) > 1 and (
102 "." in first or ":" in first or first.casefold() == "localhost"
103 )
104 repository_segments = segments
105 if has_registry:
106 if not _valid_registry(first):
107 return None
108 repository_segments = segments[1:]
109 if not all(_valid_repository_component(component) for component in repository_segments):
110 return None
111 return digest