1use 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
13pub const MANIFEST_PATH: &str = "typst-pack.toml";
15
16pub const FORMAT_VERSION: u32 = 1;
18
19#[derive(Debug, Clone, PartialEq, Serialize)]
21#[serde(rename_all = "kebab-case")]
22pub struct PackManifest {
23 format_version: u32,
25 project: ProjectManifest,
27 #[serde(default, skip_serializing_if = "PackagesManifest::is_empty")]
29 packages: PackagesManifest,
30 #[serde(default, skip_serializing_if = "Vec::is_empty")]
32 fonts: Vec<FontManifest>,
33 #[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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
77#[serde(rename_all = "kebab-case", deny_unknown_fields)]
78pub struct ProjectManifest {
79 entrypoint: String,
81}
82
83#[derive(Debug, Clone, Default, PartialEq, Serialize)]
85#[serde(rename_all = "kebab-case", deny_unknown_fields)]
86pub struct PackagesManifest {
87 #[serde(default, skip_serializing_if = "Vec::is_empty")]
89 vendored: Vec<PackageManifest>,
90 #[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#[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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
141#[serde(rename_all = "kebab-case", deny_unknown_fields)]
142pub struct FontManifest {
143 path: String,
145 #[serde(default, skip_serializing_if = "is_zero")]
147 index: u32,
148 #[serde(default, skip_serializing_if = "Vec::is_empty")]
150 families: Vec<String>,
151 #[serde(default, skip_serializing_if = "is_false")]
153 external: bool,
154 #[serde(default, skip_serializing_if = "Option::is_none")]
156 container_digest: Option<String>,
157 #[serde(default, skip_serializing_if = "Option::is_none")]
159 container_identity_kind: Option<String>,
160 #[serde(default, skip_serializing_if = "Option::is_none")]
162 container_identity_schema: Option<String>,
163 #[serde(default, skip_serializing_if = "Option::is_none")]
165 container_identity_algorithm: Option<String>,
166 #[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#[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 pub fn entrypoint(&self) -> &str {
194 &self.entrypoint
195 }
196}
197
198impl PackagesManifest {
199 pub fn vendored(&self) -> &[PackageManifest] {
201 &self.vendored
202 }
203
204 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 pub fn path(&self) -> &str {
268 &self.path
269 }
270
271 pub fn index(&self) -> u32 {
273 self.index
274 }
275
276 pub fn families(&self) -> &[String] {
278 &self.families
279 }
280
281 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 pub fn new() -> Self {
331 Self::default()
332 }
333
334 pub fn with_name(mut self, name: impl Into<String>) -> Self {
336 self.name = Some(name.into());
337 self
338 }
339
340 pub fn with_description(mut self, description: impl Into<String>) -> Self {
342 self.description = Some(description.into());
343 self
344 }
345
346 pub fn with_author(mut self, author: impl Into<String>) -> Self {
348 self.authors.push(author.into());
349 self
350 }
351
352 pub fn name(&self) -> Option<&str> {
354 self.name.as_deref()
355 }
356
357 pub fn description(&self) -> Option<&str> {
359 self.description.as_deref()
360 }
361
362 pub fn authors(&self) -> &[String] {
364 &self.authors
365 }
366}
367
368#[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 pub fn format_version(&self) -> u32 {
405 self.format_version
406 }
407
408 pub fn project(&self) -> &ProjectManifest {
410 &self.project
411 }
412
413 pub fn packages(&self) -> &PackagesManifest {
415 &self.packages
416 }
417
418 pub fn fonts(&self) -> &[FontManifest] {
420 &self.fonts
421 }
422
423 pub fn metadata(&self) -> Option<&PackMetadata> {
425 self.metadata.as_ref()
426 }
427
428 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 pub fn to_toml(&self) -> String {
439 toml::to_string_pretty(self).expect("manifest is always serializable")
440 }
441
442 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}