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:
user 2026-02-10 18:35:37 -08:00 committed by clawbot
parent 41c1c69f52
commit 333dc8059c
2 changed files with 53 additions and 0 deletions

View File

@ -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,

View File

@ -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()