Skip to main content

mdzip_core/
manifest.rs

1//! `manifest.json` handling (Section 6).
2//!
3//! A manifest is kept as a [`serde_json::Map`] in its original field order,
4//! so every field, including ones this library doesn't know, survives a
5//! read, modify and write cycle (Section 6.3).
6
7use 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
16/// A parsed `manifest.json`: the top-level JSON object, with field order kept.
17pub type Manifest = Map<String, Value>;
18
19/// The spec name written in `spec.name`.
20pub const SPEC_NAME: &str = "mdzip-spec";
21/// The MDZip specification version this library implements and writes.
22pub const SPEC_VERSION: &str = "1.2.0";
23/// The newest spec major version this library reads (Section 11.2).
24pub const SUPPORTED_MAJOR: u64 = 1;
25
26/// How an archive is interpreted (Section 5.7).
27#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
28pub enum Mode {
29    /// One logical document, possibly split across several Markdown files.
30    #[default]
31    Document,
32    /// Several related documents (Section 5.7.3).
33    Project,
34}
35
36impl Mode {
37    /// `"document"` or `"project"`.
38    pub const fn as_str(self) -> &'static str {
39        match self {
40            Mode::Document => "document",
41            Mode::Project => "project",
42        }
43    }
44
45    /// Parse a manifest `mode` value.
46    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/// A parsed manifest and the names of members it repeats (Section 6.4).
62#[derive(Clone, Debug, PartialEq)]
63pub struct ParsedManifest {
64    /// The manifest. For a repeated member, the last occurrence wins.
65    pub manifest: Manifest,
66    /// Each repeated member name, at any nesting level, once per repeat.
67    pub duplicate_members: Vec<String>,
68}
69
70/// Parse `manifest.json` bytes (UTF-8 JSON, optional BOM).
71///
72/// Fails with [`ErrorCode::ManifestInvalid`] if the bytes aren't UTF-8 JSON
73/// or the top-level value isn't an object. Repeated members are reported in
74/// [`ParsedManifest::duplicate_members`]; the last one is kept.
75pub 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
106/// Deserializes a JSON value like `serde_json::Value` does, recording
107/// repeated object members.
108struct 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
169/// Serialize a manifest as UTF-8 JSON with LF line endings, keeping field order.
170pub 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
180/// The archive's mode (Section 5.7): `document` when `mode` is absent.
181///
182/// Fails with [`ErrorCode::ManifestInvalid`] for a non-string value and
183/// [`ErrorCode::ModeUnsupported`] for an unrecognized one (Section 5.7.5).
184pub 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
202/// The manifest's `spec.version`, if it has one.
203pub fn spec_version(manifest: Option<&Manifest>) -> Option<&str> {
204    manifest?.get("spec")?.get("version")?.as_str()
205}
206
207/// Reject a `spec` or `spec.version` with the wrong type, a version that
208/// isn't semantic versioning, or a newer major version than this library
209/// supports (Section 11.2). A missing version is read as 1.x (Section 11.3).
210pub 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
244/// Validate a parsed manifest (Section 6.4) and return the descriptive
245/// fields to ignore because they have the wrong type or an invalid value.
246///
247/// Fails for a critical field with a wrong type or value: `spec` and
248/// `spec.version` ([`ErrorCode::ManifestInvalid`], or
249/// [`ErrorCode::VersionUnsupported`] for a newer major), `mode`
250/// ([`ErrorCode::ManifestInvalid`] or [`ErrorCode::ModeUnsupported`]) and,
251/// when `check_entry_point` is set, `entryPoint`
252/// ([`ErrorCode::EntryPointInvalid`]). Unrecognized fields are never checked.
253pub 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
270/// Why an `entryPoint` JSON value isn't a valid entry point path, or `None`.
271pub(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
304/// RFC 3339 `date-time`, such as `2026-03-08T14:30:00Z` (Section 6.1).
305pub(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
341/// The major version of a Semantic Versioning 2.0.0 string, or `None` if it
342/// isn't one.
343fn 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
374/// Set `modified` to `when`, keeping its existing form: an object with
375/// `when` (the current form), or a plain datetime string (the legacy form).
376pub(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}