use crate::errors::PackError;
use crate::types::{PackInputs, PackOutput};
use crate::workspace::synthesize_workspace;
use crate::zip_writer::zip_entries;
use sha2::{Digest, Sha256};
pub fn build_pack(inputs: &PackInputs<'_>) -> Result<PackOutput, PackError> {
let entries = synthesize_workspace(inputs)?;
let bytes = zip_entries(&entries)?;
let sha256 = hex_sha256(&bytes);
let filename = format!(
"{}-{}.gtpack",
inputs.config.metadata.name, inputs.config.metadata.version
);
Ok(PackOutput {
filename,
bytes,
sha256,
})
}
fn hex_sha256(bytes: &[u8]) -> String {
let mut h = Sha256::new();
h.update(bytes);
let out = h.finalize();
const HEX: &[u8; 16] = b"0123456789abcdef";
let mut s = String::with_capacity(64);
for b in out {
s.push(HEX[(b >> 4) as usize] as char);
s.push(HEX[(b & 0x0f) as usize] as char);
}
s
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::{CardContentEntry, FlowEntry, I18nConfig, StandardConfig, StandardMetadata};
use serde_json::json;
fn min_inputs<'a>(
cfg: &'a StandardConfig,
flows: &'a [FlowEntry],
cards: &'a [CardContentEntry],
) -> PackInputs<'a> {
PackInputs {
config: cfg,
flows,
cards,
assets: &[],
capabilities: &[],
}
}
#[test]
fn happy_path() {
let cfg = StandardConfig {
metadata: StandardMetadata {
name: "demo".into(),
version: "0.1.0".into(),
author: None,
},
channels: vec!["webchat".into()],
embed_ui: "webchat".into(),
i18n: I18nConfig::default(),
format: "gtpack-legacy".into(),
};
let flows = vec![FlowEntry {
name: "main".into(),
yaml: "schema_version: 2".into(),
}];
let cards = vec![CardContentEntry {
id: "welcome".into(),
json: json!({"type":"AdaptiveCard"}),
}];
let out = build_pack(&min_inputs(&cfg, &flows, &cards)).unwrap();
assert_eq!(out.filename, "demo-0.1.0.gtpack");
assert_eq!(out.sha256.len(), 64);
assert!(!out.bytes.is_empty());
}
#[test]
fn deterministic_sha() {
let cfg = StandardConfig {
metadata: StandardMetadata {
name: "x".into(),
version: "1.0.0".into(),
author: None,
},
channels: vec![],
embed_ui: "none".into(),
i18n: I18nConfig::default(),
format: "gtpack-legacy".into(),
};
let a = build_pack(&min_inputs(&cfg, &[], &[])).unwrap();
let b = build_pack(&min_inputs(&cfg, &[], &[])).unwrap();
assert_eq!(a.sha256, b.sha256);
}
#[test]
fn pack_includes_manifest_cbor() {
use greentic_types::{PackKind, decode_pack_manifest};
use std::io::Read;
let cfg = StandardConfig {
metadata: StandardMetadata {
name: "demo".into(),
version: "0.1.0".into(),
author: Some("Acme".into()),
},
channels: vec!["webchat".into()],
embed_ui: "webchat".into(),
i18n: I18nConfig::default(),
format: "gtpack-legacy".into(),
};
let out = build_pack(&min_inputs(&cfg, &[], &[])).unwrap();
let cursor = std::io::Cursor::new(&out.bytes);
let mut zip = zip::ZipArchive::new(cursor).unwrap();
let mut entry = zip.by_name("manifest.cbor").unwrap();
let mut buf = Vec::new();
entry.read_to_end(&mut buf).unwrap();
let m = decode_pack_manifest(&buf).unwrap();
assert_eq!(m.pack_id.as_str(), "demo");
assert_eq!(m.version.to_string(), "0.1.0");
assert!(matches!(m.kind, PackKind::Application));
assert_eq!(m.publisher, "Acme");
assert_eq!(m.schema_version, "pack-v1");
assert!(m.components.is_empty());
assert!(m.flows.is_empty());
}
}