Skip to content

compile()/check()/lint() on a string fail when the base directory is a filesystem root ("cannot resolve path /: file not found: /") #371

Description

@dean0x

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::iocannot 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_dirNativeFs::canonicalize):

  • @mdscript/mds-napi string compile / check / lintcrates/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 / lintcrates/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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingcliCLI commands and options

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions