diff --git a/mfer/builder.go b/mfer/builder.go index 7864897..2f58ea9 100644 --- a/mfer/builder.go +++ b/mfer/builder.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "io" + "sort" "strings" "sync" "time" @@ -222,6 +223,11 @@ func (b *Builder) Build(w io.Writer) error { b.mu.Lock() defer b.mu.Unlock() + // Sort files by path for deterministic output (#23) + sort.Slice(b.files, func(i, j int) bool { + return b.files[i].Path < b.files[j].Path + }) + // Create inner manifest inner := &MFFile{ Version: MFFile_VERSION_ONE, diff --git a/mfer/builder_test.go b/mfer/builder_test.go index 761b2d5..c9297a4 100644 --- a/mfer/builder_test.go +++ b/mfer/builder_test.go @@ -115,6 +115,53 @@ func TestNewTimestampFromTimeExtremeDate(t *testing.T) { } } +func TestBuilderBuildDeterministicOrder(t *testing.T) { + // Regression test for #23: files should be sorted by path in the manifest + // to ensure deterministic output regardless of insertion order. + buildManifest := func(paths []string) []byte { + b := NewBuilder() + for _, p := range paths { + content := []byte("content of " + p) + reader := bytes.NewReader(content) + _, err := b.AddFile(RelFilePath(p), FileSize(len(content)), ModTime(time.Now()), reader, nil) + require.NoError(t, err) + } + var buf bytes.Buffer + require.NoError(t, b.Build(&buf)) + return buf.Bytes() + } + + // Build with files in two different orders + order1 := []string{"z.txt", "a.txt", "m/b.txt", "m/a.txt", "b.txt"} + order2 := []string{"b.txt", "m/a.txt", "a.txt", "z.txt", "m/b.txt"} + + manifest1 := buildManifest(order1) + manifest2 := buildManifest(order2) + + // Parse both and verify file order is sorted + m1, err := NewManifestFromReader(bytes.NewReader(manifest1)) + require.NoError(t, err) + m2, err := NewManifestFromReader(bytes.NewReader(manifest2)) + require.NoError(t, err) + + files1 := m1.Files() + files2 := m2.Files() + require.Len(t, files1, 5) + require.Len(t, files2, 5) + + // Both should have same order + for i := range files1 { + assert.Equal(t, files1[i].Path, files2[i].Path, "file %d path mismatch", i) + } + + // Verify the order is lexicographic + assert.Equal(t, "a.txt", files1[0].Path) + assert.Equal(t, "b.txt", files1[1].Path) + assert.Equal(t, "m/a.txt", files1[2].Path) + assert.Equal(t, "m/b.txt", files1[3].Path) + assert.Equal(t, "z.txt", files1[4].Path) +} + func TestBuilderBuildEmpty(t *testing.T) { b := NewBuilder()