Skip to main content

typst_pack/
extract.rs

1//! Extracting a pack back into a directory.
2
3#![cfg(feature = "fs")]
4
5use std::path::{Path, PathBuf};
6
7use crate::pack::Pack;
8
9/// Options for [`extract`].
10#[derive(Debug, Clone, Default)]
11pub struct ExtractOptions {
12    /// Also write vendored packages to `packages/<ns>/<name>/<version>/...`.
13    pub packages: bool,
14    /// Also write embedded fonts to their archive paths (`fonts/...`).
15    pub fonts: bool,
16    /// Overwrite existing files.
17    pub force: bool,
18}
19
20/// A summary of an extraction.
21#[derive(Debug, Clone, Default)]
22pub struct ExtractReport {
23    /// Paths written, relative to the target directory.
24    pub written: Vec<PathBuf>,
25}
26
27/// A failure while extracting a pack.
28#[derive(Debug, thiserror::Error)]
29pub enum ExtractError {
30    #[error("`{0}` already exists (pass force to overwrite)")]
31    Exists(PathBuf),
32    #[error("failed to write `{path}`: {source}")]
33    Io {
34        path: PathBuf,
35        #[source]
36        source: std::io::Error,
37    },
38}
39
40/// Writes the project files of a pack into a directory.
41///
42/// Project files are written directly into `dir` so that the result is a
43/// compilable project. With [`packages`](ExtractOptions::packages) and
44/// [`fonts`](ExtractOptions::fonts), the vendored packages and embedded fonts
45/// are additionally written to `packages/` and `fonts/` subdirectories. The
46/// manifest itself is not recreated; it lives only inside the archive.
47pub fn extract(
48    pack: &Pack,
49    dir: &Path,
50    options: &ExtractOptions,
51) -> Result<ExtractReport, ExtractError> {
52    let mut report = ExtractReport::default();
53
54    for (path, data) in pack.files() {
55        write_file(dir, Path::new(path), data, options, &mut report)?;
56    }
57
58    if options.packages {
59        for (spec, files) in pack.packages() {
60            let base = PathBuf::from("packages")
61                .join(spec.namespace.as_str())
62                .join(spec.name.as_str())
63                .join(spec.version.to_string());
64            for (path, data) in files {
65                write_file(dir, &base.join(path), data, options, &mut report)?;
66            }
67        }
68    }
69
70    if options.fonts {
71        for font in pack.fonts() {
72            let path = Path::new(&font.entry.path);
73            if report.written.iter().any(|written| written == path) {
74                continue;
75            }
76            write_file(dir, path, &font.data, options, &mut report)?;
77        }
78    }
79
80    Ok(report)
81}
82
83fn write_file(
84    dir: &Path,
85    relative: &Path,
86    data: &[u8],
87    options: &ExtractOptions,
88    report: &mut ExtractReport,
89) -> Result<(), ExtractError> {
90    let target = dir.join(relative);
91    if !options.force && target.exists() {
92        return Err(ExtractError::Exists(target));
93    }
94    if let Some(parent) = target.parent() {
95        std::fs::create_dir_all(parent).map_err(|source| ExtractError::Io {
96            path: parent.to_owned(),
97            source,
98        })?;
99    }
100    std::fs::write(&target, data).map_err(|source| ExtractError::Io {
101        path: target.clone(),
102        source,
103    })?;
104    report.written.push(relative.to_owned());
105    Ok(())
106}