reference.go 11 KB

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