From 21de6da9db529745017cb020989f5c42e672074d Mon Sep 17 00:00:00 2001 From: mfw78 Date: Mon, 1 Jun 2026 06:53:58 +0000 Subject: [PATCH] fix(manifest): infer refBytesSize at marshal time when never set `Add()` defers setting `n.refBytesSize` until the first non-empty entry is added. When the only adds against a node use an empty entry (e.g. the bzz endpoint's `m.Add(ctx, manifest.RootPath, manifest.NewEntry(swarm.ZeroAddress, rootMetadata))` at `pkg/api/bzz.go:266`), the field stays at zero. If such a node is then populated with non-empty children, `MarshalBinary` writes a header with `refBytesSize = 0` while the fork bodies still carry full-width refs, and the v0.2 `UnmarshalBinary` early-return at the (previous) `marshal.go:285` silently drops every fork. The mantaray-js reference impl documents this with an explicit FIXME ("in Bee, if one uploads a file on the bzz endpoint, the node under `/` gets 0 refsize"); downstream Rust consumers (nectar, isheika) hit it in the wild and have to add bee-tolerance paths in their decoders. Infer the correct width at the top of `MarshalBinary`: use the node's own entry width if present, else the first child ref's width. The early-return guard in `UnmarshalBinary` is left in place as backward-compat for any already-persisted manifests that pre-date this fix. Adds `TestPersistDirectoryOnlyAdds` exercising the directory-only-adds path; without the inference, the test fails with `entry on 'foo': not found` after save+reload (silent fork loss). --- pkg/manifest/mantaray/marshal.go | 18 ++++++++++++ pkg/manifest/mantaray/persist_test.go | 40 +++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/pkg/manifest/mantaray/marshal.go b/pkg/manifest/mantaray/marshal.go index 8d510ceb57f..5a1afcf6fb2 100644 --- a/pkg/manifest/mantaray/marshal.go +++ b/pkg/manifest/mantaray/marshal.go @@ -105,6 +105,24 @@ func (n *Node) MarshalBinary() (bytes []byte, err error) { return nil, ErrInvalidInput } + // Infer refBytesSize when Add() never set it (directory-only adds): use + // our own entry width if present, else the first child ref's width. + // Without this, the header reports refBytesSize=0 while the body still + // carries full-width data, and the v0.2 reader at line 280 silently drops + // every fork. + if n.refBytesSize == 0 { + if len(n.entry) > 0 { + n.refBytesSize = len(n.entry) + } else { + for _, f := range n.forks { + if len(f.ref) > 0 { + n.refBytesSize = len(f.ref) + break + } + } + } + } + // header headerBytes := make([]byte, nodeHeaderSize) diff --git a/pkg/manifest/mantaray/persist_test.go b/pkg/manifest/mantaray/persist_test.go index 0025a0d5f64..f474d43e086 100644 --- a/pkg/manifest/mantaray/persist_test.go +++ b/pkg/manifest/mantaray/persist_test.go @@ -174,6 +174,46 @@ func TestPersistRemove(t *testing.T) { } } +// TestPersistDirectoryOnlyAdds covers the case where every Add() to a node +// carries an empty entry (only forks/metadata), so the lazy-init in Add() +// never sets refBytesSize. Without the marshal-time inference, the header +// would report refBytesSize=0 even though the children have full-width refs, +// and the v0.2 reader would silently drop every fork. +func TestPersistDirectoryOnlyAdds(t *testing.T) { + t.Parallel() + + ctx := context.Background() + ls := mantaray.LoadSaver(newMockLoadSaver()) + + n := mantaray.New() + n.SetObfuscationKey(mantaray.ZeroObfuscationKey) + + paths := [][]byte{[]byte("foo"), []byte("bar"), []byte("baz")} + for _, p := range paths { + // empty entry; only metadata. Mirrors bee/pkg/api/bzz.go:266 which + // adds the root path with swarm.ZeroAddress (a nil []byte). + if err := n.Add(ctx, p, nil, map[string]string{"k": "v"}, ls); err != nil { + t.Fatalf("add %q: %v", p, err) + } + } + + if err := n.Save(ctx, ls); err != nil { + t.Fatalf("save: %v", err) + } + ref := n.Reference() + + nn := mantaray.NewNodeRef(ref) + for _, p := range paths { + ln, err := nn.LookupNode(ctx, p, ls) + if err != nil { + t.Fatalf("lookup %q: %v", p, err) + } + if v := ln.Metadata()["k"]; v != "v" { + t.Fatalf("lookup %q: expected metadata k=v, got %q", p, v) + } + } +} + type ( addr [32]byte mockLoadSaver struct {