empyrean-sys 0.10.0

Low-level FFI bindings to the libempyrean astrodynamics C ABI. For an ergonomic Rust API, use the empyrean crate.
Documentation
//! The pin that ties `checksums.txt`'s prebuilt binaries to the ABI
//! surface they were built from.
//!
//! `checksums.txt` pins the SHA-256 of each published
//! `libempyrean-<target>.tar.gz`. Those hashes prove the downloaded bytes
//! are the bytes the release served — they say nothing about whether that
//! library's struct layouts match the `include/empyrean.h` /
//! [`crate::bindings`] pair the caller is compiling against.
//! `EMPYREAN_ABI_VERSION` cannot close that gap either: it encodes the
//! base version, so every build inside one release cycle reports the same
//! number while `EmpyreanOrbit` can grow underneath it.
//!
//! So `checksums.txt` carries one more line — the SHA-256 of the
//! `include/empyrean.h` those pinned binaries were built from — and the
//! build script refuses to link a downloaded prebuilt when the header in
//! the checkout hashes to something else. See `build.rs`.
//!
//! This module holds only the parse and the comparison so both the build
//! script (via `#[path]`) and the crate's tests read one implementation.

/// Line prefix in `checksums.txt` carrying the SHA-256 of the
/// `include/empyrean.h` the pinned prebuilt binaries were built from.
///
/// It is written as a comment so the existing `<asset-stem> <sha256>`
/// readers — this crate's `target_asset` and the release workflow's
/// verification loop — skip it as they always have, and so the pin
/// travels in the same file, regenerated by the same job, as the hashes
/// it qualifies.
pub const HEADER_PIN_PREFIX: &str = "# Header-SHA256:";

/// The pinned header SHA-256, or `None` when `checksums.txt` records no
/// pin at all.
pub fn pinned_header_sha(checksums: &str) -> Option<&str> {
    checksums.lines().find_map(|line| {
        let rest = line.trim().strip_prefix(HEADER_PIN_PREFIX)?;
        let sha = rest.trim();
        (!sha.is_empty()).then_some(sha)
    })
}

/// Whether a prebuilt pinned against `pinned` may be linked against a
/// checkout whose `include/empyrean.h` hashes to `actual`.
///
/// Hex case is not significant; nothing else is tolerated. Two headers
/// that differ by one comment line describe the same ABI, but proving
/// that requires parsing C — so the rule is byte equality, which is what
/// the release workflow's own header-currency check already enforces on
/// the way out.
pub fn header_matches(pinned: &str, actual: &str) -> bool {
    pinned.eq_ignore_ascii_case(actual)
}

#[cfg(test)]
mod tests {
    use super::*;

    /// The real file, so a regeneration that drops the pin is caught
    /// here rather than by every consumer's build script at once.
    const CHECKSUMS: &str = include_str!("../checksums.txt");

    #[test]
    fn the_committed_checksums_carry_a_header_pin() {
        let sha = pinned_header_sha(CHECKSUMS).expect(
            "empyrean-sys/checksums.txt must carry a `# Header-SHA256:` line — build.rs \
             refuses the prebuilt download without one",
        );
        assert_eq!(sha.len(), 64, "header pin must be a 64-hex-digit SHA-256");
        assert!(
            sha.chars().all(|c| c.is_ascii_hexdigit()),
            "header pin must be hex, got {sha}"
        );
    }

    #[test]
    fn a_file_with_no_pin_reads_as_absent() {
        assert_eq!(
            pinned_header_sha("# Pinned for v0.10.0.\nlibempyrean-macos-aarch64 abc\n"),
            None,
        );
        // Present-but-empty is absent too: a truncated regeneration must
        // not read as "pinned to the empty string".
        assert_eq!(pinned_header_sha("# Header-SHA256:   \n"), None);
    }

    #[test]
    fn the_pin_is_read_out_of_its_comment_line() {
        let file = concat!(
            "# SHA-256 of each asset.\n",
            "# Header-SHA256: 1c1c51d1b0fd8108\n",
            "libempyrean-macos-aarch64 57b0b45a\n",
        );
        assert_eq!(pinned_header_sha(file), Some("1c1c51d1b0fd8108"));
    }

    #[test]
    fn matching_is_case_insensitive_hex_but_nothing_looser() {
        assert!(header_matches("ABCD", "abcd"));
        assert!(!header_matches("abcd", "abce"));
        assert!(!header_matches("abcd", ""));
    }
}