1use crate::{CommitEntry, Entry, KeyEntry, Store};
7use cyphr::Principal;
8
9#[derive(Debug, thiserror::Error)]
11pub enum ExportError {
12 #[error("serialization error: {0}")]
14 Json(#[from] serde_json::Error),
15
16 #[error("entry error: {0}")]
18 Entry(#[from] crate::EntryError),
19
20 #[error("empty state digest: {0}")]
22 EmptyDigest(#[from] cyphr::Error),
23}
24
25pub fn export_entries(principal: &Principal) -> Result<Vec<Entry>, ExportError> {
48 let mut entries = Vec::new();
49
50 for cz in principal.iter_all_cozies() {
51 let raw = serde_json::to_value(cz.raw())?;
53
54 entries.push(Entry::from_value(&raw)?);
56 }
57
58 for action in principal.actions() {
59 let raw = serde_json::to_value(action.raw())?;
60 entries.push(Entry::from_value(&raw)?);
61 }
62
63 Ok(entries)
64}
65
66pub fn export_commits(principal: &Principal) -> Result<Vec<CommitEntry>, ExportError> {
93 use coz::base64ct::{Base64UrlUnpadded, Encoding};
94
95 let mut commit_entries = Vec::new();
96
97 for commit in principal.commits() {
98 let mut cozies = Vec::new();
99 let mut keys = Vec::new();
100
101 for cz in commit.iter_all_cozies() {
102 let raw = serde_json::to_value(cz.raw())?;
104 cozies.push(raw);
105
106 if let Some(key) = cz.new_key() {
108 keys.push(KeyEntry {
109 alg: key.alg.clone(),
110 pub_key: Base64UrlUnpadded::encode_string(&key.pub_key),
111 tmb: key.tmb.to_b64(),
112 tag: key.tag.clone(),
113 now: Some(key.first_seen),
114 });
115 }
116 }
117
118 let tr_bytes = commit.tr().0.first_variant()?;
121 let tr_alg = commit
122 .tr()
123 .0
124 .algorithms()
125 .next()
126 .ok_or(cyphr::Error::EmptyMultihash)?;
127 let commit_id = format!("{}:{}", tr_alg, Base64UrlUnpadded::encode_string(tr_bytes));
128
129 let as_bytes = commit.auth_root().as_multihash().first_variant()?;
130 let as_alg = commit
131 .auth_root()
132 .as_multihash()
133 .algorithms()
134 .next()
135 .ok_or(cyphr::Error::EmptyMultihash)?;
136 let auth_root = format!("{}:{}", as_alg, Base64UrlUnpadded::encode_string(as_bytes));
137
138 let sr_bytes = commit.sr().as_multihash().first_variant()?;
139 let sr_alg = commit
140 .sr()
141 .as_multihash()
142 .algorithms()
143 .next()
144 .ok_or(cyphr::Error::EmptyMultihash)?;
145 let sr = format!("{}:{}", sr_alg, Base64UrlUnpadded::encode_string(sr_bytes));
146
147 let ps_bytes = commit.pr().as_multihash().first_variant()?;
148 let ps_alg = commit
149 .pr()
150 .as_multihash()
151 .algorithms()
152 .next()
153 .ok_or(cyphr::Error::EmptyMultihash)?;
154 let ps = format!("{}:{}", ps_alg, Base64UrlUnpadded::encode_string(ps_bytes));
155
156 commit_entries.push(CommitEntry::new(cozies, keys, commit_id, auth_root, sr, ps));
157 }
158
159 Ok(commit_entries)
160}
161
162pub fn persist_entries<S: Store>(
170 store: &S,
171 principal: &Principal,
172) -> Result<usize, PersistError<S::Error>> {
173 let entries = export_entries(principal).map_err(PersistError::Export)?;
174 let pg = principal.pg().ok_or(PersistError::NoPrincipalGenesis)?;
175 let count = entries.len();
176 for entry in entries {
177 store
178 .append_entry(pg, &entry)
179 .map_err(PersistError::Store)?;
180 }
181 Ok(count)
182}
183
184#[derive(Debug, thiserror::Error)]
186pub enum PersistError<E: std::error::Error> {
187 #[error("export: {0}")]
189 Export(#[from] ExportError),
190 #[error("store: {0}")]
192 Store(E),
193 #[error("persist_entries requires a Level 3+ principal with PrincipalGenesis")]
195 NoPrincipalGenesis,
196}
197
198#[cfg(test)]
199mod tests {
200 use super::*;
201 use coz::Thumbprint;
202 use cyphr::Key;
203 use serde_json::json;
204
205 fn make_test_key(id: u8) -> Key {
206 Key {
207 alg: "ES256".to_string(),
208 tmb: Thumbprint::from_bytes(vec![id; 32]),
209 pub_key: vec![id; 64],
210 first_seen: 1000,
211 last_used: None,
212 revocation: None,
213 tag: None,
214 }
215 }
216
217 #[test]
218 fn export_implicit_genesis_no_entries() {
219 let principal = Principal::implicit(make_test_key(0xAA)).unwrap();
221 let entries = export_entries(&principal).unwrap();
222
223 assert_eq!(entries.len(), 0);
225 }
226
227 #[test]
228 fn entry_from_value_extracts_now() {
229 use crate::Entry;
230
231 let raw = json!({
232 "pay": {"now": 12345, "typ": "test"},
233 "sig": "AAAA"
234 });
235
236 let entry = Entry::from_value(&raw).unwrap();
237 assert_eq!(entry.now, 12345);
238 }
239
240 #[test]
241 fn exported_entry_has_pay_and_sig() {
242 let coz_json = coz::CozJson {
245 pay: json!({"typ": "test", "now": 1000}),
246 sig: vec![0xDE, 0xAD, 0xBE, 0xEF],
247 };
248
249 let serialized = serde_json::to_value(&coz_json).unwrap();
250
251 assert!(serialized.get("pay").is_some(), "missing pay field");
253 assert!(serialized.get("sig").is_some(), "missing sig field");
254
255 let sig_str = serialized["sig"].as_str().unwrap();
257 assert!(!sig_str.is_empty(), "sig should not be empty");
258 }
259}