BurntSushi / BurntSushi/same-file

`Handle` always fails on WASI targets

Open
#68 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
Rust
Stars
124
Forks
26
PR merge metrics
No merged PRs in 30d

Description

On `wasm32-wasip1` (and the other `target_os = "wasi"` targets) `same-file` compiles into the `unknown` module, so `Handle::from_path` and `Handle::from_file` always return `same-file is not supported on this platform.`

The effect that reaches users is through `walkdir`. With `follow_links(true)`, `walkdir` calls `Handle::from_path` for its symlink loop check, so on WASI every directory symlink errors and the whole subtree behind it is dropped. We hit this in rolldown, whose wasm build walks a project tree for `import.meta.glob`: the native binding matches the files behind a directory symlink, the wasm binding matches none (rolldown/rolldown#10609).

WASI's file API has the same shape as the unix one, and `std::os::fd::{AsRawFd, FromRawFd, IntoRawFd, RawFd}` is stable on WASI, so `src/unix.rs` can serve both. The one piece std does not provide is `dev`/`ino`: `std::os::wasi::fs::MetadataExt` is still unstable (`wasi_ext`, rust-lang/rust#71213) as of Rust 1.97.1.

## Proposal

Read the two numbers through `libc` on WASI, and let WASI share the existing `unix` module.

```toml
[target.'cfg(target_os = "wasi")'.dependencies]
libc = "0.2"
```

```rust
#[cfg(unix)]
fn file_ident(file: &File) -> io::Result<(u64, u64)> {
let md = file.metadata()?;
Ok((md.dev(), md.ino()))
}

/// WASI exposes `dev` and `ino` on `std::fs::Metadata` only through the
/// still-unstable `std::os::wasi::fs::MetadataExt`, so read the same values
/// from `fstat` instead.
#[cfg(target_os = "wasi")]
fn file_ident(file: &File) -> io::Result<(u64, u64)> {
// SAFETY: `fstat` writes into a fully owned `stat` and the fd is kept
// alive by the `file` borrow for the duration of the call.
unsafe {
let mut st: libc::stat = std::mem::zeroed();
if libc::fstat(file.as_raw_fd(), &mut st) != 0 {
return Err(io::Error::last_os_error());
}
Ok((st.st_dev, st.st_ino))
}
}
```

The rest is `cfg` only: `unix` becomes `any(unix, target_os = "wasi")` for the `imp` selection, the `mod` declarations and the `dev()`/`ino()` accessors, `unknown` loses WASI, and the two `std::os::unix` imports in `unix.rs` gain WASI counterparts. `from_file` calls `file_ident` instead of reading the metadata inline. About 30 lines in total, and nothing changes for unix or windows.

`libc` is a new dependency, which is why I am opening an issue first. Windows already pulls `winapi-util`, so a target-gated dependency has precedent here, and this one only covers `target_os = "wasi"`.

## Alternative without a new dependency

preview1 exposes `fd_filestat_get`, and `dev` and `ino` are the first two `u64`s of `filestat`, so the same values can be read through a small `extern` block and no dependency at all. That version only covers `target_env = "p1"`, so `wasm32-wasip2` would keep falling back to `unknown`. I have it working as well and can send it instead if you would rather not take `libc`.

## What I verified

- Builds: `wasm32-wasip1`, `wasm32-wasip1-threads`, `wasm32-wasip2`, and the host target. `cargo test` and `cargo fmt --check` are unchanged on the host.
- Runtime: a probe built for `wasm32-wasip1` and run under Node 24's `node:wasi`, over a tree containing a directory symlink and a symlink loop. Two paths to the same directory compare equal, different directories compare unequal, and a directory symlink compares equal to its target. `walkdir` with `follow_links(true)` then yields exactly the entries and the `File system loop found` error that the same tree yields natively. Without the change the same probe drops both symlinked subtrees.
- Same probe under `@tybys/wasm-util`, the WASI shim npm packages use in the browser, with the same result.

MSRV: on WASI this needs a newer Rust than the declared 1.60, because of `std::os::fd`. Other targets are unaffected. I have not pinned the exact floor yet, and can do that or use a different import if you want to keep a single MSRV.

## Unrelated but adjacent

`master` no longer compiles on unsupported platforms at all: `unknown.rs` uses `unreachable!(ERROR_MESSAGE)`, which edition 2021 rejects with "format argument must be a string literal". 1.0.6 is edition 2018 so it still builds; a release cut from `master` today would turn `wasm32-unknown-unknown` and friends from a runtime error into a build failure. `unreachable!("{}", ERROR_MESSAGE)` fixes it. I can fold that into the same PR or keep it separate.

Happy to send a PR — just say which of the two implementations you would rather take.

Contributor guide

No contributing guide indexed for this repository

Research direction

Start with Cargo.toml and src/unix.rs, then inspect the cfg module selection, declarations, and accessors described in the issue; also review the adjacent unknown.rs concern. Run cargo test and cargo fmt --check on the host, then build the listed WASI targets and verify the probe's file-identity and symlink-loop behavior; settle the dependency approach and WASI MSRV before considering it done.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust, wasm
Domain
operating-systems
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.