From 2a34c4719a0299968373799bead840a54bbf97da Mon Sep 17 00:00:00 2001 From: Levi Neely Date: Fri, 21 Aug 2026 13:11:46 +0200 Subject: [PATCH] fix(virtfs): support Each() with non-template directory names Each('peer', ...) creates a named directory whose children come from Bindings(). Previously, listDir and findChild only checked Bindings for template names like {foo}. Now they also handle directories that have Bindings but no Children. This fixes the peer/ directory in olliesrv which was listing empty even though peers were configured via peeradd. Added test for the non-template Each pattern. --- virtfs/builder.go | 36 +++++++++++++++++++++++++++ virtfs/decl_test.go | 60 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+) diff --git a/virtfs/builder.go b/virtfs/builder.go index a721995..4306a6e 100644 --- a/virtfs/builder.go +++ b/virtfs/builder.go @@ -125,6 +125,27 @@ func build(d *FsNodeDecl, parentUid, parentGid string) *Tree { func listDir(d *FsNodeDecl, uid, gid string) ([]os.DirEntry, error) { var entries []os.DirEntry + + // If this directory has Bindings (from Each), enumerate them directly. + if d.Bindings != nil && len(d.Children) == 0 { + nodes, err := d.Bindings() + if err != nil { + return nil, err + } + for _, n := range nodes { + m := n.Mode + if m == 0 { + m = d.Mode + } + if len(n.Children) > 0 || n.Bindings != nil { + entries = append(entries, dirEntry(n.Name, 0755)) + } else { + entries = append(entries, fileEntry(n.Name, m)) + } + } + return entries, nil + } + for _, c := range d.Children { if isTemplateName(c.Name) { if c.Bindings != nil { @@ -355,6 +376,21 @@ func findChild(d *FsNodeDecl, name string) *FsNodeDecl { return findChild(child, rest) } + // If this directory has Bindings and no Children (from Each with non-template name), + // look up children in the bindings directly. + if d.Bindings != nil && len(d.Children) == 0 { + entries, err := d.Bindings() + if err != nil { + return nil + } + for i := range entries { + if entries[i].Name == name || matchAlias(entries[i].Aliases, name) { + return &entries[i] + } + } + return nil + } + // Static children. for i := range d.Children { if !isTemplateName(d.Children[i].Name) && d.Children[i].Name == name { diff --git a/virtfs/decl_test.go b/virtfs/decl_test.go index 968fa60..284195c 100644 --- a/virtfs/decl_test.go +++ b/virtfs/decl_test.go @@ -668,6 +668,66 @@ func TestNestedPath(t *testing.T) { } } +// ══════════════════════════════════════════════════════════════════ +// Each with non-template name (like "peer/") +// ══════════════════════════════════════════════════════════════════ + +func TestEachNonTemplateName(t *testing.T) { + // This tests the pattern used by peer/ in olliesrv: + // Each("peer", ...) creates a directory whose children come from Bindings + writeData := "" + spec := DirNode("/", + Each("peer", func() ([]FsNodeDecl, error) { + return []FsNodeDecl{ + {Name: "alice", Mode: 0222, Write: func(data []byte) error { writeData = string(data); return nil }}, + {Name: "bob", Mode: 0222, Write: func(data []byte) error { writeData = string(data); return nil }}, + }, nil + }), + ) + + tree := BuildTree(spec) + + // List root should show "peer" as a directory + entries, err := tree.List() + if err != nil { + t.Fatalf("List root: %v", err) + } + if len(entries) != 1 { + t.Fatalf("got %d root entries, want 1", len(entries)) + } + if entries[0].Name() != "peer" || !entries[0].IsDir() { + t.Errorf("root entry = %v (dir=%v), want peer dir", entries[0].Name(), entries[0].IsDir()) + } + + // List peer/ should show alice and bob + entries, err = tree.Readdir("peer") + if err != nil { + t.Fatalf("Readdir peer: %v", err) + } + if len(entries) != 2 { + t.Fatalf("got %d peer entries, want 2", len(entries)) + } + names := map[string]bool{} + for _, e := range entries { + names[e.Name()] = true + } + if !names["alice"] || !names["bob"] { + t.Errorf("peer entries = %v, want alice and bob", entries) + } + + // Open and write to peer/alice + f, err := tree.Open("peer/alice") + if err != nil { + t.Fatalf("Open peer/alice: %v", err) + } + if err := f.Write([]byte("hello")); err != nil { + t.Fatalf("Write: %v", err) + } + if writeData != "hello" { + t.Errorf("writeData = %q, want hello", writeData) + } +} + // ══════════════════════════════════════════════════════════════════ // GenerateHelp test // ══════════════════════════════════════════════════════════════════