diff options
Diffstat (limited to 'crates/typst-cli/src/world.rs')
| -rw-r--r-- | crates/typst-cli/src/world.rs | 256 |
1 files changed, 256 insertions, 0 deletions
diff --git a/crates/typst-cli/src/world.rs b/crates/typst-cli/src/world.rs new file mode 100644 index 00000000..f09a3f6c --- /dev/null +++ b/crates/typst-cli/src/world.rs @@ -0,0 +1,256 @@ +use std::cell::{OnceCell, RefCell, RefMut}; +use std::collections::HashMap; +use std::fs; +use std::hash::Hash; +use std::path::{Path, PathBuf}; + +use chrono::Datelike; +use comemo::Prehashed; +use same_file::Handle; +use siphasher::sip128::{Hasher128, SipHasher13}; +use typst::diag::{FileError, FileResult, StrResult}; +use typst::eval::{eco_format, Datetime, Library}; +use typst::file::FileId; +use typst::font::{Font, FontBook}; +use typst::syntax::Source; +use typst::util::{Bytes, PathExt}; +use typst::World; + +use crate::args::CompileCommand; +use crate::fonts::{FontSearcher, FontSlot}; +use crate::package::prepare_package; + +/// A world that provides access to the operating system. +pub struct SystemWorld { + /// The root relative to which absolute paths are resolved. + root: PathBuf, + /// The input path. + main: FileId, + /// Typst's standard library. + library: Prehashed<Library>, + /// Metadata about discovered fonts. + book: Prehashed<FontBook>, + /// Locations of and storage for lazily loaded fonts. + fonts: Vec<FontSlot>, + /// Maps package-path combinations to canonical hashes. All package-path + /// combinations that point to the same file are mapped to the same hash. To + /// be used in conjunction with `paths`. + hashes: RefCell<HashMap<FileId, FileResult<PathHash>>>, + /// Maps canonical path hashes to source files and buffers. + paths: RefCell<HashMap<PathHash, PathSlot>>, + /// The current date if requested. This is stored here to ensure it is + /// always the same within one compilation. Reset between compilations. + today: OnceCell<Option<Datetime>>, +} + +impl SystemWorld { + /// Create a new system world. + pub fn new(command: &CompileCommand) -> StrResult<Self> { + let mut searcher = FontSearcher::new(); + searcher.search(&command.font_paths); + + // Resolve the system-global input path. + let system_input = command.input.canonicalize().map_err(|_| { + eco_format!("input file not found (searched at {})", command.input.display()) + })?; + + // Resolve the system-global root directory. + let root = { + let path = command + .root + .as_deref() + .or_else(|| system_input.parent()) + .unwrap_or(Path::new(".")); + path.canonicalize().map_err(|_| { + eco_format!("root directory not found (searched at {})", path.display()) + })? + }; + + // Resolve the input path within the project. + let project_input = system_input + .strip_prefix(&root) + .map(|path| Path::new("/").join(path)) + .map_err(|_| "input file must be contained in project root")?; + + Ok(Self { + root, + main: FileId::new(None, &project_input), + library: Prehashed::new(typst_library::build()), + book: Prehashed::new(searcher.book), + fonts: searcher.fonts, + hashes: RefCell::default(), + paths: RefCell::default(), + today: OnceCell::new(), + }) + } + + /// The id of the main source file. + pub fn main(&self) -> FileId { + self.main + } + + /// Return all paths the last compilation depended on. + pub fn dependencies(&mut self) -> impl Iterator<Item = &Path> { + self.paths.get_mut().values().map(|slot| slot.system_path.as_path()) + } + + /// Reset the compilation state in preparation of a new compilation. + pub fn reset(&mut self) { + self.hashes.borrow_mut().clear(); + self.paths.borrow_mut().clear(); + self.today.take(); + } + + /// Lookup a source file by id. + #[track_caller] + pub fn lookup(&self, id: FileId) -> Source { + self.source(id).expect("file id does not point to any source file") + } +} + +impl World for SystemWorld { + fn library(&self) -> &Prehashed<Library> { + &self.library + } + + fn book(&self) -> &Prehashed<FontBook> { + &self.book + } + + fn main(&self) -> Source { + self.source(self.main).unwrap() + } + + fn source(&self, id: FileId) -> FileResult<Source> { + self.slot(id)?.source() + } + + fn file(&self, id: FileId) -> FileResult<Bytes> { + self.slot(id)?.file() + } + + fn font(&self, index: usize) -> Option<Font> { + self.fonts[index].get() + } + + fn today(&self, offset: Option<i64>) -> Option<Datetime> { + *self.today.get_or_init(|| { + let naive = match offset { + None => chrono::Local::now().naive_local(), + Some(o) => (chrono::Utc::now() + chrono::Duration::hours(o)).naive_utc(), + }; + + Datetime::from_ymd( + naive.year(), + naive.month().try_into().ok()?, + naive.day().try_into().ok()?, + ) + }) + } +} + +impl SystemWorld { + /// Access the canonical slot for the given file id. + #[tracing::instrument(skip_all)] + fn slot(&self, id: FileId) -> FileResult<RefMut<PathSlot>> { + let mut system_path = PathBuf::new(); + let hash = self + .hashes + .borrow_mut() + .entry(id) + .or_insert_with(|| { + // Determine the root path relative to which the file path + // will be resolved. + let root = match id.package() { + Some(spec) => prepare_package(spec)?, + None => self.root.clone(), + }; + + // Join the path to the root. If it tries to escape, deny + // access. Note: It can still escape via symlinks. + system_path = + root.join_rooted(id.path()).ok_or(FileError::AccessDenied)?; + + PathHash::new(&system_path) + }) + .clone()?; + + Ok(RefMut::map(self.paths.borrow_mut(), |paths| { + paths.entry(hash).or_insert_with(|| PathSlot { + id, + // This will only trigger if the `or_insert_with` above also + // triggered. + system_path, + source: OnceCell::new(), + buffer: OnceCell::new(), + }) + })) + } +} + +/// Holds canonical data for all paths pointing to the same entity. +/// +/// Both fields can be populated if the file is both imported and read(). +struct PathSlot { + /// The slot's canonical file id. + id: FileId, + /// The slot's path on the system. + system_path: PathBuf, + /// The lazily loaded source file for a path hash. + source: OnceCell<FileResult<Source>>, + /// The lazily loaded buffer for a path hash. + buffer: OnceCell<FileResult<Bytes>>, +} + +impl PathSlot { + fn source(&self) -> FileResult<Source> { + self.source + .get_or_init(|| { + let buf = read(&self.system_path)?; + let text = decode_utf8(buf)?; + Ok(Source::new(self.id, text)) + }) + .clone() + } + + fn file(&self) -> FileResult<Bytes> { + self.buffer + .get_or_init(|| read(&self.system_path).map(Bytes::from)) + .clone() + } +} + +/// A hash that is the same for all paths pointing to the same entity. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +struct PathHash(u128); + +impl PathHash { + fn new(path: &Path) -> FileResult<Self> { + let f = |e| FileError::from_io(e, path); + let handle = Handle::from_path(path).map_err(f)?; + let mut state = SipHasher13::new(); + handle.hash(&mut state); + Ok(Self(state.finish128().as_u128())) + } +} + +/// Read a file. +fn read(path: &Path) -> FileResult<Vec<u8>> { + let f = |e| FileError::from_io(e, path); + if fs::metadata(path).map_err(f)?.is_dir() { + Err(FileError::IsDirectory) + } else { + fs::read(path).map_err(f) + } +} + +/// Decode UTF-8 with an optional BOM. +fn decode_utf8(buf: Vec<u8>) -> FileResult<String> { + Ok(if buf.starts_with(b"\xef\xbb\xbf") { + // Remove UTF-8 BOM. + std::str::from_utf8(&buf[3..])?.into() + } else { + // Assume UTF-8. + String::from_utf8(buf)? + }) +} |
