File System Operations

Path Manipulation

Path and PathBuf provide platform-aware methods for decomposing paths into components (parent, file_name, file_stem, extension) and constructing new paths (join, push, with_file_name, with_extension) using the target platform's path separator (/ on Unix, \ on Windows).

Decomposing a Path into Its Parts

Path:  `/Users/prime/Developer/rust/archive.tar.gz`
        |<------------ parent() ----------->|
                                    |<-- file_name() -->|
                                    |<- file_stem() ->| |<- extension() ->|
       `/Users/prime/Developer/rust` `archive.tar.gz`   `archive.tar`     `gz`
Decomposition Method Result on "/dir/archive.tar.gz" Result on "/dir/.gitignore"
.parent() Some("/dir") Some("/dir")
.file_name() Some("archive.tar.gz") Some(".gitignore")
.file_stem() Some("archive.tar") Some(".gitignore")
.extension() Some("gz") None (leading dot alone is not an extension!)
.components() [RootDir, Normal("dir"), Normal("archive.tar.gz")] [RootDir, Normal("dir"), Normal(".gitignore")]

Constructing and Modifying Paths (join, push, with_extension)

Borrowed &Path Method (Returns New PathBuf) Mutating PathBuf Method (Modifies in Place) Behavior
path.join(child) path_buf.push(child) Appends child with a platform separator (Important: if child is an absolute path like "/etc", it replaces the entire existing path!)
path.parent() path_buf.pop() Truncates path_buf to its parent directory
path.with_file_name(name) path_buf.set_file_name(name) Replaces the final filename component
path.with_extension(ext) path_buf.set_extension(ext) Replaces (or adds) the file extension (pass "" to remove extension)
use std::path::{Component, Path, PathBuf};

fn main() {
    let mut path = PathBuf::from("/workspace/project/src/main.rs");

    println!("parent:     {:?}", path.parent());
    println!("file_name:  {:?}", path.file_name());
    println!("file_stem:  {:?}", path.file_stem());
    println!("extension:  {:?}", path.extension());

    // Create sibling paths non-destructively via `.with_extension` and `.with_file_name`:
    let object_file = path.with_extension("o");
    let lib_file = path.with_file_name("lib.rs");
    println!("object_file: {}", object_file.display());
    println!("lib_file:    {}", lib_file.display());

    // Inspect structured `Component` variants:
    let comps: Vec<Component<'_>> = Path::new("../src/./lib.rs").components().collect();
    println!("components of '../src/./lib.rs': {:?}", comps);

    // Watch out for the absolute-path replacement rule on `.push()` / `.join()`:
    path.push("/etc/hosts");
    println!("after pushing absolute '/etc/hosts': {}", path.display());
}
parent:     Some("/workspace/project/src")
file_name:  Some("main.rs")
file_stem:  Some("main")
extension:  Some("rs")
object_file: /workspace/project/src/main.o
lib_file:    /workspace/project/src/lib.rs
components of '../src/./lib.rs': [ParentDir, Normal("src"), CurDir, Normal("lib.rs")]
after pushing absolute '/etc/hosts': /etc/hosts

Security Trap: .join(untrusted_input) and Absolute / .. Path Traversal

Because base_dir.join("/etc/passwd") returns "/etc/passwd" (completely discarding base_dir!) and base_dir.join("../../secret") traverses above base_dir, never pass untrusted user input directly to .join() or .push() without first verifying that every component in untrusted_path.components() is Component::Normal(_):

use std::path::{Component, Path, PathBuf};

fn safe_subpath_join(base: &Path, untrusted_rel: &Path) -> Option<PathBuf> {
    // Reject absolute paths, `..` (`ParentDir`), and drive prefixes:
    if untrusted_rel
        .components()
        .all(|c| matches!(c, Component::Normal(_)))
    {
        Some(base.join(untrusted_rel))
    } else {
        None
    }
}

fn main() {
    let base = Path::new("/srv/www/public");
    println!(
        "safe 'assets/logo.png': {:?}",
        safe_subpath_join(base, Path::new("assets/logo.png"))
    );
    println!(
        "blocked '/etc/passwd':  {:?}",
        safe_subpath_join(base, Path::new("/etc/passwd"))
    );
    println!(
        "blocked '../secret':    {:?}",
        safe_subpath_join(base, Path::new("../secret"))
    );
}
safe 'assets/logo.png': Some("/srv/www/public/assets/logo.png")
blocked '/etc/passwd':  None
blocked '../secret':    None