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

Chapter 6: Ecosystem Integration

“Compose strict-path with ecosystem tools — security primitives, not wrappers.”

You’ve mastered the core concepts: boundaries, markers, authorization, and virtual paths. Now you’ll learn how to integrate strict-path with popular Rust ecosystem crates for real-world applications.

The Philosophy

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

We don’t wrap external crates behind feature flags. Instead, we show you how to use them together effectively — giving you full control and explicit security.


Quick Integration Examples

Temporary Directories (tempfile)

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

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

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

    // Safe operations within boundary
    let user_file = upload_boundary.strict_join("user/data.txt")?;
    user_file.create_parent_dir_all()?;
    user_file.write(b"uploaded content")?;

    // Escape attempts are blocked
    match upload_boundary.strict_join("../../etc/passwd") {
        Ok(_) => panic!("Should not escape!"),
        Err(e) => println!("✓ Attack blocked: {}", e),
    }

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

Why this works: RAII cleanup from tempfile, security from strict-path.


Portable App Paths (app-path)

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

fn setup_portable_app() -> Result<(), Box<dyn std::error::Error>> {
    // Executable-relative path
    let app_path = AppPath::new("MyPortableApp");
    let app_dir = app_path.get_app_dir();

    // Establish boundary
    let app_data_dir = PathBoundary::try_new_create(&app_dir)?;

    // Safe config access
    let config = app_data_dir.strict_join("config/settings.ini")?;
    config.create_parent_dir_all()?;
    config.write(b"[settings]\nportable=true\n")?;

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

    Ok(())
}
}

Use cases: USB drives, CI/CD, containers with custom paths.


OS Directories (dirs)

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

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

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

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

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

    Ok(())
}
}

Config with serde — Typed Fields Are The Ingestion Boundary

Runtime Config structs declare typed PathBoundary<Marker> / VirtualRoot<Marker> fields directly — never raw PathBuf. The typed field is the ingestion boundary; raw PathBuf erases the type-system guarantee.

FromStr forwards to try_new_create: string input becomes a fully-constructed, canonicalized, validated boundary. Per-field policy variation (some fields must already exist, others bootstrap) is expressed at the serde wiring layer using whatever mechanism fits your project (deserialize_with, serde_with, a wrapper type). This crate commits to the FromStr contract; the serde glue is yours.

struct UploadDir;
struct DataDir;

struct AppConfig {
    upload_dir: PathBoundary<UploadDir>,     // policy: bootstrap ok
    data_dir: PathBoundary<DataDir>,         // policy: must already exist
    user_paths: Vec<String>,                  // per-request path strings
}

Per-request path strings (entries from HTTP bodies, archive entries, CLI free-form args) stay as String and are validated against a pre-constructed boundary via boundary.strict_join(…) at the use site.

Key insight: The typed field is the ingestion boundary. Raw PathBuf in runtime Config/Cli structs erases the type-system guarantee — nothing stops a later reader (or refactor) from passing the unvalidated path around.


Real-World Application

Combining all integrations:

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

struct AppConfig;
struct UserFiles;
struct TempProcessing;

struct Config {
    config_dir: PathBoundary<AppConfig>,   // serde-wire via your chosen mechanism
}

struct Application {
    config: PathBoundary<AppConfig>,
    user_root: VirtualRoot<UserFiles>,
}

impl Application {
    fn new(user_id: u64) -> Result<Self, Box<dyn std::error::Error>> {
        // OS-specific config
        let config_base = dirs::config_dir()
            .ok_or("No config directory")?;
        let config = PathBoundary::try_new_create(
            config_base.join("myapp")
        )?;

        // Per-user virtual root
        let user_storage = format!("users/user_{}", user_id);
        let user_root = VirtualRoot::try_new_create(user_storage)?;

        Ok(Self { config, user_root })
    }

    fn process_file(&self, filename: &str)
        -> Result<String, Box<dyn std::error::Error>>
    {
        // Temp directory for processing
        let temp = TempDir::new()?;
        let temp_dir: PathBoundary<TempProcessing> =
            PathBoundary::try_new(temp.path())?;

        // Get user file (virtual path)
        let user_file = self.user_root.virtual_join(filename)?;
        let data = user_file.read()?;

        // Process in temp
        let temp_file = temp_dir.strict_join("processing.tmp")?;
        temp_file.write(&data)?;

        Ok(format!("Processed {} bytes", data.len()))
        // temp auto-cleaned
    }
}
}

Why No Feature Flags?

Previous approach (deprecated):

strict-path = { version = "0.2", features = ["tempfile", "dirs", "app-path", "serde"] }

New approach (recommended):

strict-path = "0.2"
tempfile = "3.0"
dirs = "5.0"
app-path = "1.0"

Benefits:

  1. Full control - Access all options of external crates
  2. No version coupling - Use any version you want
  3. Explicit dependencies - Clear what you’re using
  4. Reduced bloat - Pay only for what you import
  5. Visible security - Validation is explicit in code

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



Complete Integration Guide

For comprehensive examples, patterns, and best practices, see:

📚 Ecosystem Integration Guide →

This guide covers:

  • Temporary directories with RAII cleanup
  • Portable application paths with env overrides
  • OS standard directories (cross-platform)
  • Serialization/deserialization patterns
  • Multi-directory application architecture
  • Web API integration examples

Key Takeaways

Compose, don’t wrap — Use ecosystem crates directly ✅ Explicit validation — Security operations are visible ✅ Full control — No feature coupling or version constraints ✅ One extra line — Small cost for explicit security ✅ Typed ingestion boundaryPathBoundary<Marker> / VirtualRoot<Marker> in runtime config, never raw PathBuf

The Final Complete Guarantee

By combining all chapters, you achieve:

  1. ✅ Paths cannot escape boundaries (Chapter 1)
  2. ✅ Paths are in the correct domain (Chapter 3)
  3. ✅ Authorization proven by compiler (Chapter 4)
  4. ✅ Clean virtual UX for users (Chapter 5)
  5. ✅ Ecosystem integration with safety (Chapter 6)

All enforced at compile time with zero runtime overhead.


Congratulations! 🎉

You’ve completed the full tutorial! You now understand:

  • ✅ How StrictPath prevents path escapes
  • ✅ How markers prevent domain mix-ups
  • ✅ How change_marker() encodes authorization
  • ✅ How VirtualPath provides user-friendly sandboxing
  • ✅ How to integrate with the Rust ecosystem

What’s Next?

Explore these resources to deepen your knowledge:

You’re ready to build secure systems! 🚀


Quick Reference Card:

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

// Portable app paths
let app_dir = app_path::AppPath::new("MyApp").get_app_dir();
let app_data_dir = PathBoundary::try_new(&app_dir)?;

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

// Config: typed boundary fields; raw PathBuf is the anti-pattern
struct Config {
    data_dir: PathBoundary,      // FromStr => try_new_create for deserialization
    user_path: String,            // per-request path; validate via strict_join later
}
}

← Back to Tutorial Overview