Summary
Any string-source compile / check / lint whose base directory resolves to a filesystem root fails with mds::io:
cannot resolve path /: file not found: /
Found by the new Alpine load test in PR #370 (#340): inside docker run node:22-alpine node /w/probe.cjs … the musl addon dlopened correctly and require.resolve landed on the right binary, but compile('Hello {{n}}!', { vars: { n: 'alpine' } }) threw. The container has no WORKDIR, so process.cwd() is /.
Trigger
The base directory is a filesystem root, either implicitly (process.cwd() / std::env::current_dir() == / — e.g. a Docker container with no WORKDIR) or explicitly (basePath: "/"). Reproduced on macOS with the prebuilt darwin addon and with the prebuilt CLI; no musl, read-only mount, bind mount, symlink or realpath involvement — it is pure std::path logic evaluated before any I/O, so Windows drive roots (C:\) hit it too.
cwd=/ compile -> THREW cannot resolve path /: file not found: / (code mds::io)
cwd=/Users/dean/Sandbox/mdl compile -> {"kind":"markdown","output":"Hello x!\n",...}
cwd=/private/tmp compile -> OK
basePath "/tmp" from cwd / -> OK
basePath "/" from cwd / -> THREW (same error)
check / lint from cwd / -> THREW (same error)
$ cd / && echo 'Hello {{n}}!' | mds build - --set n=x
mds::io
× cannot resolve path /: file not found: /
A read-only cwd is fine; a direct child of / is fine. Only a root fails.
Root cause
crates/mds-core/src/lib.rs:456-462 (resolve_base_dir): the None arm returns std::env::current_dir() verbatim (/).
crates/mds-core/src/resolver.rs:626 and :652 pass it to self.fs.canonicalize(base_dir).
crates/mds-core/src/fs.rs:538-556 (NativeFs::canonicalize) delegates to check_symlink, whose first statement in check_symlink_named (crates/mds-core/src/fs.rs:408-410) is
let file_name = path.file_name().ok_or_else(|| MdsError::file_not_found(shown.to_string()))?;
Path::new("/").file_name() is None, so it returns FileNotFound("/") before any syscall; fs.rs:549-553 re-wraps it as mds::io → cannot resolve path /: file not found: /.
effective_parent (fs.rs:310-316) is never reached; its doc covers the PF-006 bare-name case but not a root.
Blast radius
Affected (all reach resolve_base_dir → NativeFs::canonicalize):
@mdscript/mds-napi string compile / check / lint — crates/mds-napi/src/lib.rs:681, :749, :984 (reproduced).
- CLI stdin paths
mds build -, mds check -, mds lint - — crates/mds-cli/src/build.rs:804, :1268, crates/mds-cli/src/main.rs:294, crates/mds-cli/src/lint.rs:844 (reproduced). File-path compiles are unaffected (a file path has a file_name()).
- Python
markdown_script.compile / check / lint — crates/mds-python/src/lib.rs:1407, :1493, :1560 (same call site; not executed locally).
@mdscript/mds on the native backend (forwards no basePath by default; packages/mds/src/types.ts:114 documents the cwd default).
Not affected: WASM (compile_virtual_with_deps_opts, VirtualFs) and every compile_virtual* entry point.
No existing test covers a root base directory: crates/mds-napi/__test__/index.spec.mjs:215 only chdirs to FIXTURES; crates/mds-core/src/fs.rs:1345-1372 covers bare / ./ / subdir / absolute paths but never a root; the '/some/path' uses in the napi spec are rejection tests that never reach the fs.
Suggested fix
Handle the root case in check_symlink_named (or in NativeFs::canonicalize, since base-dir resolution is a directory resolve, not an import resolve): when path.parent() is None, a root can never itself be a symlink, so std::fs::canonicalize(path) plus an existence / is_dir check is sufficient and the file_name() split is unnecessary. Tests: NativeFs::canonicalize("/") succeeds; a napi and a Python string compile with basePath: "/" succeed; a CLI stdin build with cwd / succeeds (guard the CLI test for Windows drive roots).
Interim
PR #370's Alpine load tests run the container with -w /w (the read-only fixture directory), which is the shape a real user gets with any non-root working directory; the probe asserts its cwd so a future removal of -w fails loudly instead of resurfacing this error.
Related
Summary
Any string-source
compile/check/lintwhose base directory resolves to a filesystem root fails withmds::io:Found by the new Alpine load test in PR #370 (#340): inside
docker run node:22-alpine node /w/probe.cjs …the musl addon dlopened correctly andrequire.resolvelanded on the right binary, butcompile('Hello {{n}}!', { vars: { n: 'alpine' } })threw. The container has noWORKDIR, soprocess.cwd()is/.Trigger
The base directory is a filesystem root, either implicitly (
process.cwd()/std::env::current_dir()==/— e.g. a Docker container with noWORKDIR) or explicitly (basePath: "/"). Reproduced on macOS with the prebuilt darwin addon and with the prebuilt CLI; no musl, read-only mount, bind mount, symlink orrealpathinvolvement — it is purestd::pathlogic evaluated before any I/O, so Windows drive roots (C:\) hit it too.A read-only cwd is fine; a direct child of
/is fine. Only a root fails.Root cause
crates/mds-core/src/lib.rs:456-462(resolve_base_dir): theNonearm returnsstd::env::current_dir()verbatim (/).crates/mds-core/src/resolver.rs:626and:652pass it toself.fs.canonicalize(base_dir).crates/mds-core/src/fs.rs:538-556(NativeFs::canonicalize) delegates tocheck_symlink, whose first statement incheck_symlink_named(crates/mds-core/src/fs.rs:408-410) isPath::new("/").file_name()isNone, so it returnsFileNotFound("/")before any syscall;fs.rs:549-553re-wraps it asmds::io→cannot resolve path /: file not found: /.effective_parent(fs.rs:310-316) is never reached; its doc covers the PF-006 bare-name case but not a root.Blast radius
Affected (all reach
resolve_base_dir→NativeFs::canonicalize):@mdscript/mds-napistringcompile/check/lint—crates/mds-napi/src/lib.rs:681,:749,:984(reproduced).mds build -,mds check -,mds lint -—crates/mds-cli/src/build.rs:804,:1268,crates/mds-cli/src/main.rs:294,crates/mds-cli/src/lint.rs:844(reproduced). File-path compiles are unaffected (a file path has afile_name()).markdown_script.compile/check/lint—crates/mds-python/src/lib.rs:1407,:1493,:1560(same call site; not executed locally).@mdscript/mdson the native backend (forwards nobasePathby default;packages/mds/src/types.ts:114documents the cwd default).Not affected: WASM (
compile_virtual_with_deps_opts,VirtualFs) and everycompile_virtual*entry point.No existing test covers a root base directory:
crates/mds-napi/__test__/index.spec.mjs:215only chdirs toFIXTURES;crates/mds-core/src/fs.rs:1345-1372covers bare /.// subdir / absolute paths but never a root; the'/some/path'uses in the napi spec are rejection tests that never reach the fs.Suggested fix
Handle the root case in
check_symlink_named(or inNativeFs::canonicalize, since base-dir resolution is a directory resolve, not an import resolve): whenpath.parent()isNone, a root can never itself be a symlink, sostd::fs::canonicalize(path)plus an existence /is_dircheck is sufficient and thefile_name()split is unnecessary. Tests:NativeFs::canonicalize("/")succeeds; a napi and a Python string compile withbasePath: "/"succeed; a CLI stdin build with cwd/succeeds (guard the CLI test for Windows drive roots).Interim
PR #370's Alpine load tests run the container with
-w /w(the read-only fixture directory), which is the shape a real user gets with any non-root working directory; the probe asserts its cwd so a future removal of-wfails loudly instead of resurfacing this error.Related