Skip to main content

standard_plugin_cli/
pack.rs

1//! `standard-plugin pack`: the deterministic account package of a plugin
2//! directory (`standard_plugin_manifest::package`): `manifest.json` at the
3//! root and each built half (a UI plugin, its companion daemon, or both)
4//! under `ui/` and `daemon/` with its runtime manifest, component, and the
5//! browser artifact when built. The same bundle always packs to the same
6//! bytes; the SHA-256 of the tarball, in lowercase hex, is the package's
7//! identity on the account.
8
9use std::path::Path;
10
11use standard_plugin_manifest::package::{AccountManifest, halves_from_dir, pack, sha256_hex};
12
13/// A packed package and what the account will record about it.
14#[derive(Clone, Debug)]
15pub struct Packed {
16    pub bytes: Vec<u8>,
17    pub manifest: AccountManifest,
18}
19
20/// Packs the built halves in `dir` (a plugin crate, a bundle directory, or
21/// a companion pair's parent).
22pub fn package(dir: &Path) -> Result<Packed, String> {
23    let (ui, daemon) = halves_from_dir(dir).map_err(|error| error.to_string())?;
24    let bytes = pack(ui.as_ref(), daemon.as_ref()).map_err(|error| error.to_string())?;
25    let manifest =
26        standard_plugin_manifest::package::account_manifest(ui.as_ref(), daemon.as_ref())
27            .map_err(|error| error.to_string())?;
28    Ok(Packed { bytes, manifest })
29}
30
31/// The package's content hash: lowercase hex SHA-256 of its bytes.
32pub fn content_hash(bytes: &[u8]) -> String {
33    sha256_hex(bytes)
34}
35
36/// The entry names of a packed package, in order.
37pub fn entry_names(bytes: &[u8]) -> Result<Vec<String>, String> {
38    let mut names = Vec::new();
39    let mut offset = 0;
40    while offset + 512 <= bytes.len() {
41        let header = &bytes[offset..offset + 512];
42        if header.iter().all(|byte| *byte == 0) {
43            break;
44        }
45        let text = |range: std::ops::Range<usize>| {
46            let field = &header[range];
47            let end = field.iter().position(|b| *b == 0).unwrap_or(field.len());
48            String::from_utf8_lossy(&field[..end]).into_owned()
49        };
50        let size = usize::from_str_radix(text(124..136).trim(), 8)
51            .map_err(|_| "malformed tar size".to_string())?;
52        let (name, prefix) = (text(0..100), text(345..500));
53        names.push(if prefix.is_empty() {
54            name
55        } else {
56            format!("{prefix}/{name}")
57        });
58        offset += 512 + size.div_ceil(512) * 512;
59    }
60    Ok(names)
61}