1use std::str::FromStr;
4
5use serde::{Deserialize, Deserializer, Serialize};
6use typst::syntax::package::PackageSpec;
7
8pub const MANIFEST_PATH: &str = "typst-pack.toml";
10
11pub const FORMAT_VERSION: u32 = 1;
13
14#[derive(Debug, Clone, PartialEq, Serialize)]
16#[serde(rename_all = "kebab-case")]
17pub(crate) struct PackManifest {
18 format_version: u32,
20 project: ProjectManifest,
22 #[serde(default, skip_serializing_if = "PackagesManifest::is_empty")]
24 packages: PackagesManifest,
25 #[serde(default, skip_serializing_if = "Vec::is_empty")]
27 fonts: Vec<FontManifest>,
28 #[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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
72#[serde(rename_all = "kebab-case", deny_unknown_fields)]
73pub(crate) struct ProjectManifest {
74 entrypoint: String,
76}
77
78#[derive(Debug, Clone, Default, PartialEq, Serialize)]
80#[serde(rename_all = "kebab-case", deny_unknown_fields)]
81pub(crate) struct PackagesManifest {
82 #[serde(default, skip_serializing_if = "Vec::is_empty")]
84 vendored: Vec<PackageManifest>,
85 #[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#[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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
136#[serde(rename_all = "kebab-case", deny_unknown_fields)]
137pub(crate) struct FontManifest {
138 path: String,
140 #[serde(default, skip_serializing_if = "is_zero")]
142 index: u32,
143 #[serde(default, skip_serializing_if = "Vec::is_empty")]
145 families: Vec<String>,
146 #[serde(default, skip_serializing_if = "is_false")]
148 external: bool,
149 #[serde(default, skip_serializing_if = "Option::is_none")]
151 container_digest: Option<String>,
152 #[serde(default, skip_serializing_if = "Option::is_none")]
154 container_identity_kind: Option<String>,
155 #[serde(default, skip_serializing_if = "Option::is_none")]
157 container_identity_schema: Option<String>,
158 #[serde(default, skip_serializing_if = "Option::is_none")]
160 container_identity_algorithm: Option<String>,
161 #[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#[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 pub fn entrypoint(&self) -> &str {
189 &self.entrypoint
190 }
191}
192
193impl PackagesManifest {
194 pub fn vendored(&self) -> &[PackageManifest] {
196 &self.vendored
197 }
198
199 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 pub fn path(&self) -> &str {
284 &self.path
285 }
286
287 pub fn index(&self) -> u32 {
289 self.index
290 }
291
292 #[cfg(test)]
294 pub(crate) fn families(&self) -> &[String] {
295 &self.families
296 }
297
298 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 pub fn new() -> Self {
327 Self::default()
328 }
329
330 pub fn with_name(mut self, name: impl Into<String>) -> Self {
332 self.name = Some(name.into());
333 self
334 }
335
336 pub fn with_description(mut self, description: impl Into<String>) -> Self {
338 self.description = Some(description.into());
339 self
340 }
341
342 pub fn with_author(mut self, author: impl Into<String>) -> Self {
344 self.authors.push(author.into());
345 self
346 }
347
348 pub fn name(&self) -> Option<&str> {
350 self.name.as_deref()
351 }
352
353 pub fn description(&self) -> Option<&str> {
355 self.description.as_deref()
356 }
357
358 pub fn authors(&self) -> &[String] {
360 &self.authors
361 }
362}
363
364#[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 #[cfg(test)]
407 pub(crate) fn format_version(&self) -> u32 {
408 self.format_version
409 }
410
411 pub fn project(&self) -> &ProjectManifest {
413 &self.project
414 }
415
416 pub fn packages(&self) -> &PackagesManifest {
418 &self.packages
419 }
420
421 pub fn fonts(&self) -> &[FontManifest] {
423 &self.fonts
424 }
425
426 pub fn metadata(&self) -> Option<&PackMetadata> {
428 self.metadata.as_ref()
429 }
430
431 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 #[cfg(test)]
442 pub fn to_toml(&self) -> String {
443 toml::to_string_pretty(self).expect("manifest is always serializable")
444 }
445
446 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}