Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Daily Usage Patterns

Common workflows and patterns for everyday strict-path usage.

Pattern 1: Validate User Input

Problem: User provides a filename, you need to ensure it stays in your directory.

#![allow(unused)]
fn main() {
use strict_path::PathBoundary;

fn handle_user_request(user_filename: &str) -> Result<String, Box<dyn std::error::Error>> {
    let uploads = PathBoundary::try_new("/var/uploads")?;
    
    // Validate the user input
    let safe_path = uploads.strict_join(user_filename)?;
    
    // Now safe to use
    let contents = safe_path.read_to_string()?;
    Ok(contents)
}

// ✅ Safe: "../etc/passwd" gets rejected
// ✅ Safe: "documents/report.pdf" gets validated
}

Pattern 2: Configuration Loading

Problem: Load config files from multiple standard locations.

#![allow(unused)]
fn main() {
use strict_path::PathBoundary;
use std::path::PathBuf;

fn find_config() -> Result<String, Box<dyn std::error::Error>> {
    let config_locations = vec![
        PathBuf::from("/etc/myapp/config.toml"),
        PathBuf::from("/usr/local/etc/myapp/config.toml"),
        dirs::config_dir().unwrap().join("myapp/config.toml"),
    ];
    
    for location in config_locations {
        if let Some(parent) = location.parent() {
            if let Ok(boundary) = PathBoundary::try_new(parent) {
                if let Some(filename) = location.file_name() {
                    if let Ok(config_path) = boundary.strict_join(filename) {
                        if config_path.exists() {
                            return config_path.read_to_string();
                        }
                    }
                }
            }
        }
    }
    
    Err("No config file found".into())
}
}

Pattern 3: Per-User Virtual Filesystem

Problem: Multiple users, each needs their own isolated filesystem view.

#![allow(unused)]
fn main() {
use strict_path::VirtualRoot;

struct UserSession {
    user_id: String,
    root: VirtualRoot<UserSpace>,
}

struct UserSpace;

impl UserSession {
    fn new(user_id: String) -> Result<Self, Box<dyn std::error::Error>> {
        let base_dir = format!("/var/lib/app/users/{}", user_id);
        let root = VirtualRoot::try_new_create(&base_dir)?;
        Ok(Self { user_id, root })
    }
    
    fn read_file(&self, virtual_path: &str) -> Result<String, Box<dyn std::error::Error>> {
        let file = self.root.virtual_join(virtual_path)?;
        Ok(file.read_to_string()?)
    }
    
    fn write_file(&self, virtual_path: &str, contents: &str) -> Result<(), Box<dyn std::error::Error>> {
        let file = self.root.virtual_join(virtual_path)?;
        file.create_parent_dir_all()?;
        file.write(contents)?;
        Ok(())
    }
    
    fn list_files(&self, virtual_dir: &str) -> Result<Vec<String>, Box<dyn std::error::Error>> {
        let dir = self.root.virtual_join(virtual_dir)?;
        let mut files = Vec::new();
        
        for entry in dir.as_unvirtual().read_dir()? {
            let entry = entry?;
            // Re-validate discovered names
            let validated = self.root.virtual_join(format!("{}/{}", virtual_dir, entry.file_name().to_string_lossy()))?;
            files.push(validated.virtualpath_display().to_string());
        }
        
        Ok(files)
    }
}

// Usage
let alice = UserSession::new("alice".to_string())?;
alice.write_file("documents/report.txt", "Quarterly report")?;
let report = alice.read_file("documents/report.txt")?;
}

Pattern 4: Safe Archive Extraction

Problem: Extract ZIP/TAR without directory traversal attacks.

