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.

fix(s3fs): support opening directories

WASI guests (e.g. python.wasm via wazero) call OpenFile(".", O_RDONLY)
during fd_prestat_get to enumerate preopens, then ask IsDir() on the
result. s3fs would issue a GetObject with an empty key, fail, and
surface as EIO -- crashing python at _start.

Add an s3DirFile pseudo-handle for directory paths and route OpenFile
through it: short-circuit "" / "." to skip the S3 call entirely, and
on NoSuchKey/NotFound probe with ListObjectsV2 so directory prefixes
return s3DirFile while real misses get fs.ErrNotExist (ENOENT).

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

Xe Iaso a39cc071 84d4f093

+82 -1
+40 -1
internal/s3fs/basic.go
··· 59 59 60 60 switch flag & SupportedOFlags { 61 61 case O_RDONLY: 62 - return newS3ReadFile(fs3.client, fs3.bucket, p) 62 + // The bucket root is always a directory; short-circuit so WASI 63 + // preopens (which OpenFile(".", O_RDONLY)) don't issue an S3 call. 64 + key := strings.TrimPrefix(fs3.cleanPath(filename), "/") 65 + if key == "" || key == "." { 66 + return newS3DirFile(p), nil 67 + } 68 + 69 + f, err := newS3ReadFile(fs3.client, fs3.bucket, p) 70 + if err == nil { 71 + return f, nil 72 + } 73 + 74 + // If the object simply doesn't exist, the path may still be a 75 + // directory prefix in S3. Probe for that before giving up. 76 + var apiErr smithy.APIError 77 + if !errors.As(err, &apiErr) { 78 + return nil, err 79 + } 80 + switch apiErr.ErrorCode() { 81 + case "NoSuchKey", "NotFound": 82 + default: 83 + return nil, err 84 + } 85 + 86 + ctx := context.TODO() 87 + prefix := key + "/" 88 + maxKeys := int32(1) 89 + list, lerr := fs3.client.ListObjectsV2(ctx, &s3.ListObjectsV2Input{ 90 + Bucket: &fs3.bucket, 91 + Prefix: &prefix, 92 + Delimiter: &fs3.separator, 93 + MaxKeys: &maxKeys, 94 + }) 95 + if lerr != nil { 96 + return nil, lerr 97 + } 98 + if len(list.Contents) > 0 || len(list.CommonPrefixes) > 0 { 99 + return newS3DirFile(p), nil 100 + } 101 + return nil, &os.PathError{Op: "open", Path: filename, Err: fs.ErrNotExist} 63 102 64 103 case O_WRONLY: 65 104 return newS3WriteFile(fs3.client, fs3.bucket, p)
+42
internal/s3fs/file.go
··· 8 8 "io/fs" 9 9 "io/ioutil" 10 10 "os" 11 + "syscall" 11 12 12 13 "github.com/aws/aws-sdk-go-v2/service/s3" 13 14 "go.uber.org/atomic" ··· 353 354 func (f *s3MultipartUploadFile) Truncate(size int64) error { 354 355 return ErrTruncateNotSupported 355 356 } 357 + 358 + // s3DirFile is a billy.File handle for a directory in S3. S3 has no real 359 + // directories, but WASI guests (via wazero) open the preopen root by calling 360 + // OpenFile(".", O_RDONLY) and then asking IsDir() — so we return a pseudo-file 361 + // that reports as a directory and rejects byte I/O with EISDIR. 362 + type s3DirFile struct { 363 + name string 364 + closed bool 365 + } 366 + 367 + func newS3DirFile(name string) *s3DirFile { 368 + return &s3DirFile{name: name} 369 + } 370 + 371 + // Name returns the name of the file as presented to Open. 372 + func (f *s3DirFile) Name() string { 373 + return f.name 374 + } 375 + 376 + func (f *s3DirFile) eisdir(op string) error { 377 + return &os.PathError{Op: op, Path: f.name, Err: syscall.EISDIR} 378 + } 379 + 380 + func (f *s3DirFile) Read(p []byte) (int, error) { return 0, f.eisdir("read") } 381 + func (f *s3DirFile) ReadAt(p []byte, off int64) (int, error) { return 0, f.eisdir("read") } 382 + func (f *s3DirFile) Write(p []byte) (int, error) { return 0, f.eisdir("write") } 383 + func (f *s3DirFile) Seek(offset int64, whence int) (int64, error) { 384 + return 0, f.eisdir("seek") 385 + } 386 + func (f *s3DirFile) Truncate(size int64) error { return f.eisdir("truncate") } 387 + 388 + func (f *s3DirFile) Close() error { 389 + if f.closed { 390 + return ErrFileClosed 391 + } 392 + f.closed = true 393 + return nil 394 + } 395 + 396 + func (f *s3DirFile) Lock() error { return ErrLockNotSupported } 397 + func (f *s3DirFile) Unlock() error { return ErrLockNotSupported }