rd-rds 0.5.0

Read-only reader for the subset of R's RDS serialization format used by installed-package help databases
Documentation
#![cfg(feature = "lazyload")]

use std::{
    fs,
    path::PathBuf,
    sync::atomic::{AtomicUsize, Ordering},
};

use rd_rds::package::{
    FormalsInspection, FormalsNotApplicable, FormalsUnavailable, InstalledCodeDb,
    InstalledCodeOptions, StoredKind,
};

fn fixture(name: &str) -> PathBuf {
    PathBuf::from(env!("CARGO_MANIFEST_DIR"))
        .join("tests/fixtures/data/lazyload")
        .join(name)
}

fn installed_fixture() -> PathBuf {
    static NEXT_FIXTURE: AtomicUsize = AtomicUsize::new(0);
    let package_dir = std::env::temp_dir()
        .join(format!(
            "rd-rds-installed-code-{}-{}",
            std::process::id(),
            NEXT_FIXTURE.fetch_add(1, Ordering::Relaxed)
        ))
        .join("lazyfixture");
    let _ = fs::remove_dir_all(&package_dir);
    fs::create_dir_all(package_dir.join("R")).unwrap();
    fs::copy(
        fixture("installed/lazyfixture.rdx"),
        package_dir.join("R/lazyfixture.rdx"),
    )
    .unwrap();
    fs::copy(
        fixture("installed/lazyfixture.rdb"),
        package_dir.join("R/lazyfixture.rdb"),
    )
    .unwrap();
    package_dir
}

#[test]
fn opens_explicit_installed_shape_and_inspects_unique_bindings() {
    let package_dir = installed_fixture();
    let db = InstalledCodeDb::open(&package_dir).unwrap();
    assert_eq!(db.package_dir(), package_dir);
    assert_eq!(db.index_path(), package_dir.join("R/lazyfixture.rdx"));
    assert_eq!(db.data_path(), package_dir.join("R/lazyfixture.rdb"));
    assert_eq!(db.provenance().package_name(), "lazyfixture");
    assert_eq!(db.compression(), rd_rds::lazyload::Compression::Zlib);
    assert!(!db.stored_bindings().is_empty());
    assert!(
        db.stored_bindings()
            .iter()
            .any(|binding| binding.name() == "lazy_fixture_value")
    );
    assert_eq!(
        db.stored_bindings()
            .iter()
            .map(|binding| binding.name())
            .collect::<Vec<_>>(),
        [
            ".__NAMESPACE__.",
            ".__S3MethodsTable__.",
            ".packageName",
            "lazy_fixture",
            "lazy_fixture_value",
        ]
    );

    let closure = db.inspect_stored_binding("lazy_fixture").unwrap();
    assert_eq!(closure.kind(), StoredKind::Closure);
    let FormalsInspection::Available(formals) = closure.formals() else {
        panic!("expected closure formals");
    };
    assert_eq!(formals.len(), 1);
    assert_eq!(formals[0].name(), "value");
    assert!(matches!(
        formals[0].default(),
        rd_rds::package::DefaultPresence::Present
    ));
    assert!(matches!(
        closure.extent(),
        rd_rds::package::InspectionExtent::ThroughFormals { .. }
    ));
    assert_eq!(
        closure.extent().body_validation(),
        Some(rd_rds::package::BodyValidation::NotValidated)
    );

    let inspection = db.inspect_stored_binding("lazy_fixture_value").unwrap();
    assert_eq!(inspection.kind(), StoredKind::Integer);
    assert!(matches!(
        inspection.formals(),
        FormalsInspection::NotApplicable(FormalsNotApplicable::NonClosure)
    ));
    assert!(inspection.extent().body_validation().is_none());

    assert!(matches!(
        db.inspect_stored_binding("missing"),
        Err(rd_rds::package::InstalledCodeError::UnknownStoredBinding { name }) if name == "missing"
    ));
    let _ = fs::remove_dir_all(package_dir);
}

