Common Operations Guide
Complete reference for all path operations with strict-path.
This chapter provides comprehensive examples for every operation you’ll need when working with validated paths. Always use dimension-specific methods—never use std::path methods on leaked paths.
Joins
Purpose: Combine a boundary/root with an untrusted segment to create a validated path.
Basic Joins
#![allow(unused)]
fn main() {
use strict_path::{PathBoundary, StrictPath};
fn join_examples(boundary: &PathBoundary) -> std::io::Result<()> {
// Single join
let file = boundary.strict_join("docs/readme.md")?;
// Join with slash or backslash - both work
let file2 = boundary.strict_join("docs\\readme.md")?;
// Multi-segment path
let deep = boundary.strict_join("a/b/c/d/file.txt")?;
Ok(())
}
}
Chained Joins
#![allow(unused)]
fn main() {
use strict_path::{PathBoundary, StrictPath};
fn chained_joins(boundary: &PathBoundary) -> std::io::Result<()> {
// Navigate through directories
let level1 = boundary.strict_join("level1")?;
let level2 = level1.strict_join("level2")?;
let file = level2.strict_join("file.txt")?;
// Or go up and down
let sibling = level2
.strictpath_parent().unwrap()
.strict_join("sibling/file.txt")?;
Ok(())
}
}
Joining Discovered Names
#![allow(unused)]
fn main() {
use strict_path::{PathBoundary, StrictPath};
fn discover_and_join(boundary: &PathBoundary) -> std::io::Result<Vec<StrictPath>> {
let mut files = Vec::new();
// Walk directory
for entry in boundary.read_dir()? {
let entry = entry?;
let name = entry.file_name();
// IMPORTANT: Re-validate each discovered name
let validated = boundary.strict_join(&name.to_string_lossy())?;
files.push(validated);
}
Ok(files)
}
}
Key rules:
- Always validate untrusted segments with
strict_join()orvirtual_join() - Re-validate discovered directory names before using them
- Never use
std::path::Path::join()on untrusted input
Parents and Ancestors
Purpose: Navigate up the directory tree safely.
Getting Parent Directory
#![allow(unused)]
fn main() {
use strict_path::StrictPath;
fn parent_examples(file: &StrictPath) -> std::io::Result<()> {
// Get parent directory
if let Some(parent) = file.strictpath_parent() {
println!("Parent: {}", parent.strictpath_display());
// Create parent if needed
parent.create_dir_all()?;
} else {
println!("At boundary root");
}
Ok(())
}
}
Walking Up to Root
#![allow(unused)]
fn main() {
use strict_path::StrictPath;
fn walk_to_root(file: &StrictPath) {
let mut current = Some(file.clone());
let mut level = 0;
while let Some(path) = current {
println!("Level {}: {}", level, path.strictpath_display());
current = path.strictpath_parent();
level += 1;
}
}
}
Finding Ancestor with Specific Name
#![allow(unused)]
fn main() {
use strict_path::StrictPath;
fn find_ancestor(file: &StrictPath, target_name: &str) -> Option<StrictPath> {
let mut current = Some(file.clone());
while let Some(path) = current {
if path.strictpath_display().to_string().ends_with(target_name) {
return Some(path);
}
current = path.strictpath_parent();
}
None
}
}
Key insight: strictpath_parent() returns None at the boundary root—you can’t escape upward.
File Name and Extension Operations
Purpose: Modify path components while staying within the boundary.
Changing File Names
#![allow(unused)]
fn main() {
use strict_path::StrictPath;
fn filename_operations(file: &StrictPath) -> std::io::Result<()> {
// Change filename (keeps directory and extension)
let renamed = file.strictpath_with_file_name("newname.txt")?;
// Change just the stem (keeps extension)
let new_stem = file.strictpath_with_file_name("report")?
.strictpath_with_extension(
file.strictpath_extension().unwrap_or("txt")
)?;
Ok(())
}
}
Changing Extensions
#![allow(unused)]
fn main() {
use strict_path::StrictPath;
fn extension_operations(file: &StrictPath) -> std::io::Result<()> {
// Change extension
let markdown = file.strictpath_with_extension("md")?;
let json = file.strictpath_with_extension("json")?;
// Remove extension
let no_ext = file.strictpath_with_extension("")?;
// Add extension if missing
let with_ext = if file.strictpath_extension().is_none() {
file.strictpath_with_extension("txt")?
} else {
file.clone()
};
Ok(())
}
}
Combining Operations
#![allow(unused)]
fn main() {
use strict_path::StrictPath;
fn combined_operations(file: &StrictPath) -> std::io::Result<()> {
// Change both filename and extension
let transformed = file
.strictpath_with_file_name("report")?
.strictpath_with_extension("pdf")?;
// Add timestamp to filename
let timestamp = "2025-10-14";
let current_name = file.strictpath_file_stem().unwrap_or("file");
let timestamped = file.strictpath_with_file_name(
format!("{}_{}", current_name, timestamp)
)?.strictpath_with_extension(
file.strictpath_extension().unwrap_or("txt")
)?;
Ok(())
}
}
Rename and Move Operations
Purpose: Move files/directories while staying within the boundary.
Simple Rename (Same Directory)
#![allow(unused)]
fn main() {
use strict_path::{PathBoundary, StrictPath};
fn simple_rename(boundary: &PathBoundary) -> std::io::Result<()> {
let current = boundary.strict_join("logs/app.log")?;
current.write(b"log data")?;
// Rename returns the new path
let renamed = current.strict_rename("logs/app.old")?;
assert!(renamed.exists());
assert!(!current.exists()); // Original path no longer exists
Ok(())
}
}
Move to Different Directory
#![allow(unused)]
fn main() {
use strict_path::{PathBoundary, StrictPath};
fn move_file(boundary: &PathBoundary) -> std::io::Result<()> {
let source = boundary.strict_join("temp/file.txt")?;
source.write(b"data")?;
// Create destination directory first
let dest_dir = boundary.strict_join("archive")?;
dest_dir.create_dir_all()?;
// Move to new directory
let moved = source.strict_rename("archive/file.txt")?;
Ok(())
}
}
Rename with Parent Directory Creation
#![allow(unused)]
fn main() {
use strict_path::PathBoundary;
fn rename_with_mkdir(boundary: &PathBoundary) -> std::io::Result<()> {
let file = boundary.strict_join("data.txt")?;
file.write(b"content")?;
// Rename to path in subdirectory (create if needed)
let new_path = boundary.strict_join("backups/2025/data.txt")?;
if let Some(parent) = new_path.strictpath_parent() {
parent.create_dir_all()?;
}
let renamed = file.strict_rename("backups/2025/data.txt")?;
Ok(())
}
}
Virtual Rename (Clean Paths)
#![allow(unused)]
fn main() {
#[cfg(feature = "virtual-path")]
use strict_path::VirtualRoot;
#[cfg(feature = "virtual-path")]
fn virtual_rename_example(vroot: &VirtualRoot) -> std::io::Result<()> {
let file = vroot.virtual_join("uploads/photo.jpg")?;
file.write(b"image data")?;
// Virtual rename - user sees clean paths
let renamed = file.virtual_rename("uploads/photo_2025.jpg")?;
println!("User sees: {}", renamed.virtualpath_display());
// Output: "/uploads/photo_2025.jpg"
println!("System path: {}", renamed.as_unvirtual().strictpath_display());
// Output: actual filesystem path
Ok(())
}
}
Deletion Operations
Purpose: Remove files and directories safely.
Delete Single File
#![allow(unused)]
fn main() {
use strict_path::PathBoundary;
fn delete_file(boundary: &PathBoundary) -> std::io::Result<()> {
let file = boundary.strict_join("temp/cache.tmp")?;
// Check existence before deleting
if file.exists() {
file.remove_file()?;
}
Ok(())
}
}
Delete Empty Directory
#![allow(unused)]
fn main() {
use strict_path::PathBoundary;
fn delete_empty_dir(boundary: &PathBoundary) -> std::io::Result<()> {
let dir = boundary.strict_join("temp/empty")?;
// Only works if directory is empty
if dir.exists() && dir.metadata()?.is_dir() {
dir.remove_dir()?;
}
Ok(())
}
}
Recursive Directory Deletion
#![allow(unused)]
fn main() {
use strict_path::PathBoundary;
fn delete_directory_tree(boundary: &PathBoundary) -> std::io::Result<()> {
let dir = boundary.strict_join("temp/data")?;
// Removes directory and ALL contents recursively
if dir.exists() {
dir.remove_dir_all()?;
}
Ok(())
}
}
Safe Cleanup with Validation
#![allow(unused)]
fn main() {
use strict_path::PathBoundary;
fn safe_cleanup(boundary: &PathBoundary, path: &str) -> std::io::Result<()> {
// Validate path first
match boundary.strict_join(path) {
Ok(safe_path) => {
if safe_path.exists() {
if safe_path.metadata()?.is_dir() {
safe_path.remove_dir_all()?;
} else {
safe_path.remove_file()?;
}
println!("Deleted: {}", safe_path.strictpath_display());
}
Ok(())
},
Err(e) => {
eprintln!("🚨 Invalid path, refusing to delete: {e}");
Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, e))
}
}
}
}
Safety note: Always validate paths before deletion. Never delete based on untrusted input without validation.
Metadata Inspection
Purpose: Query file/directory properties without reading contents.
Basic Metadata
#![allow(unused)]
fn main() {
use strict_path::StrictPath;
use std::time::SystemTime;
fn inspect_metadata(file: &StrictPath) -> std::io::Result<()> {
let meta = file.metadata()?;
// File type checks
println!("Is file: {}", meta.is_file());
println!("Is directory: {}", meta.is_dir());
println!("Is symlink: {}", meta.file_type().is_symlink());
// Size and permissions
println!("Size: {} bytes", meta.len());
println!("Read-only: {}", meta.permissions().readonly());
// Timestamps
if let Ok(modified) = meta.modified() {
let duration = SystemTime::now().duration_since(modified).unwrap();
println!("Modified {} seconds ago", duration.as_secs());
}
Ok(())
}
}
Conditional Operations Based on Metadata
#![allow(unused)]
fn main() {
use strict_path::StrictPath;
fn cleanup_empty_files(file: &StrictPath) -> std::io::Result<()> {
let meta = file.metadata()?;
if meta.is_file() && meta.len() == 0 {
println!("Empty file, removing: {}", file.strictpath_display());
file.remove_file()?;
}
Ok(())
}
}
Finding Files by Criteria
#![allow(unused)]
fn main() {
use strict_path::{PathBoundary, StrictPath};
fn find_large_files(boundary: &PathBoundary, min_size: u64) -> std::io::Result<Vec<StrictPath>> {
let mut large_files = Vec::new();
for entry in boundary.read_dir()? {
let entry = entry?;
let name = entry.file_name();
let path = boundary.strict_join(&name.to_string_lossy())?;
if let Ok(meta) = path.metadata() {
if meta.is_file() && meta.len() > min_size {
large_files.push(path);
}
}
}
Ok(large_files)
}
}
Copy Operations
Purpose: Duplicate files while preserving validation.
Simple Copy
#![allow(unused)]
fn main() {
use strict_path::PathBoundary;
fn simple_copy(boundary: &PathBoundary) -> std::io::Result<()> {
let source = boundary.strict_join("docs/original.txt")?;
let dest = boundary.strict_join("docs/copy.txt")?;
// Returns number of bytes copied
let bytes_copied = source.copy(&dest)?;
println!("Copied {bytes_copied} bytes");
Ok(())
}
}
Copy with Overwrite Protection
#![allow(unused)]
fn main() {
use strict_path::PathBoundary;
fn copy_if_not_exists(boundary: &PathBoundary) -> std::io::Result<()> {
let source = boundary.strict_join("docs/original.txt")?;
let dest = boundary.strict_join("docs/backup.txt")?;
if !dest.exists() {
source.copy(&dest)?;
println!("Copied to {}", dest.strictpath_display());
} else {
println!("Destination already exists, skipping");
}
Ok(())
}
}
Copy to Different Directory
#![allow(unused)]
fn main() {
use strict_path::PathBoundary;
fn copy_to_archive(boundary: &PathBoundary) -> std::io::Result<()> {
let source = boundary.strict_join("docs/report.pdf")?;
// Create backup directory
let backup_dir = boundary.strict_join("backups/2025")?;
backup_dir.create_dir_all()?;
// Copy to backup location
let dest = boundary.strict_join("backups/2025/report.pdf")?;
source.copy(&dest)?;
Ok(())
}
}
Comprehensive Example: File Management
Putting it all together—a complete file management function:
#![allow(unused)]
fn main() {
use strict_path::{PathBoundary, StrictPath};
use std::time::{SystemTime, Duration};
fn manage_user_file(
uploads_dir: &PathBoundary,
filename: &str
) -> Result<FileInfo, Box<dyn std::error::Error>> {
// 1. Validate path
let file = uploads_dir.strict_join(filename)?;
// 2. Check existence
if !file.exists() {
return Err("File not found".into());
}
// 3. Get metadata
let meta = file.metadata()?;
// 4. Archive old files
if should_archive(&meta)? {
let archive_dir = uploads_dir.strict_join("archive")?;
archive_dir.create_dir_all()?;
let archived = file.strict_rename(&format!("archive/{filename}"))?;
// 5. Compress large files
if meta.len() > 10_000_000 {
compress_file(&archived)?;
}
return Ok(FileInfo {
path: archived.strictpath_display().to_string(),
status: FileStatus::Archived,
size: meta.len(),
});
}
Ok(FileInfo {
path: file.strictpath_display().to_string(),
status: FileStatus::Active,
size: meta.len(),
})
}
fn should_archive(meta: &std::fs::Metadata) -> std::io::Result<bool> {
let modified = meta.modified()?;
let age = SystemTime::now().duration_since(modified)
.unwrap_or(Duration::ZERO);
Ok(age > Duration::from_secs(30 * 24 * 60 * 60)) // 30 days
}
fn compress_file(_file: &StrictPath) -> std::io::Result<()> {
// Compression implementation
Ok(())
}
#[derive(Debug)]
struct FileInfo {
path: String,
status: FileStatus,
size: u64,
}
#[derive(Debug)]
enum FileStatus {
Active,
Archived,
}
}
Key Principles
Always use dimension-specific methods:
- Use
strict_join()/virtual_join()for joins - Use
strictpath_parent()/virtualpath_parent()for parents - Use
strictpath_with_*()/virtualpath_with_*()for modifications - Never use
std::pathmethods on leaked paths
Handle errors explicitly:
- Path operations can fail (permissions, disk full, invalid paths)
- Use
?operator or explicitmatchfor error handling - Log security incidents when paths escape boundaries
Check before destructive operations:
- Use
.exists()before deletion - Use
.metadata()to check file vs. directory - Create parent directories with
.create_dir_all()before moves
Validate discovered paths:
- Re-validate directory entries with
strict_join()/virtual_join() - Don’t trust filesystem listings—validate before use
Learn More
- Best Practices Overview → - Core guidelines and decision matrices
- Real-World Patterns → - Production-ready examples
- Policy & Reuse → - When to use VirtualRoot/PathBoundary
- Authorization Patterns → - Compile-time authorization