rfc2822.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608
  1. package manifest
  2. import (
  3. "bufio"
  4. "fmt"
  5. "io"
  6. "path"
  7. "regexp"
  8. "sort"
  9. "strings"
  10. "github.com/docker-library/go-dockerlibrary/architecture"
  11. "github.com/docker-library/go-dockerlibrary/pkg/stripper"
  12. "pault.ag/go/debian/control"
  13. )
  14. var (
  15. GitCommitRegex = regexp.MustCompile(`^[0-9a-f]{1,64}$`)
  16. GitFetchRegex = regexp.MustCompile(`^refs/(heads|tags)/[^*?:]+$`)
  17. // https://github.com/docker/distribution/blob/v2.7.1/reference/regexp.go#L37
  18. ValidTagRegex = regexp.MustCompile(`^\w[\w.-]{0,127}$`)
  19. )
  20. type Manifest2822 struct {
  21. Global Manifest2822Entry
  22. Entries []Manifest2822Entry
  23. }
  24. type Manifest2822Entry struct {
  25. control.Paragraph
  26. Maintainers []string `delim:"," strip:"\n\r\t "`
  27. Tags []string `delim:"," strip:"\n\r\t "`
  28. SharedTags []string `delim:"," strip:"\n\r\t "`
  29. Architectures []string `delim:"," strip:"\n\r\t "`
  30. GitRepo string
  31. GitFetch string
  32. GitCommit string
  33. Directory string
  34. File string
  35. // architecture-specific versions of the above fields
  36. ArchValues map[string]string
  37. // "ARCH-FIELD: VALUE"
  38. // ala, "s390x-GitCommit: deadbeef"
  39. // (sourced from Paragraph.Values via .SeedArchValues())
  40. Constraints []string `delim:"," strip:"\n\r\t "`
  41. }
  42. var (
  43. DefaultArchitecture = "amd64"
  44. DefaultManifestEntry = Manifest2822Entry{
  45. Architectures: []string{DefaultArchitecture},
  46. GitFetch: "refs/heads/master",
  47. Directory: ".",
  48. File: "Dockerfile",
  49. }
  50. )
  51. func deepCopyStringsMap(a map[string]string) map[string]string {
  52. b := map[string]string{}
  53. for k, v := range a {
  54. b[k] = v
  55. }
  56. return b
  57. }
  58. func (entry Manifest2822Entry) Clone() Manifest2822Entry {
  59. // SLICES! grr
  60. entry.Maintainers = append([]string{}, entry.Maintainers...)
  61. entry.Tags = append([]string{}, entry.Tags...)
  62. entry.SharedTags = append([]string{}, entry.SharedTags...)
  63. entry.Architectures = append([]string{}, entry.Architectures...)
  64. entry.Constraints = append([]string{}, entry.Constraints...)
  65. // and MAPS, oh my
  66. entry.ArchValues = deepCopyStringsMap(entry.ArchValues)
  67. return entry
  68. }
  69. func (entry *Manifest2822Entry) SeedArchValues() {
  70. for field, val := range entry.Paragraph.Values {
  71. if strings.HasSuffix(field, "-GitRepo") || strings.HasSuffix(field, "-GitFetch") || strings.HasSuffix(field, "-GitCommit") || strings.HasSuffix(field, "-Directory") || strings.HasSuffix(field, "-File") {
  72. entry.ArchValues[field] = val
  73. }
  74. }
  75. }
  76. func (entry *Manifest2822Entry) CleanDirectoryValues() {
  77. entry.Directory = path.Clean(entry.Directory)
  78. for field, val := range entry.ArchValues {
  79. if strings.HasSuffix(field, "-Directory") && val != "" {
  80. entry.ArchValues[field] = path.Clean(val)
  81. }
  82. }
  83. }
  84. const StringSeparator2822 = ", "
  85. func (entry Manifest2822Entry) MaintainersString() string {
  86. return strings.Join(entry.Maintainers, StringSeparator2822)
  87. }
  88. func (entry Manifest2822Entry) TagsString() string {
  89. return strings.Join(entry.Tags, StringSeparator2822)
  90. }
  91. func (entry Manifest2822Entry) SharedTagsString() string {
  92. return strings.Join(entry.SharedTags, StringSeparator2822)
  93. }
  94. func (entry Manifest2822Entry) ArchitecturesString() string {
  95. return strings.Join(entry.Architectures, StringSeparator2822)
  96. }
  97. func (entry Manifest2822Entry) ConstraintsString() string {
  98. return strings.Join(entry.Constraints, StringSeparator2822)
  99. }
  100. // if this method returns "true", then a.Tags and b.Tags can safely be combined (for the purposes of building)
  101. func (a Manifest2822Entry) SameBuildArtifacts(b Manifest2822Entry) bool {
  102. // check xxxarch-GitRepo, etc. fields for sameness first
  103. for _, key := range append(a.archFields(), b.archFields()...) {
  104. if a.ArchValues[key] != b.ArchValues[key] {
  105. return false
  106. }
  107. }
  108. return a.ArchitecturesString() == b.ArchitecturesString() && a.GitRepo == b.GitRepo && a.GitFetch == b.GitFetch && a.GitCommit == b.GitCommit && a.Directory == b.Directory && a.File == b.File && a.ConstraintsString() == b.ConstraintsString()
  109. }
  110. // returns a list of architecture-specific fields in an Entry
  111. func (entry Manifest2822Entry) archFields() []string {
  112. ret := []string{}
  113. for key, val := range entry.ArchValues {
  114. if val != "" {
  115. ret = append(ret, key)
  116. }
  117. }
  118. sort.Strings(ret)
  119. return ret
  120. }
  121. // returns a new Entry with any of the values that are equal to the values in "defaults" cleared
  122. func (entry Manifest2822Entry) ClearDefaults(defaults Manifest2822Entry) Manifest2822Entry {
  123. entry = entry.Clone() // make absolutely certain we have a deep clone
  124. if entry.MaintainersString() == defaults.MaintainersString() {
  125. entry.Maintainers = nil
  126. }
  127. if entry.TagsString() == defaults.TagsString() {
  128. entry.Tags = nil
  129. }
  130. if entry.SharedTagsString() == defaults.SharedTagsString() {
  131. entry.SharedTags = nil
  132. }
  133. if entry.ArchitecturesString() == defaults.ArchitecturesString() {
  134. entry.Architectures = nil
  135. }
  136. if entry.GitRepo == defaults.GitRepo {
  137. entry.GitRepo = ""
  138. }
  139. if entry.GitFetch == defaults.GitFetch {
  140. entry.GitFetch = ""
  141. }
  142. if entry.GitCommit == defaults.GitCommit {
  143. entry.GitCommit = ""
  144. }
  145. if entry.Directory == defaults.Directory {
  146. entry.Directory = ""
  147. }
  148. if entry.File == defaults.File {
  149. entry.File = ""
  150. }
  151. for _, key := range defaults.archFields() {
  152. if defaults.ArchValues[key] == entry.ArchValues[key] {
  153. delete(entry.ArchValues, key)
  154. }
  155. }
  156. if entry.ConstraintsString() == defaults.ConstraintsString() {
  157. entry.Constraints = nil
  158. }
  159. return entry
  160. }
  161. func (entry Manifest2822Entry) String() string {
  162. ret := []string{}
  163. if str := entry.MaintainersString(); str != "" {
  164. ret = append(ret, "Maintainers: "+str)
  165. }
  166. if str := entry.TagsString(); str != "" {
  167. ret = append(ret, "Tags: "+str)
  168. }
  169. if str := entry.SharedTagsString(); str != "" {
  170. ret = append(ret, "SharedTags: "+str)
  171. }
  172. if str := entry.ArchitecturesString(); str != "" {
  173. ret = append(ret, "Architectures: "+str)
  174. }
  175. if str := entry.GitRepo; str != "" {
  176. ret = append(ret, "GitRepo: "+str)
  177. }
  178. if str := entry.GitFetch; str != "" {
  179. ret = append(ret, "GitFetch: "+str)
  180. }
  181. if str := entry.GitCommit; str != "" {
  182. ret = append(ret, "GitCommit: "+str)
  183. }
  184. if str := entry.Directory; str != "" {
  185. ret = append(ret, "Directory: "+str)
  186. }
  187. if str := entry.File; str != "" {
  188. ret = append(ret, "File: "+str)
  189. }
  190. for _, key := range entry.archFields() {
  191. ret = append(ret, key+": "+entry.ArchValues[key])
  192. }
  193. if str := entry.ConstraintsString(); str != "" {
  194. ret = append(ret, "Constraints: "+str)
  195. }
  196. return strings.Join(ret, "\n")
  197. }
  198. func (manifest Manifest2822) String() string {
  199. entries := []Manifest2822Entry{manifest.Global.ClearDefaults(DefaultManifestEntry)}
  200. entries = append(entries, manifest.Entries...)
  201. ret := []string{}
  202. for i, entry := range entries {
  203. if i > 0 {
  204. entry = entry.ClearDefaults(manifest.Global)
  205. }
  206. ret = append(ret, entry.String())
  207. }
  208. return strings.Join(ret, "\n\n")
  209. }
  210. func (entry *Manifest2822Entry) SetGitRepo(arch string, repo string) {
  211. if entry.ArchValues == nil {
  212. entry.ArchValues = map[string]string{}
  213. }
  214. entry.ArchValues[arch+"-GitRepo"] = repo
  215. }
  216. func (entry Manifest2822Entry) ArchGitRepo(arch string) string {
  217. if val, ok := entry.ArchValues[arch+"-GitRepo"]; ok && val != "" {
  218. return val
  219. }
  220. return entry.GitRepo
  221. }
  222. func (entry Manifest2822Entry) ArchGitFetch(arch string) string {
  223. if val, ok := entry.ArchValues[arch+"-GitFetch"]; ok && val != "" {
  224. return val
  225. }
  226. return entry.GitFetch
  227. }
  228. func (entry *Manifest2822Entry) SetGitCommit(arch string, commit string) {
  229. if entry.ArchValues == nil {
  230. entry.ArchValues = map[string]string{}
  231. }
  232. entry.ArchValues[arch+"-GitCommit"] = commit
  233. }
  234. func (entry Manifest2822Entry) ArchGitCommit(arch string) string {
  235. if val, ok := entry.ArchValues[arch+"-GitCommit"]; ok && val != "" {
  236. return val
  237. }
  238. return entry.GitCommit
  239. }
  240. func (entry Manifest2822Entry) ArchDirectory(arch string) string {
  241. if val, ok := entry.ArchValues[arch+"-Directory"]; ok && val != "" {
  242. return val
  243. }
  244. return entry.Directory
  245. }
  246. func (entry Manifest2822Entry) ArchFile(arch string) string {
  247. if val, ok := entry.ArchValues[arch+"-File"]; ok && val != "" {
  248. return val
  249. }
  250. return entry.File
  251. }
  252. func (entry Manifest2822Entry) HasTag(tag string) bool {
  253. for _, existingTag := range entry.Tags {
  254. if tag == existingTag {
  255. return true
  256. }
  257. }
  258. return false
  259. }
  260. // HasSharedTag returns true if the given tag exists in entry.SharedTags.
  261. func (entry Manifest2822Entry) HasSharedTag(tag string) bool {
  262. for _, existingTag := range entry.SharedTags {
  263. if tag == existingTag {
  264. return true
  265. }
  266. }
  267. return false
  268. }
  269. // HasArchitecture returns true if the given architecture exists in entry.Architectures
  270. func (entry Manifest2822Entry) HasArchitecture(arch string) bool {
  271. for _, existingArch := range entry.Architectures {
  272. if arch == existingArch {
  273. return true
  274. }
  275. }
  276. return false
  277. }
  278. func (manifest Manifest2822) GetTag(tag string) *Manifest2822Entry {
  279. for i, entry := range manifest.Entries {
  280. if entry.HasTag(tag) {
  281. return &manifest.Entries[i]
  282. }
  283. }
  284. return nil
  285. }
  286. // GetSharedTag returns a list of entries with the given tag in entry.SharedTags (or the empty list if there are no entries with the given tag).
  287. func (manifest Manifest2822) GetSharedTag(tag string) []*Manifest2822Entry {
  288. ret := []*Manifest2822Entry{}
  289. for i, entry := range manifest.Entries {
  290. if entry.HasSharedTag(tag) {
  291. ret = append(ret, &manifest.Entries[i])
  292. }
  293. }
  294. return ret
  295. }
  296. // GetAllSharedTags returns a list of the sum of all SharedTags in all entries of this image manifest (in the order they appear in the file).
  297. func (manifest Manifest2822) GetAllSharedTags() []string {
  298. fakeEntry := Manifest2822Entry{}
  299. for _, entry := range manifest.Entries {
  300. fakeEntry.SharedTags = append(fakeEntry.SharedTags, entry.SharedTags...)
  301. }
  302. fakeEntry.DeduplicateSharedTags()
  303. return fakeEntry.SharedTags
  304. }
  305. type SharedTagGroup struct {
  306. SharedTags []string
  307. Entries []*Manifest2822Entry
  308. }
  309. // GetSharedTagGroups returns a map of shared tag groups to the list of entries they share (as described in https://github.com/docker-library/go-dockerlibrary/pull/2#issuecomment-277853597).
  310. func (manifest Manifest2822) GetSharedTagGroups() []SharedTagGroup {
  311. inter := map[string][]string{}
  312. interOrder := []string{} // order matters, and maps randomize order
  313. interKeySep := ","
  314. for _, sharedTag := range manifest.GetAllSharedTags() {
  315. interKeyParts := []string{}
  316. for _, entry := range manifest.GetSharedTag(sharedTag) {
  317. interKeyParts = append(interKeyParts, entry.Tags[0])
  318. }
  319. interKey := strings.Join(interKeyParts, interKeySep)
  320. if _, ok := inter[interKey]; !ok {
  321. interOrder = append(interOrder, interKey)
  322. }
  323. inter[interKey] = append(inter[interKey], sharedTag)
  324. }
  325. ret := []SharedTagGroup{}
  326. for _, tags := range interOrder {
  327. group := SharedTagGroup{
  328. SharedTags: inter[tags],
  329. Entries: []*Manifest2822Entry{},
  330. }
  331. for _, tag := range strings.Split(tags, interKeySep) {
  332. group.Entries = append(group.Entries, manifest.GetTag(tag))
  333. }
  334. ret = append(ret, group)
  335. }
  336. return ret
  337. }
  338. func (manifest *Manifest2822) AddEntry(entry Manifest2822Entry) error {
  339. if len(entry.Tags) < 1 {
  340. return fmt.Errorf("missing Tags")
  341. }
  342. if entry.GitRepo == "" || entry.GitFetch == "" || entry.GitCommit == "" {
  343. return fmt.Errorf("Tags %q missing one of GitRepo, GitFetch, or GitCommit", entry.TagsString())
  344. }
  345. if invalidMaintainers := entry.InvalidMaintainers(); len(invalidMaintainers) > 0 {
  346. return fmt.Errorf("Tags %q has invalid Maintainers: %q (expected format %q)", entry.TagsString(), strings.Join(invalidMaintainers, ", "), MaintainersFormat)
  347. }
  348. entry.DeduplicateSharedTags()
  349. entry.CleanDirectoryValues()
  350. if invalidTags := entry.InvalidTags(); len(invalidTags) > 0 {
  351. return fmt.Errorf("Tags %q has invalid (Shared)Tags: %q", entry.TagsString(), strings.Join(invalidTags, ", "))
  352. }
  353. if invalidArchitectures := entry.InvalidArchitectures(); len(invalidArchitectures) > 0 {
  354. return fmt.Errorf("Tags %q has invalid Architectures: %q", entry.TagsString(), strings.Join(invalidArchitectures, ", "))
  355. }
  356. seenTag := map[string]bool{}
  357. for _, tag := range entry.Tags {
  358. if otherEntry := manifest.GetTag(tag); otherEntry != nil {
  359. return fmt.Errorf("Tags %q includes duplicate tag: %q (duplicated in %q)", entry.TagsString(), tag, otherEntry.TagsString())
  360. }
  361. if otherEntries := manifest.GetSharedTag(tag); len(otherEntries) > 0 {
  362. return fmt.Errorf("Tags %q includes tag conflicting with a shared tag: %q (shared tag in %q)", entry.TagsString(), tag, otherEntries[0].TagsString())
  363. }
  364. if seenTag[tag] {
  365. return fmt.Errorf("Tags %q includes duplicate tag: %q", entry.TagsString(), tag)
  366. }
  367. seenTag[tag] = true
  368. }
  369. for _, tag := range entry.SharedTags {
  370. if otherEntry := manifest.GetTag(tag); otherEntry != nil {
  371. return fmt.Errorf("Tags %q includes conflicting shared tag: %q (duplicated in %q)", entry.TagsString(), tag, otherEntry.TagsString())
  372. }
  373. if seenTag[tag] {
  374. return fmt.Errorf("Tags %q includes duplicate tag: %q (in SharedTags)", entry.TagsString(), tag)
  375. }
  376. seenTag[tag] = true
  377. }
  378. for i, existingEntry := range manifest.Entries {
  379. if existingEntry.SameBuildArtifacts(entry) {
  380. manifest.Entries[i].Tags = append(existingEntry.Tags, entry.Tags...)
  381. manifest.Entries[i].SharedTags = append(existingEntry.SharedTags, entry.SharedTags...)
  382. manifest.Entries[i].DeduplicateSharedTags()
  383. return nil
  384. }
  385. }
  386. manifest.Entries = append(manifest.Entries, entry)
  387. return nil
  388. }
  389. const (
  390. MaintainersNameRegex = `[^\s<>()][^<>()]*`
  391. MaintainersEmailRegex = `[^\s<>()]+`
  392. MaintainersGitHubRegex = `[^\s<>()]+`
  393. MaintainersFormat = `Full Name <contact-email-or-url> (@github-handle) OR Full Name (@github-handle)`
  394. )
  395. var (
  396. MaintainersRegex = regexp.MustCompile(`^(` + MaintainersNameRegex + `)(?:\s+<(` + MaintainersEmailRegex + `)>)?\s+[(]@(` + MaintainersGitHubRegex + `)[)]$`)
  397. )
  398. func (entry Manifest2822Entry) InvalidMaintainers() []string {
  399. invalid := []string{}
  400. for _, maintainer := range entry.Maintainers {
  401. if !MaintainersRegex.MatchString(maintainer) {
  402. invalid = append(invalid, maintainer)
  403. }
  404. }
  405. return invalid
  406. }
  407. func (entry Manifest2822Entry) InvalidTags() []string {
  408. invalid := []string{}
  409. for _, tag := range append(append([]string{}, entry.Tags...), entry.SharedTags...) {
  410. if !ValidTagRegex.MatchString(tag) {
  411. invalid = append(invalid, tag)
  412. }
  413. }
  414. return invalid
  415. }
  416. func (entry Manifest2822Entry) InvalidArchitectures() []string {
  417. invalid := []string{}
  418. for _, arch := range entry.Architectures {
  419. if _, ok := architecture.SupportedArches[arch]; !ok {
  420. invalid = append(invalid, arch)
  421. }
  422. }
  423. return invalid
  424. }
  425. // DeduplicateSharedTags will remove duplicate values from entry.SharedTags, preserving order.
  426. func (entry *Manifest2822Entry) DeduplicateSharedTags() {
  427. aggregate := []string{}
  428. seen := map[string]bool{}
  429. for _, tag := range entry.SharedTags {
  430. if seen[tag] {
  431. continue
  432. }
  433. seen[tag] = true
  434. aggregate = append(aggregate, tag)
  435. }
  436. entry.SharedTags = aggregate
  437. }
  438. // DeduplicateArchitectures will remove duplicate values from entry.Architectures and sort the result.
  439. func (entry *Manifest2822Entry) DeduplicateArchitectures() {
  440. aggregate := []string{}
  441. seen := map[string]bool{}
  442. for _, arch := range entry.Architectures {
  443. if seen[arch] {
  444. continue
  445. }
  446. seen[arch] = true
  447. aggregate = append(aggregate, arch)
  448. }
  449. sort.Strings(aggregate)
  450. entry.Architectures = aggregate
  451. }
  452. type decoderWrapper struct {
  453. *control.Decoder
  454. }
  455. func (decoder *decoderWrapper) Decode(entry *Manifest2822Entry) error {
  456. // reset Architectures and SharedTags so that they can be either inherited or replaced, not additive
  457. sharedTags := entry.SharedTags
  458. entry.SharedTags = nil
  459. arches := entry.Architectures
  460. entry.Architectures = nil
  461. for {
  462. err := decoder.Decoder.Decode(entry)
  463. if err != nil {
  464. return err
  465. }
  466. // ignore empty paragraphs (blank lines at the start, excess blank lines between paragraphs, excess blank lines at EOF)
  467. if len(entry.Paragraph.Order) == 0 {
  468. continue
  469. }
  470. // if we had no SharedTags or Architectures, restore our "default" (original) values
  471. if len(entry.SharedTags) == 0 {
  472. entry.SharedTags = sharedTags
  473. }
  474. if len(entry.Architectures) == 0 {
  475. entry.Architectures = arches
  476. }
  477. entry.DeduplicateArchitectures()
  478. // pull out any new architecture-specific values from Paragraph.Values
  479. entry.SeedArchValues()
  480. return nil
  481. }
  482. }
  483. func Parse2822(readerIn io.Reader) (*Manifest2822, error) {
  484. reader := stripper.NewCommentStripper(readerIn)
  485. realDecoder, err := control.NewDecoder(bufio.NewReader(reader), nil)
  486. if err != nil {
  487. return nil, err
  488. }
  489. decoder := decoderWrapper{realDecoder}
  490. manifest := Manifest2822{
  491. Global: DefaultManifestEntry.Clone(),
  492. }
  493. if err := decoder.Decode(&manifest.Global); err != nil {
  494. return nil, err
  495. }
  496. if len(manifest.Global.Maintainers) < 1 {
  497. return nil, fmt.Errorf("missing Maintainers")
  498. }
  499. if invalidMaintainers := manifest.Global.InvalidMaintainers(); len(invalidMaintainers) > 0 {
  500. return nil, fmt.Errorf("invalid Maintainers: %q (expected format %q)", strings.Join(invalidMaintainers, ", "), MaintainersFormat)
  501. }
  502. if len(manifest.Global.Tags) > 0 {
  503. return nil, fmt.Errorf("global Tags not permitted")
  504. }
  505. if invalidArchitectures := manifest.Global.InvalidArchitectures(); len(invalidArchitectures) > 0 {
  506. return nil, fmt.Errorf("invalid global Architectures: %q", strings.Join(invalidArchitectures, ", "))
  507. }
  508. for {
  509. entry := manifest.Global.Clone()
  510. err := decoder.Decode(&entry)
  511. if err == io.EOF {
  512. break
  513. }
  514. if err != nil {
  515. return nil, err
  516. }
  517. if !GitFetchRegex.MatchString(entry.GitFetch) {
  518. return nil, fmt.Errorf(`Tags %q has invalid GitFetch (must be "refs/heads/..." or "refs/tags/..."): %q`, entry.TagsString(), entry.GitFetch)
  519. }
  520. if !GitCommitRegex.MatchString(entry.GitCommit) {
  521. return nil, fmt.Errorf(`Tags %q has invalid GitCommit (must be a commit, not a tag or ref): %q`, entry.TagsString(), entry.GitCommit)
  522. }
  523. err = manifest.AddEntry(entry)
  524. if err != nil {
  525. return nil, err
  526. }
  527. }
  528. return &manifest, nil
  529. }