1use std::collections::BTreeSet;
13
14use serde::{Deserialize, Serialize};
15use serde_json::Value;
16
17use crate::access::{Viewer, view_history, view_record};
18use crate::error::{Error, Result};
19use crate::event::Event;
20use crate::id::{Origin, RecordId};
21use crate::model::{Record, Timestamp};
22use crate::projection::project;
23use crate::provenance::ContentHash;
24use crate::repository::Repository;
25use crate::schema::{BUNDLE_FORMAT_VERSION, ENVELOPE_SCHEMA_VERSION, SchemaVersion};
26use crate::storage::Store;
27
28pub const BUNDLE_FORMAT: &str = "pardarsh.bundle";
29
30pub fn canonical_json<T: Serialize>(value: &T) -> Result<Vec<u8>> {
32 let v = serde_json::to_value(value)?;
33 let mut out = Vec::new();
34 write_canonical(&v, &mut out)?;
35 Ok(out)
36}
37
38fn write_canonical(v: &Value, out: &mut Vec<u8>) -> Result<()> {
39 match v {
40 Value::Object(map) => {
41 let mut keys: Vec<&String> = map.keys().collect();
42 keys.sort();
43 out.push(b'{');
44 for (i, k) in keys.into_iter().enumerate() {
45 if i > 0 {
46 out.push(b',');
47 }
48 serde_json::to_writer(&mut *out, k)?;
49 out.push(b':');
50 write_canonical(&map[k], out)?;
51 }
52 out.push(b'}');
53 }
54 Value::Array(items) => {
55 out.push(b'[');
56 for (i, item) in items.iter().enumerate() {
57 if i > 0 {
58 out.push(b',');
59 }
60 write_canonical(item, out)?;
61 }
62 out.push(b']');
63 }
64 scalar => serde_json::to_writer(&mut *out, scalar)?,
65 }
66 Ok(())
67}
68
69pub fn content_hash<T: Serialize>(value: &T) -> Result<ContentHash> {
71 Ok(ContentHash::sha256(&canonical_json(value)?))
72}
73
74#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
76#[serde(rename_all = "snake_case")]
77pub enum Completeness {
78 Complete,
80 Filtered,
82}
83
84#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
85pub struct BundleEntry {
86 pub record: Record,
87 pub history: Vec<Event>,
88 pub record_hash: ContentHash,
89}
90
91#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
92pub struct Bundle {
93 pub format: String,
94 pub format_version: SchemaVersion,
95 pub envelope_version: SchemaVersion,
96 pub origin: Origin,
97 pub exported_at: Timestamp,
98 pub completeness: Completeness,
99 pub entries: Vec<BundleEntry>,
100 #[serde(default)]
102 pub bundle_hash: Option<ContentHash>,
103}
104
105impl Bundle {
106 fn compute_hash(&self) -> Result<ContentHash> {
107 let mut copy = self.clone();
108 copy.bundle_hash = None;
109 content_hash(©)
110 }
111
112 pub fn to_json_pretty(&self) -> Result<String> {
113 Ok(serde_json::to_string_pretty(self)?)
114 }
115}
116
117pub fn export<S: Store>(repo: &Repository<S>, ids: &[RecordId], viewer: &Viewer) -> Result<Bundle> {
120 let mut entries = Vec::new();
121 let mut seen = BTreeSet::new();
122 for id in ids {
123 if !seen.insert(id.clone()) {
124 continue;
125 }
126 let Some(record) = repo.find(id)? else { continue };
127 let Some(record) = view_record(&record, viewer) else { continue };
128 let history = view_history(&repo.history(id)?, viewer);
129 let record_hash = content_hash(&record)?;
130 entries.push(BundleEntry { record, history, record_hash });
131 }
132 let mut bundle = Bundle {
133 format: BUNDLE_FORMAT.to_string(),
134 format_version: BUNDLE_FORMAT_VERSION,
135 envelope_version: ENVELOPE_SCHEMA_VERSION,
136 origin: repo.store().origin().clone(),
137 exported_at: repo.now(),
138 completeness: if viewer.privileged { Completeness::Complete } else { Completeness::Filtered },
139 entries,
140 bundle_hash: None,
141 };
142 bundle.bundle_hash = Some(bundle.compute_hash()?);
143 Ok(bundle)
144}
145
146#[derive(Clone, Debug)]
148pub struct ImportPolicy {
149 pub max_bytes: usize,
150 pub max_records: usize,
151 pub max_events_per_record: usize,
152 pub allow_own_origin: bool,
156 pub trusted_origins: Option<BTreeSet<Origin>>,
158}
159
160impl Default for ImportPolicy {
161 fn default() -> Self {
162 Self {
163 max_bytes: 16 * 1024 * 1024,
164 max_records: 10_000,
165 max_events_per_record: 10_000,
166 allow_own_origin: false,
167 trusted_origins: None,
168 }
169 }
170}
171
172pub fn parse_bundle(bytes: &[u8], policy: &ImportPolicy) -> Result<Bundle> {
174 if bytes.len() > policy.max_bytes {
175 return Err(Error::Import(format!("bundle is {} bytes; limit is {}", bytes.len(), policy.max_bytes)));
176 }
177 let bundle: Bundle = serde_json::from_slice(bytes).map_err(|e| Error::Import(format!("malformed bundle: {e}")))?;
178 verify_bundle(&bundle, policy)?;
179 Ok(bundle)
180}
181
182pub fn verify_bundle(bundle: &Bundle, policy: &ImportPolicy) -> Result<()> {
183 let err = |m: String| Err(Error::Import(m));
184 if bundle.format != BUNDLE_FORMAT {
185 return err(format!("unknown format {:?}", bundle.format));
186 }
187 if !BUNDLE_FORMAT_VERSION.can_read(&bundle.format_version) {
188 return err(format!("unsupported bundle version {}", bundle.format_version));
189 }
190 if !ENVELOPE_SCHEMA_VERSION.can_read(&bundle.envelope_version) {
191 return err(format!("unsupported envelope version {}", bundle.envelope_version));
192 }
193 if bundle.entries.len() > policy.max_records {
194 return err(format!("{} records exceed the limit of {}", bundle.entries.len(), policy.max_records));
195 }
196 match &bundle.bundle_hash {
197 Some(h) if *h == bundle.compute_hash()? => {}
198 Some(_) => return err("bundle hash does not match its contents".into()),
199 None => return err("bundle hash is missing".into()),
200 }
201 for entry in &bundle.entries {
202 let id = &entry.record.id;
203 if entry.history.len() > policy.max_events_per_record {
204 return err(format!("{id}: too many events"));
205 }
206 if content_hash(&entry.record)? != entry.record_hash {
207 return err(format!("{id}: record hash does not match"));
208 }
209 if entry.history.iter().any(|e| &e.record_id != id) {
210 return err(format!("{id}: history contains events of another record"));
211 }
212 if bundle.completeness == Completeness::Complete {
213 let projected = project(&entry.history).map_err(|e| Error::Import(format!("{id}: {e}")))?;
214 if projected.as_ref() != Some(&entry.record) {
215 return err(format!("{id}: record does not match its history"));
216 }
217 }
218 }
219 Ok(())
220}
221
222#[derive(Clone, Debug, Default, PartialEq, Eq)]
223pub struct ImportReport {
224 pub records: usize,
225 pub events_written: usize,
226 pub events_already_present: usize,
227}
228
229pub fn import<S: Store>(repo: &mut Repository<S>, bundle: &Bundle, policy: &ImportPolicy) -> Result<ImportReport> {
233 verify_bundle(bundle, policy)?;
234 if bundle.completeness != Completeness::Complete {
235 return Err(Error::Import("filtered bundles cannot be imported".into()));
236 }
237 let own = repo.store().origin().clone();
238 for entry in &bundle.entries {
239 let origin = entry.record.id.origin();
240 if origin == &own && !policy.allow_own_origin {
241 return Err(Error::Import(format!("{}: records of the local origin are not accepted", entry.record.id)));
242 }
243 if let Some(trusted) = &policy.trusted_origins {
244 if !trusted.contains(origin) {
245 return Err(Error::Import(format!("{}: origin {origin} is not trusted", entry.record.id)));
246 }
247 }
248 }
249 let mut report = ImportReport::default();
250 for entry in &bundle.entries {
251 let existing = repo.history(&entry.record.id)?;
252 for (i, local) in existing.iter().enumerate() {
253 if entry.history.get(i).map(|e| e.id) != Some(local.id) {
254 return Err(Error::Import(format!(
255 "{}: local history diverges from the bundle at sequence {}",
256 entry.record.id, local.sequence
257 )));
258 }
259 }
260 report.events_already_present += existing.len();
261 for event in entry.history.iter().skip(existing.len()) {
262 repo.ingest(event.clone())?;
263 report.events_written += 1;
264 }
265 report.records += 1;
266 }
267 Ok(report)
268}