Skip to main content

typst_pack/
manifest.rs

1//! The pack manifest stored as `typst-pack.toml` inside the archive.
2
3use std::str::FromStr;
4
5use serde::{Deserialize, Deserializer, Serialize};
6use typst::syntax::package::PackageSpec;
7
8/// The archive entry name of the manifest.
9pub const MANIFEST_PATH: &str = "typst-pack.toml";
10
11/// The pack format version this crate reads and writes.
12pub const FORMAT_VERSION: u32 = 1;
13
14/// The parsed contents of `typst-pack.toml`.
15#[derive(Debug, Clone, PartialEq, Serialize)]
16#[serde(rename_all = "kebab-case")]
17pub(crate) struct PackManifest {
18    /// The pack format version. Readers must reject versions they don't know.
19    format_version: u32,
20    /// The packed Typst project.
21    project: ProjectManifest,
22    /// Package dependencies observed while creating the pack.
23    #[serde(default, skip_serializing_if = "PackagesManifest::is_empty")]
24    packages: PackagesManifest,
25    /// Fonts embedded in the pack.
26    #[serde(default, skip_serializing_if = "Vec::is_empty")]
27    fonts: Vec<FontManifest>,
28    /// Optional descriptive metadata about the packed project.
29    #[serde(default, skip_serializing_if = "Option::is_none")]
30    metadata: Option<PackMetadata>,
31}
32
33#[derive(Deserialize)]
34#[serde(rename_all = "kebab-case", deny_unknown_fields)]
35struct Version1Manifest {
36    format_version: u32,
37    project: ProjectManifest,
38    #[serde(default)]
39    packages: Version1PackagesManifest,
40    #[serde(default)]
41    fonts: Vec<FontManifest>,
42    #[serde(default)]
43    metadata: Option<PackMetadata>,
44}
45
46impl TryFrom<Version1Manifest> for PackManifest {
47    type Error = PackManifestError;
48
49    fn try_from(manifest: Version1Manifest) -> Result<Self, Self::Error> {
50        Ok(Self {
51            format_version: manifest.format_version,
52            project: manifest.project,
53            packages: manifest.packages.try_into()?,
54            fonts: manifest.fonts,
55            metadata: manifest.metadata,
56        })
57    }
58}
59
60impl<'de> Deserialize<'de> for PackManifest {
61    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
62    where
63        D: Deserializer<'de>,
64    {
65        let value = toml::Value::deserialize(deserializer)?;
66        parse_manifest_value(value).map_err(serde::de::Error::custom)
67    }
68}
69
70/// The `[project]` section.
71#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
72#[serde(rename_all = "kebab-case", deny_unknown_fields)]
73pub(crate) struct ProjectManifest {
74    /// The root-relative path of the entrypoint file, e.g. `main.typ`.
75    entrypoint: String,
76}
77
78/// The `[packages]` section.
79#[derive(Debug, Clone, Default, PartialEq, Serialize)]
80#[serde(rename_all = "kebab-case", deny_unknown_fields)]
81pub(crate) struct PackagesManifest {
82    /// Exact package trees whose files are stored inside the Pack.
83    #[serde(default, skip_serializing_if = "Vec::is_empty")]
84    vendored: Vec<PackageManifest>,
85    /// Exact package trees that must be externally fulfilled.
86    #[serde(default, skip_serializing_if = "Vec::is_empty")]
87    unvendored: Vec<PackageManifest>,
88}
89
90#[derive(Default, Deserialize)]
91#[serde(rename_all = "kebab-case", deny_unknown_fields)]
92struct Version1PackagesManifest {
93    #[serde(default)]
94    vendored: Vec<PackageManifest>,
95    #[serde(default)]
96    unvendored: Vec<PackageManifest>,
97}
98
99impl TryFrom<Version1PackagesManifest> for PackagesManifest {
100    type Error = PackManifestError;
101
102    fn try_from(packages: Version1PackagesManifest) -> Result<Self, Self::Error> {
103        Ok(Self {
104            vendored: packages.vendored,
105            unvendored: packages.unvendored,
106        })
107    }
108}
109
110impl<'de> Deserialize<'de> for PackagesManifest {
111    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
112    where
113        D: Deserializer<'de>,
114    {
115        Version1PackagesManifest::deserialize(deserializer)?
116            .try_into()
117            .map_err(serde::de::Error::custom)
118    }
119}
120
121/// One exact Package Tree declaration.
122#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
123#[serde(rename_all = "kebab-case", deny_unknown_fields)]
124pub(crate) struct PackageManifest {
125    spec: String,
126    tree_digest: String,
127    tree_identity_kind: String,
128    tree_identity_schema: String,
129    tree_identity_algorithm: String,
130    file_count: u64,
131    byte_length: u64,
132}
133
134/// One `[[fonts]]` entry.
135#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
136#[serde(rename_all = "kebab-case", deny_unknown_fields)]
137pub(crate) struct FontManifest {
138    /// The archive entry holding the font data, e.g. `fonts/dejavu-sans.ttf`.
139    path: String,
140    /// The face index inside the font file (non-zero for collections).
141    #[serde(default, skip_serializing_if = "is_zero")]
142    index: u32,
143    /// Family names provided by this face, informational only.
144    #[serde(default, skip_serializing_if = "Vec::is_empty")]
145    families: Vec<String>,
146    /// Whether the exact container must be supplied when compiling.
147    #[serde(default, skip_serializing_if = "is_false")]
148    external: bool,
149    /// The canonical container digest, encoded as 32 lowercase hexadecimal digits.
150    #[serde(default, skip_serializing_if = "Option::is_none")]
151    container_digest: Option<String>,
152    /// Canonical identity kind.
153    #[serde(default, skip_serializing_if = "Option::is_none")]
154    container_identity_kind: Option<String>,
155    /// Canonical identity schema.
156    #[serde(default, skip_serializing_if = "Option::is_none")]
157    container_identity_schema: Option<String>,
158    /// Canonical identity digest algorithm.
159    #[serde(default, skip_serializing_if = "Option::is_none")]
160    container_identity_algorithm: Option<String>,
161    /// The exact container byte length.
162    #[serde(default, skip_serializing_if = "Option::is_none")]
163    container_length: Option<u64>,
164}
165
166fn is_zero(index: &u32) -> bool {
167    *index == 0
168}
169
170fn is_false(value: &bool) -> bool {
171    !*value
172}
173
174/// The optional `[metadata]` section.
175#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
176#[serde(rename_all = "kebab-case", deny_unknown_fields)]
177pub struct PackMetadata {
178    #[serde(default, skip_serializing_if = "Option::is_none")]
179    name: Option<String>,
180    #[serde(default, skip_serializing_if = "Option::is_none")]
181    description: Option<String>,
182    #[serde(default, skip_serializing_if = "Vec::is_empty")]
183    authors: Vec<String>,
184}
185
186impl ProjectManifest {
187    /// The root-relative entrypoint path.
188    pub fn entrypoint(&self) -> &str {
189        &self.entrypoint
190    }
191}
192
193impl PackagesManifest {
194    /// Exact package trees stored inside the Pack.
195    pub fn vendored(&self) -> &[PackageManifest] {
196        &self.vendored
197    }
198
199    /// Exact package trees fulfilled outside the Pack.
200    pub fn unvendored(&self) -> &[PackageManifest] {
201        &self.unvendored
202    }
203
204    fn is_empty(&self) -> bool {
205        self.vendored.is_empty() && self.unvendored.is_empty()
206    }
207}
208
209impl PackageManifest {
210    #[cfg(test)]
211    pub(crate) fn new(
212        spec: PackageSpec,
213        tree_digest: String,
214        file_count: u64,
215        byte_length: u64,
216    ) -> Self {
217        Self {
218            spec: spec.to_string(),
219            tree_digest,
220            tree_identity_kind: "complete-package-tree".to_owned(),
221            tree_identity_schema: "typst-pack-complete-package-tree-v1".to_owned(),
222            tree_identity_algorithm: "typst-hash128-0.15".to_owned(),
223            file_count,
224            byte_length,
225        }
226    }
227
228    pub(crate) fn spec(&self) -> Result<PackageSpec, InvalidPackageSpec> {
229        PackageSpec::from_str(&self.spec).map_err(|error| InvalidPackageSpec {
230            spec: self.spec.clone(),
231            message: error.to_string(),
232        })
233    }
234
235    pub fn tree_digest(&self) -> &str {
236        &self.tree_digest
237    }
238    pub fn tree_identity_kind(&self) -> &str {
239        &self.tree_identity_kind
240    }
241    pub fn tree_identity_schema(&self) -> &str {
242        &self.tree_identity_schema
243    }
244    pub fn tree_identity_algorithm(&self) -> &str {
245        &self.tree_identity_algorithm
246    }
247    pub fn file_count(&self) -> u64 {
248        self.file_count
249    }
250    pub fn byte_length(&self) -> u64 {
251        self.byte_length
252    }
253}
254
255impl std::fmt::Display for PackageManifest {
256    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
257        formatter.write_str(&self.spec)
258    }
259}
260
261impl FontManifest {
262    #[cfg(test)]
263    pub(crate) fn with_identity_fields(
264        container_digest: Option<String>,
265        container_identity_kind: Option<String>,
266        container_identity_schema: Option<String>,
267        container_identity_algorithm: Option<String>,
268    ) -> Self {
269        Self {
270            path: "fonts/test.ttf".to_owned(),
271            index: 0,
272            families: Vec::new(),
273            external: false,
274            container_digest,
275            container_identity_kind,
276            container_identity_schema,
277            container_identity_algorithm,
278            container_length: None,
279        }
280    }
281
282    /// The archive path containing this font's bytes.
283    pub fn path(&self) -> &str {
284        &self.path
285    }
286
287    /// The face index within the font data.
288    pub fn index(&self) -> u32 {
289        self.index
290    }
291
292    /// Informational family names declared for this face.
293    #[cfg(test)]
294    pub(crate) fn families(&self) -> &[String] {
295        &self.families
296    }
297
298    /// Whether this face's container is externally fulfilled.
299    pub fn is_external(&self) -> bool {
300        self.external
301    }
302
303    pub(crate) fn container_digest(&self) -> Option<&str> {
304        self.container_digest.as_deref()
305    }
306
307    pub(crate) fn container_length(&self) -> Option<u64> {
308        self.container_length
309    }
310
311    pub(crate) fn container_identity_kind(&self) -> Option<&str> {
312        self.container_identity_kind.as_deref()
313    }
314
315    pub(crate) fn container_identity_schema(&self) -> Option<&str> {
316        self.container_identity_schema.as_deref()
317    }
318
319    pub(crate) fn container_identity_algorithm(&self) -> Option<&str> {
320        self.container_identity_algorithm.as_deref()
321    }
322}
323
324impl PackMetadata {
325    /// Creates empty Pack metadata.
326    pub fn new() -> Self {
327        Self::default()
328    }
329
330    /// Sets the human-readable Pack name.
331    pub fn with_name(mut self, name: impl Into<String>) -> Self {
332        self.name = Some(name.into());
333        self
334    }
335
336    /// Sets the Pack description.
337    pub fn with_description(mut self, description: impl Into<String>) -> Self {
338        self.description = Some(description.into());
339        self
340    }
341
342    /// Adds a Pack author.
343    pub fn with_author(mut self, author: impl Into<String>) -> Self {
344        self.authors.push(author.into());
345        self
346    }
347
348    /// The human-readable Pack name.
349    pub fn name(&self) -> Option<&str> {
350        self.name.as_deref()
351    }
352
353    /// The Pack description.
354    pub fn description(&self) -> Option<&str> {
355        self.description.as_deref()
356    }
357
358    /// The Pack authors.
359    pub fn authors(&self) -> &[String] {
360        &self.authors
361    }
362}
363
364/// A manifest that could not be accepted.
365#[derive(Debug, thiserror::Error)]
366#[non_exhaustive]
367pub enum PackManifestError {
368    #[error("failed to parse manifest: {0}")]
369    Parse(#[from] toml::de::Error),
370    #[error("missing or invalid `format-version`")]
371    InvalidFormatVersion,
372    #[error("unsupported pack format version {0} (this reader supports version {FORMAT_VERSION})")]
373    UnsupportedVersion(u32),
374    #[error("the {MANIFEST_PATH} manifest is not valid UTF-8: {0}")]
375    NotUtf8(#[source] std::str::Utf8Error),
376}
377
378#[derive(Debug)]
379pub(crate) struct InvalidPackageSpec {
380    pub(crate) spec: String,
381    pub(crate) message: String,
382}
383
384impl PackManifest {
385    #[cfg(test)]
386    pub(crate) fn new(
387        entrypoint: String,
388        vendored_packages: Vec<PackageManifest>,
389        unvendored_packages: Vec<PackageManifest>,
390        fonts: Vec<FontManifest>,
391        metadata: Option<PackMetadata>,
392    ) -> Self {
393        Self {
394            format_version: FORMAT_VERSION,
395            project: ProjectManifest { entrypoint },
396            packages: PackagesManifest {
397                vendored: vendored_packages,
398                unvendored: unvendored_packages,
399            },
400            fonts,
401            metadata,
402        }
403    }
404
405    /// The Pack format version.
406    #[cfg(test)]
407    pub(crate) fn format_version(&self) -> u32 {
408        self.format_version
409    }
410
411    /// The project declarations.
412    pub fn project(&self) -> &ProjectManifest {
413        &self.project
414    }
415
416    /// The package declarations.
417    pub fn packages(&self) -> &PackagesManifest {
418        &self.packages
419    }
420
421    /// The embedded font declarations.
422    pub fn fonts(&self) -> &[FontManifest] {
423        &self.fonts
424    }
425
426    /// Optional descriptive Pack metadata.
427    pub fn metadata(&self) -> Option<&PackMetadata> {
428        self.metadata.as_ref()
429    }
430
431    /// Parses and validates a manifest from TOML text.
432    pub fn from_toml(text: &str) -> Result<Self, PackManifestError> {
433        Self::from_toml_value(toml::from_str(text)?)
434    }
435
436    pub(crate) fn from_toml_value(value: toml::Value) -> Result<Self, PackManifestError> {
437        parse_manifest_value(value)
438    }
439
440    /// Serializes the manifest to TOML text.
441    #[cfg(test)]
442    pub fn to_toml(&self) -> String {
443        toml::to_string_pretty(self).expect("manifest is always serializable")
444    }
445
446    /// Checks internal consistency of the manifest.
447    fn validate(&self) -> Result<(), PackManifestError> {
448        if self.format_version != FORMAT_VERSION {
449            return Err(PackManifestError::UnsupportedVersion(self.format_version));
450        }
451        Ok(())
452    }
453}
454
455fn parse_manifest_value(value: toml::Value) -> Result<PackManifest, PackManifestError> {
456    let version = value
457        .get("format-version")
458        .and_then(toml::Value::as_integer)
459        .ok_or(PackManifestError::InvalidFormatVersion)?;
460    let version = u32::try_from(version).map_err(|_| PackManifestError::InvalidFormatVersion)?;
461    if version != FORMAT_VERSION {
462        return Err(PackManifestError::UnsupportedVersion(version));
463    }
464    let wire: Version1Manifest = value.try_into()?;
465    let manifest = PackManifest::try_from(wire)?;
466    manifest.validate()?;
467    Ok(manifest)
468}