Skip to main content

typst_pack/
manifest.rs

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