1use std::cell::RefCell;
8use std::fmt;
9
10use serde::de::{self, DeserializeSeed, Deserializer, MapAccess, SeqAccess, Visitor};
11use serde_json::{Map, Value};
12
13use crate::error::{ErrorCode, MdzError, Result};
14use crate::paths::{cover_problem, entry_point_problem};
15
16pub type Manifest = Map<String, Value>;
18
19pub const SPEC_NAME: &str = "mdzip-spec";
21pub const SPEC_VERSION: &str = "1.2.0";
23pub const SUPPORTED_MAJOR: u64 = 1;
25
26#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
28pub enum Mode {
29 #[default]
31 Document,
32 Project,
34}
35
36impl Mode {
37 pub const fn as_str(self) -> &'static str {
39 match self {
40 Mode::Document => "document",
41 Mode::Project => "project",
42 }
43 }
44
45 pub fn parse(value: &str) -> Option<Mode> {
47 match value {
48 "document" => Some(Mode::Document),
49 "project" => Some(Mode::Project),
50 _ => None,
51 }
52 }
53}
54
55impl fmt::Display for Mode {
56 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57 f.write_str(self.as_str())
58 }
59}
60
61#[derive(Clone, Debug, PartialEq)]
63pub struct ParsedManifest {
64 pub manifest: Manifest,
66 pub duplicate_members: Vec<String>,
68}
69
70pub fn parse_manifest(data: &[u8]) -> Result<ParsedManifest> {
76 let data = data.strip_prefix(b"\xEF\xBB\xBF").unwrap_or(data);
77 let text = std::str::from_utf8(data).map_err(|e| {
78 MdzError::new(
79 ErrorCode::ManifestInvalid,
80 format!("manifest.json is not valid UTF-8: {e}"),
81 )
82 })?;
83 let duplicates = RefCell::new(Vec::new());
84 let mut de = serde_json::Deserializer::from_str(text);
85 let value = TrackDuplicates(&duplicates)
86 .deserialize(&mut de)
87 .and_then(|v| de.end().map(|_| v))
88 .map_err(|e| {
89 MdzError::new(
90 ErrorCode::ManifestInvalid,
91 format!("manifest.json is not valid JSON: {e}"),
92 )
93 })?;
94 match value {
95 Value::Object(manifest) => Ok(ParsedManifest {
96 manifest,
97 duplicate_members: duplicates.into_inner(),
98 }),
99 _ => Err(MdzError::new(
100 ErrorCode::ManifestInvalid,
101 "manifest.json must be a JSON object",
102 )),
103 }
104}
105
106struct TrackDuplicates<'a>(&'a RefCell<Vec<String>>);
109
110impl<'de> DeserializeSeed<'de> for TrackDuplicates<'_> {
111 type Value = Value;
112 fn deserialize<D: Deserializer<'de>>(self, deserializer: D) -> Result<Value, D::Error> {
113 deserializer.deserialize_any(self)
114 }
115}
116
117impl<'de> Visitor<'de> for TrackDuplicates<'_> {
118 type Value = Value;
119
120 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
121 f.write_str("a JSON value")
122 }
123 fn visit_bool<E>(self, v: bool) -> Result<Value, E> {
124 Ok(Value::Bool(v))
125 }
126 fn visit_i64<E>(self, v: i64) -> Result<Value, E> {
127 Ok(Value::from(v))
128 }
129 fn visit_u64<E>(self, v: u64) -> Result<Value, E> {
130 Ok(Value::from(v))
131 }
132 fn visit_f64<E: de::Error>(self, v: f64) -> Result<Value, E> {
133 serde_json::Number::from_f64(v)
134 .map(Value::Number)
135 .ok_or_else(|| E::custom("number out of range"))
136 }
137 fn visit_str<E>(self, v: &str) -> Result<Value, E> {
138 Ok(Value::String(v.to_owned()))
139 }
140 fn visit_string<E>(self, v: String) -> Result<Value, E> {
141 Ok(Value::String(v))
142 }
143 fn visit_unit<E>(self) -> Result<Value, E> {
144 Ok(Value::Null)
145 }
146 fn visit_none<E>(self) -> Result<Value, E> {
147 Ok(Value::Null)
148 }
149 fn visit_seq<A: SeqAccess<'de>>(self, mut seq: A) -> Result<Value, A::Error> {
150 let mut out = Vec::new();
151 while let Some(v) = seq.next_element_seed(TrackDuplicates(self.0))? {
152 out.push(v);
153 }
154 Ok(Value::Array(out))
155 }
156 fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Value, A::Error> {
157 let mut out = Map::new();
158 while let Some(key) = map.next_key::<String>()? {
159 let value = map.next_value_seed(TrackDuplicates(self.0))?;
160 if out.contains_key(&key) {
161 self.0.borrow_mut().push(key.clone());
162 }
163 out.insert(key, value);
164 }
165 Ok(Value::Object(out))
166 }
167}
168
169pub fn serialize_manifest(manifest: &Manifest) -> Vec<u8> {
171 let mut out = serde_json::to_vec_pretty(manifest).expect("a JSON map always serializes");
172 out.push(b'\n');
173 out
174}
175
176fn describe(value: &Value) -> String {
177 serde_json::to_string(value).unwrap_or_default()
178}
179
180pub fn manifest_mode(manifest: Option<&Manifest>) -> Result<Mode> {
185 let Some(value) = manifest.and_then(|m| m.get("mode")) else {
186 return Ok(Mode::Document);
187 };
188 let Value::String(text) = value else {
189 return Err(MdzError::new(
190 ErrorCode::ManifestInvalid,
191 format!("mode must be a string, not {}", describe(value)),
192 ));
193 };
194 Mode::parse(text).ok_or_else(|| {
195 MdzError::new(
196 ErrorCode::ModeUnsupported,
197 format!("unrecognized mode '{text}'; supported: document, project"),
198 )
199 })
200}
201
202pub fn spec_version(manifest: Option<&Manifest>) -> Option<&str> {
204 manifest?.get("spec")?.get("version")?.as_str()
205}
206
207pub fn check_spec_version(manifest: Option<&Manifest>) -> Result<()> {
211 let Some(spec) = manifest.and_then(|m| m.get("spec")) else {
212 return Ok(());
213 };
214 let Value::Object(spec) = spec else {
215 return Err(MdzError::new(
216 ErrorCode::ManifestInvalid,
217 format!("spec must be an object, not {}", describe(spec)),
218 ));
219 };
220 let Some(version) = spec.get("version") else {
221 return Ok(());
222 };
223 let Value::String(version) = version else {
224 return Err(MdzError::new(
225 ErrorCode::ManifestInvalid,
226 format!("spec.version must be a string, not {}", describe(version)),
227 ));
228 };
229 let Some(major) = semver_major(version) else {
230 return Err(MdzError::new(
231 ErrorCode::ManifestInvalid,
232 format!("spec.version '{version}' is not a semantic version"),
233 ));
234 };
235 if major > SUPPORTED_MAJOR {
236 return Err(MdzError::new(
237 ErrorCode::VersionUnsupported,
238 format!("archive targets spec {version}; this library supports {SUPPORTED_MAJOR}.x"),
239 ));
240 }
241 Ok(())
242}
243
244pub fn validate_manifest(manifest: &Manifest, check_entry_point: bool) -> Result<Vec<&'static str>> {
254 check_spec_version(Some(manifest))?;
255 manifest_mode(Some(manifest))?;
256 if check_entry_point {
257 if let Some(value) = manifest.get("entryPoint") {
258 if let Some(problem) = entry_point_value_problem(value) {
259 return Err(MdzError::new(ErrorCode::EntryPointInvalid, problem));
260 }
261 }
262 }
263 Ok(DESCRIPTIVE
264 .iter()
265 .filter(|(name, ok)| manifest.get(*name).is_some_and(|v| !ok(v)))
266 .map(|(name, _)| *name)
267 .collect())
268}
269
270pub(crate) fn entry_point_value_problem(value: &Value) -> Option<String> {
272 match value {
273 Value::String(s) => entry_point_problem(s),
274 _ => Some("entryPoint must be a non-empty string".into()),
275 }
276}
277
278type FieldCheck = fn(&Value) -> bool;
279
280const DESCRIPTIVE: [(&str, FieldCheck); 11] = [
281 ("title", |v| v.as_str().is_some_and(|s| !s.is_empty())),
282 ("language", Value::is_string),
283 ("description", Value::is_string),
284 ("version", Value::is_string),
285 ("license", Value::is_string),
286 ("keywords", |v| {
287 v.as_array().is_some_and(|a| a.iter().all(Value::is_string))
288 }),
289 ("author", Value::is_object),
290 ("producer", Value::is_object),
291 ("created", is_timestamp),
292 ("modified", is_timestamp),
293 ("cover", |v| v.as_str().is_some_and(|s| cover_problem(s).is_none())),
294];
295
296fn is_timestamp(value: &Value) -> bool {
297 match value {
298 Value::String(s) => is_date_time(s),
299 Value::Object(o) => o.get("when").and_then(Value::as_str).is_some_and(is_date_time),
300 _ => false,
301 }
302}
303
304pub(crate) fn is_date_time(s: &str) -> bool {
306 let b = s.as_bytes();
307 let digits = |r: std::ops::Range<usize>| b.get(r).is_some_and(|d| d.iter().all(u8::is_ascii_digit));
308 if b.len() < 20
309 || !digits(0..4)
310 || b[4] != b'-'
311 || !digits(5..7)
312 || b[7] != b'-'
313 || !digits(8..10)
314 || !matches!(b[10], b'T' | b't')
315 || !digits(11..13)
316 || b[13] != b':'
317 || !digits(14..16)
318 || b[16] != b':'
319 || !digits(17..19)
320 {
321 return false;
322 }
323 let mut i = 19;
324 if b[i] == b'.' {
325 let start = i + 1;
326 i = start;
327 while i < b.len() && b[i].is_ascii_digit() {
328 i += 1;
329 }
330 if i == start {
331 return false;
332 }
333 }
334 match &b[i..] {
335 [b'Z' | b'z'] => true,
336 [b'+' | b'-', h1, h2, b':', m1, m2] => [h1, h2, m1, m2].iter().all(|d| d.is_ascii_digit()),
337 _ => false,
338 }
339}
340
341fn semver_major(version: &str) -> Option<u64> {
344 let (rest, build) = match version.split_once('+') {
345 Some((r, b)) => (r, Some(b)),
346 None => (version, None),
347 };
348 let (core, pre) = match rest.split_once('-') {
349 Some((c, p)) => (c, Some(p)),
350 None => (rest, None),
351 };
352 let numeric = |s: &str| !s.is_empty() && s.bytes().all(|c| c.is_ascii_digit()) && (s == "0" || !s.starts_with('0'));
353 let ident = |s: &str| !s.is_empty() && s.bytes().all(|c| c.is_ascii_alphanumeric() || c == b'-');
354 let parts: Vec<&str> = core.split('.').collect();
355 if parts.len() != 3 || !parts.iter().all(|p| numeric(p)) {
356 return None;
357 }
358 if let Some(pre) = pre {
359 let ok = pre
360 .split('.')
361 .all(|id| ident(id) && (!id.bytes().all(|c| c.is_ascii_digit()) || numeric(id)));
362 if !ok {
363 return None;
364 }
365 }
366 if let Some(build) = build {
367 if !build.split('.').all(ident) {
368 return None;
369 }
370 }
371 parts[0].parse().ok()
372}
373
374pub(crate) fn set_modified(manifest: &mut Manifest, when: &str) {
377 match manifest.get_mut("modified") {
378 Some(Value::Object(o)) => {
379 o.insert("when".into(), Value::String(when.into()));
380 }
381 Some(Value::String(s)) => *s = when.into(),
382 _ => {
383 let mut o = Map::new();
384 o.insert("when".into(), Value::String(when.into()));
385 manifest.insert("modified".into(), Value::Object(o));
386 }
387 }
388}
389
390#[cfg(test)]
391mod tests {
392 use super::*;
393
394 #[test]
395 fn semver() {
396 assert_eq!(semver_major("1.2.0"), Some(1));
397 assert_eq!(semver_major("2.0.0-rc.1+build.5"), Some(2));
398 assert_eq!(semver_major("01.0.0"), None);
399 assert_eq!(semver_major("1.0"), None);
400 assert_eq!(semver_major("1.0.0-01"), None);
401 assert_eq!(semver_major("1.0.0-"), None);
402 }
403
404 #[test]
405 fn date_times() {
406 assert!(is_date_time("2026-03-08T14:30:00Z"));
407 assert!(is_date_time("2026-03-08T09:30:00.123-05:00"));
408 assert!(!is_date_time("2026-03-08"));
409 assert!(!is_date_time("2026-03-08 14:30:00Z"));
410 assert!(!is_date_time("2026-03-08T14:30:00"));
411 }
412
413 #[test]
414 fn duplicates_are_reported_and_the_last_wins() {
415 let parsed = parse_manifest(br#"{"title":"a","x":{"k":1,"k":2},"title":"b"}"#).unwrap();
416 assert_eq!(parsed.manifest["title"], "b");
417 assert_eq!(parsed.manifest["x"]["k"], 2);
418 assert_eq!(parsed.duplicate_members, vec!["k", "title"]);
419 }
420}