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"