Skip to main content

typst_pack/
project_snapshot.rs

1//! One stabilized set of project files, assembled without a filesystem.
2
3use std::collections::{BTreeMap, BTreeSet};
4use std::fmt;
5
6use crate::pack::Pack;
7use crate::payload::SharedBytes;
8
9/// One stabilized set of project files: canonical root-relative paths, exact
10/// bytes, and the entrypoint they were assembled around.
11#[derive(Clone, Debug, PartialEq, Eq)]
12pub struct ProjectSnapshot {
13    entrypoint: String,
14    files: BTreeMap<String, SharedBytes>,
15}
16
17impl ProjectSnapshot {
18    /// The canonical root-relative path of the entrypoint.
19    pub fn entrypoint(&self) -> &str {
20        &self.entrypoint
21    }
22
23    /// The contained project files in canonical path order.
24    pub fn files(&self) -> impl Iterator<Item = (&str, &[u8])> {
25        self.files
26            .iter()
27            .map(|(path, data)| (path.as_str(), data.as_slice()))
28    }
29
30    /// Looks up a contained project file by canonical root-relative path.
31    pub fn file(&self, path: &str) -> Option<&[u8]> {
32        self.files.get(path).map(SharedBytes::as_slice)
33    }
34
35    pub(crate) fn shared_files(&self) -> impl Iterator<Item = (&str, &SharedBytes)> {
36        self.files.iter().map(|(path, data)| (path.as_str(), data))
37    }
38
39    pub(crate) fn shared_file(&self, path: &str) -> Option<&SharedBytes> {
40        self.files.get(path)
41    }
42}
43
44/// Assembles a [`ProjectSnapshot`] from already selected path-and-bytes entries.
45///
46/// Source readers decide membership before this operation. Assembly owns only
47/// universal snapshot invariants: canonical paths, duplicate and `.typk` path
48/// rejection, exact bytes, canonical order, and entrypoint presence.
49#[derive(Debug)]
50pub struct ProjectSnapshotAssembly {
51    entrypoint: String,
52}
53
54impl ProjectSnapshotAssembly {
55    /// Prepares assembly of a project with the selected entrypoint.
56    pub fn new(entrypoint: impl Into<String>) -> Self {
57        Self {
58            entrypoint: entrypoint.into(),
59        }
60    }
61
62    /// Assembles the snapshot from `entries`.
63    pub fn assemble(
64        &self,
65        entries: impl IntoIterator<Item = (impl AsRef<str>, impl Into<Vec<u8>>)>,
66    ) -> Result<ProjectSnapshot, ProjectSnapshotError> {
67        let mut issues = Vec::new();
68        let entrypoint = match canonical_project_path(&self.entrypoint) {
69            Ok(path) => Some(path),
70            Err(issue) => {
71                issues.push(issue);
72                None
73            }
74        };
75
76        let mut selected_entries = Vec::new();
77        for (path, data) in entries {
78            match canonical_project_path(path.as_ref()) {
79                Ok(path) => selected_entries.push((path, SharedBytes::new(data.into()))),
80                Err(issue) => issues.push(issue),
81            }
82        }
83
84        let mut paths = BTreeSet::new();
85        let mut duplicate_paths = BTreeSet::new();
86        for (path, _) in &selected_entries {
87            if !paths.insert(path.as_str()) {
88                duplicate_paths.insert(path.clone());
89            }
90        }
91        issues.extend(
92            duplicate_paths
93                .into_iter()
94                .map(|path| ProjectSnapshotIssue::DuplicatePath { path }),
95        );
96
97        if let Some(entrypoint) = &entrypoint
98            && !paths.contains(entrypoint.as_str())
99        {
100            issues.push(ProjectSnapshotIssue::MissingEntrypoint {
101                path: entrypoint.clone(),
102            });
103        }
104        issues.sort_by(|left, right| left.sort_key().cmp(&right.sort_key()));
105        if !issues.is_empty() {
106            return Err(ProjectSnapshotError { issues });
107        }
108        let files = selected_entries.into_iter().collect::<BTreeMap<_, _>>();
109        Ok(ProjectSnapshot {
110            entrypoint: entrypoint.expect("a valid Project Snapshot has a canonical entrypoint"),
111            files,
112        })
113    }
114}
115
116/// Canonicalizes a supplied path under the universal project membership rules.
117fn canonical_project_path(path: &str) -> Result<String, ProjectSnapshotIssue> {
118    Pack::canonical_project_path(path).map_err(|message| ProjectSnapshotIssue::InvalidPath {
119        path: path.to_owned(),
120        message,
121    })
122}
123
124/// One independently detectable issue while assembling a [`ProjectSnapshot`].
125#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
126#[non_exhaustive]
127pub enum ProjectSnapshotIssue {
128    /// A supplied path cannot name a root-relative project file.
129    #[error("project path {path:?} cannot be represented: {message}")]
130    InvalidPath { path: String, message: String },
131    /// Two supplied entries name one canonical project file.
132    #[error("project path {path:?} is supplied more than once")]
133    DuplicatePath { path: String },
134    /// The entrypoint is not among the supplied project files.
135    #[error("entrypoint {path:?} is not a supplied project file")]
136    MissingEntrypoint { path: String },
137}
138
139impl ProjectSnapshotIssue {
140    fn sort_key(&self) -> (&str, u8, &str) {
141        match self {
142            Self::InvalidPath { path, message } => (path, 0, message),
143            Self::DuplicatePath { path } => (path, 1, ""),
144            Self::MissingEntrypoint { path } => (path, 2, ""),
145        }
146    }
147}
148
149/// A failure while assembling a [`ProjectSnapshot`].
150#[derive(Debug, Clone, PartialEq, Eq)]
151pub struct ProjectSnapshotError {
152    issues: Vec<ProjectSnapshotIssue>,
153}
154
155impl ProjectSnapshotError {
156    /// Every independently detectable issue in canonical path order.
157    pub fn issues(&self) -> &[ProjectSnapshotIssue] {
158        &self.issues
159    }
160}
161
162impl fmt::Display for ProjectSnapshotError {
163    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
164        if let [issue] = self.issues.as_slice() {
165            return issue.fmt(formatter);
166        }
167        write!(
168            formatter,
169            "Project Snapshot assembly failed with {} issue(s)",
170            self.issues.len()
171        )?;
172        for issue in &self.issues {
173            write!(formatter, ": {issue}")?;
174        }
175        Ok(())
176    }
177}
178
179impl std::error::Error for ProjectSnapshotError {}