reference.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436
  1. // Package reference provides a general type to represent any way of referencing images within the registry.
  2. // Its main purpose is to abstract tags and digests (content-addressable hash).
  3. //
  4. // Grammar
  5. //
  6. // reference := name [ ":" tag ] [ "@" digest ]
  7. // name := [domain '/'] remote-name
  8. // domain := host [':' port-number]
  9. // host := domain-name | IPv4address | \[ IPv6address \] ; rfc3986 appendix-A
  10. // domain-name := domain-component ['.' domain-component]*
  11. // domain-component := /([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])/
  12. // port-number := /[0-9]+/
  13. // path-component := alpha-numeric [separator alpha-numeric]*
  14. // path (or "remote-name") := path-component ['/' path-component]*
  15. // alpha-numeric := /[a-z0-9]+/
  16. // separator := /[_.]|__|[-]*/
  17. //
  18. // tag := /[\w][\w.-]{0,127}/
  19. //
  20. // digest := digest-algorithm ":" digest-hex
  21. // digest-algorithm := digest-algorithm-component [ digest-algorithm-separator digest-algorithm-component ]*
  22. // digest-algorithm-separator := /[+.-_]/
  23. // digest-algorithm-component := /[A-Za-z][A-Za-z0-9]*/
  24. // digest-hex := /[0-9a-fA-F]{32,}/ ; At least 128 bit digest value
  25. //
  26. // identifier := /[a-f0-9]{64}/
  27. package reference
  28. import (
  29. "errors"
  30. "fmt"
  31. "strings"
  32. "github.com/opencontainers/go-digest"
  33. )
  34. const (
  35. // NameTotalLengthMax is the maximum total number of characters in a repository name.
  36. NameTotalLengthMax = 255
  37. )
  38. var (
  39. // ErrReferenceInvalidFormat represents an error while trying to parse a string as a reference.
  40. ErrReferenceInvalidFormat = errors.New("invalid reference format")
  41. // ErrTagInvalidFormat represents an error while trying to parse a string as a tag.
  42. ErrTagInvalidFormat = errors.New("invalid tag format")
  43. // ErrDigestInvalidFormat represents an error while trying to parse a string as a tag.
  44. ErrDigestInvalidFormat = errors.New("invalid digest format")
  45. // ErrNameContainsUppercase is returned for invalid repository names that contain uppercase characters.
  46. ErrNameContainsUppercase = errors.New("repository name must be lowercase")
  47. // ErrNameEmpty is returned for empty, invalid repository names.
  48. ErrNameEmpty = errors.New("repository name must have at least one component")
  49. // ErrNameTooLong is returned when a repository name is longer than NameTotalLengthMax.
  50. ErrNameTooLong = fmt.Errorf("repository name must not be more than %v characters", NameTotalLengthMax)
  51. // ErrNameNotCanonical is returned when a name is not canonical.
  52. ErrNameNotCanonical = errors.New("repository name must be canonical")
  53. )
  54. // Reference is an opaque object reference identifier that may include
  55. // modifiers such as a hostname, name, tag, and digest.
  56. type Reference interface {
  57. // String returns the full reference
  58. String() string
  59. }
  60. // Field provides a wrapper type for resolving correct reference types when
  61. // working with encoding.
  62. type Field struct {
  63. reference Reference
  64. }
  65. // AsField wraps a reference in a Field for encoding.
  66. func AsField(reference Reference) Field {
  67. return Field{reference}
  68. }
  69. // Reference unwraps the reference type from the field to
  70. // return the Reference object. This object should be
  71. // of the appropriate type to further check for different
  72. // reference types.
  73. func (f Field) Reference() Reference {
  74. return f.reference
  75. }
  76. // MarshalText serializes the field to byte text which
  77. // is the string of the reference.
  78. func (f Field) MarshalText() (p []byte, err error) {
  79. return []byte(f.reference.String()), nil
  80. }
  81. // UnmarshalText parses text bytes by invoking the
  82. // reference parser to ensure the appropriately
  83. // typed reference object is wrapped by field.
  84. func (f *Field) UnmarshalText(p []byte) error {
  85. r, err := Parse(string(p))
  86. if err != nil {
  87. return err
  88. }
  89. f.reference = r
  90. return nil
  91. }
  92. // Named is an object with a full name
  93. type Named interface {
  94. Reference
  95. Name() string
  96. }
  97. // Tagged is an object which has a tag
  98. type Tagged interface {
  99. Reference
  100. Tag() string
  101. }
  102. // NamedTagged is an object including a name and tag.
  103. type NamedTagged interface {
  104. Named
  105. Tag() string
  106. }
  107. // Digested is an object which has a digest
  108. // in which it can be referenced by
  109. type Digested interface {
  110. Reference
  111. Digest() digest.Digest
  112. }
  113. // Canonical reference is an object with a fully unique
  114. // name including a name with domain and digest
  115. type Canonical interface {
  116. Named
  117. Digest() digest.Digest
  118. }
  119. // namedRepository is a reference to a repository with a name.
  120. // A namedRepository has both domain and path components.
  121. type namedRepository interface {
  122. Named
  123. Domain() string
  124. Path() string
  125. }
  126. // Domain returns the domain part of the [Named] reference.
  127. func Domain(named Named) string {
  128. if r, ok := named.(namedRepository); ok {
  129. return r.Domain()
  130. }
  131. domain, _ := splitDomain(named.Name())
  132. return domain
  133. }
  134. // Path returns the name without the domain part of the [Named] reference.
  135. func Path(named Named) (name string) {
  136. if r, ok := named.(namedRepository); ok {
  137. return r.Path()
  138. }
  139. _, path := splitDomain(named.Name())
  140. return path
  141. }
  142. func splitDomain(name string) (string, string) {
  143. match := anchoredNameRegexp.FindStringSubmatch(name)
  144. if len(match) != 3 {
  145. return "", name
  146. }
  147. return match[1], match[2]
  148. }
  149. // SplitHostname splits a named reference into a
  150. // hostname and name string. If no valid hostname is
  151. // found, the hostname is empty and the full value
  152. // is returned as name
  153. //
  154. // Deprecated: Use [Domain] or [Path].
  155. func SplitHostname(named Named) (string, string) {
  156. if r, ok := named.(namedRepository); ok {
  157. return r.Domain(), r.Path()
  158. }
  159. return splitDomain(named.Name())
  160. }
  161. // Parse parses s and returns a syntactically valid Reference.
  162. // If an error was encountered it is returned, along with a nil Reference.
  163. func Parse(s string) (Reference, error) {
  164. matches := ReferenceRegexp.FindStringSubmatch(s)
  165. if matches == nil {
  166. if s == "" {
  167. return nil, ErrNameEmpty
  168. }
  169. if ReferenceRegexp.FindStringSubmatch(strings.ToLower(s)) != nil {
  170. return nil, ErrNameContainsUppercase
  171. }
  172. return nil, ErrReferenceInvalidFormat
  173. }
  174. if len(matches[1]) > NameTotalLengthMax {
  175. return nil, ErrNameTooLong
  176. }
  177. var repo repository
  178. nameMatch := anchoredNameRegexp.FindStringSubmatch(matches[1])
  179. if len(nameMatch) == 3 {
  180. repo.domain = nameMatch[1]
  181. repo.path = nameMatch[2]
  182. } else {
  183. repo.domain = ""
  184. repo.path = matches[1]
  185. }
  186. ref := reference{
  187. namedRepository: repo,
  188. tag: matches[2],
  189. }
  190. if matches[3] != "" {
  191. var err error
  192. ref.digest, err = digest.Parse(matches[3])
  193. if err != nil {
  194. return nil, err
  195. }
  196. }
  197. r := getBestReferenceType(ref)
  198. if r == nil {
  199. return nil, ErrNameEmpty
  200. }
  201. return r, nil
  202. }
  203. // ParseNamed parses s and returns a syntactically valid reference implementing
  204. // the Named interface. The reference must have a name and be in the canonical
  205. // form, otherwise an error is returned.
  206. // If an error was encountered it is returned, along with a nil Reference.
  207. func ParseNamed(s string) (Named, error) {
  208. named, err := ParseNormalizedNamed(s)
  209. if err != nil {
  210. return nil, err
  211. }
  212. if named.String() != s {
  213. return nil, ErrNameNotCanonical
  214. }
  215. return named, nil
  216. }
  217. // WithName returns a named object representing the given string. If the input
  218. // is invalid ErrReferenceInvalidFormat will be returned.
  219. func WithName(name string) (Named, error) {
  220. if len(name) > NameTotalLengthMax {
  221. return nil, ErrNameTooLong
  222. }
  223. match := anchoredNameRegexp.FindStringSubmatch(name)
  224. if match == nil || len(match) != 3 {
  225. return nil, ErrReferenceInvalidFormat
  226. }
  227. return repository{
  228. domain: match[1],
  229. path: match[2],
  230. }, nil
  231. }
  232. // WithTag combines the name from "name" and the tag from "tag" to form a
  233. // reference incorporating both the name and the tag.
  234. func WithTag(name Named, tag string) (NamedTagged, error) {
  235. if !anchoredTagRegexp.MatchString(tag) {
  236. return nil, ErrTagInvalidFormat
  237. }
  238. var repo repository
  239. if r, ok := name.(namedRepository); ok {
  240. repo.domain = r.Domain()
  241. repo.path = r.Path()
  242. } else {
  243. repo.path = name.Name()
  244. }
  245. if canonical, ok := name.(Canonical); ok {
  246. return reference{
  247. namedRepository: repo,
  248. tag: tag,
  249. digest: canonical.Digest(),
  250. }, nil
  251. }
  252. return taggedReference{
  253. namedRepository: repo,
  254. tag: tag,
  255. }, nil
  256. }
  257. // WithDigest combines the name from "name" and the digest from "digest" to form
  258. // a reference incorporating both the name and the digest.
  259. func WithDigest(name Named, digest digest.Digest) (Canonical, error) {
  260. if !anchoredDigestRegexp.MatchString(digest.String()) {
  261. return nil, ErrDigestInvalidFormat
  262. }
  263. var repo repository
  264. if r, ok := name.(namedRepository); ok {
  265. repo.domain = r.Domain()
  266. repo.path = r.Path()
  267. } else {
  268. repo.path = name.Name()
  269. }
  270. if tagged, ok := name.(Tagged); ok {
  271. return reference{
  272. namedRepository: repo,
  273. tag: tagged.Tag(),
  274. digest: digest,
  275. }, nil
  276. }
  277. return canonicalReference{
  278. namedRepository: repo,
  279. digest: digest,
  280. }, nil
  281. }
  282. // TrimNamed removes any tag or digest from the named reference.
  283. func TrimNamed(ref Named) Named {
  284. repo := repository{}
  285. if r, ok := ref.(namedRepository); ok {
  286. repo.domain, repo.path = r.Domain(), r.Path()
  287. } else {
  288. repo.domain, repo.path = splitDomain(ref.Name())
  289. }
  290. return repo
  291. }
  292. func getBestReferenceType(ref reference) Reference {
  293. if ref.Name() == "" {
  294. // Allow digest only references
  295. if ref.digest != "" {
  296. return digestReference(ref.digest)
  297. }
  298. return nil
  299. }
  300. if ref.tag == "" {
  301. if ref.digest != "" {
  302. return canonicalReference{
  303. namedRepository: ref.namedRepository,
  304. digest: ref.digest,
  305. }
  306. }
  307. return ref.namedRepository
  308. }
  309. if ref.digest == "" {
  310. return taggedReference{
  311. namedRepository: ref.namedRepository,
  312. tag: ref.tag,
  313. }
  314. }
  315. return ref
  316. }
  317. type reference struct {
  318. namedRepository
  319. tag string
  320. digest digest.Digest
  321. }
  322. func (r reference) String() string {
  323. return r.Name() + ":" + r.tag + "@" + r.digest.String()
  324. }
  325. func (r reference) Tag() string {
  326. return r.tag
  327. }
  328. func (r reference) Digest() digest.Digest {
  329. return r.digest
  330. }
  331. type repository struct {
  332. domain string
  333. path string
  334. }
  335. func (r repository) String() string {
  336. return r.Name()
  337. }
  338. func (r repository) Name() string {
  339. if r.domain == "" {
  340. return r.path
  341. }
  342. return r.domain + "/" + r.path
  343. }
  344. func (r repository) Domain() string {
  345. return r.domain
  346. }
  347. func (r repository) Path() string {
  348. return r.path
  349. }
  350. type digestReference digest.Digest
  351. func (d digestReference) String() string {
  352. return digest.Digest(d).String()
  353. }
  354. func (d digestReference) Digest() digest.Digest {
  355. return digest.Digest(d)
  356. }
  357. type taggedReference struct {
  358. namedRepository
  359. tag string
  360. }
  361. func (t taggedReference) String() string {
  362. return t.Name() + ":" + t.tag
  363. }
  364. func (t taggedReference) Tag() string {
  365. return t.tag
  366. }
  367. type canonicalReference struct {
  368. namedRepository
  369. digest digest.Digest
  370. }
  371. func (c canonicalReference) String() string {
  372. return c.Name() + "@" + c.digest.String()
  373. }
  374. func (c canonicalReference) Digest() digest.Digest {
  375. return c.digest
  376. }