#[test]
fn loaded_record_inspection_matches_package_results_and_limits() {
    use rd_rds::inspection::{self, InspectionOptions};

    let package_dir = installed_fixture();
    let records = rd_rds::lazyload::LazyLoadDb::open(
        package_dir.join("R/lazyfixture.rdx"),
        package_dir.join("R/lazyfixture.rdb"),
    )
    .unwrap();
    let db = InstalledCodeDb::open(&package_dir).unwrap();
    for name in ["lazy_fixture", "lazy_fixture_value"] {
        let record = records.read(name).unwrap();
        let result: rd_rds::package::StoredObjectInspection =
            inspection::inspect(record.decompressed_bytes()).unwrap();
        assert_eq!(result, db.inspect_stored_binding(name).unwrap());
    }

    let formal_limited = InstalledCodeDb::open_with_options(
        &package_dir,
        InstalledCodeOptions::default().max_formals(0),
    )
    .unwrap();
    let record = records.read("lazy_fixture").unwrap();
    let result = inspection::inspect_with_options(
        record.decompressed_bytes(),
        InspectionOptions::default().max_formals(0),
    )
    .unwrap();
    assert!(matches!(
        result.formals(),
        FormalsInspection::Unavailable(_)
    ));
    assert_eq!(
        result,
        formal_limited
            .inspect_stored_binding("lazy_fixture")
            .unwrap()
    );

    let limited = InstalledCodeDb::open_with_options(
        &package_dir,
        InstalledCodeOptions::default().max_bytes_visited(2),
    )
    .unwrap();
    let error = inspection::inspect_with_options(
        record.decompressed_bytes(),
        InspectionOptions::default().max_bytes_visited(2),
    )
    .unwrap_err();
    let rd_rds::package::InstalledCodeError::Inspection { failure, name } =
        limited.inspect_stored_binding("lazy_fixture").unwrap_err()
    else {
        panic!("expected an inspection failure");
    };
    assert_eq!(name, "lazy_fixture");
    assert_eq!(failure, error);
    drop((records, db, formal_limited, limited));
    fs::remove_dir_all(package_dir).unwrap();
}

#[test]
fn missing_index_is_distinct_from_missing_data_file() {
    let package_dir = std::env::temp_dir()
        .join(format!(
            "rd-rds-installed-code-missing-{}",
            std::process::id()
        ))
        .join("missing");
    let _ = fs::remove_dir_all(&package_dir);
    fs::create_dir_all(package_dir.join("R")).unwrap();
    assert!(matches!(
        InstalledCodeDb::open(&package_dir),
        Err(rd_rds::package::InstalledCodeError::NoCodeDatabase { .. })
    ));
    fs::copy(
        fixture("installed/lazyfixture.rdx"),
        package_dir.join("R/missing.rdx"),
    )
    .unwrap();
    assert!(matches!(
        InstalledCodeDb::open(&package_dir),
        Err(rd_rds::package::InstalledCodeError::Open { .. })
    ));
    let _ = fs::remove_dir_all(package_dir);
}

#[test]
fn duplicate_names_are_reported_without_selecting_last_wins() {
    let package_dir = std::env::temp_dir()
        .join(format!(
            "rd-rds-installed-code-duplicate-{}",
            std::process::id()
        ))
        .join("zlib");
    let _ = fs::remove_dir_all(&package_dir);
    fs::create_dir_all(package_dir.join("R")).unwrap();
    fs::copy(fixture("zlib.rdx"), package_dir.join("R/zlib.rdx")).unwrap();
    fs::copy(fixture("zlib.rdb"), package_dir.join("R/zlib.rdb")).unwrap();
    let db = InstalledCodeDb::open(&package_dir).unwrap();
    assert_eq!(
        db.stored_bindings()
            .iter()
            .map(|binding| binding.name())
            .collect::<Vec<_>>(),
        ["duplicate", "other", "duplicate"]
    );
    assert!(matches!(
        db.inspect_stored_binding("duplicate"),
        Err(rd_rds::package::InstalledCodeError::AmbiguousStoredBinding { count: 2, .. })
    ));
    let _ = fs::remove_dir_all(package_dir);
}

#[test]
fn replacement_with_changed_metadata_is_reported_and_new_open_gets_a_new_generation() {
    let package_dir = installed_fixture();
    let db = InstalledCodeDb::open(&package_dir).unwrap();
    let old_generation = db.provenance().generation();
    let data_path = package_dir.join("R/lazyfixture.rdb");
    let replacement = package_dir.join("R/replacement.rdb");
    fs::copy(fixture("installed/lazyfixture.rdb"), &replacement).unwrap();
    let mut replacement_file = fs::OpenOptions::new()
        .append(true)
        .open(&replacement)
        .unwrap();
    std::io::Write::write_all(&mut replacement_file, &[0]).unwrap();
    drop(replacement_file);
    fs::remove_file(&data_path).unwrap();
    fs::rename(replacement, &data_path).unwrap();
    assert!(matches!(
        db.inspect_stored_binding("lazy_fixture_value"),
        Err(rd_rds::package::InstalledCodeError::DatabaseChanged { .. })
    ));

    let replacement_db = InstalledCodeDb::open(&package_dir).unwrap();
    assert_ne!(old_generation, replacement_db.provenance().generation());
    let _ = fs::remove_dir_all(package_dir);
}

