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

1"""Linear-time validation helpers for immutable container image references.""" 

2 

3from __future__ import annotations 

4 

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:" 

11 

12 

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 ) 

20 

21 

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(".")) 

35 

36 

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 

61 

62 

63def immutable_sha256_digest(value: object) -> str | None: 

64 """Return a lowercase SHA-256 digest for one strict image reference. 

65 

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 

81 

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 

96 

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