#![allow(unused)]
fn main() {
use strict_path::PathBoundary;
use zip::ZipArchive;
use std::fs::File;

fn safe_extract_zip(
    zip_path: &std::path::Path,
    extract_to: &str
) -> Result<(), Box<dyn std::error::Error>> {
    let extract_dir = PathBoundary::try_new_create(extract_to)?;
    let mut archive = ZipArchive::new(File::open(zip_path)?)?;
    
    for i in 0..archive.len() {
        let mut file = archive.by_index(i)?;
        let filename = file.name();
        
        // Validate each file path before extraction
        let safe_path = match extract_dir.strict_join(filename) {
            Ok(p) => p,
            Err(_) => {
                eprintln!("Skipping malicious path: {}", filename);
                continue; // Skip paths that escape
            }
        };
        
        // Create parent directories
        safe_path.create_parent_dir_all()?;
        
        // Extract to validated path
        let mut outfile = safe_path.create_file()?;
        std::io::copy(&mut file, &mut outfile)?;
    }
    
    Ok(())
}
}

Pattern 5: Temporary File Processing

Problem: Create temp directory for processing, auto-cleanup.

#![allow(unused)]
fn main() {
use strict_path::PathBoundary;

fn process_upload(
    data: &[u8]
) -> Result<ProcessedData, Box<dyn std::error::Error>> {
    // Create temp directory with RAII cleanup
    let temp_dir = PathBoundary::<()>::try_new_temp()?;
    
    // Write input
    let input_file = temp_dir.strict_join("input.dat")?;
    input_file.write_bytes(data)?;
    
    // Process
    let output_file = temp_dir.strict_join("output.dat")?;
    process_data(&input_file, &output_file)?;
    
    // Read result
    let result = output_file.read()?;
    
    // temp_dir dropped here, automatically cleaned up
    Ok(ProcessedData::from_bytes(&result))
}
}

Pattern 6: Chaining Operations

Problem: Multiple sequential operations on paths.

#![allow(unused)]
fn main() {
use strict_path::PathBoundary;

fn backup_and_update_config(
    new_config: &str
) -> Result<(), Box<dyn std::error::Error>> {
    let config_dir = PathBoundary::try_new("/etc/myapp")?;
    
    let config = config_dir.strict_join("config.toml")?;
    let backup = config_dir.strict_join("config.toml.backup")?;
    
    // Chain operations
    config.strict_copy(&backup)?;           // Backup current
    config.write(new_config)?;               // Write new
    
    // Verify
    if config.read_to_string()? == new_config {
        println!("Config updated successfully");
    }
    
    Ok(())
}
}

Pattern 7: Authorization with Markers

Problem: Different users need different access levels.

#![allow(unused)]
fn main() {
use strict_path::{PathBoundary, StrictPath};

struct ReadOnly;
struct ReadWrite;

fn authenticate_user(
    username: &str,
    password: &str
) -> Result<PathBoundary<ReadWrite>, AuthError> {
    // Check credentials
    if verify_credentials(username, password) {
        let user_dir = format!("/var/data/users/{}", username);
        let boundary: PathBoundary<ReadOnly> = PathBoundary::try_new(&user_dir)
            .map_err(|_| AuthError::NoAccess)?;
        
        // Escalate to write access after auth check
        Ok(boundary.change_marker())
    } else {
        Err(AuthError::InvalidCredentials)
    }
}

fn write_user_data(
    boundary: &PathBoundary<ReadWrite>, // Requires write marker
    filename: &str,
    data: &str
) -> Result<(), Box<dyn std::error::Error>> {
    let file = boundary.strict_join(filename)?;
    file.write(data)?;
    Ok(())
}

// ✅ Can only call write_user_data with ReadWrite marker
let rw_boundary = authenticate_user("alice", "secret123")?;
write_user_data(&rw_boundary, "notes.txt", "My notes")?;
}

Pattern 8: Error Handling

Problem: Gracefully handle path validation failures.

