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