debrepo 0.4.0

Library for manifest-driven Debian/Ubuntu bootstrap and APT archive resolution.
Documentation
use {
    debrepo::{
        control::{
            ControlFile, ControlParser, ControlStanza, Field, FindFields, MutableControlField,
            MutableControlFile, MutableControlStanza, ParseError,
        },
        PackageOrigin, Packages,
    },
    serde::{Deserialize, Serialize},
};

fn sample_stanza_src() -> &'static str {
    "Package: demo
Architecture: amd64
Version: 1.0
Description:
 Demo package
"
}

#[derive(Serialize, Deserialize)]
struct StanzaWire {
    stanza: MutableControlStanza,
}

#[test]
fn parse_error_formats_and_converts_to_io_error() {
    let static_err = ParseError::from("static parse error");
    assert_eq!(static_err.to_string(), "static parse error");

    let owned_err = ParseError::from("owned parse error".to_string());
    assert_eq!(owned_err.to_string(), "owned parse error");

    let io_err: std::io::Error = owned_err.into();
    assert_eq!(io_err.kind(), std::io::ErrorKind::InvalidData);
    assert_eq!(io_err.to_string(), "owned parse error");
}

#[test]
fn control_fields_display_and_trait_accessors_cover_inline_and_multiline_values() {
    let mut parser = ControlParser::new("Package: demo\nDescription:\n Long text\n");
    let package = parser.field().unwrap().unwrap();
    assert!(package.is_a("package"));
    assert_eq!(package.name(), "Package");
    assert_eq!(package.value(), "demo");
    assert_eq!(package.to_string(), "Package: demo\n");

    let description = parser.field().unwrap().unwrap();
    assert!(description.is_a("description"));
    assert_eq!(description.name(), "Description");
    assert_eq!(description.value(), "\n Long text");
    assert_eq!(description.to_string(), "Description:\n Long text\n");

    let mut mutable = MutableControlField::new("Homepage", "https://example.invalid");
    assert!(mutable.is_a("homepage"));
    assert_eq!(mutable.name(), "Homepage");
    assert_eq!(mutable.value(), "https://example.invalid");
    assert_eq!(mutable.to_string(), "Homepage: https://example.invalid\n");

    mutable.set("\n Detail line");
    assert_eq!(mutable.value(), "\n Detail line");
    assert_eq!(mutable.to_string(), "Homepage:\n Detail line\n");

    let from_owned = MutableControlField::from(package);
    assert_eq!(from_owned.name(), "Package");
    assert_eq!(from_owned.value(), "demo");

    let from_borrowed = MutableControlField::from(&description);
    assert_eq!(from_borrowed.name(), "Description");
    assert_eq!(from_borrowed.value(), "\n Long text");
}

#[test]
fn control_stanza_roundtrips_and_rejects_empty_input() {
    let stanza = ControlStanza::parse(sample_stanza_src()).unwrap();
    assert_eq!(stanza.len(), sample_stanza_src().len());
    assert_eq!(stanza.field("package"), Some("demo"));
    assert_eq!(stanza.field("missing"), None);

    let fields: Vec<_> = stanza.fields().map(|field| field.name()).collect();
    assert_eq!(
        fields,
        vec!["Package", "Architecture", "Version", "Description"]
    );
    assert_eq!(stanza.to_string(), sample_stanza_src());

    let err = ControlStanza::parse("\n").unwrap_err();
    assert_eq!(err.to_string(), "Empty control stanza");
}

#[test]
fn mutable_stanza_mutation_methods_cover_insert_update_remove_retain_and_package_name() {
    let empty = MutableControlStanza::default();
    assert!(empty.is_empty());
    assert_eq!(empty.len(), 0);

    let mut stanza = MutableControlStanza::parse(sample_stanza_src()).unwrap();
    assert!(!stanza.is_empty());
    assert_eq!(stanza.field("architecture"), Some("amd64"));

    stanza
        .set("Architecture", "arm64".to_string())
        .set("Priority", "required".to_string())
        .set("X-Extra", "kept".to_string());
    assert_eq!(stanza.field("Architecture"), Some("arm64"));
    assert_eq!(stanza.field("Priority"), Some("required"));
    assert_eq!(stanza.package_name().unwrap(), "demo_1.0_arm64");

    stanza.remove("Priority").remove("Not-There");
    assert_eq!(stanza.field("Priority"), None);

    stanza.retain(|field| !field.is_a("X-Extra"));
    assert_eq!(stanza.field("X-Extra"), None);

    let field_names: Vec<_> = stanza
        .fields()
        .map(|field| field.name().into_owned())
        .collect();
    assert_eq!(
        field_names,
        vec!["Package", "Architecture", "Version", "Description"]
    );

    let copied = MutableControlStanza::from(&ControlStanza::parse(sample_stanza_src()).unwrap());
    assert_eq!(copied.to_string(), sample_stanza_src());

    let mut missing = MutableControlStanza::new();
    missing.set("Package", "demo".to_string());
    missing.set("Version", "1.0".to_string());
    assert_eq!(
        missing.package_name().unwrap_err().to_string(),
        "Architecture"
    );
}

