use std::path::Path;
use serde::Serialize;
use void_core::{
cid,
crypto::{CommitCid, CommitReader, EncryptedCommit, KeyVault},
metadata::Commit,
store::{FsStore, ObjectStoreExt},
};
use crate::context::{open_repo, resolve_ref, void_err_to_cli};
use crate::output::{run_command, CliError, CliOptions};
#[derive(Debug, Clone, Serialize)]
pub struct VerifyOutput {
pub commit: String,
pub valid: bool,
pub signed: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub author: Option<String>,
}
fn read_commit(store: &FsStore, vault: &KeyVault, commit_cid: &CommitCid) -> Result<Commit, CliError> {
let cid_obj =
cid::from_bytes(commit_cid.as_bytes()).map_err(|e| CliError::internal(format!("invalid CID: {e}")))?;
let encrypted: EncryptedCommit = store
.get_blob(&cid_obj)
.map_err(|e| CliError::not_found(format!("commit not found: {e}")))?;
let (commit_bytes, _reader) = CommitReader::open_with_vault(vault, &encrypted)
.map_err(|e| CliError::internal(format!("failed to open commit: {e}")))?;
let commit = commit_bytes.parse()
.map_err(|e| CliError::internal(format!("failed to parse commit: {e}")))?;
Ok(commit)
}
pub fn run(cwd: &Path, commit_ref: &str, opts: &CliOptions) -> Result<(), CliError> {
run_command("verify", opts, |ctx| {
ctx.progress("Verifying commit signature...");
ctx.verbose("Reading repository context...");
let repo = open_repo(cwd)?;
ctx.verbose(format!("Resolving reference: {}", commit_ref));
let cid_bytes = resolve_ref(repo.void_dir().as_std_path(), commit_ref)?;
let cid_str = cid::from_bytes(cid_bytes.as_bytes())
.map(|c| c.to_string())
.unwrap_or_else(|_| hex::encode(cid_bytes.as_bytes()));
ctx.verbose(format!(
"Loading commit {}",
&cid_str[..12.min(cid_str.len())]
));
let store = repo.store().map_err(void_err_to_cli)?;
let commit = read_commit(&store, &repo.vault(), &cid_bytes)?;
let (valid, signed, author) = match commit.verify() {
Ok(true) => {
let author_hex = commit.author.map(|a| format!("ed25519:{}", a.to_hex()));
(true, true, author_hex)
}
Ok(false) => {
(true, false, None)
}
Err(_) => {
let author_hex = commit.author.map(|a| format!("ed25519:{}", a.to_hex()));
(false, true, author_hex)
}
};
if !ctx.use_json() {
if signed {
if valid {
let author_display = author.as_deref().unwrap_or("unknown");
ctx.info(format!(
"Commit {} is signed by {}",
cid_str, author_display
));
} else {
let author_display = author.as_deref().unwrap_or("unknown");
ctx.info(format!(
"Commit {} has INVALID signature from {}",
cid_str, author_display
));
}
} else {
ctx.info(format!("Commit {} is unsigned", cid_str));
}
}
ctx.progress("Verification complete");
Ok(VerifyOutput {
commit: cid_str,
valid,
signed,
author,
})
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_verify_output_serialization_signed() {
let output = VerifyOutput {
commit: "bafytest123".to_string(),
valid: true,
signed: true,
author: Some("ed25519:abcd1234".to_string()),
};
let json = serde_json::to_string(&output).unwrap();
assert!(json.contains("\"commit\":\"bafytest123\""));
assert!(json.contains("\"valid\":true"));
assert!(json.contains("\"signed\":true"));
assert!(json.contains("\"author\":\"ed25519:abcd1234\""));
}
#[test]
fn test_verify_output_serialization_unsigned() {
let output = VerifyOutput {
commit: "bafytest456".to_string(),
valid: true,
signed: false,
author: None,
};
let json = serde_json::to_string(&output).unwrap();
assert!(json.contains("\"commit\":\"bafytest456\""));
assert!(json.contains("\"valid\":true"));
assert!(json.contains("\"signed\":false"));
assert!(!json.contains("\"author\""));
}
#[test]
fn test_verify_output_serialization_invalid() {
let output = VerifyOutput {
commit: "bafytest789".to_string(),
valid: false,
signed: true,
author: Some("ed25519:deadbeef".to_string()),
};
let json = serde_json::to_string(&output).unwrap();
assert!(json.contains("\"commit\":\"bafytest789\""));
assert!(json.contains("\"valid\":false"));
assert!(json.contains("\"signed\":true"));
assert!(json.contains("\"author\":\"ed25519:deadbeef\""));
}
}