diff --git a/internal/manifest/manifest.go b/internal/manifest/manifest.go index 89ca4b9b..893c96f4 100644 --- a/internal/manifest/manifest.go +++ b/internal/manifest/manifest.go @@ -334,6 +334,7 @@ func fastValidate(options *WriteOptions) (err error) { } sliceExist[slice.String()] = true } + hardLinkGroups := make(map[uint64][]*ReportEntry) for _, entry := range options.Report.Entries { err := validateReportEntry(&entry) if err != nil { @@ -344,7 +345,38 @@ func fastValidate(options *WriteOptions) (err error) { return fmt.Errorf("path %q refers to missing slice %s", entry.Path, slice.String()) } } + if entry.HardLinkId != NON_HARD_LINK { + e := entry + hardLinkGroups[e.HardLinkId] = append(hardLinkGroups[e.HardLinkId], &e) + } + } + // Entries within a hard link group must have same content. + var linkIDs []uint64 + for id := range hardLinkGroups { + linkIDs = append(linkIDs, id) + } + sort.Slice(linkIDs, func(i, j int) bool { + return linkIDs[i] < linkIDs[j] + }) + for i, id := range linkIDs { + if uint64(i+1) != id { + return fmt.Errorf("skipped hard link ID %d, but %d exists", i+1, id) + } + entries := hardLinkGroups[id] + if len(entries) == 1 { + return fmt.Errorf("hard link group %d has only one path: %s", id, entries[0].Path) + } + sort.Slice(entries, func(i, j int) bool { + return entries[i].Path < entries[j].Path + }) + e0 := entries[0] + for _, e := range entries[1:] { + if e.Link != e0.Link || e.Mode != e0.Mode || e.SHA256 != e0.SHA256 || e.Size != e0.Size { + return fmt.Errorf("hard linked paths %q and %q have diverging contents", e0.Path, e.Path) + } + } } + return nil } diff --git a/internal/manifest/manifest_test.go b/internal/manifest/manifest_test.go index 1259d819..521f3449 100644 --- a/internal/manifest/manifest_test.go +++ b/internal/manifest/manifest_test.go @@ -295,6 +295,16 @@ var generateManifestTests = []struct { Size: 1234, Slices: map[*setup.Slice]bool{slice1: true}, FinalSHA256: "final-hash", + HardLinkId: 1, + }, + "/hardlink": { + Path: "/hardlink", + Mode: 0456, + SHA256: "hash", + Size: 1234, + Slices: map[*setup.Slice]bool{slice1: true}, + FinalSHA256: "final-hash", + HardLinkId: 1, }, "/link": { Path: "/link", @@ -324,6 +334,16 @@ var generateManifestTests = []struct { Size: 1234, SHA256: "hash", FinalSHA256: "final-hash", + HardLinkId: 1, + }, { + Kind: "path", + Path: "/hardlink", + Mode: "0456", + Slices: []string{"package1_slice1"}, + Size: 1234, + SHA256: "hash", + FinalSHA256: "final-hash", + HardLinkId: 1, }, { Kind: "path", Path: "/link", @@ -355,6 +375,10 @@ var generateManifestTests = []struct { Kind: "content", Slice: "package1_slice1", Path: "/file", + }, { + Kind: "content", + Slice: "package1_slice1", + Path: "/hardlink", }, { Kind: "content", Slice: "package1_slice1", @@ -539,6 +563,56 @@ var generateManifestTests = []struct { }, }, error: `internal error: invalid manifest: path "/dir" has invalid options: size set for directory`, +}, { + summary: "Invalid path: skipped hard link ID", + report: &manifest.Report{ + Root: "/", + Entries: map[string]manifest.ReportEntry{ + "/file": { + Path: "/file", + Slices: map[*setup.Slice]bool{slice1: true}, + HardLinkId: 2, + }, + }, + }, + error: `internal error: invalid manifest: skipped hard link ID 1, but 2 exists`, +}, { + summary: "Invalid path: hard link group has only one path", + report: &manifest.Report{ + Root: "/", + Entries: map[string]manifest.ReportEntry{ + "/file": { + Path: "/file", + Slices: map[*setup.Slice]bool{slice1: true}, + HardLinkId: 1, + }, + }, + }, + error: `internal error: invalid manifest: hard link group 1 has only one path: /file`, +}, { + summary: "Invalid path: hard linked paths differ", + report: &manifest.Report{ + Root: "/", + Entries: map[string]manifest.ReportEntry{ + "/file": { + Path: "/file", + Mode: 0456, + SHA256: "hash", + Size: 1234, + Slices: map[*setup.Slice]bool{slice1: true}, + HardLinkId: 1, + }, + "/hardlink": { + Path: "/hardlink", + Mode: 0456, + SHA256: "different-hash", + Size: 1234, + Slices: map[*setup.Slice]bool{slice1: true}, + HardLinkId: 1, + }, + }, + }, + error: `internal error: invalid manifest: hard linked paths "/file" and "/hardlink" have diverging contents`, }, { summary: "Invalid package: missing name", packageInfo: []*archive.PackageInfo{{