infotheory/spec/document/
io.rs1use super::{DOCUMENT_MAGIC, SpecDocument, SpecError, SpecResult};
4use std::path::Path;
5
6pub fn load_spec_document(path: &str) -> SpecResult<SpecDocument> {
15 let full = Path::new(path);
16 let base_dir = full.parent().unwrap_or_else(|| Path::new("."));
17 let raw = std::fs::read(full)?;
18
19 let extension = full.extension().and_then(|value| value.to_str());
20 if extension.is_some_and(|value| value.eq_ignore_ascii_case("json")) {
21 let value = serde_json::from_slice::<serde_json::Value>(&raw).map_err(|err| {
22 SpecError::new(format!(
23 "invalid JSON spec document '{}': {err}",
24 full.display()
25 ))
26 })?;
27 return SpecDocument::parse_json_value(&value, base_dir);
28 }
29
30 if extension.is_some_and(|value| value.eq_ignore_ascii_case("itsd"))
31 || raw.starts_with(DOCUMENT_MAGIC)
32 {
33 return SpecDocument::from_binary(&raw, base_dir);
34 }
35
36 let value = serde_json::from_slice::<serde_json::Value>(&raw).map_err(|err| {
37 SpecError::new(format!(
38 "failed to parse spec document '{}': expected JSON or binary '{}' envelope; JSON parse error: {err}",
39 full.display(),
40 String::from_utf8_lossy(DOCUMENT_MAGIC)
41 ))
42 })?;
43 SpecDocument::parse_json_value(&value, base_dir)
44}
45
46#[cfg(test)]
47mod tests {
48 use super::*;
49 #[cfg(feature = "backend-ctw")]
50 use crate::api::RateBackend;
51 #[cfg(feature = "backend-ctw")]
52 use crate::spec::CanonicalJson;
53 use std::path::PathBuf;
54 use std::time::{SystemTime, UNIX_EPOCH};
55
56 fn temp_path(prefix: &str, ext: &str) -> PathBuf {
57 let nanos = SystemTime::now()
58 .duration_since(UNIX_EPOCH)
59 .expect("clock before epoch")
60 .as_nanos();
61 std::env::temp_dir().join(format!("infotheory-spec-io-{prefix}-{nanos}.{ext}"))
62 }
63
64 #[cfg(feature = "backend-ctw")]
65 #[test]
66 fn load_spec_document_detects_json_extension() {
67 let path = temp_path("json", "json");
68 let expected = SpecDocument::RateBackend(RateBackend::Ctw { depth: 8 });
69 std::fs::write(&path, expected.to_canonical_json().expect("json"))
70 .expect("write json spec");
71
72 let parsed = load_spec_document(path.to_string_lossy().as_ref()).expect("load json spec");
73 assert_eq!(
74 parsed.to_canonical_json().expect("parsed json"),
75 expected.to_canonical_json().expect("expected json")
76 );
77
78 let _ = std::fs::remove_file(path);
79 }
80
81 #[cfg(feature = "backend-ctw")]
82 #[test]
83 fn load_spec_document_detects_binary_extension_and_magic() {
84 let path = temp_path("binary", "itsd");
85 let expected = SpecDocument::RateBackend(RateBackend::Ctw { depth: 12 });
86 std::fs::write(&path, expected.to_binary()).expect("write binary spec");
87
88 let parsed =
89 load_spec_document(path.to_string_lossy().as_ref()).expect("load binary extension");
90 assert_eq!(
91 parsed.to_canonical_json().expect("parsed json"),
92 expected.to_canonical_json().expect("expected json")
93 );
94
95 let magic_path = temp_path("magic", "bin");
96 std::fs::write(&magic_path, expected.to_binary()).expect("write magic-detected binary");
97 let magic_parsed =
98 load_spec_document(magic_path.to_string_lossy().as_ref()).expect("load binary magic");
99 assert_eq!(
100 magic_parsed.to_canonical_json().expect("parsed json"),
101 expected.to_canonical_json().expect("expected json")
102 );
103
104 let _ = std::fs::remove_file(path);
105 let _ = std::fs::remove_file(magic_path);
106 }
107
108 #[test]
109 fn load_spec_document_reports_non_json_non_binary_payloads_directly() {
110 let path = temp_path("invalid", "txt");
111 std::fs::write(&path, b"not json and not binary").expect("write invalid payload");
112
113 let err = match load_spec_document(path.to_string_lossy().as_ref()) {
114 Ok(_) => panic!("invalid payload must fail"),
115 Err(err) => err,
116 };
117 let msg = err.to_string();
118 assert!(msg.contains("expected JSON or binary 'itsd' envelope"));
119 assert!(msg.contains(path.to_string_lossy().as_ref()));
120
121 let _ = std::fs::remove_file(path);
122 }
123}