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

Ecosystem Integration

"Compose strict-path with ecosystem tools — no feature flags needed."

strict-path provides security primitives for path operations. You compose these with popular ecosystem crates directly — no coupling, no feature flags, just clean integration.

Philosophy: We don't wrap ecosystem crates. We show you how to use them together effectively.


Table of Contents


Temporary Directories (tempfile)

The tempfile crate provides RAII temporary directories that auto-cleanup on drop. Perfect for extraction staging, upload processing, and test fixtures.

Basic Integration

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

fn process_upload() -> Result<(), Box<dyn std::error::Error>> {
    // Create temporary directory with RAII cleanup
    let temp_dir = tempfile::tempdir()?;

    // Establish strict boundary
    let upload_boundary = PathBoundary::try_new(temp_dir.path())?;

    // Now all operations are bounded
    let user_file = upload_boundary.strict_join("user/data.txt")?;
    user_file.create_parent_dir_all()?;
    user_file.write(b"uploaded content")?;

    // Process files...
    let contents = user_file.read_to_string()?;
    println!("Processed: {}", contents);

    Ok(())
    // temp_dir automatically deleted here when dropped
}
}

With Custom Prefix

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

fn extraction_staging() -> Result<(), Box<dyn std::error::Error>> {
    // Temp directory with identifiable prefix
    let temp_dir = tempfile::Builder::new()
        .prefix("archive-extract-")
        .tempdir()?;

    let extract_boundary = PathBoundary::try_new(temp_dir.path())?;

    // Extract archive entries safely
    for entry_name in &["file1.txt", "../../etc/passwd", "file2.txt"] {
        match extract_boundary.strict_join(entry_name) {
            Ok(safe_path) => {
                safe_path.create_parent_dir_all()?;
                safe_path.write(b"extracted")?;
                println!("✓ Extracted: {}", safe_path.strictpath_display());
            }
            Err(e) => {
                eprintln!("✗ Blocked malicious path '{}': {}", entry_name, e);
            }
        }
    }

    Ok(())
}
}

Test Fixtures Pattern

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

#[test]
fn test_file_processing() {
    let temp = tempfile::tempdir().unwrap();
    let boundary = PathBoundary::try_new(temp.path()).unwrap();

    // Setup test files
    let input = boundary.strict_join("input.txt").unwrap();
    input.write(b"test data").unwrap();

    // Run your code
    process_file(&boundary, "input.txt").unwrap();

    // Verify results
    let output = boundary.strict_join("output.txt").unwrap();
    assert!(output.exists());

    // temp auto-cleans on drop
}

fn process_file(boundary: &PathBoundary, name: &str) -> std::io::Result<()> {
    let input = boundary.strict_join(name).unwrap();
    let output = boundary.strict_join("output.txt").unwrap();

    let data = input.read_to_string()?;
    output.write(data.to_uppercase().as_bytes())?;
    Ok(())
}
}

VirtualRoot with Temporary Directories

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

fn temp_sandbox() -> Result<(), Box<dyn std::error::Error>> {
    let temp_dir = tempfile::tempdir()?;
    let sandbox = VirtualRoot::try_new(temp_dir.path())?;

    // Escape attempts are clamped
    let user_path = sandbox.virtual_join("../../../etc/passwd")?;

    // Stays within temp directory
    println!("Virtual: {}", user_path.virtualpath_display()); // "/etc/passwd"
    println!("Real: {}", user_path.realpath_display());       // "/<tempdir>/etc/passwd"

    user_path.create_parent_dir_all()?;
    user_path.write(b"safe content")?;

    Ok(())
}
}

Portable Application Paths (app-path)

The app-path crate creates executable-relative paths for truly portable applications (USB drives, different install locations).

Key API:

  • AppPath::new() - Returns executable directory
  • AppPath::with("subdir") - Returns executable_dir/subdir
  • Implements Deref<Target=Path> so it can be used directly as a path

Basic Portable App

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

fn setup_portable_app() -> Result<(), Box<dyn std::error::Error>> {
    // AppPath::with() returns executable_dir/MyPortableApp
    let app_dir = AppPath::with("MyPortableApp");

    // Establish boundary for the app directory
    let boundary = PathBoundary::try_new_create(app_dir)?;

    // Safe operations within app directory
    let config = boundary.strict_join("config/settings.ini")?;
    config.create_parent_dir_all()?;
    config.write(b"[settings]\nportable=true\n")?;

    let data = boundary.strict_join("data/userfiles")?;
    data.create_dir_all()?;

    println!("App directory: {}", boundary.strictpath_display());

    Ok(())
}
}

Environment Variable Overrides (Testing/CI/CD)

Perfect for testing, CI/CD pipelines, and container deployments where you need to control the data location.

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

fn setup_app_with_override() -> Result<(), Box<dyn std::error::Error>> {
    // Use AppPath's built-in override support
    let env_var = "MY_APP_DATA_DIR";
    let app_path = AppPath::with_override("MyApp", Some(env_var));

    let boundary = PathBoundary::try_new_create(app_path)?;

    println!("Using app directory: {}", boundary.strictpath_display());
    // In production: /path/to/exe/MyApp
    // In CI with MY_APP_DATA_DIR=/tmp/ci-test: /tmp/ci-test

    let log_file = boundary.strict_join("logs/app.log")?;
    log_file.create_parent_dir_all()?;
    log_file.write(b"Application started\n")?;

    Ok(())
}
}

