diff --git a/api/sync15/tree.go b/api/sync15/tree.go index ff603358..3e58c302 100644 --- a/api/sync15/tree.go +++ b/api/sync15/tree.go @@ -153,10 +153,13 @@ func parseIndex(f io.Reader) ([]*Entry, string, error) { func (t *HashTree) IndexReader() (io.Reader, error) { var w bytes.Buffer - schemaVersion := t.SchemaVersion - if schemaVersion == "" { - schemaVersion = SchemaVersionV3 - } + // t.SchemaVersion records the format read from the server during Mirror(). + // For writes, always emit v4 root indexes: current servers reject new + // v3-format root docSchema uploads even when the v3 HashEntries hash + // matches the body. Rehash() always SHA-256s the serialized output, so + // RMAPI_FORCE_SCHEMA_VERSION=3 changes the body schema but not the hash + // algorithm — use it only to inspect failure modes, not as a working mode. + schemaVersion := SchemaVersionV4 if envSchema := os.Getenv("RMAPI_FORCE_SCHEMA_VERSION"); envSchema != "" { log.Info.Printf("forcing schema version to %s via RMAPI_FORCE_SCHEMA_VERSION", envSchema) @@ -228,42 +231,19 @@ func (t *HashTree) Remove(id string) error { } func (t *HashTree) Rehash() error { - schemaVersion := t.SchemaVersion - if schemaVersion == "" { - schemaVersion = SchemaVersionV3 + reader, err := t.IndexReader() + if err != nil { + return err } - if envSchema := os.Getenv("RMAPI_FORCE_SCHEMA_VERSION"); envSchema != "" { - schemaVersion = envSchema + schemaBytes, err := io.ReadAll(reader) + if err != nil { + return err } - var hash string - var err error - - if schemaVersion == SchemaVersionV3 { - entries := []*Entry{} - for _, e := range t.Docs { - entries = append(entries, &e.Entry) - } - hash, err = HashEntries(entries) - if err != nil { - return err - } - } else { - reader, err := t.IndexReader() - if err != nil { - return err - } - - schemaBytes, err := io.ReadAll(reader) - if err != nil { - return err - } - - hasher := sha256.New() - hasher.Write(schemaBytes) - hash = hex.EncodeToString(hasher.Sum(nil)) - } + hasher := sha256.New() + hasher.Write(schemaBytes) + hash := hex.EncodeToString(hasher.Sum(nil)) log.Info.Println("New root hash: ", hash) t.Hash = hash @@ -300,7 +280,6 @@ func (t *HashTree) Mirror(r RemoteStorage, maxconcurrent int) error { return fmt.Errorf("cannot get root hash %v", err) } defer rootIndexReader.Close() - entries, schema, err := parseIndex(rootIndexReader) if err != nil { return fmt.Errorf("cannot parse rootIndex, %v", err) diff --git a/api/sync15/tree_test.go b/api/sync15/tree_test.go index ea3eba2b..c77a19d4 100644 --- a/api/sync15/tree_test.go +++ b/api/sync15/tree_test.go @@ -1,6 +1,8 @@ package sync15 import ( + "crypto/sha256" + "encoding/hex" "io" "strings" "testing" @@ -97,6 +99,48 @@ blah:0:someid:0:10 } } +// TestRootIndexWritesV4WhenMirroredV3 is the regression test for the bug +// introduced in f5c7b9c: a tree whose SchemaVersion was read as v3 from the +// server must still produce a v4 root docSchema on write. +func TestRootIndexWritesV4WhenMirroredV3(t *testing.T) { + tree := HashTree{ + SchemaVersion: SchemaVersionV3, // simulates a tree mirrored from a v3 root + } + doc := &BlobDoc{ + Entry: Entry{Hash: "somehash", DocumentID: "someid"}, + } + doc.AddFile(&Entry{Hash: "filehash", DocumentID: "someid.pdf", Size: 100}) + tree.Add(doc) + + reader, err := tree.IndexReader() + if err != nil { + t.Fatal(err) + } + body, err := io.ReadAll(reader) + if err != nil { + t.Fatal(err) + } + + _, schema, err := parseIndex(strings.NewReader(string(body))) + if err != nil { + t.Fatal(err) + } + if schema != SchemaVersionV4 { + t.Errorf("IndexReader() produced schema %s for v3-mirrored tree; want v4", schema) + } + + if err := tree.Rehash(); err != nil { + t.Fatal(err) + } + reader2, _ := tree.IndexReader() + body2, _ := io.ReadAll(reader2) + h := sha256.Sum256(body2) + expectedHash := hex.EncodeToString(h[:]) + if tree.Hash != expectedHash { + t.Errorf("Rehash() = %s; want sha256(IndexReader) = %s", tree.Hash, expectedHash) + } +} + func TestCreateRootIndex(t *testing.T) { tree := HashTree{ SchemaVersion: SchemaVersionV4,