standard-plugin-cli 0.1.1

standard-plugin: scaffold, build, check and pack Standard Code plugins
Documentation
//! `standard-plugin pack`: the deterministic account package of a plugin
//! directory (`standard_plugin_manifest::package`): `manifest.json` at the
//! root and each built half (a UI plugin, its companion daemon, or both)
//! under `ui/` and `daemon/` with its runtime manifest, component, and the
//! browser artifact when built. The same bundle always packs to the same
//! bytes; the SHA-256 of the tarball, in lowercase hex, is the package's
//! identity on the account.

use std::path::Path;

use standard_plugin_manifest::package::{AccountManifest, halves_from_dir, pack, sha256_hex};

/// A packed package and what the account will record about it.
#[derive(Clone, Debug)]
pub struct Packed {
    pub bytes: Vec<u8>,
    pub manifest: AccountManifest,
}

/// Packs the built halves in `dir` (a plugin crate, a bundle directory, or
/// a companion pair's parent).
pub fn package(dir: &Path) -> Result<Packed, String> {
    let (ui, daemon) = halves_from_dir(dir).map_err(|error| error.to_string())?;
    let bytes = pack(ui.as_ref(), daemon.as_ref()).map_err(|error| error.to_string())?;
    let manifest =
        standard_plugin_manifest::package::account_manifest(ui.as_ref(), daemon.as_ref())
            .map_err(|error| error.to_string())?;
    Ok(Packed { bytes, manifest })
}

/// The package's content hash: lowercase hex SHA-256 of its bytes.
pub fn content_hash(bytes: &[u8]) -> String {
    sha256_hex(bytes)
}

/// The entry names of a packed package, in order.
pub fn entry_names(bytes: &[u8]) -> Result<Vec<String>, String> {
    let mut names = Vec::new();
    let mut offset = 0;
    while offset + 512 <= bytes.len() {
        let header = &bytes[offset..offset + 512];
        if header.iter().all(|byte| *byte == 0) {
            break;
        }
        let text = |range: std::ops::Range<usize>| {
            let field = &header[range];
            let end = field.iter().position(|b| *b == 0).unwrap_or(field.len());
            String::from_utf8_lossy(&field[..end]).into_owned()
        };
        let size = usize::from_str_radix(text(124..136).trim(), 8)
            .map_err(|_| "malformed tar size".to_string())?;
        let (name, prefix) = (text(0..100), text(345..500));
        names.push(if prefix.is_empty() {
            name
        } else {
            format!("{prefix}/{name}")
        });
        offset += 512 + size.div_ceil(512) * 512;
    }
    Ok(names)
}