Multi-Directory Pattern

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

struct AppPaths {
    config: PathBoundary<ConfigDir>,
    data: PathBoundary<DataDir>,
    cache: PathBoundary<CacheDir>,
}

struct ConfigDir;
struct DataDir;
struct CacheDir;

impl AppPaths {
    fn new(app_name: &str) -> Result<Self, Box<dyn std::error::Error>> {
        // AppPath::with() returns executable_dir/app_name
        let base_dir = AppPath::with(app_name);

        Ok(Self {
            config: PathBoundary::try_new_create(base_dir.join("config"))?,
            data: PathBoundary::try_new_create(base_dir.join("data"))?,
            cache: PathBoundary::try_new_create(base_dir.join("cache"))?,
        })
    }
}

fn use_app_paths() -> Result<(), Box<dyn std::error::Error>> {
    let paths = AppPaths::new("MyApp")?;

    let settings = paths.config.strict_join("settings.toml")?;
    settings.write(b"theme = 'dark'\n")?;

    let user_db = paths.data.strict_join("users.db")?;
    user_db.write(b"database content")?;

    let temp_cache = paths.cache.strict_join("thumbnails/thumb1.png")?;
    temp_cache.create_parent_dir_all()?;
    temp_cache.write(b"cached data")?;

    Ok(())
}
}

OS Standard Directories (dirs)

The dirs crate provides cross-platform access to standard user directories (config, data, cache, downloads, etc.).

Configuration Directory

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

fn setup_config() -> Result<(), Box<dyn std::error::Error>> {
    // Get platform-specific config directory
    let config_base = dirs::config_dir()
        .ok_or("No config directory available")?;

    // Create app-specific subdirectory boundary
    let app_config = config_base.join("myapp");
    let boundary = PathBoundary::try_new_create(&app_config)?;

    // Platform-specific locations:
    // Linux:   ~/.config/myapp/
    // Windows: C:\Users\Alice\AppData\Roaming\myapp\
    // macOS:   ~/Library/Application Support/myapp/

    let settings = boundary.strict_join("settings.toml")?;
    settings.write(b"[app]\nversion = '1.0'\n")?;

    Ok(())
}
}

Multi-Directory Application

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

struct AppDirectories {
    config: PathBoundary,
    data: PathBoundary,
    cache: PathBoundary,
}

impl AppDirectories {
    fn new(app_name: &str) -> Result<Self, Box<dyn std::error::Error>> {
        let config_base = dirs::config_dir()
            .ok_or("No config directory")?;
        let data_base = dirs::data_dir()
            .ok_or("No data directory")?;
        let cache_base = dirs::cache_dir()
            .ok_or("No cache directory")?;

        Ok(Self {
            config: PathBoundary::try_new_create(config_base.join(app_name))?,
            data: PathBoundary::try_new_create(data_base.join(app_name))?,
            cache: PathBoundary::try_new_create(cache_base.join(app_name))?,
        })
    }
}

fn use_standard_dirs() -> Result<(), Box<dyn std::error::Error>> {
    let dirs = AppDirectories::new("MyApp")?;

    // Config: user preferences
    let prefs = dirs.config.strict_join("preferences.json")?;
    prefs.write(br#"{"theme": "dark"}"#)?;

    // Data: persistent user data
    let database = dirs.data.strict_join("app.db")?;
    database.write(b"database data")?;

    // Cache: temporary/regenerable data
    let thumbnail = dirs.cache.strict_join("thumbs/image1.jpg")?;
    thumbnail.create_parent_dir_all()?;
    thumbnail.write(b"thumbnail data")?;

    Ok(())
}
}

User Content Directories

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

fn access_user_content() -> Result<(), Box<dyn std::error::Error>> {
    // Downloads directory
    if let Some(downloads) = dirs::download_dir() {
        let boundary = PathBoundary::try_new(&downloads)?;

        // Safe access to user-selected file
        let user_input = "report.pdf"; // From file picker or CLI
        let file = boundary.strict_join(user_input)?;

        if file.exists() {
            let data = file.read()?;
            println!("Processing file: {} bytes", data.len());
        }
    }

    // Documents directory
    if let Some(documents) = dirs::document_dir() {
        let boundary = PathBoundary::try_new(&documents)?;

        let export = boundary.strict_join("exports/data.csv")?;
        export.create_parent_dir_all()?;
        export.write(b"col1,col2\nval1,val2\n")?;

        println!("Exported to: {}", export.strictpath_display());
    }

    Ok(())
}
}

Serialization & Deserialization (serde)

For JSON, TOML, YAML, and other formats, use FromStr trait with manual validation — giving you explicit control over path validation.

Deserializing Boundaries with FromStr

PathBoundary and VirtualRoot implement FromStr, so they deserialize automatically with serde:

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

