Daily Usage Patterns
Common workflows and patterns for everyday strict-path usage.
Typical Workflow
- Establish a root (policy type):
PathBoundaryfor strict detection, orVirtualRootfor virtual containment. - Accept safe types in internal APIs:
&StrictPath<Marker>/&VirtualPath<Marker>(or roots + raw segments). - Validate any untrusted input (CLI, config, DB, user I/O) via
strict_join/virtual_joinbefore any I/O.
Strict link creation (hard link)
#![allow(unused)]
fn main() {
use strict_path::PathBoundary;
let boundary: PathBoundary = PathBoundary::try_new_create("./link_demo")?;
let target = boundary.strict_join("data/target.txt")?;
target.create_parent_dir_all()?;
target.write(b"hello")?;
// Create a sibling hard link under the same directory
target.strict_hard_link("alias.txt")?; // mirrors std::fs::hard_link
let alias = boundary.strict_join("data/alias.txt")?;
assert_eq!(alias.read_to_string()?, "hello");
}
Virtual link creation (hard link)
#![allow(unused)]
fn main() {
use strict_path::VirtualRoot;
let tenant_id = "tenant42";
let vroot: VirtualRoot = VirtualRoot::try_new_create(format!("./vlink_demo/{tenant_id}"))?;
let vtarget = vroot.virtual_join("/data/target.txt")?;
vtarget.create_parent_dir_all()?;
vtarget.write(b"hi")?;
// Create a sibling hard link (virtual semantics)
vtarget.virtual_hard_link("alias.txt")?;
let valias = vroot.virtual_join("/data/alias.txt")?;
assert_eq!(valias.read_to_string()?, "hi");
}
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 (tempfile)
let tmp = tempfile::tempdir()?;
let temp_dir = PathBoundary::<()>::try_new(tmp.path())?;
// Write input
let input_file = temp_dir.strict_join("input.dat")?;
input_file.write(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(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