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)
- Portable Application Paths (app-path)
- OS Standard Directories (dirs)
- Serialization & Deserialization (serde)
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 directoryAppPath::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:
- ✅ Full control - Access all options of external crates, not just what we expose
- ✅ No version coupling - Use any version of
tempfile,dirs, etc. - ✅ Clear dependencies - You explicitly add what you use
- ✅ Reduced bloat - Pay only for what you import
- ✅ 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 } }