Add deterministic file ordering in Builder.Build() (closes #23)
Sort file entries by path (lexicographic byte-order) before serializing the manifest. This ensures identical output regardless of file insertion order. Add test verifying two different insertion orders produce the same manifest file order.
This commit is contained in:
parent
41c1c69f52
commit
333dc8059c
@ -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,
|
||||
|
||||
@ -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()
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user