1use std::fmt;
31use std::marker::PhantomData;
32use std::path::Path;
33
34use serde::de::{DeserializeOwned, DeserializeSeed, IgnoredAny, MapAccess, Visitor};
35use serde::{Deserialize, Deserializer, Serialize};
36use serde_json::{Map, Value};
37
38pub type Migration = fn(Value) -> Result<Value, String>;
40
41pub type LegacyProbe = fn(&Value) -> Option<u64>;
43
44pub const DEFAULT_MAX_BYTES: u64 = 2 << 30;
46
47const FORMAT: &str = "format";
48const SCHEMA: &str = "schema_version";
49const GENERATOR: &str = "generator";
50
51#[derive(Debug, thiserror::Error)]
52pub enum FormatError {
53 #[error("{0}")]
54 Io(#[from] std::io::Error),
55 #[error("the file is damaged: {message}")]
58 Corrupt { message: String, detail: Option<JsonError> },
59 #[error("not a {expected} file: {why}")]
60 NotThisFormat {
61 expected: String,
62 why: String,
63 detail: Option<JsonError>,
65 },
66 #[error("this is a `{found}` file, not `{expected}`")]
67 WrongFormat { found: String, expected: String },
68 #[error("this file was saved by a newer version (schema v{found}); this build reads up to v{supported}. Update to open it.")]
69 TooNew { found: u64, supported: u64 },
70 #[error("schema v{found} is too old for this build (oldest supported: v{oldest})")]
71 TooOld { found: u64, oldest: u64 },
72 #[error("upgrading from schema v{from} to v{}: {msg}", from.saturating_add(1))]
73 Migration { from: u64, msg: String },
74 #[error("could not encode the document: {0}")]
75 Encode(String),
76}
77
78#[derive(Clone, Debug, PartialEq, Eq)]
80pub struct JsonError {
81 pub message: String,
83 pub line: usize,
85 pub column: usize,
86 pub syntax: bool,
88}
89
90impl JsonError {
91 pub fn new(e: &serde_json::Error) -> JsonError {
92 JsonError { message: e.to_string(), line: e.line(), column: e.column(), syntax: e.is_syntax() || e.is_eof() }
93 }
94}
95
96impl FormatError {
97 pub fn corrupt(message: impl Into<String>) -> FormatError {
99 FormatError::Corrupt { message: message.into(), detail: None }
100 }
101
102 pub fn corrupt_json(context: &str, e: &serde_json::Error) -> FormatError {
104 let message = if context.is_empty() { e.to_string() } else { format!("{context}: {e}") };
105 FormatError::Corrupt { message, detail: Some(JsonError::new(e)) }
106 }
107
108 pub fn json(&self) -> Option<&JsonError> {
111 match self {
112 FormatError::NotThisFormat { detail, .. } | FormatError::Corrupt { detail, .. } => detail.as_ref(),
113 _ => None,
114 }
115 }
116}
117
118#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
120pub struct Envelope {
121 pub format: String,
122 pub schema_version: u64,
123 pub generator: Option<String>,
124}
125
126#[derive(Debug)]
128pub struct Loaded<D> {
129 pub doc: D,
130 pub envelope: Envelope,
132 pub extras: Map<String, Value>,
134}
135
136impl<D> Loaded<D> {
137 pub fn migrated(&self, current: u64) -> bool {
140 self.envelope.schema_version < current
141 }
142}
143
144pub struct Format<D> {
146 id: String,
147 generator: String,
148 payload_key: String,
149 migrations: Vec<Migration>,
150 legacy: Option<LegacyProbe>,
151 oldest: u64,
152 pretty: bool,
153 max_bytes: u64,
154 extras_after_payload: bool,
155 accept_untagged: bool,
156 _doc: PhantomData<fn() -> D>,
157}
158
159impl<D> fmt::Debug for Format<D> {
160 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
161 f.debug_struct("Format").field("id", &self.id).field("schema_version", &(self.migrations.len() as u64 + 1)).finish()
162 }
163}
164
165impl<D: Serialize + DeserializeOwned> Format<D> {
166 pub fn new(id: impl Into<String>, generator: impl Into<String>) -> Format<D> {
168 Format {
169 id: id.into(),
170 generator: generator.into(),
171 payload_key: "document".into(),
172 migrations: Vec::new(),
173 legacy: None,
174 oldest: 1,
175 pretty: true,
176 max_bytes: DEFAULT_MAX_BYTES,
177 extras_after_payload: false,
178 accept_untagged: false,
179 _doc: PhantomData,
180 }
181 }
182
183 pub fn migrations(mut self, m: Vec<Migration>) -> Self {
185 self.migrations = m;
186 self
187 }
188
189 pub fn payload_key(mut self, key: impl Into<String>) -> Self {
191 self.payload_key = key.into();
192 self
193 }
194
195 pub fn legacy(mut self, probe: LegacyProbe) -> Self {
197 self.legacy = Some(probe);
198 self
199 }
200
201 pub fn oldest(mut self, v: u64) -> Self {
203 self.oldest = v.max(1);
204 self
205 }
206
207 pub fn pretty(mut self, on: bool) -> Self {
209 self.pretty = on;
210 self
211 }
212
213 pub fn max_bytes(mut self, n: u64) -> Self {
215 self.max_bytes = n;
216 self
217 }
218
219 pub fn extras_after_payload(mut self, on: bool) -> Self {
222 self.extras_after_payload = on;
223 self
224 }
225
226 pub fn accept_untagged(mut self, on: bool) -> Self {
231 self.accept_untagged = on;
232 self
233 }
234
235 pub fn id(&self) -> &str {
236 &self.id
237 }
238
239 pub fn schema_version(&self) -> u64 {
241 self.migrations.len() as u64 + 1
242 }
243
244 pub fn encode(&self, doc: &D) -> Result<Vec<u8>, FormatError> {
248 self.encode_with(doc, &Map::new())
249 }
250
251 pub fn encode_with(&self, doc: &D, extras: &Map<String, Value>) -> Result<Vec<u8>, FormatError> {
253 let out = EnvelopeOut {
254 format: &self.id,
255 schema_version: self.schema_version(),
256 generator: &self.generator,
257 key: &self.payload_key,
258 doc,
259 extras,
260 extras_after: self.extras_after_payload,
261 };
262 let r = if self.pretty { serde_json::to_vec_pretty(&out) } else { serde_json::to_vec(&out) };
263 r.map_err(|e| FormatError::Encode(e.to_string()))
264 }
265
266 pub fn save(&self, path: &Path, doc: &D) -> Result<(), FormatError> {
268 let bytes = self.encode(doc)?;
269 ashlar_fs::atomic_write(path, &bytes)?;
270 Ok(())
271 }
272
273 pub fn load(&self, path: &Path) -> Result<Loaded<D>, FormatError> {
277 let len = std::fs::metadata(path)?.len();
278 if len > self.max_bytes {
279 return Err(FormatError::corrupt(format!("file is {len} bytes, more than the {} allowed", self.max_bytes)));
280 }
281 let bytes = std::fs::read(path)?;
282 self.decode(&bytes)
283 }
284
285 pub fn decode(&self, bytes: &[u8]) -> Result<Loaded<D>, FormatError> {
287 if bytes.len() as u64 > self.max_bytes {
288 return Err(FormatError::corrupt(format!("{} bytes, more than the {} allowed", bytes.len(), self.max_bytes)));
289 }
290 let mut de = serde_json::Deserializer::from_slice(bytes);
291 let probe = ProbeSeed { key: &self.payload_key }.deserialize(&mut de).and_then(|p| de.end().map(|()| p)).map_err(|e| self.parse_error(e))?;
292 match probe.schema_version {
293 Some(v) => {
294 let found = version_of(&v)?;
295 match probe.format {
296 Some(f) if f == self.id => {}
297 Some(f) => return Err(FormatError::WrongFormat { found: f, expected: self.id.clone() }),
298 None if self.accept_untagged && probe.has_payload => {}
299 None => return Err(self.not_this("no `format` tag")),
300 }
301 self.check_supported(found)?;
302 if found == self.schema_version() {
303 let mut de = serde_json::Deserializer::from_slice(bytes);
305 let (doc, extras, generator) = EnvelopeSeed::<D> { key: &self.payload_key, _d: PhantomData }
306 .deserialize(&mut de)
307 .map_err(|e| FormatError::corrupt_json("", &e))?;
308 let doc = doc.ok_or_else(|| FormatError::corrupt(format!("missing `{}`", self.payload_key)))?;
309 let envelope = Envelope { format: self.id.clone(), schema_version: found, generator };
310 return Ok(Loaded { doc, envelope, extras });
311 }
312 let value: Value = serde_json::from_slice(bytes).map_err(|e| self.parse_error(e))?;
313 self.upgrade(value, found)
314 }
315 None => {
316 let value: Value = serde_json::from_slice(bytes).map_err(|e| self.parse_error(e))?;
317 let found = self.legacy.and_then(|p| p(&value)).ok_or_else(|| self.not_this("no `schema_version`"))?;
318 self.check_supported(found)?;
319 self.upgrade(value, found)
320 }
321 }
322 }
323
324 fn upgrade(&self, value: Value, found: u64) -> Result<Loaded<D>, FormatError> {
326 let generator = value.get(GENERATOR).and_then(Value::as_str).map(str::to_string);
327 let format = value.get(FORMAT).and_then(Value::as_str).unwrap_or(&self.id).to_string();
328 let envelope = Envelope { format, schema_version: found, generator };
329 let mut out = migrate(&self.migrations, value, found)?;
330 let Some(obj) = out.as_object_mut() else { return Err(FormatError::corrupt("not a JSON object")) };
331 let payload = obj
332 .remove(&self.payload_key)
333 .ok_or_else(|| FormatError::corrupt(format!("after upgrading from schema v{found}: missing `{}`", self.payload_key)))?;
334 let doc = D::deserialize(payload).map_err(|e| FormatError::corrupt_json(&format!("after upgrading from schema v{found}"), &e))?;
335 for k in [FORMAT, SCHEMA, GENERATOR] {
336 obj.remove(k);
337 }
338 Ok(Loaded { doc, envelope, extras: std::mem::take(obj) })
339 }
340
341 fn check_supported(&self, found: u64) -> Result<(), FormatError> {
342 if found > self.schema_version() {
343 return Err(FormatError::TooNew { found, supported: self.schema_version() });
344 }
345 if found < self.oldest {
346 return Err(FormatError::TooOld { found, oldest: self.oldest });
347 }
348 Ok(())
349 }
350
351 fn not_this(&self, why: &str) -> FormatError {
352 FormatError::NotThisFormat { expected: self.id.clone(), why: why.into(), detail: None }
353 }
354
355 fn parse_error(&self, e: serde_json::Error) -> FormatError {
356 if e.is_syntax() || e.is_eof() {
357 FormatError::corrupt_json("", &e)
358 } else {
359 FormatError::NotThisFormat { expected: self.id.clone(), why: e.to_string(), detail: Some(JsonError::new(&e)) }
360 }
361 }
362}
363
364pub fn migrate(table: &[Migration], mut doc: Value, from: u64) -> Result<Value, FormatError> {
367 let target = table.len() as u64 + 1;
368 if from > target {
369 return Err(FormatError::TooNew { found: from, supported: target });
370 }
371 if from == 0 {
372 return Err(FormatError::TooOld { found: 0, oldest: 1 });
373 }
374 for v in from..target {
375 let step = usize::try_from(v - 1).ok().and_then(|i| table.get(i)).ok_or(FormatError::Migration { from: v, msg: "no migration".into() })?;
376 doc = step(doc).map_err(|msg| FormatError::Migration { from: v, msg })?;
377 match doc.as_object_mut() {
378 Some(o) => {
379 o.insert(SCHEMA.into(), Value::from(v + 1));
380 }
381 None => return Err(FormatError::Migration { from: v, msg: "migration produced a non-object document".into() }),
382 }
383 }
384 Ok(doc)
385}
386
387fn version_of(v: &Value) -> Result<u64, FormatError> {
390 if let Some(n) = v.as_u64() {
391 return Ok(n);
392 }
393 match v.as_f64() {
394 Some(f) if f >= u64::MAX as f64 && f.fract() == 0.0 => Ok(u64::MAX),
395 _ => Err(FormatError::corrupt(format!("`schema_version` is not a version number: {v}"))),
396 }
397}
398
399#[derive(Default)]
401struct Probe {
402 format: Option<String>,
403 schema_version: Option<Value>,
404 has_payload: bool,
405}
406
407struct ProbeSeed<'a> {
409 key: &'a str,
410}
411
412impl<'de> DeserializeSeed<'de> for ProbeSeed<'_> {
413 type Value = Probe;
414 fn deserialize<De: Deserializer<'de>>(self, de: De) -> Result<Probe, De::Error> {
415 de.deserialize_any(self)
416 }
417}
418
419impl<'de> Visitor<'de> for ProbeSeed<'_> {
420 type Value = Probe;
421 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
422 f.write_str("a document envelope object")
423 }
424 fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Probe, A::Error> {
425 let mut p = Probe::default();
426 let (mut seen_format, mut seen_schema) = (false, false);
427 while let Some(k) = map.next_key::<String>()? {
428 if k == FORMAT {
429 if std::mem::replace(&mut seen_format, true) {
430 return Err(serde::de::Error::duplicate_field(FORMAT));
431 }
432 p.format = map.next_value()?;
433 } else if k == SCHEMA {
434 if std::mem::replace(&mut seen_schema, true) {
435 return Err(serde::de::Error::duplicate_field(SCHEMA));
436 }
437 p.schema_version = map.next_value()?;
438 } else {
439 p.has_payload |= k == self.key;
440 map.next_value::<IgnoredAny>()?;
441 }
442 }
443 Ok(p)
444 }
445 fn visit_seq<A: serde::de::SeqAccess<'de>>(self, mut seq: A) -> Result<Probe, A::Error> {
447 while seq.next_element::<IgnoredAny>()?.is_some() {}
448 Ok(Probe::default())
449 }
450}
451
452struct EnvelopeOut<'a, D> {
453 format: &'a str,
454 schema_version: u64,
455 generator: &'a str,
456 key: &'a str,
457 doc: &'a D,
458 extras: &'a Map<String, Value>,
459 extras_after: bool,
460}
461
462impl<D: Serialize> Serialize for EnvelopeOut<'_, D> {
463 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
464 use serde::ser::SerializeMap;
465 let reserved = [FORMAT, SCHEMA, GENERATOR, self.key];
466 let mut m = s.serialize_map(None)?;
467 m.serialize_entry(FORMAT, self.format)?;
468 m.serialize_entry(SCHEMA, &self.schema_version)?;
469 m.serialize_entry(GENERATOR, self.generator)?;
470 let extras = self.extras.iter().filter(|(k, _)| !reserved.contains(&k.as_str()));
471 if self.extras_after {
472 m.serialize_entry(self.key, self.doc)?;
473 for (k, v) in extras {
474 m.serialize_entry(k, v)?;
475 }
476 } else {
477 for (k, v) in extras {
478 m.serialize_entry(k, v)?;
479 }
480 m.serialize_entry(self.key, self.doc)?;
481 }
482 m.end()
483 }
484}
485
486struct EnvelopeSeed<'a, D> {
488 key: &'a str,
489 _d: PhantomData<fn() -> D>,
490}
491
492type Parts<D> = (Option<D>, Map<String, Value>, Option<String>);
493
494impl<'de, D: DeserializeOwned> DeserializeSeed<'de> for EnvelopeSeed<'_, D> {
495 type Value = Parts<D>;
496 fn deserialize<De: Deserializer<'de>>(self, de: De) -> Result<Parts<D>, De::Error> {
497 de.deserialize_map(self)
498 }
499}
500
501impl<'de, D: DeserializeOwned> Visitor<'de> for EnvelopeSeed<'_, D> {
502 type Value = Parts<D>;
503 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
504 f.write_str("a document envelope object")
505 }
506 fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Parts<D>, A::Error> {
507 let (mut doc, mut extras, mut generator) = (None, Map::new(), None);
508 while let Some(k) = map.next_key::<String>()? {
509 if k == self.key {
510 if doc.is_some() {
511 return Err(serde::de::Error::custom(format!("duplicate `{k}`")));
512 }
513 doc = Some(map.next_value::<D>()?);
514 } else if k == GENERATOR {
515 generator = map.next_value::<Option<String>>()?;
516 } else if k == FORMAT || k == SCHEMA {
517 map.next_value::<IgnoredAny>()?;
518 } else {
519 extras.insert(k, map.next_value::<Value>()?);
520 }
521 }
522 Ok((doc, extras, generator))
523 }
524}
525
526#[cfg(test)]
527mod tests {
528 use super::*;
529 use serde_json::json;
530
531 #[derive(Debug, PartialEq, Serialize, Deserialize)]
532 struct Doc {
533 title: String,
534 #[serde(default)]
535 tags: Vec<String>,
536 }
537
538 fn doc() -> Doc {
539 Doc { title: "hello".into(), tags: vec!["a".into()] }
540 }
541
542 fn v1_to_v2(v: Value) -> Result<Value, String> {
543 let Value::Object(mut o) = v else { return Err("expected an object".into()) };
545 let name = o.remove("name").unwrap_or(Value::from(""));
546 o.insert("title".into(), name);
547 Ok(json!({ "format": "test.doc", "schema_version": 2, "generator": "v1", "document": o }))
548 }
549 fn v2_to_v3(v: Value) -> Result<Value, String> {
550 Ok(v)
551 }
552 fn failing(_: Value) -> Result<Value, String> {
553 Err("cannot".into())
554 }
555 fn not_object(_: Value) -> Result<Value, String> {
556 Ok(Value::from(3))
557 }
558
559 fn fmt() -> Format<Doc> {
560 Format::new("test.doc", "Test 1.0").migrations(vec![v1_to_v2, v2_to_v3]).legacy(|v| v.get("name").map(|_| 1))
561 }
562
563 #[test]
564 fn round_trip_current_schema() {
565 let f = fmt();
566 assert_eq!(f.schema_version(), 3);
567 let bytes = f.encode(&doc()).unwrap();
568 let text = String::from_utf8(bytes.clone()).unwrap();
569 assert!(text.starts_with("{\n \"format\": \"test.doc\""), "{text}");
570 let l = f.decode(&bytes).unwrap();
571 assert_eq!(l.doc, doc());
572 assert_eq!(l.envelope, Envelope { format: "test.doc".into(), schema_version: 3, generator: Some("Test 1.0".into()) });
573 assert!(!l.migrated(f.schema_version()));
574 assert!(l.extras.is_empty());
575 }
576
577 #[test]
578 fn extras_round_trip_and_cannot_clobber_reserved_keys() {
579 let f = fmt().pretty(false);
580 let mut extras = Map::new();
581 extras.insert("view".into(), json!({"zoom": 2}));
582 extras.insert("schema_version".into(), json!(99));
583 extras.insert("document".into(), json!("evil"));
584 let bytes = f.encode_with(&doc(), &extras).unwrap();
585 let l = f.decode(&bytes).unwrap();
586 assert_eq!(l.doc, doc());
587 assert_eq!(l.envelope.schema_version, 3);
588 assert_eq!(l.extras.len(), 1);
589 assert_eq!(l.extras["view"], json!({"zoom": 2}));
590 }
591
592 #[test]
593 fn extras_after_payload_reencodes_byte_for_byte() {
594 #[derive(Serialize)]
596 struct Written<'a> {
597 format: &'a str,
598 schema_version: u64,
599 generator: &'a str,
600 document: &'a Doc,
601 view: Value,
602 }
603 let written = Written { format: "test.doc", schema_version: 3, generator: "Test 1.0", document: &doc(), view: json!({"zoom": 2}) };
604 for pretty in [true, false] {
605 let file = if pretty { serde_json::to_vec_pretty(&written) } else { serde_json::to_vec(&written) }.unwrap();
606 let f = fmt().pretty(pretty).extras_after_payload(true);
607 let l = f.decode(&file).unwrap();
608 assert_eq!(l.extras["view"], json!({"zoom": 2}));
609 assert_eq!(f.encode_with(&l.doc, &l.extras).unwrap(), file, "pretty: {pretty}");
610 let before = fmt().pretty(pretty).encode_with(&l.doc, &l.extras).unwrap();
612 assert_ne!(before, file);
613 assert_eq!(f.decode(&before).unwrap().extras, l.extras);
614 }
615 let mut extras = Map::new();
617 extras.insert("document".into(), json!("evil"));
618 extras.insert("format".into(), json!("other"));
619 let f = fmt().extras_after_payload(true);
620 let l = f.decode(&f.encode_with(&doc(), &extras).unwrap()).unwrap();
621 assert_eq!(l.doc, doc());
622 assert!(l.extras.is_empty());
623 }
624
625 #[test]
626 fn untagged_envelopes_only_when_accepted() {
627 let current = br#"{"schema_version":3,"generator":"Early","document":{"title":"t"},"view":1}"#;
628 let older = br#"{"schema_version":2,"document":{"title":"o"}}"#;
629 let refuse = fmt();
630 for b in [¤t[..], &older[..]] {
631 let e = refuse.decode(b).unwrap_err();
632 assert!(matches!(&e, FormatError::NotThisFormat { why, detail: None, .. } if why.contains("format")), "{e}");
633 }
634 let accept = fmt().accept_untagged(true);
635 let l = accept.decode(current).unwrap();
636 assert_eq!(l.doc.title, "t");
637 assert_eq!(l.envelope, Envelope { format: "test.doc".into(), schema_version: 3, generator: Some("Early".into()) });
638 assert_eq!(l.extras["view"], json!(1));
639 let l = accept.decode(older).unwrap();
640 assert_eq!(l.doc.title, "o");
641 assert_eq!(l.envelope.format, "test.doc");
642 assert!(l.migrated(accept.schema_version()));
643 let e = accept.decode(br#"{"schema_version":3,"project":{}}"#).unwrap_err();
645 assert!(matches!(e, FormatError::NotThisFormat { .. }), "{e}");
646 let e = accept.decode(br#"{"format":"other.doc","schema_version":3,"document":{}}"#).unwrap_err();
647 assert!(matches!(e, FormatError::WrongFormat { .. }), "{e}");
648 let e = accept.decode(br#"{"schema_version":4,"document":{}}"#).unwrap_err();
649 assert!(matches!(e, FormatError::TooNew { found: 4, .. }), "{e}");
650 let e = accept.decode(br#"{"format":null,"schema_version":3,"document":{"title":"n"}}"#);
651 assert_eq!(e.unwrap().doc.title, "n", "a null tag is no tag");
652 }
653
654 #[test]
655 fn errors_keep_the_serde_error() {
656 let f = fmt();
657 let bad = b"{\n \"format\": 7,\n \"schema_version\": 3\n}";
659 let e = f.decode(bad).unwrap_err();
660 let j = e.json().cloned().unwrap();
661 assert_eq!((j.line, j.column), (2, 13), "{j:?}");
662 assert!(!j.syntax);
663 assert!(j.message.contains("invalid type: integer `7`") && j.message.ends_with("at line 2 column 13"), "{}", j.message);
664 assert!(matches!(&e, FormatError::NotThisFormat { why, .. } if *why == j.message));
665 let e = f.decode(b"[1, 2]").unwrap_err();
666 assert!(e.json().is_none(), "not legacy, not a parse error: {e}");
667 let e = f.decode(br#"{"format":"test.doc","format":"test.doc","schema_version":3}"#).unwrap_err();
668 assert!(e.json().is_some_and(|j| j.message.contains("duplicate field `format`")), "{e}");
669 let broken = b"{\n \"format\": \"test.doc\",\n \"schema_version\": 3,,\n}";
671 let expected = serde_json::from_slice::<Value>(broken).unwrap_err().to_string();
672 let e = f.decode(broken).unwrap_err();
673 assert!(matches!(&e, FormatError::Corrupt { message, .. } if *message == expected), "{e}");
674 assert!(expected.ends_with("at line 3 column 23"), "{expected}");
675 let j = e.json().expect("structured detail");
676 assert_eq!((j.line, j.column, j.syntax), (3, 23, true));
677 let e = f.decode(br#"{"format":"test.doc","schema_version":3,"document":{}} trailing"#).unwrap_err();
678 assert!(matches!(e, FormatError::Corrupt { .. }), "{e}");
679 }
680
681 #[test]
682 fn migrates_legacy_and_older_envelopes() {
683 let f = fmt();
684 let l = f.decode(br#"{"name":"old","tags":["x"]}"#).unwrap();
685 assert_eq!(l.doc, Doc { title: "old".into(), tags: vec!["x".into()] });
686 assert_eq!(l.envelope.schema_version, 1);
687 assert!(l.migrated(f.schema_version()));
688 let l = f.decode(br#"{"format":"test.doc","schema_version":2,"generator":"Old","document":{"title":"t"},"view":1}"#).unwrap();
689 assert_eq!(l.doc.title, "t");
690 assert_eq!(l.envelope.generator.as_deref(), Some("Old"));
691 assert_eq!(l.extras["view"], json!(1));
692 }
693
694 #[test]
695 fn refuses_newer_and_other_formats() {
696 let f = fmt();
697 let e = f.decode(br#"{"format":"test.doc","schema_version":4,"document":{}}"#).unwrap_err();
698 assert!(matches!(e, FormatError::TooNew { found: 4, supported: 3 }), "{e}");
699 let e = f.decode(br#"{"format":"test.doc","schema_version":18446744073709551615,"document":{}}"#).unwrap_err();
700 assert!(matches!(e, FormatError::TooNew { found: u64::MAX, .. }), "{e}");
701 let e = f.decode(br#"{"format":"test.doc","schema_version":1e300,"document":{}}"#).unwrap_err();
702 assert!(matches!(e, FormatError::TooNew { found: u64::MAX, .. }), "{e}");
703 let e = f.decode(br#"{"format":"test.doc","schema_version":999999999999999999999999999,"document":{}}"#).unwrap_err();
704 assert!(matches!(e, FormatError::TooNew { .. }), "{e}");
705 let e = f.decode(br#"{"format":"other.doc","schema_version":3,"document":{}}"#).unwrap_err();
706 assert!(matches!(e, FormatError::WrongFormat { .. }), "{e}");
707 let e = f.decode(br#"{"schema_version":3,"document":{}}"#).unwrap_err();
708 assert!(matches!(e, FormatError::NotThisFormat { .. }), "{e}");
709 let e = f.decode(br#"{"title":"no envelope, not legacy"}"#).unwrap_err();
710 assert!(matches!(e, FormatError::NotThisFormat { .. }), "{e}");
711 let e = Format::<Doc>::new("test.doc", "t").oldest(2).decode(br#"{"format":"test.doc","schema_version":1,"document":{}}"#).unwrap_err();
712 assert!(matches!(e, FormatError::TooOld { found: 1, oldest: 2 }), "{e}");
713 }
714
715 #[test]
716 fn hostile_inputs_are_errors_not_crashes() {
717 let f = fmt();
718 let cases: &[&[u8]] = &[
719 b"",
720 b"{",
721 b"\xff\xfe",
722 b"[]",
723 b"null",
724 b"42",
725 br#"{"format":"test.doc","schema_version":-1,"document":{}}"#,
726 br#"{"format":"test.doc","schema_version":0,"document":{}}"#,
727 br#"{"format":"test.doc","schema_version":2.5,"document":{}}"#,
728 br#"{"format":"test.doc","schema_version":"3","document":{}}"#,
729 br#"{"format":"test.doc","schema_version":3}"#,
730 br#"{"format":"test.doc","schema_version":3,"document":{"title":5}}"#,
731 br#"{"format":"test.doc","schema_version":3,"document":{"title":"a"},"document":{"title":"b"}}"#,
732 br#"{"format":"test.doc","schema_version":2,"document":[]}"#,
733 br#"{"format":7,"schema_version":3,"document":{}}"#,
734 br#"{"format":"test.doc","schema_version":3,"generator":5,"document":{"title":"a"}}"#,
735 ];
736 for c in cases {
737 assert!(f.decode(c).is_err(), "{}", String::from_utf8_lossy(c));
738 }
739 let good = f.encode(&doc()).unwrap();
741 for n in 0..good.len() {
742 assert!(f.decode(&good[..n]).is_err(), "truncated at {n}");
743 }
744 let deep =
746 format!(r#"{{"format":"test.doc","schema_version":3,"document":{{"title":"x","tags":{}{}}}}}"#, "[".repeat(100_000), "]".repeat(100_000));
747 assert!(f.decode(deep.as_bytes()).is_err());
748 let small = Format::<Doc>::new("test.doc", "t").max_bytes(10);
749 assert!(matches!(small.decode(&good), Err(FormatError::Corrupt { .. })));
750 }
751
752 #[test]
753 fn migration_errors() {
754 let f = Format::<Doc>::new("test.doc", "t").migrations(vec![failing]);
755 let e = f.decode(br#"{"format":"test.doc","schema_version":1,"document":{"title":"a"}}"#).unwrap_err();
756 assert!(matches!(e, FormatError::Migration { from: 1, .. }), "{e}");
757 let f = Format::<Doc>::new("test.doc", "t").migrations(vec![not_object]);
758 let e = f.decode(br#"{"format":"test.doc","schema_version":1,"document":{"title":"a"}}"#).unwrap_err();
759 assert!(matches!(e, FormatError::Migration { from: 1, .. }), "{e}");
760 assert!(matches!(migrate(&[], json!({}), 0), Err(FormatError::TooOld { .. })));
761 assert!(matches!(migrate(&[], json!({}), 5), Err(FormatError::TooNew { .. })));
762 assert_eq!(migrate(&[v2_to_v3], json!({}), 1).unwrap()["schema_version"], 2);
763 }
764
765 #[test]
766 fn save_and_load_files() {
767 let tmp = tempfile::tempdir().unwrap();
768 let dir = tmp.path();
769 let p = dir.join("a.testdoc");
770 let f = fmt();
771 f.save(&p, &doc()).unwrap();
772 assert_eq!(f.load(&p).unwrap().doc, doc());
773 assert!(matches!(f.load(&dir.join("missing")), Err(FormatError::Io(_))));
774 assert!(matches!(fmt().max_bytes(5).load(&p), Err(FormatError::Corrupt { .. })));
775 }
776}