#[test]
fn mutable_stanza_sorting_covers_custom_sort_and_dpkg_order_tiebreaks() {
    let mut stanza = MutableControlStanza::new();
    stanza.set("z-last", "1".to_string());
    stanza.set("alpha", "2".to_string());
    stanza.set("Beta", "3".to_string());

    stanza.sort_fields_by_name(|left, right| right.cmp(left));
    let sorted: Vec<_> = stanza
        .fields()
        .map(|field| field.name().into_owned())
        .collect();
    assert_eq!(sorted, vec!["z-last", "alpha", "Beta"]);

    let mut dpkg = MutableControlStanza::new();
    dpkg.set("z-last", "1".to_string());
    dpkg.set("description", "\n body".to_string());
    dpkg.set("Package", "demo".to_string());
    dpkg.set("Version", "1.0".to_string());
    dpkg.set("architecture", "amd64".to_string());
    dpkg.set("beta", "2".to_string());
    dpkg.set("Alpha", "3".to_string());

    dpkg.sort_fields_deb_order();
    let names: Vec<_> = dpkg
        .fields()
        .map(|field| field.name().into_owned())
        .collect();
    assert_eq!(
        names,
        vec![
            "Package",
            "Version",
            "architecture",
            "description",
            "Alpha",
            "beta",
            "z-last",
        ]
    );
}

#[test]
fn find_fields_supports_single_pair_triple_for_immutable_and_mutable_fields() {
    let stanza = ControlStanza::parse(sample_stanza_src()).unwrap();

    let package = stanza.fields().cloned().find_fields("Package").unwrap();
    assert_eq!(package, "demo");
    assert_eq!(
        stanza
            .fields()
            .cloned()
            .find_fields(("Package", "Version"))
            .unwrap(),
        ("demo", "1.0")
    );
    assert_eq!(
        stanza
            .fields()
            .cloned()
            .find_fields(("Architecture", "Package", "Version"))
            .unwrap(),
        ("amd64", "demo", "1.0")
    );
    assert_eq!(
        stanza.fields().cloned().find_fields("Missing").unwrap_err(),
        "Missing"
    );
    assert_eq!(
        stanza
            .fields()
            .cloned()
            .find_fields(("Package", "Missing"))
            .unwrap_err(),
        "Missing"
    );
    assert_eq!(
        stanza
            .fields()
            .cloned()
            .find_fields(("Missing-A", "Package", "Version"))
            .unwrap_err(),
        "Missing-A"
    );

    let mutable = MutableControlStanza::parse(sample_stanza_src()).unwrap();
    let borrowed = mutable.fields().find_fields("Version").unwrap();
    assert_eq!(borrowed, "1.0");
    assert_eq!(
        mutable
            .fields()
            .find_fields(("Package", "Architecture"))
            .unwrap(),
        ("demo".into(), "amd64".into())
    );
    assert_eq!(
        mutable
            .fields()
            .find_fields(("Architecture", "Package", "Version"))
            .unwrap(),
        ("amd64".into(), "demo".into(), "1.0".into())
    );

    let owned_fields = vec![
        MutableControlField::new("Package", "demo"),
        MutableControlField::new("Version", "1.0"),
    ];
    assert_eq!(
        owned_fields
            .clone()
            .into_iter()
            .find_fields(("Package", "Version"))
            .unwrap(),
        ("demo".into(), "1.0".into())
    );
    assert_eq!(
        owned_fields
            .into_iter()
            .find_fields("Architecture")
            .unwrap_err(),
        "Architecture"
    );
}