#[cfg(unix)]
#[test]
fn unix_same_size_replacement_is_reported_by_device_and_inode() {
    let package_dir = installed_fixture();
    let db = InstalledCodeDb::open(&package_dir).unwrap();
    let old_generation = db.provenance().generation();
    let data_path = package_dir.join("R/lazyfixture.rdb");
    let replacement = package_dir.join("R/replacement.rdb");
    fs::copy(fixture("installed/lazyfixture.rdb"), &replacement).unwrap();
    fs::remove_file(&data_path).unwrap();
    fs::rename(replacement, &data_path).unwrap();
    assert!(matches!(
        db.inspect_stored_binding("lazy_fixture_value"),
        Err(rd_rds::package::InstalledCodeError::DatabaseChanged { .. })
    ));

    let replacement_db = InstalledCodeDb::open(&package_dir).unwrap();
    assert_ne!(old_generation, replacement_db.provenance().generation());
    let _ = fs::remove_dir_all(package_dir);
}

#[test]
fn inspection_and_container_limits_remain_structured() {
    let package_dir = installed_fixture();
    let index_limited = InstalledCodeDb::open_with_options(
        &package_dir,
        InstalledCodeOptions::default().max_index_bytes(1),
    )
    .unwrap_err();
    assert!(matches!(
        index_limited,
        rd_rds::package::InstalledCodeError::Index {
            source: rd_rds::lazyload::Error::IndexSizeLimitExceeded { limit: 1 },
            ..
        }
    ));

    let limited = InstalledCodeDb::open_with_options(
        &package_dir,
        InstalledCodeOptions::default().max_formals(0),
    )
    .unwrap();
    let inspection = limited.inspect_stored_binding("lazy_fixture").unwrap();
    assert!(matches!(
        inspection.formals(),
        FormalsInspection::Unavailable(FormalsUnavailable::Prefix(_))
    ));

    let byte_limited = InstalledCodeDb::open_with_options(
        &package_dir,
        InstalledCodeOptions::default().max_bytes_visited(2),
    )
    .unwrap();
    assert!(matches!(
        byte_limited.inspect_stored_binding("lazy_fixture"),
        Err(rd_rds::package::InstalledCodeError::Inspection { .. })
    ));

    let record_limited = InstalledCodeDb::open_with_options(
        &package_dir,
        InstalledCodeOptions::default().max_decompressed_record_bytes(1),
    )
    .unwrap();
    assert!(matches!(
        record_limited.inspect_stored_binding("lazy_fixture_value"),
        Err(rd_rds::package::InstalledCodeError::Record { .. })
    ));
    let _ = fs::remove_dir_all(package_dir);
}

#[test]
fn empty_variables_database_is_not_missing() {
    let package_dir = std::env::temp_dir()
        .join(format!(
            "rd-rds-installed-code-empty-{}",
            std::process::id()
        ))
        .join("empty");
    let _ = fs::remove_dir_all(&package_dir);
    fs::create_dir_all(package_dir.join("R")).unwrap();
    fs::copy(
        fixture("installed/empty.rdx"),
        package_dir.join("R/empty.rdx"),
    )
    .unwrap();
    fs::copy(
        fixture("installed/empty.rdb"),
        package_dir.join("R/empty.rdb"),
    )
    .unwrap();
    let db = InstalledCodeDb::open(&package_dir).unwrap();
    assert!(db.stored_bindings().is_empty());
    assert!(matches!(
        db.inspect_stored_binding("anything"),
        Err(rd_rds::package::InstalledCodeError::UnknownStoredBinding { .. })
    ));
    let _ = fs::remove_dir_all(package_dir);
}

#[test]
fn one_bad_record_does_not_hide_other_records_or_the_index() {
    let package_dir = installed_fixture();
    let data_path = package_dir.join("R/lazyfixture.rdb");
    let low_level =
        rd_rds::lazyload::LazyLoadDb::open(package_dir.join("R/lazyfixture.rdx"), &data_path)
            .unwrap();
    let bad_location = low_level
        .variable("lazy_fixture_value")
        .unwrap()
        .location()
        .unwrap();
    let mut data = fs::read(&data_path).unwrap();
    data[bad_location.offset() as usize] ^= 0xff;
    fs::write(&data_path, data).unwrap();
    let db = InstalledCodeDb::open(&package_dir).unwrap();

    assert!(matches!(
        db.inspect_stored_binding("lazy_fixture_value"),
        Err(rd_rds::package::InstalledCodeError::Record { .. })
    ));
    assert_eq!(db.stored_bindings().len(), 5);
    let closure = db.inspect_stored_binding("lazy_fixture").unwrap();
    assert_eq!(closure.kind(), StoredKind::Closure);
    let _ = fs::remove_dir_all(package_dir);
}