File System Operations

Cross-Platform File Handling

Writing cross-platform file handling code in Rust ensures that path construction, line endings, file locking/deletion semantics, and symbolic links behave consistently across Linux, macOS, and Windows.

Major Filesystem Differences Across Unix and Windows

Filesystem Behavior Unix (Linux, macOS, FreeBSD) Windows (NTFS) Portable Rust Practice
Path separator Forward slash / (MAIN_SEPARATOR == '/') Backslash \ (MAIN_SEPARATOR == '\\', also accepts /) Always build paths with Path::join or PathBuf::push, never hardcoded format!("{a}/{b}")
Line endings \n (0x0A) \r\n (0x0D 0x0A) Use .lines() or .trim_end_matches(&['\r', '\n'][..]) when reading text
Deleting or renaming an open file Allowed while File is still open (inode stays alive until last fd closes) Forbidden by default unless shared-delete is enabled or File is closed first! Always drop(file) before calling fs::remove_file or fs::rename on that path
Case sensitivity Case-sensitive on Linux (ext4); case-insensitive by default on macOS (APFS) Case-insensitive by default Never rely on "Config.toml" and "config.toml" being two distinct files
Reserved filenames Only / and \0 are forbidden CON, PRN, AUX, NUL, COM1..COM9, and characters < > : " | ? * are forbidden Sanitize user-supplied filenames before creating files
fs::canonicalize output Standard absolute path "/Users/..." Extended-length UNC prefix "\\\\?\\C:\\Users\\..." Avoid passing canonicalize() paths to legacy Windows tools that reject \\?\ prefixes

Portable Path Construction and Normalization

Instead of splitting strings on '/' or concatenating with format!, use std::path::MAIN_SEPARATOR, Path::components(), and Path::join() so paths work identically on all platforms:

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

/// Joins path segments portably and normalizes `.` and `..` lexically without
/// touching the filesystem or producing Windows `\\?\` UNC prefixes.
fn normalize_lexical(path: &Path) -> PathBuf {
    use std::path::Component;
    let mut out = PathBuf::new();

    for comp in path.components() {
        match comp {
            Component::CurDir => {}
            Component::ParentDir => {
                out.pop();
            }
            other => out.push(other.as_os_str()),
        }
    }
    out
}

fn main() {
    let raw = Path::new("workspace/./crates/../src/main.rs");
    let normalized = normalize_lexical(raw);

    println!(
        "platform separator: {:?}, normalized components: {:?}",
        std::path::MAIN_SEPARATOR,
        normalized.iter().collect::<Vec<_>>()
    );
}
platform separator: '/', normalized components: ["workspace", "src", "main.rs"]

Creating Symbolic Links Across Platforms (os::unix::fs::symlink vs. os::windows::fs)

Because Windows distinguishes between file symlinks (symlink_file) and directory symlinks (symlink_dir) at the OS syscall level—and requires Developer Mode or Administrator privileges on older Windows configurations—symbolic link creation is placed in platform-specific modules rather than a single std::fs::symlink function:

use std::fs;
use std::io;
use std::path::Path;

pub fn create_file_symlink(original: &Path, link: &Path) -> io::Result<()> {
    #[cfg(unix)]
    {
        std::os::unix::fs::symlink(original, link)
    }
    #[cfg(windows)]
    {
        std::os::windows::fs::symlink_file(original, link)
    }
}

fn main() -> io::Result<()> {
    let dir = std::env::temp_dir();
    let target = dir.join("rust_ch50_symlink_target.txt");
    let link = dir.join("rust_ch50_symlink_link.txt");
    let _ = fs::remove_file(&link);

    fs::write(&target, "target payload\n")?;
    create_file_symlink(&target, &link)?;

    // `fs::read_link` reads the path stored inside the symbolic link on all platforms:
    let pointed_to = fs::read_link(&link)?;
    println!(
        "link points to target: {}, content via link: {:?}",
        pointed_to == target,
        fs::read_to_string(&link)?.trim()
    );

    fs::remove_file(&link)?;
    fs::remove_file(&target)?;
    Ok(())
}
link points to target: true, content via link: "target payload"