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, Serialize};
6use typst::syntax::VirtualPath;
7use typst::syntax::package::PackageSpec;
8
9/// The archive entry name of the manifest.
10pub const MANIFEST_PATH: &str = "typst-pack.toml";
11
12/// The pack format version this crate reads and writes.
13pub const FORMAT_VERSION: u32 = 1;
14
15/// The parsed contents of `typst-pack.toml`.
16#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
17#[serde(rename_all = "kebab-case", deny_unknown_fields)]
18pub struct Manifest {
19    /// The pack format version. Readers must reject versions they don't know.
20    pub format_version: u32,
21    /// The packed Typst project.
22    pub project: ProjectManifest,
23    /// Package dependencies observed while creating the pack.
24    #[serde(default, skip_serializing_if = "PackagesManifest::is_empty")]
25    pub packages: PackagesManifest,
26    /// Fonts embedded in the pack.
27    #[serde(default, skip_serializing_if = "Vec::is_empty")]
28    pub fonts: Vec<FontManifest>,
29    /// Optional descriptive metadata about the packed project.
30    #[serde(default, skip_serializing_if = "Option::is_none")]
31    pub metadata: Option<Metadata>,
32}
33
34/// The `[project]` section.
35#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
36#[serde(rename_all = "kebab-case", deny_unknown_fields)]
37pub struct ProjectManifest {
38    /// The root-relative path of the entrypoint file, e.g. `main.typ`.
39    pub entrypoint: String,
40}
41
42/// The `[packages]` section.
43#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
44#[serde(rename_all = "kebab-case", deny_unknown_fields)]
45pub struct PackagesManifest {
46    /// Exact specs of packages whose files are stored inside the pack.
47    #[serde(default, skip_serializing_if = "Vec::is_empty")]
48    pub vendored: Vec<String>,
49    /// Exact specs of observed dependencies that are *not* stored inside the
50    /// pack and must be resolved from a package directory, cache, or registry
51    /// when compiling.
52    #[serde(default, skip_serializing_if = "Vec::is_empty")]
53    pub external: Vec<String>,
54}
55
56impl PackagesManifest {
57    fn is_empty(&self) -> bool {
58        self.vendored.is_empty() && self.external.is_empty()
59    }
60}
61
62/// One `[[fonts]]` entry.
63#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
64#[serde(rename_all = "kebab-case", deny_unknown_fields)]
65pub struct FontManifest {
66    /// The archive entry holding the font data, e.g. `fonts/dejavu-sans.ttf`.
67    pub path: String,
68    /// The face index inside the font file (non-zero for collections).
69    #[serde(default, skip_serializing_if = "is_zero")]
70    pub index: u32,
71    /// Family names provided by this face, informational only.
72    #[serde(default, skip_serializing_if = "Vec::is_empty")]
73    pub families: Vec<String>,
74}
75
76fn is_zero(index: &u32) -> bool {
77    *index == 0
78}
79
80/// The optional `[metadata]` section.
81#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
82#[serde(rename_all = "kebab-case", deny_unknown_fields)]
83pub struct Metadata {
84    #[serde(default, skip_serializing_if = "Option::is_none")]
85    pub name: Option<String>,
86    #[serde(default, skip_serializing_if = "Option::is_none")]
87    pub description: Option<String>,
88    #[serde(default, skip_serializing_if = "Vec::is_empty")]
89    pub authors: Vec<String>,
90}
91
92/// A manifest that could not be accepted.
93#[derive(Debug, thiserror::Error)]
94pub enum ManifestError {
95    #[error("failed to parse manifest: {0}")]
96    Parse(#[from] toml::de::Error),
97    #[error("unsupported pack format version {0} (this reader supports up to {FORMAT_VERSION})")]
98    UnsupportedVersion(u32),
99    #[error("invalid entrypoint path `{path}`: {message}")]
100    InvalidEntrypoint { path: String, message: String },
101    #[error("invalid package spec `{spec}`: {message}")]
102    InvalidPackageSpec { spec: String, message: String },
103    #[error("invalid font path `{path}`: {message}")]
104    InvalidFontPath { path: String, message: String },
105}
106
107impl Manifest {
108    /// Parses and validates a manifest from TOML text.
109    pub fn from_toml(text: &str) -> Result<Self, ManifestError> {
110        let manifest: Manifest = toml::from_str(text)?;
111        manifest.validate()?;
112        Ok(manifest)
113    }
114
115    /// Serializes the manifest to TOML text.
116    pub fn to_toml(&self) -> String {
117        toml::to_string_pretty(self).expect("manifest is always serializable")
118    }
119
120    /// Checks internal consistency of the manifest.
121    pub fn validate(&self) -> Result<(), ManifestError> {
122        if self.format_version > FORMAT_VERSION {
123            return Err(ManifestError::UnsupportedVersion(self.format_version));
124        }
125        self.entrypoint()?;
126        for spec in self.packages.vendored.iter().chain(&self.packages.external) {
127            parse_spec(spec)?;
128        }
129        for font in &self.fonts {
130            if let Err(err) = VirtualPath::new(&font.path) {
131                return Err(ManifestError::InvalidFontPath {
132                    path: font.path.clone(),
133                    message: err.to_string(),
134                });
135            }
136        }
137        Ok(())
138    }
139
140    /// The entrypoint as a validated virtual path.
141    pub fn entrypoint(&self) -> Result<VirtualPath, ManifestError> {
142        VirtualPath::new(&self.project.entrypoint).map_err(|err| ManifestError::InvalidEntrypoint {
143            path: self.project.entrypoint.clone(),
144            message: err.to_string(),
145        })
146    }
147
148    /// The vendored package specs, parsed.
149    pub fn vendored_packages(&self) -> Result<Vec<PackageSpec>, ManifestError> {
150        self.packages
151            .vendored
152            .iter()
153            .map(|spec| parse_spec(spec))
154            .collect()
155    }
156
157    /// The external (non-vendored) package specs, parsed.
158    pub fn external_packages(&self) -> Result<Vec<PackageSpec>, ManifestError> {
159        self.packages
160            .external
161            .iter()
162            .map(|spec| parse_spec(spec))
163            .collect()
164    }
165}
166
167fn parse_spec(spec: &str) -> Result<PackageSpec, ManifestError> {
168    PackageSpec::from_str(spec).map_err(|err| ManifestError::InvalidPackageSpec {
169        spec: spec.to_owned(),
170        message: err.to_string(),
171    })
172}