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:
- ✅ Full control - Access all options of external crates
- ✅ No version coupling - Use any version you want
- ✅ Explicit dependencies - Clear what you’re using
- ✅ Reduced bloat - Pay only for what you import
- ✅ 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 boundary — PathBoundary<Marker> / VirtualRoot<Marker> in runtime config, never raw PathBuf
The Final Complete Guarantee
By combining all chapters, you achieve:
- ✅ Paths cannot escape boundaries (Chapter 1)
- ✅ Paths are in the correct domain (Chapter 3)
- ✅ Authorization proven by compiler (Chapter 4)
- ✅ Clean virtual UX for users (Chapter 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
StrictPathprevents path escapes - ✅ How markers prevent domain mix-ups
- ✅ How
change_marker()encodes authorization - ✅ How
VirtualPathprovides user-friendly sandboxing - ✅ How to integrate with the Rust ecosystem
What’s Next?
Explore these resources to deepen your knowledge:
- Ecosystem Integration Guide — Comprehensive integration patterns
- Real-World Examples — Copy-pasteable patterns for web servers, CLI tools, archives
- Best Practices — Decision matrices, design patterns, and guidelines
- Anti-Patterns — Common mistakes and how to fix them
- Axum Tutorial — Build a complete web service with strict-path
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
}
}