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
7c91f43d76
commit
bbf7b31940
@ -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