portable-network-archive 0.34.0

Portable-Network-Archive cli
Documentation
mod edge_cases;
mod glob_pattern;
mod keep_solid;
mod missing_file;
mod multiple_clauses;
mod numeric;
mod password;
mod password_file;
mod symbolic_all;
mod symbolic_combined;
mod symbolic_group;
mod symbolic_other;
mod symbolic_user;
mod unsolid;

use crate::utils::{archive, archive::FileEntryDef, setup};
use clap::Parser;
#[allow(deprecated)]
use pna::Permission;
use pna::{Archive, EntryBuilder, EntryName, WriteOptions};
use portable_network_archive::cli;
use std::fs::File;
use std::io::Write;

const ENTRY_PATH: &str = "test.txt";
const ENTRY_CONTENT: &[u8] = b"test content";

/// Precondition: An archive contains a file with permission 0o777 (rwxrwxrwx).
/// Action: Run `pna experimental chmod` with `-x` to remove execute permission for all.
/// Expectation: The archive entry's permission becomes 0o666 (rw-rw-rw-).
#[test]
fn archive_chmod() {
    setup();

    archive::create_archive_with_permissions(
        "chmod.pna",
        &[FileEntryDef {
            path: ENTRY_PATH,
            content: ENTRY_CONTENT,
            permission: 0o777,
        }],
    )
    .unwrap();

    cli::Cli::try_parse_from([
        "pna",
        "--quiet",
        "experimental",
        "chmod",
        "-f",
        "chmod.pna",
        "--",
        "-x",
        ENTRY_PATH,
    ])
    .unwrap()
    .execute()
    .unwrap();

    archive::for_each_entry("chmod.pna", |entry| {
        if entry.header().path() == ENTRY_PATH {
            let mode = entry
                .metadata()
                .permission_mode()
                .expect("entry should have permission mode metadata")
                .get();
            assert_eq!(mode & 0o777, 0o666, "-x on 0o777 should yield 0o666");
        }
    })
    .unwrap();
}

/// Precondition: An archive entry carries legacy fPRM metadata.
/// Action: Run `pna experimental chmod` to change its mode.
/// Expectation: The updated mode is emitted as fMOd, and stale fPRM is removed.
#[test]
#[allow(deprecated)]
fn archive_chmod_drops_legacy_fprm() {
    setup();
    let path = "chmod_legacy_fprm.pna";
    {
        let mut archive = Archive::write_header(File::create(path).unwrap()).unwrap();
        let mut builder = EntryBuilder::new_file(
            EntryName::from_utf8_preserve_root(ENTRY_PATH),
            WriteOptions::store(),
        )
        .unwrap();
        builder.permission(Permission::new(
            1000,
            "user".to_string(),
            1000,
            "group".to_string(),
            0o777,
        ));
        builder.write_all(ENTRY_CONTENT).unwrap();
        archive.add_entry(builder.build().unwrap()).unwrap();
        archive.finalize().unwrap();
    }

    cli::Cli::try_parse_from([
        "pna",
        "--quiet",
        "experimental",
        "chmod",
        "-f",
        path,
        "644",
        ENTRY_PATH,
    ])
    .unwrap()
    .execute()
    .unwrap();

    archive::for_each_entry(path, |entry| {
        assert!(
            entry.metadata().permission().is_none(),
            "legacy fPRM must be removed after chmod"
        );
        assert_eq!(entry.metadata().permission_mode().unwrap().get(), 0o644);
    })
    .unwrap();
}

/// Precondition: An archive entry has no permission metadata.
/// Action: Run `pna experimental chmod 600` on that entry.
/// Expectation: The requested mode is emitted as fMOd without generating legacy fPRM.
#[test]
#[allow(deprecated)]
fn archive_chmod_numeric_sets_missing_permission_mode() {
    setup();
    let path = "chmod_missing_mode_numeric.pna";
    {
        let mut archive = Archive::write_header(File::create(path).unwrap()).unwrap();
        let mut builder = EntryBuilder::new_file(
            EntryName::from_utf8_preserve_root(ENTRY_PATH),
            WriteOptions::store(),
        )
        .unwrap();
        builder.write_all(ENTRY_CONTENT).unwrap();
        archive.add_entry(builder.build().unwrap()).unwrap();
        archive.finalize().unwrap();
    }

    cli::Cli::try_parse_from([
        "pna",
        "--quiet",
        "experimental",
        "chmod",
        "-f",
        path,
        "600",
        ENTRY_PATH,
    ])
    .unwrap()
    .execute()
    .unwrap();

    archive::for_each_entry(path, |entry| {
        assert!(
            entry.metadata().permission().is_none(),
            "legacy fPRM must not be generated by chmod"
        );
        assert_eq!(entry.metadata().permission_mode().unwrap().get(), 0o600);
    })
    .unwrap();
}

/// Precondition: Archive entries of each supported kind have no permission metadata.
/// Action: Run symbolic `pna experimental chmod a-r` on all entries.
/// Expectation: Missing modes use default bases: directory 0755, all other
/// entry kinds 0644.
#[test]
fn archive_chmod_symbolic_uses_default_mode_for_missing_permission() {
    setup();
    let path = "chmod_missing_mode_symbolic.pna";
    {
        let mut archive = Archive::write_header(File::create(path).unwrap()).unwrap();

        let mut file = EntryBuilder::new_file("file.txt".into(), WriteOptions::store()).unwrap();
        file.write_all(b"file").unwrap();
        archive.add_entry(file.build().unwrap()).unwrap();

        archive
            .add_entry(EntryBuilder::new_dir("dir".into()).build().unwrap())
            .unwrap();

        archive
            .add_entry(
                EntryBuilder::new_symlink("link.txt".into(), "file.txt".into())
                    .unwrap()
                    .build()
                    .unwrap(),
            )
            .unwrap();

        archive
            .add_entry(
                EntryBuilder::new_hard_link("hard.txt".into(), "file.txt".into())
                    .unwrap()
                    .build()
                    .unwrap(),
            )
            .unwrap();

        archive.finalize().unwrap();
    }

    cli::Cli::try_parse_from([
        "pna",
        "--quiet",
        "experimental",
        "chmod",
        "-f",
        path,
        "a-r",
        "file.txt",
        "dir",
        "link.txt",
        "hard.txt",
    ])
    .unwrap()
    .execute()
    .unwrap();

    archive::for_each_entry(path, |entry| {
        let expected = match entry.header().path().as_str() {
            "dir" => 0o311,
            "file.txt" | "link.txt" | "hard.txt" => 0o200,
            other => panic!("unexpected entry: {other}"),
        };
        assert_eq!(
            entry.metadata().permission_mode().unwrap().get() & 0o777,
            expected
        );
    })
    .unwrap();
}