A virtual jailed shell environment for Go apps backed by an io/fs#FS.
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: convert kefka to use billy instead of an io/fs#FS

Swap command.ExecContext.FS, the registry, ls, and the kefka entrypoint
over to github.com/go-git/go-billy/v5.Filesystem. Add wasm/billyfs, a
shim that adapts a billy.Filesystem to wazero's experimental/sys.FS so
the embedded python3 WASM module can read and write through it.

The python3 command now propagates WASI exit codes via interp.ExitStatus
instead of swallowing them.

Assisted-by: Claude Opus 4.7 via Claude Code
Signed-off-by: Xe Iaso <me@xeiaso.net>

Xe Iaso 85a08fac 3390bb31

+369 -76
+19 -9
cmd/kefka/main.go
··· 10 10 "strings" 11 11 "time" 12 12 13 + "github.com/go-git/go-billy/v5" 14 + "github.com/go-git/go-billy/v5/osfs" 13 15 "github.com/spf13/pflag" 14 16 "golang.org/x/term" 15 17 "mvdan.cc/sh/v3/interp" ··· 42 44 coreutils.Register(reg) 43 45 wasmprog.Register(reg) 44 46 45 - fsys := os.DirFS(".") 47 + fsys := osfs.New(".") 46 48 47 49 middleware := func(next interp.ExecHandlerFunc) interp.ExecHandlerFunc { 48 50 return func(ctx context.Context, args []string) error { ··· 100 102 // so we can route directory state through the registry's fsys-relative pwd 101 103 // instead of interp's host-rooted Dir. Intercepted calls are replaced with 102 104 // `:` (no-op) so interp's builtin doesn't run. 103 - func callHandler(reg *registry.Impl, fsys fs.FS, stdout, stderr io.Writer) interp.CallHandlerFunc { 105 + func callHandler(reg *registry.Impl, fsys billy.Filesystem, stdout, stderr io.Writer) interp.CallHandlerFunc { 104 106 return func(ctx context.Context, args []string) ([]string, error) { 105 107 if len(args) == 0 { 106 108 return args, nil ··· 129 131 } 130 132 } 131 133 132 - func fsysStatHandler(reg *registry.Impl, fsys fs.FS) interp.StatHandlerFunc { 134 + func fsysStatHandler(reg *registry.Impl, fsys billy.Filesystem) interp.StatHandlerFunc { 133 135 return func(ctx context.Context, name string, followSymlinks bool) (fs.FileInfo, error) { 134 136 resolved := reg.Resolve(name) 135 137 if !followSymlinks { 136 - if r, ok := fsys.(fs.ReadLinkFS); ok { 138 + if r, ok := fsys.(billy.Symlink); ok { 137 139 return r.Lstat(resolved) 138 140 } 139 141 } 140 - return fs.Stat(fsys, resolved) 142 + return fsys.Stat(resolved) 141 143 } 142 144 } 143 145 144 - func fsysOpenHandler(reg *registry.Impl, fsys fs.FS) interp.OpenHandlerFunc { 146 + func fsysOpenHandler(reg *registry.Impl, fsys billy.Filesystem) interp.OpenHandlerFunc { 145 147 return func(ctx context.Context, name string, flag int, perm os.FileMode) (io.ReadWriteCloser, error) { 146 148 if flag&(os.O_WRONLY|os.O_RDWR|os.O_CREATE|os.O_APPEND|os.O_TRUNC) != 0 { 147 149 return nil, &os.PathError{Op: "open", Path: name, Err: fs.ErrPermission} ··· 154 156 } 155 157 } 156 158 157 - func fsysReadDirHandler(reg *registry.Impl, fsys fs.FS) interp.ReadDirHandlerFunc2 { 159 + func fsysReadDirHandler(reg *registry.Impl, fsys billy.Filesystem) interp.ReadDirHandlerFunc2 { 158 160 return func(ctx context.Context, name string) ([]fs.DirEntry, error) { 159 - return fs.ReadDir(fsys, reg.Resolve(name)) 161 + entries, err := fsys.ReadDir(reg.Resolve(name)) 162 + if err != nil { 163 + return nil, err 164 + } 165 + out := make([]fs.DirEntry, len(entries)) 166 + for i, e := range entries { 167 + out[i] = fs.FileInfoToDirEntry(e) 168 + } 169 + return out, nil 160 170 } 161 171 } 162 172 163 - type readOnlyFile struct{ fs.File } 173 + type readOnlyFile struct{ billy.File } 164 174 165 175 func (readOnlyFile) Write([]byte) (int, error) { return 0, fs.ErrPermission } 166 176
+2 -2
command/command.go
··· 3 3 import ( 4 4 "context" 5 5 "io" 6 - "io/fs" 7 6 7 + "github.com/go-git/go-billy/v5" 8 8 "mvdan.cc/sh/v3/expand" 9 9 ) 10 10 ··· 13 13 Stdout, Stderr io.Writer 14 14 Dir string 15 15 Environ expand.Environ 16 - FS fs.FS 16 + FS billy.Filesystem 17 17 } 18 18 19 19 type Execer interface {
+20 -15
command/internal/ls/ls.go
··· 7 7 "io" 8 8 "io/fs" 9 9 "math" 10 + "os" 10 11 "path" 11 12 "sort" 12 13 "strconv" 13 14 "strings" 14 15 "time" 15 16 17 + "github.com/go-git/go-billy/v5" 18 + "github.com/go-git/go-billy/v5/util" 16 19 "github.com/spf13/pflag" 17 20 "mvdan.cc/sh/v3/interp" 18 21 "tangled.org/xeiaso.net/kefka/command" ··· 155 158 return fmt.Sprintf("%dG", int64(math.Round(g))) 156 159 } 157 160 158 - func formatDate(t time.Time) string { 161 + func realFormatDate(t time.Time) string { 159 162 month := t.Month().String()[:3] 160 163 day := fmt.Sprintf("%2d", t.Day()) 161 164 sixMonthsAgo := time.Now().Add(-180 * 24 * time.Hour) ··· 164 167 } 165 168 return fmt.Sprintf("%s %s %d", month, day, t.Year()) 166 169 } 170 + 171 + var formatDate = realFormatDate 167 172 168 173 func classifySuffix(info fs.FileInfo) string { 169 174 if info.IsDir() { ··· 178 183 return "" 179 184 } 180 185 181 - func lstatFS(fsys fs.FS, name string) (fs.FileInfo, error) { 182 - if r, ok := fsys.(fs.ReadLinkFS); ok { 186 + func lstatFS(fsys billy.Filesystem, name string) (fs.FileInfo, error) { 187 + if r, ok := fsys.(billy.Symlink); ok { 183 188 return r.Lstat(name) 184 189 } 185 - return fs.Stat(fsys, name) 190 + return fsys.Stat(name) 186 191 } 187 192 188 193 func padLeft(s string, n int) string { ··· 215 220 216 221 func listDirectoryEntry(ec *command.ExecContext, p string, longFormat, humanReadable, classifyFiles bool) (string, string, int) { 217 222 full := resolvePath(ec, p) 218 - info, err := fs.Stat(ec.FS, full) 223 + info, err := ec.FS.Stat(full) 219 224 if err != nil { 220 225 return "", fmt.Sprintf("ls: cannot access '%s': No such file or directory\n", p), 2 221 226 } ··· 259 264 fsPattern = path.Join(dir, pattern) 260 265 } 261 266 262 - matched, err := fs.Glob(ec.FS, fsPattern) 267 + matched, err := util.Glob(ec.FS, fsPattern) 263 268 if err != nil || len(matched) == 0 { 264 269 return "", fmt.Sprintf("ls: %s: No such file or directory\n", pattern), 2 265 270 } ··· 284 289 if sortBySize { 285 290 sizes := make(map[string]int64, len(displayPaths)) 286 291 for _, m := range displayPaths { 287 - if info, e := fs.Stat(ec.FS, resolvePath(ec, m)); e == nil { 292 + if info, e := ec.FS.Stat(resolvePath(ec, m)); e == nil { 288 293 sizes[m] = info.Size() 289 294 } 290 295 } ··· 304 309 if longFormat { 305 310 for _, m := range displayPaths { 306 311 full := resolvePath(ec, m) 307 - info, statErr := fs.Stat(ec.FS, full) 312 + info, statErr := ec.FS.Stat(full) 308 313 if statErr != nil { 309 314 fmt.Fprintf(&errOut, "ls: cannot access '%s': %v\n", m, statErr) 310 315 exitCode = 2 ··· 344 349 showHidden := showAll || showAlmostAll 345 350 full := resolvePath(ec, p) 346 351 347 - info, err := fs.Stat(ec.FS, full) 352 + info, err := ec.FS.Stat(full) 348 353 if err != nil { 349 354 return "", fmt.Sprintf("ls: %s: No such file or directory\n", p), 2 350 355 } ··· 362 367 return p + suffix + "\n", "", 0 363 368 } 364 369 365 - dirEntries, err := fs.ReadDir(ec.FS, full) 370 + dirEntries, err := ec.FS.ReadDir(full) 366 371 if err != nil { 367 372 return "", fmt.Sprintf("ls: %s: %v\n", p, err), 2 368 373 } 369 374 370 375 names := make([]string, 0, len(dirEntries)) 371 - entryByName := make(map[string]fs.DirEntry, len(dirEntries)) 376 + entryByName := make(map[string]os.FileInfo, len(dirEntries)) 372 377 for _, e := range dirEntries { 373 378 name := e.Name() 374 379 if !showHidden && strings.HasPrefix(name, ".") { ··· 381 386 if sortBySize { 382 387 sizes := make(map[string]int64, len(names)) 383 388 for _, name := range names { 384 - if einfo, e := fs.Stat(ec.FS, path.Join(full, name)); e == nil { 389 + if einfo, e := ec.FS.Stat(path.Join(full, name)); e == nil { 385 390 sizes[name] = einfo.Size() 386 391 } 387 392 } ··· 424 429 default: 425 430 entryPath = path.Join(full, name) 426 431 } 427 - einfo, errE := fs.Stat(ec.FS, entryPath) 432 + einfo, errE := ec.FS.Stat(entryPath) 428 433 if errE != nil { 429 434 fmt.Fprintf(&errOut, "ls: cannot access '%s': %v\n", name, errE) 430 435 exitCode = 2 ··· 479 484 if ok { 480 485 if entry.IsDir() { 481 486 isDir = true 482 - } else if entry.Type()&fs.ModeSymlink != 0 { 483 - if einfo, e := fs.Stat(ec.FS, path.Join(full, name)); e == nil && einfo.IsDir() { 487 + } else if entry.Mode()&fs.ModeSymlink != 0 { 488 + if einfo, e := ec.FS.Stat(path.Join(full, name)); e == nil && einfo.IsDir() { 484 489 isDir = true 485 490 } 486 491 }
+34 -25
command/internal/ls/ls_test.go
··· 3 3 import ( 4 4 "bytes" 5 5 "context" 6 - "io/fs" 6 + "os" 7 7 "testing" 8 - "testing/fstest" 9 8 "time" 10 9 10 + "github.com/go-git/go-billy/v5" 11 + "github.com/go-git/go-billy/v5/memfs" 11 12 "tangled.org/xeiaso.net/kefka/command" 12 13 ) 13 14 14 - // fixedTime is far enough in the past that formatDate always emits the 15 - // year-form ("Jan 15 2020"), keeping expected output stable as the 16 - // test clock advances. 17 - var fixedTime = time.Date(2020, 1, 15, 12, 0, 0, 0, time.UTC) 15 + func newTestFS() billy.Filesystem { 16 + fs := memfs.New() 17 + write := func(name string, data []byte, perm os.FileMode) { 18 + f, err := fs.OpenFile(name, os.O_CREATE|os.O_WRONLY, perm) 19 + if err != nil { 20 + panic(err) 21 + } 22 + f.Write(data) 23 + f.Close() 24 + } 25 + write("alpha.txt", []byte("aaaa"), 0o644) 26 + write("beta.txt", []byte("bbbbbbbb"), 0o644) 27 + write("gamma.txt", []byte("cc"), 0o644) 28 + write(".hidden", []byte("h"), 0o644) 29 + write("script.sh", []byte("#!"), 0o755) 30 + write("huge.bin", bytes.Repeat([]byte("x"), 1500), 0o644) 31 + write("sub/inner.txt", []byte("inner"), 0o644) 32 + write("sub/deeper/leaf.txt", []byte("L"), 0o644) 33 + return fs 34 + } 18 35 19 - func newTestFS() fstest.MapFS { 20 - return fstest.MapFS{ 21 - ".": {Mode: fs.ModeDir | 0o755, ModTime: fixedTime}, 22 - "alpha.txt": {Data: []byte("aaaa"), ModTime: fixedTime}, // 4 bytes 23 - "beta.txt": {Data: []byte("bbbbbbbb"), ModTime: fixedTime}, // 8 bytes 24 - "gamma.txt": {Data: []byte("cc"), ModTime: fixedTime}, // 2 bytes 25 - ".hidden": {Data: []byte("h"), ModTime: fixedTime}, // 1 byte hidden 26 - "script.sh": {Data: []byte("#!"), Mode: 0o755, ModTime: fixedTime}, // executable 27 - "huge.bin": {Data: bytes.Repeat([]byte("x"), 1500), ModTime: fixedTime}, 28 - "sub": {Mode: fs.ModeDir | 0o755, ModTime: fixedTime}, 29 - "sub/inner.txt": {Data: []byte("inner"), ModTime: fixedTime}, 30 - "sub/deeper": {Mode: fs.ModeDir | 0o755, ModTime: fixedTime}, 31 - "sub/deeper/leaf.txt": {Data: []byte("L"), ModTime: fixedTime}, 32 - } 36 + func withFixedDate(t *testing.T) { 37 + t.Helper() 38 + prev := formatDate 39 + formatDate = func(time.Time) string { return "Jan 15 2020" } 40 + t.Cleanup(func() { formatDate = prev }) 33 41 } 34 42 35 43 func TestExec(t *testing.T) { ··· 165 173 wantStdout: "alpha.txt\n", 166 174 }, 167 175 { 168 - name: "single file argument long", 169 - args: []string{"-l", "alpha.txt"}, 176 + name: "single file argument long", 177 + args: []string{"-l", "alpha.txt"}, 170 178 wantStdout: "-rw-r--r-- 1 user user 4 Jan 15 2020 alpha.txt\n", 171 179 }, 172 180 { ··· 184 192 185 193 for _, tc := range tests { 186 194 t.Run(tc.name, func(t *testing.T) { 195 + withFixedDate(t) 187 196 var stdout, stderr bytes.Buffer 188 197 dir := tc.dir 189 198 if dir == "" { ··· 259 268 260 269 // Far in the past renders as month/day/year. 261 270 old := time.Date(2020, 7, 4, 9, 30, 0, 0, time.UTC) 262 - if got, want := formatDate(old), "Jul 4 2020"; got != want { 271 + if got, want := realFormatDate(old), "Jul 4 2020"; got != want { 263 272 t.Errorf("formatDate(%v) = %q, want %q", old, got, want) 264 273 } 265 274 266 275 // Within the last 6 months renders as month/day/HH:MM. 267 276 recent := now.Add(-3 * 24 * time.Hour) 268 - got := formatDate(recent) 277 + got := realFormatDate(recent) 269 278 month := recent.Month().String()[:3] 270 279 wantPrefix := month + " " 271 280 if !bytes.HasPrefix([]byte(got), []byte(wantPrefix)) { ··· 295 304 t.Errorf("resolvePath(dir=%q, in=%q) = %q, want %q", tc.dir, tc.in, got, tc.want) 296 305 } 297 306 } 298 - } 307 + }
+23 -20
command/internal/python3/python3.go
··· 3 3 import ( 4 4 "context" 5 5 _ "embed" 6 + "errors" 6 7 7 8 "github.com/tetratelabs/wazero" 9 + "github.com/tetratelabs/wazero/experimental/sysfs" 8 10 "github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1" 11 + wsys "github.com/tetratelabs/wazero/sys" 12 + "mvdan.cc/sh/v3/interp" 9 13 "tangled.org/xeiaso.net/kefka/command" 14 + "tangled.org/xeiaso.net/kefka/wasm/billyfs" 10 15 ) 11 16 12 - var ( 13 - //go:embed python.wasm 14 - pyWASM []byte 17 + //go:embed python.wasm 18 + var pyWASM []byte 15 19 16 - r wazero.Runtime 17 - code wazero.CompiledModule 20 + var ( 21 + runtime wazero.Runtime 22 + compiled wazero.CompiledModule 18 23 ) 19 24 20 25 func init() { 21 26 ctx := context.Background() 22 - r = wazero.NewRuntime(ctx) 23 - 24 - wasi_snapshot_preview1.MustInstantiate(ctx, r) 27 + runtime = wazero.NewRuntime(ctx) 28 + wasi_snapshot_preview1.MustInstantiate(ctx, runtime) 25 29 26 30 var err error 27 - code, err = r.CompileModule(ctx, pyWASM) 31 + compiled, err = runtime.CompileModule(ctx, pyWASM) 28 32 if err != nil { 29 33 panic(err) 30 34 } ··· 33 37 type Impl struct{} 34 38 35 39 func (Impl) Exec(ctx context.Context, ec *command.ExecContext, args []string) error { 36 - fsConfig := wazero.NewFSConfig(). 37 - WithFSMount(ec.FS, "/") 40 + fsConfig := wazero.NewFSConfig().(sysfs.FSConfig). 41 + WithSysFSMount(billyfs.New(ec.FS), "/") 38 42 39 43 config := wazero.NewModuleConfig(). 40 - // stdio 41 44 WithStdin(ec.Stdin). 42 45 WithStdout(ec.Stdout). 43 46 WithStderr(ec.Stderr). 44 - // argv 45 47 WithArgs(append([]string{"python3"}, args...)...). 46 48 WithName("python3"). 47 - // filesystem 48 49 WithFSConfig(fsConfig). 49 - // time 50 50 WithSysNanosleep(). 51 51 WithSysNanotime(). 52 52 WithSysWalltime() 53 53 54 - mod, err := r.InstantiateModule(ctx, code, config) 54 + mod, err := runtime.InstantiateModule(ctx, compiled, config) 55 55 if err != nil { 56 + if exitErr, ok := errors.AsType[*wsys.ExitError](err); ok { 57 + if code := exitErr.ExitCode(); code != 0 { 58 + return interp.ExitStatus(uint8(code)) 59 + } 60 + return nil 61 + } 56 62 return err 57 63 } 58 - 59 - defer mod.Close(ctx) 60 - 61 - return nil 64 + return mod.Close(ctx) 62 65 }
+4 -4
command/registry/registry.go
··· 3 3 import ( 4 4 "context" 5 5 "fmt" 6 - "io/fs" 7 6 "path" 8 7 "strings" 9 8 "sync" 10 9 10 + "github.com/go-git/go-billy/v5" 11 11 "mvdan.cc/sh/v3/interp" 12 12 "tangled.org/xeiaso.net/kefka/command" 13 13 ) ··· 66 66 } 67 67 68 68 // Chdir changes the fsys-relative working directory. Validates against fsys. 69 - func (i *Impl) Chdir(fsys fs.FS, target string) error { 69 + func (i *Impl) Chdir(fsys billy.Filesystem, target string) error { 70 70 if target == "" { 71 71 target = "." 72 72 } 73 73 next := i.Resolve(target) 74 74 75 - info, err := fs.Stat(fsys, next) 75 + info, err := fsys.Stat(next) 76 76 if err != nil { 77 77 return fmt.Errorf("cd: %s: No such file or directory", target) 78 78 } ··· 86 86 return nil 87 87 } 88 88 89 - func (i *Impl) Exec(ctx context.Context, fsys fs.FS, args []string) error { 89 + func (i *Impl) Exec(ctx context.Context, fsys billy.Filesystem, args []string) error { 90 90 hc := interp.HandlerCtx(ctx) 91 91 92 92 if len(args) == 0 {
+5 -1
go.mod
··· 3 3 go 1.26.2 4 4 5 5 require ( 6 + github.com/go-git/go-billy/v5 v5.8.0 6 7 github.com/spf13/pflag v1.0.10 7 8 github.com/tetratelabs/wazero v1.11.0 8 9 golang.org/x/term v0.41.0 9 10 mvdan.cc/sh/v3 v3.13.1 10 11 ) 11 12 12 - require golang.org/x/sys v0.42.0 // indirect 13 + require ( 14 + github.com/cyphar/filepath-securejoin v0.3.6 // indirect 15 + golang.org/x/sys v0.42.0 // indirect 16 + )
+22
go.sum
··· 1 1 github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= 2 2 github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= 3 + github.com/cyphar/filepath-securejoin v0.3.6 h1:4d9N5ykBnSp5Xn2JkhocYDkOpURL/18CYMpo6xB9uWM= 4 + github.com/cyphar/filepath-securejoin v0.3.6/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= 5 + github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 + github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 + github.com/go-git/go-billy/v5 v5.8.0 h1:I8hjc3LbBlXTtVuFNJuwYuMiHvQJDq1AT6u4DwDzZG0= 8 + github.com/go-git/go-billy/v5 v5.8.0/go.mod h1:RpvI/rw4Vr5QA+Z60c6d6LXH0rYJo0uD5SqfmrrheCY= 3 9 github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= 4 10 github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= 5 11 github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= ··· 8 14 github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 9 15 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 10 16 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 17 + github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= 18 + github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= 19 + github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 20 + github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 11 21 github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= 12 22 github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= 13 23 github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= 14 24 github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 25 + github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 26 + github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 15 27 github.com/tetratelabs/wazero v1.11.0 h1:+gKemEuKCTevU4d7ZTzlsvgd1uaToIDtlQlmNbwqYhA= 16 28 github.com/tetratelabs/wazero v1.11.0/go.mod h1:eV28rsN8Q+xwjogd7f4/Pp4xFxO7uOGbLcD/LzB1wiU= 29 + golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= 30 + golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= 31 + golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM= 32 + golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= 17 33 golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= 18 34 golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= 19 35 golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= 20 36 golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= 37 + golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= 38 + golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= 39 + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 40 + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 41 + gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 42 + gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 21 43 mvdan.cc/sh/v3 v3.13.1 h1:DP3TfgZhDkT7lerUdnp6PTGKyxxzz6T+cOlY/xEvfWk= 22 44 mvdan.cc/sh/v3 v3.13.1/go.mod h1:lXJ8SexMvEVcHCoDvAGLZgFJ9Wsm2sulmoNEXGhYZD0=
+240
wasm/billyfs/billyfs.go
··· 1 + // Package billyfs adapts a github.com/go-git/go-billy/v5 Filesystem into a 2 + // github.com/tetratelabs/wazero experimental/sys.FS so a WASI guest can read 3 + // and write through the billy filesystem. 4 + package billyfs 5 + 6 + import ( 7 + "errors" 8 + "io" 9 + stdfs "io/fs" 10 + "os" 11 + 12 + "github.com/go-git/go-billy/v5" 13 + expsys "github.com/tetratelabs/wazero/experimental/sys" 14 + wsys "github.com/tetratelabs/wazero/sys" 15 + ) 16 + 17 + // New returns an experimental/sys.FS backed by the given billy.Filesystem. 18 + // All paths passed in by the WASM guest are interpreted as relative to the 19 + // billy filesystem's root. 20 + func New(fsys billy.Filesystem) expsys.FS { 21 + return &billyFS{fsys: fsys} 22 + } 23 + 24 + type billyFS struct { 25 + expsys.UnimplementedFS 26 + fsys billy.Filesystem 27 + } 28 + 29 + func (b *billyFS) OpenFile(path string, flag expsys.Oflag, perm stdfs.FileMode) (expsys.File, expsys.Errno) { 30 + f, err := b.fsys.OpenFile(path, toOSFlag(flag), perm) 31 + if err != nil { 32 + return nil, toErrno(err) 33 + } 34 + return &billyFile{file: f, fsys: b.fsys}, 0 35 + } 36 + 37 + func (b *billyFS) Stat(path string) (wsys.Stat_t, expsys.Errno) { 38 + info, err := b.fsys.Stat(path) 39 + if err != nil { 40 + return wsys.Stat_t{}, toErrno(err) 41 + } 42 + return wsys.NewStat_t(info), 0 43 + } 44 + 45 + func (b *billyFS) Lstat(path string) (wsys.Stat_t, expsys.Errno) { 46 + if sym, ok := b.fsys.(billy.Symlink); ok { 47 + info, err := sym.Lstat(path) 48 + if err != nil { 49 + return wsys.Stat_t{}, toErrno(err) 50 + } 51 + return wsys.NewStat_t(info), 0 52 + } 53 + return b.Stat(path) 54 + } 55 + 56 + func (b *billyFS) Mkdir(path string, perm stdfs.FileMode) expsys.Errno { 57 + return toErrno(b.fsys.MkdirAll(path, perm)) 58 + } 59 + 60 + func (b *billyFS) Unlink(path string) expsys.Errno { 61 + return toErrno(b.fsys.Remove(path)) 62 + } 63 + 64 + func (b *billyFS) Rmdir(path string) expsys.Errno { 65 + return toErrno(b.fsys.Remove(path)) 66 + } 67 + 68 + func (b *billyFS) Rename(from, to string) expsys.Errno { 69 + return toErrno(b.fsys.Rename(from, to)) 70 + } 71 + 72 + func (b *billyFS) Readlink(path string) (string, expsys.Errno) { 73 + sym, ok := b.fsys.(billy.Symlink) 74 + if !ok { 75 + return "", expsys.ENOSYS 76 + } 77 + target, err := sym.Readlink(path) 78 + if err != nil { 79 + return "", toErrno(err) 80 + } 81 + return target, 0 82 + } 83 + 84 + func (b *billyFS) Symlink(oldPath, linkName string) expsys.Errno { 85 + sym, ok := b.fsys.(billy.Symlink) 86 + if !ok { 87 + return expsys.ENOSYS 88 + } 89 + return toErrno(sym.Symlink(oldPath, linkName)) 90 + } 91 + 92 + type billyFile struct { 93 + expsys.UnimplementedFile 94 + file billy.File 95 + fsys billy.Filesystem 96 + dirEntries []os.FileInfo 97 + dirRead bool 98 + dirOffset int 99 + } 100 + 101 + func (f *billyFile) Stat() (wsys.Stat_t, expsys.Errno) { 102 + info, err := f.fsys.Stat(f.file.Name()) 103 + if err != nil { 104 + return wsys.Stat_t{}, toErrno(err) 105 + } 106 + return wsys.NewStat_t(info), 0 107 + } 108 + 109 + func (f *billyFile) Read(buf []byte) (int, expsys.Errno) { 110 + n, err := f.file.Read(buf) 111 + if err == io.EOF { 112 + return n, 0 113 + } 114 + if err != nil { 115 + return n, toErrno(err) 116 + } 117 + return n, 0 118 + } 119 + 120 + func (f *billyFile) Write(buf []byte) (int, expsys.Errno) { 121 + n, err := f.file.Write(buf) 122 + if err != nil { 123 + return n, toErrno(err) 124 + } 125 + return n, 0 126 + } 127 + 128 + func (f *billyFile) Seek(offset int64, whence int) (int64, expsys.Errno) { 129 + n, err := f.file.Seek(offset, whence) 130 + if err != nil { 131 + return n, toErrno(err) 132 + } 133 + return n, 0 134 + } 135 + 136 + func (f *billyFile) Pread(buf []byte, off int64) (int, expsys.Errno) { 137 + n, err := f.file.ReadAt(buf, off) 138 + if err == io.EOF { 139 + return n, 0 140 + } 141 + if err != nil { 142 + return n, toErrno(err) 143 + } 144 + return n, 0 145 + } 146 + 147 + func (f *billyFile) Truncate(size int64) expsys.Errno { 148 + return toErrno(f.file.Truncate(size)) 149 + } 150 + 151 + func (f *billyFile) Close() expsys.Errno { 152 + return toErrno(f.file.Close()) 153 + } 154 + 155 + func (f *billyFile) IsDir() (bool, expsys.Errno) { 156 + info, err := f.fsys.Stat(f.file.Name()) 157 + if err != nil { 158 + return false, toErrno(err) 159 + } 160 + return info.IsDir(), 0 161 + } 162 + 163 + func (f *billyFile) Readdir(n int) ([]expsys.Dirent, expsys.Errno) { 164 + if !f.dirRead { 165 + entries, err := f.fsys.ReadDir(f.file.Name()) 166 + if err != nil { 167 + return nil, toErrno(err) 168 + } 169 + f.dirEntries = entries 170 + f.dirRead = true 171 + } 172 + 173 + remaining := len(f.dirEntries) - f.dirOffset 174 + if remaining <= 0 { 175 + return nil, 0 176 + } 177 + 178 + count := remaining 179 + if n > 0 && n < count { 180 + count = n 181 + } 182 + 183 + dirents := make([]expsys.Dirent, 0, count) 184 + for i := 0; i < count; i++ { 185 + info := f.dirEntries[f.dirOffset+i] 186 + dirents = append(dirents, expsys.Dirent{ 187 + Name: info.Name(), 188 + Type: info.Mode() & stdfs.ModeType, 189 + }) 190 + } 191 + f.dirOffset += count 192 + return dirents, 0 193 + } 194 + 195 + func toErrno(err error) expsys.Errno { 196 + if err == nil { 197 + return 0 198 + } 199 + switch { 200 + case errors.Is(err, os.ErrNotExist): 201 + return expsys.ENOENT 202 + case errors.Is(err, os.ErrExist): 203 + return expsys.EEXIST 204 + case errors.Is(err, os.ErrPermission): 205 + return expsys.EPERM 206 + case errors.Is(err, billy.ErrNotSupported): 207 + return expsys.ENOSYS 208 + case errors.Is(err, billy.ErrReadOnly): 209 + return expsys.EROFS 210 + } 211 + if e := expsys.UnwrapOSError(err); e != 0 { 212 + return e 213 + } 214 + return expsys.EIO 215 + } 216 + 217 + func toOSFlag(flag expsys.Oflag) int { 218 + var f int 219 + switch flag & 0b11 { 220 + case expsys.O_RDONLY: 221 + f = os.O_RDONLY 222 + case expsys.O_RDWR: 223 + f = os.O_RDWR 224 + case expsys.O_WRONLY: 225 + f = os.O_WRONLY 226 + } 227 + if flag&expsys.O_APPEND != 0 { 228 + f |= os.O_APPEND 229 + } 230 + if flag&expsys.O_CREAT != 0 { 231 + f |= os.O_CREATE 232 + } 233 + if flag&expsys.O_EXCL != 0 { 234 + f |= os.O_EXCL 235 + } 236 + if flag&expsys.O_TRUNC != 0 { 237 + f |= os.O_TRUNC 238 + } 239 + return f 240 + }