#[derive(Deserialize)]
struct AppConfig {
    // Deserializes via FromStr automatically
    upload_dir: PathBoundary,
    data_dir: PathBoundary,
}

fn load_config() -> Result<(), Box<dyn std::error::Error>> {
    let json = r#"{
        "upload_dir": "./uploads",
        "data_dir": "./data"
    }"#;

    let config: AppConfig = serde_json::from_str(json)?;

    // Boundaries are ready to use
    let file = config.upload_dir.strict_join("user/file.txt")?;
    file.create_parent_dir_all()?;
    file.write(b"content")?;

    Ok(())
}
}

Explicit Path Validation Pattern

For paths within boundaries, deserialize as String and validate explicitly:

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

#[derive(Deserialize)]
struct UploadRequest {
    boundary: PathBoundary,
    user_paths: Vec<String>, // Validate these manually
}

fn handle_upload(json: &str) -> Result<(), Box<dyn std::error::Error>> {
    let request: UploadRequest = serde_json::from_str(json)?;

    // Explicit validation - security-conscious and visible
    for path_str in &request.user_paths {
        match request.boundary.strict_join(path_str) {
            Ok(safe_path) => {
                safe_path.create_parent_dir_all()?;
                safe_path.write(b"uploaded")?;
                println!("✓ Uploaded: {}", safe_path.strictpath_display());
            }
            Err(e) => {
                eprintln!("✗ Rejected '{}': {}", path_str, e);
            }
        }
    }

    Ok(())
}
}

Web API Example (Axum)

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

#[derive(Deserialize)]
struct FileUpload {
    filename: String, // User input - must validate!
    content: String,
}

#[derive(Serialize)]
struct UploadResponse {
    success: bool,
    path: String,
}

struct AppState {
    upload_boundary: PathBoundary,
}

async fn upload_file(
    State(state): State<AppState>,
    Json(upload): Json<FileUpload>,
) -> Json<UploadResponse> {
    // Explicit validation of user input
    match state.upload_boundary.strict_join(&upload.filename) {
        Ok(safe_path) => {
            safe_path.create_parent_dir_all().ok();
            safe_path.write(upload.content.as_bytes()).ok();

            Json(UploadResponse {
                success: true,
                path: safe_path.strictpath_display().to_string(),
            })
        }
        Err(e) => {
            Json(UploadResponse {
                success: false,
                path: format!("Error: {}", e),
            })
        }
    }
}
}

Config File Pattern

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

#[derive(Deserialize)]
struct ServerConfig {
    // Boundaries deserialize via FromStr
    public_assets: PathBoundary<PublicAssets>,
    user_uploads: PathBoundary<UserUploads>,

    // Other config
    port: u16,
    host: String,
}

struct PublicAssets;
struct UserUploads;

fn load_server_config() -> Result<(), Box<dyn std::error::Error>> {
    let toml_str = r#"
        public_assets = "./public"
        user_uploads = "./uploads"
        port = 8080
        host = "127.0.0.1"
    "#;

    let config: ServerConfig = toml::from_str(toml_str)?;

    // Use boundaries immediately
    let favicon = config.public_assets.strict_join("favicon.ico")?;
    println!("Favicon: {}", favicon.strictpath_display());

    let user_file = config.user_uploads.strict_join("user123/file.txt")?;
    user_file.create_parent_dir_all()?;

    Ok(())
}
}

Serializing Paths

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

fn serialize_paths() -> Result<(), Box<dyn std::error::Error>> {
    let boundary = PathBoundary::try_new_create("./data")?;
    let file = boundary.strict_join("config/settings.json")?;

    // Serialize to JSON using display methods
    let response = json!({
        "boundary": boundary.strictpath_display().to_string(),
        "file": file.strictpath_display().to_string(),
        "file_name": file.strictpath_file_name()
            .unwrap()
            .to_string_lossy(),
    });

    println!("{}", serde_json::to_string_pretty(&response)?);

    Ok(())
}
}

Why No Feature Flags?

Philosophy: strict-path provides security primitives. You compose them with ecosystem tools.

Benefits of direct integration:

  1. Full control - Access all options of external crates, not just what we expose
  2. No version coupling - Use any version of tempfile, dirs, etc.
  3. Clear dependencies - You explicitly add what you use
  4. Reduced bloat - Pay only for what you import
  5. Explicit validation - Security operations are visible in your code

Trade-off: Write one extra line of code for explicit, secure integration.


Quick Reference

#![allow(unused)]
fn main() {
// Temporary directories
let temp = tempfile::tempdir()?;
let boundary = PathBoundary::try_new(temp.path())?;

// Portable app paths
use app_path::AppPath;
let app_path = AppPath::with("MyApp");  // Relative to executable directory
let boundary = PathBoundary::try_new_create(&app_path)?;

// OS directories
let config = dirs::config_dir().ok_or("No config dir")?;
let boundary = PathBoundary::try_new_create(config.join("myapp"))?;

// Deserialization (FromStr)
#[derive(Deserialize)]
struct Config {
    boundary: PathBoundary,  // Automatic via FromStr
    user_path: String,        // Manual validation
}
}

See Also