archive-trait 0.0.13

Format-neutral, asynchronous archive construction and extraction
Documentation
pub mod support;

use archive_trait::{
    Archive as _, ExtractError, ExtractPolicyViolation,
    extract::{ExtractPolicy, LinkPolicy, SymlinkPolicy},
};
use support::{TestArchive, entry};
use tempfile::tempdir;

#[cfg(unix)]
#[tokio::test]
async fn preserves_symlink_chains_and_supports_hard_link_payloads() {
    let temp = tempdir().expect("temporary directory should be created");
    let destination = temp.path().join("out");
    let archive = TestArchive::new([
        entry::file("target", b"old"),
        entry::symbolic_link("second", "target"),
        entry::symbolic_link("first", "second"),
        entry::hard_link("hard", "target", b"new"),
    ]);
    let policy = ExtractPolicy::default().link_policy(LinkPolicy::default().allow_hard_links(true));

    archive
        .extract_in(&destination, policy)
        .await
        .expect("links should extract");

    for path in ["target", "first", "second", "hard"] {
        assert_eq!(
            std::fs::read(destination.join(path)).expect("link target should be readable"),
            b"new"
        );
    }
}

#[tokio::test]
async fn link_policies_reject_or_skip_without_creating_links() {
    let temp = tempdir().expect("temporary directory should be created");
    for (case, path, member, policy, expected) in [
        (
            "symbolic",
            "link",
            entry::symbolic_link("link", "target"),
            LinkPolicy::default().symlink_policy(SymlinkPolicy::Reject),
            ExtractPolicyViolation::SymbolicLink,
        ),
        (
            "hard",
            "hard",
            entry::hard_link("hard", "target", b"new"),
            LinkPolicy::default(),
            ExtractPolicyViolation::HardLink,
        ),
    ] {
        let destination = temp.path().join(case);
        let result = TestArchive::new([entry::file("target", b"keep"), member])
            .extract_in(&destination, ExtractPolicy::default().link_policy(policy))
            .await;
        assert!(matches!(
            result,
            Err(ExtractError::PolicyViolation { violation, .. }) if violation == expected
        ));
        assert_eq!(
            std::fs::read(destination.join("target")).expect("prior output should remain readable"),
            b"keep"
        );
        assert!(!destination.join(path).exists());
    }

    let destination = temp.path().join("hard-only");
    TestArchive::new([
        entry::file("target", b"old"),
        entry::hard_link("hard", "target", b"new"),
    ])
    .extract_in(
        &destination,
        ExtractPolicy::default().link_policy(
            LinkPolicy::default()
                .symlink_policy(SymlinkPolicy::Reject)
                .allow_hard_links(true),
        ),
    )
    .await
    .expect("hard-link policy should be independent of symbolic links");
    for path in ["target", "hard"] {
        assert_eq!(
            std::fs::read(destination.join(path)).expect("hard link should be readable"),
            b"new"
        );
    }

    let destination = temp.path().join("skipped");
    TestArchive::new([
        entry::file("same", b"keep"),
        entry::symbolic_link("same", "missing"),
        entry::symbolic_link("skipped", "missing"),
    ])
    .extract_in(
        &destination,
        ExtractPolicy::default()
            .link_policy(LinkPolicy::default().symlink_policy(SymlinkPolicy::Skip)),
    )
    .await
    .expect("skipped link should not fail");
    assert_eq!(
        std::fs::read(destination.join("same")).expect("existing file should remain readable"),
        b"keep"
    );
    assert!(!destination.join("skipped").exists());
}