1212//
1313//===----------------------------------------------------------------------===//
1414
15- import RegexBuilder
16-
1715// https://github.com/distribution/distribution/blob/v2.7.1/reference/reference.go
1816// Split the image reference into a registry and a name part.
1917func splitReference( _ reference: String ) throws -> ( String ? , String ) {
@@ -30,29 +28,43 @@ func splitReference(_ reference: String) throws -> (String?, String) {
3028}
3129
3230// Split the name into repository and tag parts
33- // distribution/distribution defines regular expressions which validate names but these seem to be very strict
34- // and reject names which clients accept
35- func splitName( _ name: String ) throws -> ( String , String ) {
31+ // distribution/distribution defines regular expressions which validate names
32+ // Some clients, such as docker CLI, accept names which violate these regular expressions for local images, but those images cannot be pushed.
33+ // Other clients, such as podman CLI, reject names which violate these regular expressions even for local images
34+ func parseName( _ name: String ) throws -> ( ImageReference . Repository , any ImageReference . Reference ) {
3635 let digestSplit = name. split ( separator: " @ " , maxSplits: 1 , omittingEmptySubsequences: false )
37- if digestSplit. count == 2 { return ( String ( digestSplit [ 0 ] ) , String ( digestSplit [ 1 ] ) ) }
36+ if digestSplit. count == 2 {
37+ return (
38+ try ImageReference . Repository ( String ( digestSplit [ 0 ] ) ) ,
39+ try ImageReference . Digest ( String ( digestSplit [ 1 ] ) )
40+ )
41+ }
3842
3943 let tagSplit = name. split ( separator: " : " , maxSplits: 1 , omittingEmptySubsequences: false )
40- if tagSplit. count == 0 { throw ImageReference . ValidationError. unexpected ( " unexpected error " ) }
44+ if tagSplit. count == 0 {
45+ throw ImageReference . ValidationError. unexpected ( " unexpected error " )
46+ }
4147
42- if tagSplit. count == 1 { return ( name, " latest " ) }
48+ if tagSplit. count == 1 {
49+ return ( try ImageReference . Repository ( name) , try ImageReference . Tag ( " latest " ) )
50+ }
4351
4452 // assert splits == 2
45- return ( String ( tagSplit [ 0 ] ) , String ( tagSplit [ 1 ] ) )
53+ return (
54+ try ImageReference . Repository ( String ( tagSplit [ 0 ] ) ) ,
55+ try ImageReference . Tag ( String ( tagSplit [ 1 ] ) )
56+ )
4657}
4758
4859/// ImageReference points to an image stored on a container registry
60+ /// This type is not found in the API - it is the reference string given by the user
4961public struct ImageReference : Sendable , Equatable , CustomStringConvertible , CustomDebugStringConvertible {
5062 /// The registry which contains this image
5163 public var registry : String
5264 /// The repository which contains this image
5365 public var repository : Repository
54- /// The tag identifying the image.
55- public var reference : String
66+ /// The tag or digest identifying the image.
67+ public var reference : Reference
5668
5769 public enum ValidationError : Error {
5870 case unexpected( String )
@@ -65,18 +77,18 @@ public struct ImageReference: Sendable, Equatable, CustomStringConvertible, Cust
6577 /// - Throws: If `reference` cannot be parsed as an image reference.
6678 public init ( fromString reference: String , defaultRegistry: String = " localhost:5000 " ) throws {
6779 let ( registry, remainder) = try splitReference ( reference)
68- let ( repository, reference) = try splitName ( remainder)
80+ let ( repository, reference) = try parseName ( remainder)
6981 self . registry = registry ?? defaultRegistry
7082 if self . registry == " docker.io " {
7183 self . registry = " index.docker.io " // Special case for docker client, there is no network-level redirect
7284 }
7385 // As a special case, official images can be referred to by a single name, such as `swift` or `swift:slim`.
7486 // moby/moby assumes that these names refer to images in `library`: `library/swift` or `library/swift:slim`.
7587 // This special case only applies when using Docker Hub, so `example.com/swift` is not expanded `example.com/library/swift`
76- if self . registry == " index.docker.io " && !repository. contains ( " / " ) {
88+ if self . registry == " index.docker.io " && !repository. value . contains ( " / " ) {
7789 self . repository = try Repository ( " library/ \( repository) " )
7890 } else {
79- self . repository = try Repository ( repository)
91+ self . repository = repository
8092 }
8193 self . reference = reference
8294 }
@@ -87,19 +99,19 @@ public struct ImageReference: Sendable, Equatable, CustomStringConvertible, Cust
8799 /// - registry: The registry which stores the image data.
88100 /// - repository: The repository within the registry which holds the image.
89101 /// - reference: The tag identifying the image.
90- init ( registry: String , repository: Repository , reference: String ) {
102+ init ( registry: String , repository: Repository , reference: Reference ) {
91103 self . registry = registry
92104 self . repository = repository
93105 self . reference = reference
94106 }
95107
108+ public static func == ( lhs: ImageReference , rhs: ImageReference ) -> Bool {
109+ " \( lhs) " == " \( rhs) "
110+ }
111+
96112 /// Printable description of an ImageReference in a form which can be understood by a runtime
97113 public var description : String {
98- if reference. starts ( with: " sha256 " ) {
99- return " \( registry) / \( repository) @ \( reference) "
100- } else {
101- return " \( registry) / \( repository) : \( reference) "
102- }
114+ " \( registry) / \( repository) \( reference. separator) \( reference) "
103115 }
104116
105117 /// Printable description of an ImageReference in a form suitable for debugging.
@@ -149,3 +161,98 @@ extension ImageReference {
149161 }
150162 }
151163}
164+
165+ extension ImageReference {
166+ /// Reference refers to an image in a repository. It can either be a tag or a digest.
167+ public protocol Reference : Sendable , CustomStringConvertible , CustomDebugStringConvertible {
168+ var separator : String { get }
169+ }
170+
171+ /// Tag is a human-readable name for an image.
172+ public struct Tag : Reference , Sendable , Equatable , CustomStringConvertible , CustomDebugStringConvertible {
173+ var value : String
174+
175+ public enum ValidationError : Error , Equatable {
176+ case emptyString
177+ case invalidReferenceFormat( String )
178+ case tooLong( String )
179+ }
180+
181+ public init ( _ rawValue: String ) throws {
182+ guard rawValue. count > 0 else {
183+ throw ValidationError . emptyString
184+ }
185+
186+ guard rawValue. count <= 128 else {
187+ throw ValidationError . tooLong ( rawValue)
188+ }
189+
190+ // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pulling-manifests
191+ let regex = /[a-zA-Z0-9_][a-zA-Z0-9._-]{0,127}/
192+ if try regex. wholeMatch ( in: rawValue) == nil {
193+ throw ValidationError . invalidReferenceFormat ( rawValue)
194+ }
195+
196+ value = rawValue
197+ }
198+
199+ public static func == ( lhs: Tag , rhs: Tag ) -> Bool {
200+ lhs. value == rhs. value
201+ }
202+
203+ public var separator : String = " : "
204+
205+ public var description : String {
206+ " \( value) "
207+ }
208+
209+ /// Printable description in a form suitable for debugging.
210+ public var debugDescription : String {
211+ " Tag( \( value) ) "
212+ }
213+ }
214+
215+ /// Digest identifies a specific blob by the hash of the blob's contents.
216+ public struct Digest : Reference , Sendable , Equatable , CustomStringConvertible , CustomDebugStringConvertible {
217+ var value : String
218+
219+ public enum ValidationError : Error , Equatable {
220+ case emptyString
221+ case invalidReferenceFormat( String )
222+ case tooLong( String )
223+ }
224+
225+ public init ( _ rawValue: String ) throws {
226+ guard rawValue. count > 0 else {
227+ throw ValidationError . emptyString
228+ }
229+
230+ if rawValue. count > 7 + 64 {
231+ throw ValidationError . tooLong ( rawValue)
232+ }
233+
234+ // https://github.com/opencontainers/distribution-spec/blob/main/spec.md#pulling-manifests
235+ let regex = /sha256:[a-fA-F0-9]{64}/
236+ if try regex. wholeMatch ( in: rawValue) == nil {
237+ throw ValidationError . invalidReferenceFormat ( rawValue)
238+ }
239+
240+ value = rawValue
241+ }
242+
243+ public static func == ( lhs: Digest , rhs: Digest ) -> Bool {
244+ lhs. value == rhs. value
245+ }
246+
247+ public var separator : String = " @ "
248+
249+ public var description : String {
250+ " \( value) "
251+ }
252+
253+ /// Printable description in a form suitable for debugging.
254+ public var debugDescription : String {
255+ " Digest( \( value) ) "
256+ }
257+ }
258+ }
0 commit comments