use std::collections::BTreeMap;
use std::fs;
use std::path::{Path, PathBuf};
use async_trait::async_trait;
use flate2::write::GzEncoder;
use flate2::Compression;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct NativeTarballManifest {
pub name: String,
pub version: String,
pub triple: String,
pub binary: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub env: BTreeMap<String, String>,
}
pub fn pack_native_tarball(
binary_path: &Path,
manifest: &NativeTarballManifest,
output_path: &Path,
) -> std::io::Result<()> {
if let Some(parent) = output_path.parent() {
fs::create_dir_all(parent)?;
}
let manifest_toml = toml::to_string_pretty(manifest)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
let file = fs::File::create(output_path)?;
let gz = GzEncoder::new(file, Compression::default());
let mut builder = tar::Builder::new(gz);
let bin_basename = binary_path.file_name().ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::InvalidInput,
format!("binary path has no filename: {}", binary_path.display()),
)
})?;
let in_tar_path = format!("bin/{}", bin_basename.to_string_lossy());
let mut bin = fs::File::open(binary_path)?;
let bin_meta = bin.metadata()?;
let mut bin_header = tar::Header::new_gnu();
bin_header.set_size(bin_meta.len());
bin_header.set_mode(0o755);
bin_header.set_mtime(0);
bin_header.set_cksum();
builder.append_data(&mut bin_header, &in_tar_path, &mut bin)?;
let manifest_bytes = manifest_toml.as_bytes();
let mut manifest_header = tar::Header::new_gnu();
manifest_header.set_size(manifest_bytes.len() as u64);
manifest_header.set_mode(0o644);
manifest_header.set_mtime(0);
manifest_header.set_cksum();
builder.append_data(&mut manifest_header, "manifest.toml", manifest_bytes)?;
let gz = builder.into_inner()?;
gz.finish()?;
Ok(())
}
pub fn tarball_stem(image_name: &str, triple: &str) -> String {
let raw = format!("{image_name}-{triple}");
raw.chars()
.map(|c| {
if c.is_ascii_alphanumeric() || matches!(c, '_' | '.' | '-') {
c
} else {
'_'
}
})
.collect()
}
pub fn native_tarball_output_path(camp_root: &Path, image_name: &str, triple: &str) -> PathBuf {
camp_root
.join(".yah/cache/native")
.join(format!("{}.tar.gz", tarball_stem(image_name, triple)))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SignedBlob {
pub signature_path: PathBuf,
pub certificate_path: PathBuf,
pub bundle_path: Option<PathBuf>,
}
#[async_trait]
pub trait SigstoreSigner: Send + Sync {
async fn sign_blob(&self, blob_path: &Path) -> std::io::Result<SignedBlob>;
}
fn append_suffix(blob: &Path, suffix: &str) -> PathBuf {
let mut s = blob.as_os_str().to_owned();
s.push(suffix);
PathBuf::from(s)
}
pub struct CosignSigner {
pub cosign_bin: PathBuf,
}
impl Default for CosignSigner {
fn default() -> Self {
Self {
cosign_bin: PathBuf::from("cosign"),
}
}
}
#[async_trait]
impl SigstoreSigner for CosignSigner {
async fn sign_blob(&self, blob_path: &Path) -> std::io::Result<SignedBlob> {
let sig = append_suffix(blob_path, ".sig");
let crt = append_suffix(blob_path, ".crt");
let bundle = append_suffix(blob_path, ".bundle");
let status = tokio::process::Command::new(&self.cosign_bin)
.arg("sign-blob")
.arg("--yes")
.arg("--output-signature")
.arg(&sig)
.arg("--output-certificate")
.arg(&crt)
.arg("--bundle")
.arg(&bundle)
.arg(blob_path)
.status()
.await?;
if !status.success() {
return Err(std::io::Error::new(
std::io::ErrorKind::Other,
format!(
"cosign sign-blob exited with status {} (blob: {})",
status,
blob_path.display(),
),
));
}
Ok(SignedBlob {
signature_path: sig,
certificate_path: crt,
bundle_path: Some(bundle),
})
}
}
pub struct LoggingSigner;
#[async_trait]
impl SigstoreSigner for LoggingSigner {
async fn sign_blob(&self, blob_path: &Path) -> std::io::Result<SignedBlob> {
if !blob_path.is_file() {
return Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("blob to sign not found: {}", blob_path.display()),
));
}
let sig = append_suffix(blob_path, ".sig");
let crt = append_suffix(blob_path, ".crt");
let bundle = append_suffix(blob_path, ".bundle");
fs::write(
&sig,
b"# yah logging-signer: placeholder signature (NOT a real cosign signature)\n",
)?;
fs::write(
&crt,
b"# yah logging-signer: placeholder certificate (NOT a real cosign cert)\n",
)?;
fs::write(
&bundle,
b"{\"_comment\":\"yah logging-signer placeholder bundle\"}\n",
)?;
tracing::warn!(
blob = %blob_path.display(),
"qed sign-native-tarball: LoggingSigner emitted placeholder \
.sig/.crt/.bundle (cosign not wired)"
);
Ok(SignedBlob {
signature_path: sig,
certificate_path: crt,
bundle_path: Some(bundle),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use flate2::read::GzDecoder;
use std::io::{Read, Write};
use tempfile::TempDir;
fn write_dummy_binary(dir: &Path, name: &str, body: &[u8]) -> std::path::PathBuf {
let path = dir.join(name);
let mut f = fs::File::create(&path).unwrap();
f.write_all(body).unwrap();
path
}
fn sample_manifest() -> NativeTarballManifest {
NativeTarballManifest {
name: "yah-yubaba".into(),
version: "0.8.6".into(),
triple: "x86_64-unknown-linux-musl".into(),
binary: "bin/yubaba".into(),
description: Some("Native musl-static yubaba".into()),
env: BTreeMap::from([("RUST_LOG".into(), "info".into())]),
}
}
fn list_tar_entries(path: &Path) -> Vec<(String, Vec<u8>, u32)> {
let f = fs::File::open(path).unwrap();
let gz = GzDecoder::new(f);
let mut archive = tar::Archive::new(gz);
let mut out = Vec::new();
for entry in archive.entries().unwrap() {
let mut entry = entry.unwrap();
let header_path = entry.path().unwrap().to_string_lossy().into_owned();
let mode = entry.header().mode().unwrap();
let mut buf = Vec::new();
entry.read_to_end(&mut buf).unwrap();
out.push((header_path, buf, mode));
}
out.sort_by(|a, b| a.0.cmp(&b.0));
out
}
#[test]
fn pack_writes_binary_and_manifest_with_expected_modes() {
let dir = TempDir::new().unwrap();
let bin = write_dummy_binary(dir.path(), "yubaba", b"\x7fELF-fake-musl-binary");
let out = dir
.path()
.join("out/yah-yubaba-x86_64-unknown-linux-musl.tar.gz");
let manifest = sample_manifest();
pack_native_tarball(&bin, &manifest, &out).unwrap();
assert!(out.is_file(), "tarball materialised at {}", out.display());
let entries = list_tar_entries(&out);
assert_eq!(entries.len(), 2);
assert_eq!(entries[0].0, "bin/yubaba");
assert_eq!(entries[0].1, b"\x7fELF-fake-musl-binary");
assert_eq!(entries[0].2, 0o755);
assert_eq!(entries[1].0, "manifest.toml");
assert_eq!(entries[1].2, 0o644);
}
#[test]
fn pack_manifest_roundtrips_through_toml() {
let dir = TempDir::new().unwrap();
let bin = write_dummy_binary(dir.path(), "yubaba", b"x");
let out = dir.path().join("yubaba.tar.gz");
let manifest = sample_manifest();
pack_native_tarball(&bin, &manifest, &out).unwrap();
let entries = list_tar_entries(&out);
let manifest_entry = entries
.iter()
.find(|(p, _, _)| p == "manifest.toml")
.expect("manifest.toml present");
let text = std::str::from_utf8(&manifest_entry.1).unwrap();
let parsed: NativeTarballManifest = toml::from_str(text).expect("manifest.toml parses");
assert_eq!(parsed, manifest);
}
#[test]
fn pack_creates_missing_parent_dirs() {
let dir = TempDir::new().unwrap();
let bin = write_dummy_binary(dir.path(), "yubaba", b"x");
let out = dir.path().join("deeply/nested/path/yubaba.tar.gz");
pack_native_tarball(&bin, &sample_manifest(), &out).unwrap();
assert!(out.is_file());
}
#[test]
fn pack_missing_binary_is_io_error() {
let dir = TempDir::new().unwrap();
let bogus = dir.path().join("does-not-exist");
let out = dir.path().join("out.tar.gz");
let err = pack_native_tarball(&bogus, &sample_manifest(), &out).unwrap_err();
assert_eq!(err.kind(), std::io::ErrorKind::NotFound);
}
#[test]
fn tarball_stem_replaces_unsafe_chars() {
assert_eq!(
tarball_stem("yah-yubaba", "x86_64-unknown-linux-musl"),
"yah-yubaba-x86_64-unknown-linux-musl",
);
assert_eq!(
tarball_stem("ghcr.io/yah-ai/yah-yubaba", "linux:musl"),
"ghcr.io_yah-ai_yah-yubaba-linux_musl",
);
}
#[test]
fn native_tarball_output_path_matches_runner_convention() {
let camp = Path::new("/camp");
let out = native_tarball_output_path(camp, "yah-yubaba", "x86_64-unknown-linux-musl");
assert_eq!(
out,
Path::new("/camp/.yah/cache/native/yah-yubaba-x86_64-unknown-linux-musl.tar.gz"),
);
}
#[tokio::test]
async fn logging_signer_writes_placeholder_sig_crt_bundle_next_to_blob() {
let dir = TempDir::new().unwrap();
let blob = dir
.path()
.join("yah-yubaba-x86_64-unknown-linux-musl.tar.gz");
fs::write(&blob, b"<fake tarball bytes>").unwrap();
let signer = LoggingSigner;
let signed = signer.sign_blob(&blob).await.unwrap();
assert_eq!(
signed.signature_path,
blob.with_file_name(format!(
"{}.sig",
blob.file_name().unwrap().to_string_lossy()
)),
);
assert_eq!(
signed.certificate_path,
blob.with_file_name(format!(
"{}.crt",
blob.file_name().unwrap().to_string_lossy()
)),
);
let bundle = signed.bundle_path.expect("LoggingSigner emits a bundle");
assert_eq!(
bundle,
blob.with_file_name(format!(
"{}.bundle",
blob.file_name().unwrap().to_string_lossy()
)),
);
assert!(fs::read(&signed.signature_path).unwrap().len() > 10);
assert!(fs::read(&signed.certificate_path).unwrap().len() > 10);
assert!(fs::read(&bundle).unwrap().len() > 10);
}
#[tokio::test]
async fn logging_signer_missing_blob_is_not_found_error() {
let dir = TempDir::new().unwrap();
let bogus = dir.path().join("does-not-exist.tar.gz");
let err = LoggingSigner.sign_blob(&bogus).await.unwrap_err();
assert_eq!(err.kind(), std::io::ErrorKind::NotFound);
}
#[tokio::test]
async fn cosign_signer_missing_binary_surfaces_not_found() {
let dir = TempDir::new().unwrap();
let blob = dir.path().join("artifact.tar.gz");
fs::write(&blob, b"x").unwrap();
let signer = CosignSigner {
cosign_bin: PathBuf::from("/definitely/not/a/real/cosign-binary"),
};
let err = signer.sign_blob(&blob).await.unwrap_err();
assert_eq!(err.kind(), std::io::ErrorKind::NotFound);
}
}