memstead_base/validator/
canonical.rs1use std::io::{Cursor, Write};
10use std::sync::Arc;
11
12use memstead_schema::{ARCHIVE_CONFIG_PATH, PublishedMemConfig, Schema, type_by_name};
13use zip::CompressionMethod;
14use zip::DateTime;
15use zip::write::SimpleFileOptions;
16
17use super::ValidationError;
18use super::archive::SchemaFile;
19use crate::entity::generator::generate_markdown;
20use crate::entity::{Entity, id::id_to_file_path};
21
22pub fn canonical_bytes(
31 config: &PublishedMemConfig,
32 entities: &[Entity],
33 schema_files: &[SchemaFile],
34 embedded_schema: Option<&Arc<Schema>>,
35 provenance_bytes: Option<&[u8]>,
36 anchors_bytes: Option<&[u8]>,
37) -> Result<Vec<u8>, ValidationError> {
38 let mut files: Vec<(String, Vec<u8>)> =
39 Vec::with_capacity(entities.len() + schema_files.len() + 3);
40
41 let config_text = canonical_json(config)?;
42 files.push((ARCHIVE_CONFIG_PATH.to_string(), config_text.into_bytes()));
43
44 if let Some(prov) = provenance_bytes {
49 files.push((
50 memstead_schema::ARCHIVE_PROVENANCE_PATH.to_string(),
51 prov.to_vec(),
52 ));
53 }
54
55 if let Some(anchors) = anchors_bytes {
60 files.push((
61 memstead_schema::ARCHIVE_ANCHORS_PATH.to_string(),
62 anchors.to_vec(),
63 ));
64 }
65
66 for sf in schema_files {
70 files.push((sf.archive_path.clone(), sf.content.clone().into_bytes()));
71 }
72
73 for entity in entities {
74 let schema = embedded_schema
78 .and_then(|s| s.get_type(&entity.entity_type))
79 .or_else(|| type_by_name(&entity.entity_type))
80 .ok_or_else(|| {
81 ValidationError::GraphConstructionFailed(format!(
82 "entity {} references unresolved type {:?}",
83 entity.id.as_ref(),
84 entity.entity_type
85 ))
86 })?;
87 let md = generate_markdown(entity, &schema);
88 let lf = normalize_lf(&md);
89 let path = id_to_file_path(&entity.id);
90 files.push((path, lf.into_bytes()));
91 }
92
93 files.sort_by(|a, b| a.0.cmp(&b.0));
94
95 let mut buf = Vec::new();
96 {
97 let cursor = Cursor::new(&mut buf);
98 let mut writer = zip::ZipWriter::new(cursor);
99 let options = SimpleFileOptions::default()
100 .compression_method(CompressionMethod::Deflated)
101 .compression_level(Some(6))
102 .last_modified_time(DateTime::default());
103
104 for (path, content) in files {
105 writer
106 .start_file(&path, options)
107 .map_err(|e| ValidationError::GraphConstructionFailed(e.to_string()))?;
108 writer
109 .write_all(&content)
110 .map_err(|e| ValidationError::GraphConstructionFailed(e.to_string()))?;
111 }
112 writer
113 .finish()
114 .map_err(|e| ValidationError::GraphConstructionFailed(e.to_string()))?;
115 }
116
117 Ok(buf)
118}
119
120fn normalize_lf(s: &str) -> String {
121 s.replace("\r\n", "\n")
122}
123
124pub fn canonical_json(config: &PublishedMemConfig) -> Result<String, ValidationError> {
129 let value = serde_json::to_value(config).map_err(|e| ValidationError::InvalidConfig {
130 reason: e.to_string(),
131 })?;
132 let mut out = String::new();
133 write_canonical(&value, &mut out, 0);
134 out.push('\n');
135 Ok(out)
136}
137
138fn write_canonical(value: &serde_json::Value, out: &mut String, indent: usize) {
139 match value {
140 serde_json::Value::Null => out.push_str("null"),
141 serde_json::Value::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
142 serde_json::Value::Number(n) => out.push_str(&n.to_string()),
143 serde_json::Value::String(s) => {
144 let escaped = serde_json::to_string(s).expect("string escape infallible");
148 out.push_str(&escaped);
149 }
150 serde_json::Value::Array(items) => {
151 if items.is_empty() {
152 out.push_str("[]");
153 return;
154 }
155 out.push('[');
156 for (i, item) in items.iter().enumerate() {
157 out.push('\n');
158 push_indent(out, indent + 1);
159 write_canonical(item, out, indent + 1);
160 if i < items.len() - 1 {
161 out.push(',');
162 }
163 }
164 out.push('\n');
165 push_indent(out, indent);
166 out.push(']');
167 }
168 serde_json::Value::Object(map) => {
169 if map.is_empty() {
170 out.push_str("{}");
171 return;
172 }
173 let mut keys: Vec<&String> = map.keys().collect();
174 keys.sort();
175 out.push('{');
176 for (i, key) in keys.iter().enumerate() {
177 out.push('\n');
178 push_indent(out, indent + 1);
179 let key_escaped = serde_json::to_string(*key).expect("string escape infallible");
180 out.push_str(&key_escaped);
181 out.push_str(": ");
182 write_canonical(&map[*key], out, indent + 1);
183 if i < keys.len() - 1 {
184 out.push(',');
185 }
186 }
187 out.push('\n');
188 push_indent(out, indent);
189 out.push('}');
190 }
191 }
192}
193
194fn push_indent(out: &mut String, indent: usize) {
195 for _ in 0..indent * 2 {
196 out.push(' ');
197 }
198}
199
200#[cfg(test)]
201mod tests {
202 use super::*;
203 use semver::Version;
204
205 fn config() -> PublishedMemConfig {
206 PublishedMemConfig {
207 format: memstead_schema::PUBLISHED_MEM_FORMAT,
208 name: "example".to_string(),
209 version: Version::parse("0.1.0").unwrap(),
210 description: Some("a test mem".to_string()),
211 title: None,
212 subject: None,
213 authors: Some(vec!["Alice".to_string(), "Bob".to_string()]),
214 schema: "default@1.0.0".parse().unwrap(),
215 }
216 }
217
218 #[test]
219 fn canonical_json_is_alpha_sorted() {
220 let json = canonical_json(&config()).unwrap();
221 let expected = "{\n \"authors\": [\n \"Alice\",\n \"Bob\"\n ],\n \"description\": \"a test mem\",\n \"format\": 4,\n \"name\": \"example\",\n \"schema\": \"default@1.0.0\",\n \"version\": \"0.1.0\"\n}\n";
223 assert_eq!(json, expected);
224 }
225
226 #[test]
227 fn canonical_json_handles_empty_array_inline() {
228 let mut c = config();
229 c.authors = Some(Vec::new());
230 let json = canonical_json(&c).unwrap();
231 assert!(json.contains("\"authors\": []"));
232 }
233
234 #[test]
235 fn canonical_json_ends_with_lf() {
236 let json = canonical_json(&config()).unwrap();
237 assert!(json.ends_with('\n'));
238 assert!(!json.ends_with("\r\n"));
239 }
240
241 #[test]
242 fn canonical_json_escapes_quotes_in_strings() {
243 let mut c = config();
244 c.description = Some("he said \"hi\"".to_string());
245 let json = canonical_json(&c).unwrap();
246 assert!(json.contains("\"he said \\\"hi\\\"\""));
247 }
248
249 #[test]
250 fn canonical_bytes_produces_deterministic_output() {
251 let c = config();
254 let a = canonical_bytes(&c, &[], &[], None, None, None).unwrap();
255 let b = canonical_bytes(&c, &[], &[], None, None, None).unwrap();
256 assert_eq!(a, b);
257 }
258
259 #[test]
260 fn normalize_lf_converts_crlf() {
261 assert_eq!(normalize_lf("a\r\nb\r\nc"), "a\nb\nc");
262 assert_eq!(normalize_lf("a\nb\nc"), "a\nb\nc");
263 assert_eq!(normalize_lf("a\rb"), "a\rb");
265 }
266}