strict-path 0.2.0

Secure path handling for untrusted input. Prevents directory traversal, symlink escapes, and 19+ real-world CVE attack patterns.
Documentation
// Test that README.md examples compile and work correctly

#[test]
fn readme_policy_types_example() -> Result<(), Box<dyn std::error::Error>> {
    use crate::PathBoundary;
    #[cfg(feature = "virtual-path")]
    use crate::VirtualRoot;

    let temp_dir = tempfile::tempdir()?.keep();

    // 1. Define the boundary - paths are contained within ./uploads
    let uploads_dir: crate::PathBoundary = PathBoundary::try_new_create(temp_dir.join("uploads"))?;

    // 2. Validate untrusted user input against the boundary
    let user_provided_path = "documents/report.pdf"; // Simulates get_filename_from_request()
    let user_file = uploads_dir.strict_join(user_provided_path)?;

    // 3. Safe I/O operations - guaranteed within boundary
    user_file.create_parent_dir_all()?;
    user_file.write(b"file contents")?;
    let contents = user_file.read_to_string()?;
    assert_eq!(contents, "file contents");

    // 4. Escape attempts are detected and rejected
    let malicious_input = "../../etc/passwd"; // Simulates attacker-controlled input
    match uploads_dir.strict_join(malicious_input) {
        Ok(_) => panic!("Escapes should be caught!"),
        Err(e) => println!("Attack blocked: {e}"), // PathEscapesBoundary error
    }

    // Virtual filesystem for multi-tenant isolation (requires "virtual-path" feature)
    #[cfg(feature = "virtual-path")]
    {
        let tenant_id = "alice";
        let tenant_vroot: crate::VirtualRoot =
            VirtualRoot::try_new_create(temp_dir.join(format!("tenant_data/{tenant_id}")))?;
        let tenant_file = tenant_vroot.virtual_join("../../../sensitive")?;
        // Escape attempt is silently clamped - stays within tenant_data
        println!("Virtual path: {}", tenant_file.virtualpath_display()); // Shows: "/sensitive"
        assert_eq!(tenant_file.virtualpath_display().to_string(), "/sensitive");
        // Cleanup (not shown in README)
    }

    // temp_dir cleanup is automatic via RAII
    Ok(())
}

#[test]
fn readme_one_liner_sugar_example() -> Result<(), Box<dyn std::error::Error>> {
    use crate::StrictPath;
    #[cfg(feature = "virtual-path")]
    use crate::VirtualPath;

    let temp_dir = tempfile::tempdir()?.keep();

    // Concise form - boundary created inline
    let config_file: crate::StrictPath =
        StrictPath::with_boundary_create(temp_dir.join("config"))?.strict_join("app.toml")?;
    config_file.write(b"settings")?;

    #[cfg(feature = "virtual-path")]
    {
        // Virtual paths require dynamic tenant/user IDs to serve their purpose
        let user_id = "alice";
        let user_avatar: crate::VirtualPath =
            VirtualPath::with_root_create(temp_dir.join(format!("user_data/{user_id}")))?
                .virtual_join("/profile/avatar.png")?;
        user_avatar.create_parent_dir_all()?;
        user_avatar.write(b"image data")?;
        // Each user sees "/profile/avatar.png" but they're isolated on disk
        assert_eq!(
            user_avatar.virtualpath_display().to_string(),
            "/profile/avatar.png"
        );
    }

    Ok(())
}

#[test]
fn readme_disaster_prevention_example() -> Result<(), Box<dyn std::error::Error>> {
    use crate::StrictPath;

    let temp_dir = tempfile::tempdir()?.keep();
    let user_input = "../../../etc/passwd";

    // ? This single line makes it mathematically impossible
    let uploads_dir: crate::StrictPath =
        StrictPath::with_boundary_create(temp_dir.join("uploads"))?;
    let result = uploads_dir.strict_join(user_input);
    // Returns Err(PathEscapesBoundary) - attack blocked!
    assert!(result.is_err());

    Ok(())
}

#[test]
fn readme_typical_workflow_strict_links_example() -> Result<(), Box<dyn std::error::Error>> {
    use crate::PathBoundary;

    let temp_dir = tempfile::tempdir()?.keep();

    // 1) Establish boundary
    let link_demo_dir: crate::PathBoundary =
        PathBoundary::try_new_create(temp_dir.join("link_demo"))?;

    // 2) Validate target path from untrusted input
    let target = link_demo_dir.strict_join("data/target.txt")?;
    target.create_parent_dir_all()?;
    target.write(b"hello")?;

    // 3) Create a sibling hard link under the same directory
    target.strict_hard_link("alias.txt")?;

    let alias = link_demo_dir.strict_join("data/alias.txt")?;
    assert_eq!(alias.read_to_string()?, "hello");

    Ok(())
}

#[cfg(feature = "virtual-path")]
#[test]
fn readme_typical_workflow_virtual_links_example() -> Result<(), Box<dyn std::error::Error>> {
    use crate::VirtualRoot;

    let temp_dir = tempfile::tempdir()?.keep();
    let tenant_id = "tenant42";

    // 1) Establish virtual root
    let vroot: crate::VirtualRoot =
        VirtualRoot::try_new_create(temp_dir.join(format!("vlink_demo/{tenant_id}")))?;

    // 2) Validate target in virtual space (absolute is clamped to root)
    let vtarget = vroot.virtual_join("/data/target.txt")?;
    vtarget.create_parent_dir_all()?;
    vtarget.write(b"hi")?;

    // 3) 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");

    Ok(())
}

#[test]
fn readme_interop_path_example() -> Result<(), Box<dyn std::error::Error>> {
    use crate::StrictPath;

    let temp_dir = tempfile::tempdir()?.keep();

    let user_input = "report.txt"; // Simulates untrusted input from HTTP request
    let validated_file: crate::StrictPath =
        StrictPath::with_boundary_create(temp_dir.join("data"))?.strict_join(user_input)?;

    // Write via built-in I/O (no interop needed)
    validated_file.write(b"report contents")?;

    // .interop_path() returns &OsStr — only usable for third-party APIs via AsRef<Path>
    // Use built-in exists() to check file existence
    assert!(validated_file.exists());

    // Built-in I/O stays within safety boundary
    let contents = validated_file.read_to_string()?;
    assert_eq!(contents, "report contents");

    // Display helpers for logging (never expose interop_path to end users)
    let display = validated_file.strictpath_display().to_string();
    assert!(display.contains("report.txt"));

    Ok(())
}