#![allow(unused)]
fn main() {
use strict_path::{PathBoundary, StrictPathError};

fn safe_file_access(
    base: &str,
    user_path: &str
) -> Result<String, AppError> {
    let boundary = PathBoundary::try_new(base)
        .map_err(|e| AppError::InvalidBase(e))?;
    
    let file = boundary.strict_join(user_path)
        .map_err(|e| match e {
            StrictPathError::PathEscapesBoundary { attempted, boundary } => {
                AppError::PathEscape { 
                    path: attempted.display().to_string(),
                    reason: "Attempted directory traversal"
                }
            }
            _ => AppError::ValidationFailed(e)
        })?;
    
    file.read_to_string()
        .map_err(|e| AppError::IoError(e))
}

#[derive(Debug)]
enum AppError {
    InvalidBase(StrictPathError),
    PathEscape { path: String, reason: &'static str },
    InvalidPath { reason: String },
    ValidationFailed(StrictPathError),
    IoError(std::io::Error),
}
}

Pattern 9: Database Path Storage

Problem: Store and retrieve validated paths from database.

#![allow(unused)]
fn main() {
use strict_path::{PathBoundary, StrictPath};
use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize)]
struct FileRecord {
    id: u64,
    // Store as string in DB
    relative_path: String,
}

struct FileService {
    boundary: PathBoundary<DataDir>,
}

struct DataDir;

impl FileService {
    fn save_file(&self, name: &str, data: &[u8]) -> Result<FileRecord, Box<dyn std::error::Error>> {
        // Validate before using
        let file = self.boundary.strict_join(name)?;
        file.write_bytes(data)?;
        
        // Store relative path in DB
        Ok(FileRecord {
            id: generate_id(),
            relative_path: name.to_string(),
        })
    }
    
    fn load_file(&self, record: &FileRecord) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
        // Re-validate on load
        let file = self.boundary.strict_join(&record.relative_path)?;
        Ok(file.read()?)
    }
}
}

Pattern 10: Logging and Auditing

Problem: Log file access for compliance.

#![allow(unused)]
fn main() {
use strict_path::StrictPath;
use tracing::{info, warn};

fn audit_read<M>(
    path: &StrictPath<M>,
    user: &str
) -> std::io::Result<String> {
    info!(
        user = user,
        path = %path.strictpath_display(),
        action = "read",
        "File access"
    );
    
    match path.read_to_string() {
        Ok(contents) => {
            info!(
                user = user,
                path = %path.strictpath_display(),
                size = contents.len(),
                "Read successful"
            );
            Ok(contents)
        }
        Err(e) => {
            warn!(
                user = user,
                path = %path.strictpath_display(),
                error = %e,
                "Read failed"
            );
            Err(e)
        }
    }
}
}

Performance Tips

Reuse Boundaries

#![allow(unused)]
fn main() {
// ✅ Good: create boundary once
let boundary = PathBoundary::try_new("/var/data")?;

for filename in filenames {
    let file = boundary.strict_join(filename)?;
    process(file)?;
}

// ❌ Bad: recreating boundary every iteration
for filename in filenames {
    let boundary = PathBoundary::try_new("/var/data")?; // Wasteful!
    let file = boundary.strict_join(filename)?;
    process(file)?;
}
}

Batch Validation

#![allow(unused)]
fn main() {
// Validate all paths upfront
let files: Result<Vec<_>, _> = filenames
    .iter()
    .map(|name| boundary.strict_join(name))
    .collect();

let files = files?; // Single error handling point

// Then process
for file in files {
    process(&file)?;
}
}

Avoid Redundant Checks

#![allow(unused)]
fn main() {
// ❌ Bad: checking twice
if file.exists() {
    file.read_to_string()?;
}

// ✅ Good: let I/O operation handle it
match file.read_to_string() {
    Ok(contents) => { /* use contents */ }
    Err(e) if e.kind() == std::io::ErrorKind::NotFound => { /* handle missing */ }
    Err(e) => return Err(e.into()),
}
}

Summary

  • Always validate user input through strict_join/virtual_join
  • Create boundaries once, reuse for multiple paths
  • Use markers to encode authorization at type level
  • Handle errors gracefully with specific error types
  • Re-validate paths loaded from databases
  • Log file operations for audit trails
  • Prefer builtin I/O methods over .interop_path()
  • Use temporary directories with RAII cleanup