rokdocs
v0.1

3.8. path:: - UTF-8 Path (Python-style)

The rok::std::path module defines path representation. The key point is that it puts a UTF-8 newtype first, to avoid the ergonomic burden of Rust's split between Path and OsStr while adopting the intuitiveness of Python's pathlib.Path.

3.8.1. Two types - Path / RawPath

rok's path representation is split into two types.

TypeMeaningFrequency of use
PathUTF-8 path (95%+)Default for ordinary code
RawPathOS native bytes (Unix bytes / Windows WTF-16)1% (non-UTF-8 file names)

Most code needs only Path. RawPath is needed only where legacy bytes are truly required, such as when dealing with old unix systems.

3.8.2. Path - UTF-8

Path is an immutable value. join, / and with_* all return a new Path; there is no PathBuf / push that modifies a path in place. p /= q is p = p / q under the ordinary compound assignment rule.

The Path interface is as follows.

struct Path { /* UTF-8 str newtype, GC heap */ }

impl Path {
    fn new() -> Path;
    fn from(s: str) -> Path;
    fn from_components<I: Iterator<Item = str>>(components: I) -> Path;

    // decomposition
    fn parent(self) -> Path | none;
    fn file_name(self) -> str | none;
    fn file_stem(self) -> str | none;
    fn extension(self) -> str | none;
    fn components(self) -> Components;   // implements Iterator<Item = Component>
    fn ancestors(self) -> Ancestors;     // implements Iterator<Item = Path>

    // manipulation
    fn join(self, other: Path) -> Path;
    fn with_file_name(self, name: str) -> Path;
    fn with_extension(self, ext: str) -> Path;
    fn strip_prefix(self, base: Path) throws (StripPrefixError) -> Path;

    fn is_absolute(self) -> bool;
    fn is_relative(self) -> bool;
    fn has_root(self) -> bool;
    fn starts_with(self, base: Path) -> bool;
    fn ends_with(self, suffix: Path) -> bool;
    fn normalize(self) -> Path;             // resolve "." / ".." (lexical)

    // OS interaction (convenience - fs::* internally)
    fn exists(self) -> bool;
    fn is_file(self) -> bool;
    fn is_dir(self) -> bool;
    fn is_symlink(self) -> bool;
    fn metadata(self) throws (IoError) -> Metadata;
    fn canonicalize(self) throws (IoError) -> Path;     // realpath (verified against the filesystem)

    fn as_str(self) -> str;
    fn to_raw(self) -> RawPath;
}

enum Component {
    RootDir,                    // "/"
    Prefix(str),                // "C:" (Windows)
    CurDir,                     // "."
    ParentDir,                  // ".."
    Normal(str),                // "data"
}

3.8.3. The / operator - Python pathlib style

The / operator is introduced to make path joining more readable.

impl Div<str> for Path {
    type Output = Path;
    #[infix("/")]
    fn div(self, rhs: str) -> Path { self.join(Path::from(rhs)) }
}

impl Div<Path> for Path { ... }

p /= "x" and p /= q (q: Path) are also allowed and mean p = p / ...

The effect is clear from the following example.

let p = Path::from("/etc") / "nginx" / "nginx.conf";
                                          //→ Path("/etc/nginx/nginx.conf")

let home = env::home_dir()? / ".config" / "myapp" / "settings.toml";

This provides the same ergonomics as Python's Path("/etc") / "nginx" / "nginx.conf", and avoids the verbosity of Rust's PathBuf::join and push.

3.8.3a. AsPath - path arguments

trait AsPath {
    fn as_path(self) -> Path;
}
impl AsPath for str { ... }
impl AsPath for Path { ... }

Path arguments of fs are taken as AsPath, so both str and Path can be passed (fs::read("a.txt"), fs::read(dir / "a.txt")).

3.8.4. UTF-8 invariant

Path is always UTF-8. At Path::from(s), s is already UTF-8, so no separate validation is needed.

There are rare situations, however, where the OS returns non-UTF-8 file names.

  • On Unix, arbitrary bytes are possible, so Path::from(...) performs a lossy conversion (replacement char U+FFFD).
  • On Windows, names are WTF-16, and surrogate pairs are handled separately.

When a strict conversion is needed, write:

let raw: RawPath = ...;
let p: Path = try Path::try_from(raw);    // throws NonUtf8Error if not UTF-8
let q: Path = try raw.try_into();          // same

This conversion is impl TryFrom<RawPath> for Path { type Error = NonUtf8Error; ... } (2.11.4).

3.8.5. RawPath - bytes / WTF-16

RawPath exists for places that need the OS's native representation as is.

struct RawPath { /* unix: Vec<u8>, windows: Vec<u16> */ }

impl RawPath {
    fn from_bytes(bytes: List<u8>) -> RawPath;        // unix
    fn from_wide(wide: List<u16>) -> RawPath;          // windows

    fn as_bytes(self) -> Slice<u8>;                    // unix only
    fn as_wide(self) -> Slice<u16>;                    // windows only

    fn try_to_path(self) throws (NonUtf8Error) -> Path;
    fn to_path_lossy(self) -> Path;
}

On Windows, NonUtf8Error.valid_up_to counts UTF-16 units. to_path_lossy replaces each maximal ill-formed subsequence (Unix) / each lone surrogate (Windows) with one U+FFFD.

Most code never encounters this type. It appears only where code exchanges syscalls directly with the OS.

3.8.6. canonicalize vs. normalize

These two functions look similar but differ clearly in behavior and cost.

FunctionMeaningFile system access
normalize()Purely lexical cleanup (resolves "..")No
canonicalize()Resolves symlinks + converts to an absolute pathYes (realpath)
Path::from("/a/b/../c").normalize();              //→ "/a/c" (lexical)
try Path::from("/a/b/../c").canonicalize();       //→ absolute path with symlinks resolved

normalize is fast and makes no syscalls, so it does the job in 90% of places.

3.8.7. WASI compatibility

Path also integrates naturally with WASI Preview 2's capability paths.

let dir: Dir = try Dir::root().open_dir("data");
let f = try dir.open(Path::from("config.toml"));
                                          // Path is relative to dir - escape rejected

The capability model is covered in detail in 3.1 fs.

3.8.8. Comparison with other languages

To pin down the design decision, compare with neighboring languages.

AspectRustPython pathlibGo filepathrok
TypePath / PathBufPathstringPath
owned vs viewborrowed &Path / ownedimmutableimmutableimmutable (/ gives a new Path)
/ operatornone (explicit .join())/none/
non-UTF-8OsStr (mixed)bytes (separate)string (confusing)separate RawPath
canonicalize.canonicalize().resolve()filepath.Abs.canonicalize()
Method chainingOK (mostly)excellentOKexcellent

In summary, rok's biggest decision is the combination of a UTF-8 default and the / operator, which brings Python pathlib's ergonomics together with Rust-level performance.

Edit this page on GitHub