internal/file: add VirtualFS.ReadDir and FileEntryFS.ReadDir

Closes #3084
This commit is contained in:
Hajime Hoshi
2025-09-20 17:36:03 +09:00
parent fa51d1d012
commit 77a8c30ca8
8 changed files with 149 additions and 0 deletions

View File

@@ -112,6 +112,33 @@ func (f *FileEntryFS) Open(name string) (fs.File, error) {
}
}
func (f *FileEntryFS) ReadDir(name string) ([]fs.DirEntry, error) {
if !fs.ValidPath(name) {
return nil, &fs.PathError{
Op: "readdir",
Path: name,
Err: fs.ErrNotExist,
}
}
ent, err := f.Open(name)
if err != nil {
return nil, err
}
defer func() {
_ = ent.Close()
}()
d, ok := ent.(*dir)
if !ok {
return nil, &fs.PathError{
Op: "readdir",
Path: name,
Err: fs.ErrInvalid,
}
}
return d.ReadDir(-1)
}
type file struct {
entry js.Value
file js.Value

View File

@@ -54,6 +54,12 @@ func (v *VirtualFS) Open(name string) (fs.File, error) {
}
}
// It is implicitly assumed that all the real paths are under the same directory.
// For name == ".", return a special virtual root directory.
// Unfortunately it is not possible to return a real directory here because
// v.paths might not include some files in the same directory.
// TODO: Calculate the common ancestor directory of v.paths and use it.
if name == "." {
return v.newRootFS(), nil
}
@@ -76,6 +82,42 @@ func (v *VirtualFS) Open(name string) (fs.File, error) {
}
}
func (v *VirtualFS) ReadDir(name string) ([]fs.DirEntry, error) {
if !fs.ValidPath(name) {
return nil, &fs.PathError{
Op: "readdir",
Path: name,
Err: fs.ErrNotExist,
}
}
if name == "." {
root := v.newRootFS()
return root.ReadDir(-1)
}
es := strings.Split(name, "/")
for _, realPath := range v.paths {
if filepath.Base(realPath) != es[0] {
continue
}
f, err := os.Open(filepath.Join(append([]string{realPath}, es[1:]...)...))
if err != nil {
return nil, err
}
defer func() {
_ = f.Close
}()
return f.ReadDir(-1)
}
return nil, &fs.PathError{
Op: "readdir",
Path: name,
Err: fs.ErrNotExist,
}
}
type virtualFSRoot struct {
realPaths []string
offset int

View File

@@ -0,0 +1,80 @@
// Copyright 2025 The Ebitengine Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build !js
package file_test
import (
"io/fs"
"slices"
"strings"
"testing"
"github.com/hajimehoshi/ebiten/v2/internal/file"
)
func TestFSReadDir(t *testing.T) {
vfs := file.NewVirtualFS([]string{"testdata/foo.txt", "testdata/dir"})
rootEnts, err := fs.ReadDir(vfs, ".")
if err != nil {
t.Fatal(err)
}
if len(rootEnts) != 2 {
t.Errorf("len(ents): got: %d, want: %d", len(rootEnts), 2)
}
slices.SortFunc(rootEnts, func(a, b fs.DirEntry) int {
return strings.Compare(a.Name(), b.Name())
})
if got, want := rootEnts[0].Name(), "dir"; got != want {
t.Errorf("ents[0].Name(): got: %s, want: %s", got, want)
}
if got, want := rootEnts[0].IsDir(), true; got != want {
t.Errorf("ents[0].IsDir(): got: false, want: true")
}
if got, want := rootEnts[1].Name(), "foo.txt"; got != want {
t.Errorf("ents[1].Name(): got: %s, want: %s", got, want)
}
if got, want := rootEnts[1].IsDir(), false; got != want {
t.Errorf("ents[1].IsDir(): got: true, want: false")
}
subEnts, err := fs.ReadDir(vfs, "dir")
if err != nil {
t.Fatal(err)
}
if len(subEnts) != 2 {
t.Errorf("len(ents): got: %d, want: %d", len(subEnts), 1)
}
slices.SortFunc(subEnts, func(a, b fs.DirEntry) int {
return strings.Compare(a.Name(), b.Name())
})
if got, want := subEnts[0].Name(), "foo.txt"; got != want {
t.Errorf("ents[0].Name(): got: %s, want: %s", got, want)
}
if got, want := subEnts[0].IsDir(), false; got != want {
t.Errorf("ents[0].IsDir(): got: false, want: true")
}
if got, want := subEnts[1].Name(), "qux.txt"; got != want {
t.Errorf("ents[1].Name(): got: %s, want: %s", got, want)
}
if got, want := subEnts[1].IsDir(), false; got != want {
t.Errorf("ents[1].IsDir(): got: true, want: false")
}
if _, err := fs.ReadDir(vfs, "baz.txt"); err == nil {
t.Errorf("fs.ReadDir on a file must return an error")
}
}

0
internal/file/testdata/bar.txt vendored Normal file
View File

0
internal/file/testdata/baz.txt vendored Normal file
View File

0
internal/file/testdata/dir/foo.txt vendored Normal file
View File

0
internal/file/testdata/dir/qux.txt vendored Normal file
View File

0
internal/file/testdata/foo.txt vendored Normal file
View File