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.
This commit is contained in:
Levi Neely 2026-08-21 13:11:46 +02:00
parent 8960fb73fd
commit 2a34c4719a
2 changed files with 96 additions and 0 deletions

View File

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

View File

@ -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
// ══════════════════════════════════════════════════════════════════