use crate::{CommitEntry, Entry, KeyEntry, Store};
use cyphr::Principal;
#[derive(Debug, thiserror::Error)]
pub enum ExportError {
#[error("serialization error: {0}")]
Json(#[from] serde_json::Error),
#[error("entry error: {0}")]
Entry(#[from] crate::EntryError),
#[error("empty state digest: {0}")]
EmptyDigest(#[from] cyphr::Error),
}
pub fn export_entries(principal: &Principal) -> Result<Vec<Entry>, ExportError> {
let mut entries = Vec::new();
for cz in principal.iter_all_cozies() {
let raw = serde_json::to_value(cz.raw())?;
entries.push(Entry::from_value(&raw)?);
}
for action in principal.actions() {
let raw = serde_json::to_value(action.raw())?;
entries.push(Entry::from_value(&raw)?);
}
Ok(entries)
}
pub fn export_commits(principal: &Principal) -> Result<Vec<CommitEntry>, ExportError> {
use coz::base64ct::{Base64UrlUnpadded, Encoding};
let mut commit_entries = Vec::new();
for commit in principal.commits() {
let mut cozies = Vec::new();
let mut keys = Vec::new();
for cz in commit.iter_all_cozies() {
let raw = serde_json::to_value(cz.raw())?;
cozies.push(raw);
if let Some(key) = cz.new_key() {
keys.push(KeyEntry {
alg: key.alg.clone(),
pub_key: Base64UrlUnpadded::encode_string(&key.pub_key),
tmb: key.tmb.to_b64(),
tag: key.tag.clone(),
now: Some(key.first_seen),
});
}
}
let tr_bytes = commit.tr().0.first_variant()?;
let tr_alg = commit
.tr()
.0
.algorithms()
.next()
.ok_or(cyphr::Error::EmptyMultihash)?;
let commit_id = format!("{}:{}", tr_alg, Base64UrlUnpadded::encode_string(tr_bytes));
let as_bytes = commit.auth_root().as_multihash().first_variant()?;
let as_alg = commit
.auth_root()
.as_multihash()
.algorithms()
.next()
.ok_or(cyphr::Error::EmptyMultihash)?;
let auth_root = format!("{}:{}", as_alg, Base64UrlUnpadded::encode_string(as_bytes));
let sr_bytes = commit.sr().as_multihash().first_variant()?;
let sr_alg = commit
.sr()
.as_multihash()
.algorithms()
.next()
.ok_or(cyphr::Error::EmptyMultihash)?;
let sr = format!("{}:{}", sr_alg, Base64UrlUnpadded::encode_string(sr_bytes));
let ps_bytes = commit.pr().as_multihash().first_variant()?;
let ps_alg = commit
.pr()
.as_multihash()
.algorithms()
.next()
.ok_or(cyphr::Error::EmptyMultihash)?;
let ps = format!("{}:{}", ps_alg, Base64UrlUnpadded::encode_string(ps_bytes));
commit_entries.push(CommitEntry::new(cozies, keys, commit_id, auth_root, sr, ps));
}
Ok(commit_entries)
}
pub fn persist_entries<S: Store>(
store: &S,
principal: &Principal,
) -> Result<usize, PersistError<S::Error>> {
let entries = export_entries(principal).map_err(PersistError::Export)?;
let pg = principal.pg().ok_or(PersistError::NoPrincipalGenesis)?;
let count = entries.len();
for entry in entries {
store
.append_entry(pg, &entry)
.map_err(PersistError::Store)?;
}
Ok(count)
}
#[derive(Debug, thiserror::Error)]
pub enum PersistError<E: std::error::Error> {
#[error("export: {0}")]
Export(#[from] ExportError),
#[error("store: {0}")]
Store(E),
#[error("persist_entries requires a Level 3+ principal with PrincipalGenesis")]
NoPrincipalGenesis,
}
#[cfg(test)]
mod tests {
use super::*;
use coz::Thumbprint;
use cyphr::Key;
use serde_json::json;
fn make_test_key(id: u8) -> Key {
Key {
alg: "ES256".to_string(),
tmb: Thumbprint::from_bytes(vec![id; 32]),
pub_key: vec![id; 64],
first_seen: 1000,
last_used: None,
revocation: None,
tag: None,
}
}
#[test]
fn export_implicit_genesis_no_entries() {
let principal = Principal::implicit(make_test_key(0xAA)).unwrap();
let entries = export_entries(&principal).unwrap();
assert_eq!(entries.len(), 0);
}
#[test]
fn entry_from_value_extracts_now() {
use crate::Entry;
let raw = json!({
"pay": {"now": 12345, "typ": "test"},
"sig": "AAAA"
});
let entry = Entry::from_value(&raw).unwrap();
assert_eq!(entry.now, 12345);
}
#[test]
fn exported_entry_has_pay_and_sig() {
let coz_json = coz::CozJson {
pay: json!({"typ": "test", "now": 1000}),
sig: vec![0xDE, 0xAD, 0xBE, 0xEF],
};
let serialized = serde_json::to_value(&coz_json).unwrap();
assert!(serialized.get("pay").is_some(), "missing pay field");
assert!(serialized.get("sig").is_some(), "missing sig field");
let sig_str = serialized["sig"].as_str().unwrap();
assert!(!sig_str.is_empty(), "sig should not be empty");
}
}