#[test]
fn mutable_stanza_serde_roundtrip_and_invalid_deserialize() {
    let wire = StanzaWire {
        stanza: MutableControlStanza::parse(sample_stanza_src()).unwrap(),
    };
    let json = serde_json::to_string(&wire).unwrap();
    assert!(json.contains("Package: demo"));

    let roundtrip: StanzaWire = serde_json::from_str(&json).unwrap();
    assert_eq!(roundtrip.stanza.to_string(), sample_stanza_src());

    let err = serde_json::from_str::<StanzaWire>(r#"{"stanza":""}"#)
        .err()
        .unwrap();
    assert!(err.to_string().contains("Empty control stanza"));
}

#[test]
fn mutable_control_file_collection_methods_and_bytes_conversion_work() {
    let mut file = MutableControlFile::default();
    assert_eq!(file.stanzas().count(), 0);

    let first = file.new_stanza();
    first.set("Package", "one".to_string());
    first.set("Architecture", "amd64".to_string());
    first.set("Version", "1.0".to_string());

    file.add(
        MutableControlStanza::parse("Package: two\nArchitecture: all\nVersion: 2.0\n").unwrap(),
    );
    assert_eq!(file.stanzas().count(), 2);

    file.set_at(
        1,
        MutableControlStanza::parse("Package: replacement\nArchitecture: all\nVersion: 3.0\n")
            .unwrap(),
    );
    assert_eq!(
        file.stanzas().nth(1).unwrap().field("Package"),
        Some("replacement")
    );

    let removed = file.remove_at(0);
    assert_eq!(removed.field("Package"), Some("one"));

    let mut collected: MutableControlFile =
        vec![
            MutableControlStanza::parse("Package: alpha\nArchitecture: amd64\nVersion: 1.0\n")
                .unwrap(),
        ]
        .into_iter()
        .collect();
    collected.extend(vec![MutableControlStanza::parse(
        "Package: beta\nArchitecture: all\nVersion: 2.0\n",
    )
    .unwrap()]);

    let rendered = collected.to_string();
    assert_eq!(
        rendered,
        "Package: alpha\nArchitecture: amd64\nVersion: 1.0\n\nPackage: beta\nArchitecture: all\nVersion: 2.0\n\n"
    );
    let bytes: Vec<u8> = collected.into();
    assert_eq!(String::from_utf8(bytes).unwrap(), rendered);
}

#[test]
fn mutable_control_file_try_from_packages_parses_valid_package_records() {
    let mut file = MutableControlFile::new();
    let stanza = file.new_stanza();
    stanza.set("Package", "demo".to_string());
    stanza.set("Architecture", "amd64".to_string());
    stanza.set("Version", "1.0".to_string());
    stanza.set("Filename", "pool/main/d/demo_1.0_amd64.deb".to_string());
    stanza.set("Size", "42".to_string());
    stanza.set(
        "SHA256",
        "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef".to_string(),
    );

    let packages = Packages::try_from(file).unwrap();
    assert_eq!(packages.origin(), PackageOrigin::Unknown);
    assert_eq!(packages.prio(), 500);
    assert_eq!(packages.packages().count(), 1);

    let package = packages.package_by_name("demo").unwrap();
    assert_eq!(package.name(), "demo");
    assert_eq!(package.arch(), "amd64");
    assert_eq!(package.version().unwrap().to_string(), "1.0");
}

#[test]
fn control_file_parse_and_display_cover_multi_stanza_files() {
    let src = "Package: one\nArchitecture: amd64\nVersion: 1.0\n\nPackage: two\nArchitecture: all\nVersion: 2.0\n";
    let file = ControlFile::parse(src).unwrap();
    assert_eq!(file.stanzas().count(), 2);
    assert_eq!(file.stanzas().next().unwrap().field("Package"), Some("one"));
    assert_eq!(
        file.stanzas().nth(1).unwrap().field("Architecture"),
        Some("all")
    );
    assert_eq!(
        file.to_string(),
        "Package: one\nArchitecture: amd64\nVersion: 1.0\nPackage: two\nArchitecture: all\nVersion: 2.0\n\n"
    );
}

#[test]
fn control_parser_handles_stanza_boundaries_blank_continuations_and_error_paths() {
    let mut parser = ControlParser::new(
        "Package: one\nArchitecture: amd64\n\nPackage: two\nArchitecture: all\n",
    );
    assert_eq!(parser.field().unwrap().unwrap().name(), "Package");
    assert_eq!(parser.field().unwrap().unwrap().name(), "Architecture");
    assert!(parser.field().unwrap().is_none());
    assert_eq!(parser.field().unwrap().unwrap().value(), "two");

    let mut parser = ControlParser::new("Field: value\n \nNext: item\n");
    assert_eq!(parser.field().unwrap().unwrap().value(), "value\n ");
    assert_eq!(parser.field().unwrap().unwrap().name(), "Next");

    let mut invalid_first = ControlParser::new("#Field: value\n");
    assert_eq!(
        invalid_first.field().unwrap_err().to_string(),
        "Invalid field name #Field: value\n"
    );

    let mut invalid_inner = ControlParser::new("Bad Name: value\n");
    assert_eq!(
        invalid_inner.field().unwrap_err().to_string(),
        "Invalid field name Bad Name: value\n"
    );

    let mut unterminated = ControlParser::new("Field");
    assert_eq!(
        unterminated.field().unwrap_err().to_string(),
        "unterminated field name Field"
    );
}