#![forbid(unsafe_code)]
use std::io::{Cursor, Read as _};
use std::path::{Path, PathBuf};
use anyhow::{Context, Result, bail};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use crate::extension_refs::{
ExtensionDependency, ExtensionDependencySource, PackExtensionsFile, read_extensions_file,
};
#[derive(Debug, Clone)]
pub struct ExtRef {
pub extension_id: String,
}
pub fn parse_ext_ref(raw: &str) -> Result<ExtRef> {
let rest = raw.strip_prefix("ext://").ok_or_else(|| {
anyhow::anyhow!("ext:// component ref must start with 'ext://' (got '{raw}')")
})?;
let (id, fragment) = rest.split_once('#').ok_or_else(|| {
anyhow::anyhow!(
"ext:// component ref must have the form 'ext://<id>#component' (got '{raw}')"
)
})?;
if fragment != "component" {
bail!("ext:// component ref fragment must be '#component' (got '#{fragment}')");
}
if id.trim().is_empty() {
bail!("ext:// component ref extension id must not be empty (got '{raw}')");
}
Ok(ExtRef {
extension_id: id.to_string(),
})
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GtxpackComponentSidecar {
pub component: GtxpackComponentEntry,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GtxpackComponentEntry {
pub id: String,
pub asset: String,
pub digest: String,
}
pub fn resolve_ext_component(pack_dir: &Path, raw_ref: &str) -> Result<(Vec<u8>, String)> {
let ext_ref = parse_ext_ref(raw_ref)?;
let extensions_path = pack_dir.join("pack.extensions.json");
let extensions = read_extensions_file(&extensions_path)
.with_context(|| format!("read pack.extensions.json from {}", pack_dir.display()))?;
let dep = find_extension_dep(&extensions, &ext_ref.extension_id).with_context(|| {
format!(
"ext:// component ref names extension '{}' not declared in pack.extensions.json",
ext_ref.extension_id
)
})?;
let gtxpack_path = resolve_extension_source(&dep.source)
.with_context(|| format!("resolve source for extension '{}'", dep.id))?;
extract_and_verify(&ext_ref.extension_id, >xpack_path)
}
fn find_extension_dep<'a>(
file: &'a PackExtensionsFile,
id: &str,
) -> Option<&'a ExtensionDependency> {
file.extensions.iter().find(|dep| dep.id == id)
}
fn resolve_extension_source(source: &ExtensionDependencySource) -> Result<PathBuf> {
let raw = source.reference.as_str();
if let Some(path_str) = raw.strip_prefix("file://") {
return Ok(PathBuf::from(path_str));
}
if !raw.contains("://") {
return Ok(PathBuf::from(raw));
}
bail!(
"ext:// component resolver Phase 1 only supports file:// or bare local extension sources, got '{raw}'"
);
}
fn extract_and_verify(extension_id: &str, gtxpack_path: &Path) -> Result<(Vec<u8>, String)> {
let zip_bytes = std::fs::read(gtxpack_path)
.with_context(|| format!("read extension .gtxpack at {}", gtxpack_path.display()))?;
let cursor = Cursor::new(zip_bytes);
let mut archive = zip::ZipArchive::new(cursor)
.with_context(|| format!("open ZIP archive {}", gtxpack_path.display()))?;
let sidecar: GtxpackComponentSidecar = {
let mut entry = archive.by_name("component.json").map_err(|_| {
anyhow::anyhow!(
"extension '{}' does not embed a runtime component: 'component.json' not found in {}",
extension_id,
gtxpack_path.display()
)
})?;
let mut buf = Vec::new();
entry
.read_to_end(&mut buf)
.with_context(|| format!("read component.json from {}", gtxpack_path.display()))?;
serde_json::from_slice(&buf)
.with_context(|| format!("parse component.json from {}", gtxpack_path.display()))?
};
let asset_path = sidecar.component.asset.as_str();
if asset_path.trim().is_empty() {
bail!(
"extension '{}' does not embed a runtime component: 'component.json' component.asset is empty",
extension_id
);
}
let wasm_bytes = {
let mut entry = archive.by_name(asset_path).map_err(|_| {
anyhow::anyhow!(
"extension '{}' does not embed a runtime component: asset '{}' not found in {}",
extension_id,
asset_path,
gtxpack_path.display()
)
})?;
let mut buf = Vec::new();
entry.read_to_end(&mut buf).with_context(|| {
format!(
"read asset '{}' from {}",
asset_path,
gtxpack_path.display()
)
})?;
buf
};
let actual_digest = format!("sha256:{}", hex::encode(Sha256::digest(&wasm_bytes)));
let expected_digest = sidecar.component.digest.as_str();
if actual_digest != expected_digest {
bail!(
"embedded component digest mismatch for extension '{}': component.json advertises '{}' but extracted wasm hashes to '{}'",
extension_id,
expected_digest,
actual_digest
);
}
Ok((wasm_bytes, actual_digest))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_ext_ref_happy() {
let r = parse_ext_ref("ext://greentic.http#component").expect("valid ref");
assert_eq!(r.extension_id, "greentic.http");
}
#[test]
fn parse_ext_ref_wrong_scheme() {
let err = parse_ext_ref("oci://foo#component").expect_err("wrong scheme");
assert!(err.to_string().contains("ext://"));
}
#[test]
fn parse_ext_ref_no_fragment() {
let err = parse_ext_ref("ext://greentic.http").expect_err("no fragment");
assert!(err.to_string().contains("#component"));
}
#[test]
fn parse_ext_ref_wrong_fragment() {
let err = parse_ext_ref("ext://greentic.http#other").expect_err("wrong fragment");
assert!(err.to_string().contains("#component"));
}
#[test]
fn parse_ext_ref_empty_id() {
let err = parse_ext_ref("ext://#component").expect_err("empty id");
assert!(err.to_string().contains("must not be empty"));
}
}