use std::collections::HashMap; use std::io::Read; use std::path::{Path, PathBuf}; use std::sync::OnceLock; use std::{fmt, fs, io, mem}; use chrono::{DateTime, Datelike, FixedOffset, Local, Utc}; use ecow::{eco_format, EcoString}; use once_cell::sync::Lazy; use parking_lot::Mutex; use typst::diag::{FileError, FileResult}; use typst::foundations::{Bytes, Datetime, Dict, IntoValue}; use typst::syntax::{FileId, Source, VirtualPath}; use typst::text::{Font, FontBook}; use typst::utils::LazyHash; use typst::{Library, World}; use typst_kit::fonts::{FontSlot, Fonts}; use typst_kit::package::PackageStorage; use typst_timing::timed; use crate::args::{Input, SharedArgs}; use crate::compile::ExportCache; use crate::download::PrintDownload; use crate::package; /// Static `FileId` allocated for stdin. /// This is to ensure that a file is read in the correct way. static STDIN_ID: Lazy = Lazy::new(|| FileId::new_fake(VirtualPath::new(""))); /// A world that provides access to the operating system. pub struct SystemWorld { /// The working directory. workdir: Option, /// The root relative to which absolute paths are resolved. root: PathBuf, /// The input path. main: FileId, /// Typst's standard library. library: LazyHash, /// Metadata about discovered fonts. book: LazyHash, /// Locations of and storage for lazily loaded fonts. fonts: Vec, /// Maps file ids to source files and buffers. slots: Mutex>, /// Holds information about where packages are stored. package_storage: PackageStorage, /// The current datetime if requested. This is stored here to ensure it is /// always the same within one compilation. /// Reset between compilations if not [`Now::Fixed`]. now: Now, /// The export cache, used for caching output files in `typst watch` /// sessions. export_cache: ExportCache, } impl SystemWorld { /// Create a new system world. pub fn new(command: &SharedArgs) -> Result { // Set up the thread pool. if let Some(jobs) = command.jobs { rayon::ThreadPoolBuilder::new() .num_threads(jobs) .use_current_thread() .build_global() .ok(); } // Resolve the system-global input path. let input = match &command.input { Input::Stdin => None, Input::Path(path) => { Some(path.canonicalize().map_err(|err| match err.kind() { io::ErrorKind::NotFound => { WorldCreationError::InputNotFound(path.clone()) } _ => WorldCreationError::Io(err), })?) } }; // Resolve the system-global root directory. let root = { let path = command .root .as_deref() .or_else(|| input.as_deref().and_then(|i| i.parent())) .unwrap_or(Path::new(".")); path.canonicalize().map_err(|err| match err.kind() { io::ErrorKind::NotFound => { WorldCreationError::RootNotFound(path.to_path_buf()) } _ => WorldCreationError::Io(err), })? }; let main = if let Some(path) = &input { // Resolve the virtual path of the main file within the project root. let main_path = VirtualPath::within_root(path, &root) .ok_or(WorldCreationError::InputOutsideRoot)?; FileId::new(None, main_path) } else { // Return the special id of STDIN otherwise *STDIN_ID }; let library = { // Convert the input pairs to a dictionary. let inputs: Dict = command .inputs .iter() .map(|(k, v)| (k.as_str().into(), v.as_str().into_value())) .collect(); Library::builder().with_inputs(inputs).build() }; let fonts = Fonts::searcher() .include_system_fonts(!command.font_args.ignore_system_fonts) .search_with(&command.font_args.font_paths); let now = match command.creation_timestamp { Some(time) => Now::Fixed(time), None => Now::System(OnceLock::new()), }; Ok(Self { workdir: std::env::current_dir().ok(), root, main, library: LazyHash::new(library), book: LazyHash::new(fonts.book), fonts: fonts.fonts, slots: Mutex::new(HashMap::new()), package_storage: package::storage(&command.package_storage_args), now, export_cache: ExportCache::new(), }) } /// The id of the main source file. pub fn main(&self) -> FileId { self.main } /// The root relative to which absolute paths are resolved. pub fn root(&self) -> &Path { &self.root } /// The current working directory. pub fn workdir(&self) -> &Path { self.workdir.as_deref().unwrap_or(Path::new(".")) } /// Return all paths the last compilation depended on. pub fn dependencies(&mut self) -> impl Iterator + '_ { self.slots .get_mut() .values() .filter(|slot| slot.accessed()) .filter_map(|slot| { system_path(&self.root, slot.id, &self.package_storage).ok() }) } /// Reset the compilation state in preparation of a new compilation. pub fn reset(&mut self) { for slot in self.slots.get_mut().values_mut() { slot.reset(); } if let Now::System(time_lock) = &mut self.now { time_lock.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") } /// Gets access to the export cache. pub fn export_cache(&self) -> &ExportCache { &self.export_cache } } impl World for SystemWorld { fn library(&self) -> &LazyHash { &self.library } fn book(&self) -> &LazyHash { &self.book } fn main(&self) -> FileId { self.main } fn source(&self, id: FileId) -> FileResult { self.slot(id, |slot| slot.source(&self.root, &self.package_storage)) } fn file(&self, id: FileId) -> FileResult { self.slot(id, |slot| slot.file(&self.root, &self.package_storage)) } fn font(&self, index: usize) -> Option { self.fonts[index].get() } fn today(&self, offset: Option) -> Option { let now = match &self.now { Now::Fixed(time) => time, Now::System(time) => time.get_or_init(Utc::now), }; // The time with the specified UTC offset, or within the local time zone. let with_offset = match offset { None => now.with_timezone(&Local).fixed_offset(), Some(hours) => { let seconds = i32::try_from(hours).ok()?.checked_mul(3600)?; now.with_timezone(&FixedOffset::east_opt(seconds)?) } }; Datetime::from_ymd( with_offset.year(), with_offset.month().try_into().ok()?, with_offset.day().try_into().ok()?, ) } } impl SystemWorld { /// Access the canonical slot for the given file id. fn slot(&self, id: FileId, f: F) -> T where F: FnOnce(&mut FileSlot) -> T, { let mut map = self.slots.lock(); f(map.entry(id).or_insert_with(|| FileSlot::new(id))) } } /// Holds the processed data for a file ID. /// /// Both fields can be populated if the file is both imported and read(). struct FileSlot { /// The slot's file id. id: FileId, /// The lazily loaded and incrementally updated source file. source: SlotCell, /// The lazily loaded raw byte buffer. file: SlotCell, } impl FileSlot { /// Create a new file slot. fn new(id: FileId) -> Self { Self { id, file: SlotCell::new(), source: SlotCell::new() } } /// Whether the file was accessed in the ongoing compilation. fn accessed(&self) -> bool { self.source.accessed() || self.file.accessed() } /// Marks the file as not yet accessed in preparation of the next /// compilation. fn reset(&mut self) { self.source.reset(); self.file.reset(); } /// Retrieve the source for this file. fn source( &mut self, project_root: &Path, package_storage: &PackageStorage, ) -> FileResult { self.source.get_or_init( || read(self.id, project_root, package_storage), |data, prev| { let text = decode_utf8(&data)?; if let Some(mut prev) = prev { prev.replace(text); Ok(prev) } else { Ok(Source::new(self.id, text.into())) } }, ) } /// Retrieve the file's bytes. fn file( &mut self, project_root: &Path, package_storage: &PackageStorage, ) -> FileResult { self.file.get_or_init( || read(self.id, project_root, package_storage), |data, _| Ok(data.into()), ) } } /// Lazily processes data for a file. struct SlotCell { /// The processed data. data: Option>, /// A hash of the raw file contents / access error. fingerprint: u128, /// Whether the slot has been accessed in the current compilation. accessed: bool, } impl SlotCell { /// Creates a new, empty cell. fn new() -> Self { Self { data: None, fingerprint: 0, accessed: false } } /// Whether the cell was accessed in the ongoing compilation. fn accessed(&self) -> bool { self.accessed } /// Marks the cell as not yet accessed in preparation of the next /// compilation. fn reset(&mut self) { self.accessed = false; } /// Gets the contents of the cell or initialize them. fn get_or_init( &mut self, load: impl FnOnce() -> FileResult>, f: impl FnOnce(Vec, Option) -> FileResult, ) -> FileResult { // If we accessed the file already in this compilation, retrieve it. if mem::replace(&mut self.accessed, true) { if let Some(data) = &self.data { return data.clone(); } } // Read and hash the file. let result = timed!("loading file", load()); let fingerprint = timed!("hashing file", typst::utils::hash128(&result)); // If the file contents didn't change, yield the old processed data. if mem::replace(&mut self.fingerprint, fingerprint) == fingerprint { if let Some(data) = &self.data { return data.clone(); } } let prev = self.data.take().and_then(Result::ok); let value = result.and_then(|data| f(data, prev)); self.data = Some(value.clone()); value } } /// Resolves the path of a file id on the system, downloading a package if /// necessary. fn system_path( project_root: &Path, id: FileId, package_storage: &PackageStorage, ) -> FileResult { // Determine the root path relative to which the file path // will be resolved. let buf; let mut root = project_root; if let Some(spec) = id.package() { buf = package_storage.prepare_package(spec, &mut PrintDownload(&spec))?; root = &buf; } // Join the path to the root. If it tries to escape, deny // access. Note: It can still escape via symlinks. id.vpath().resolve(root).ok_or(FileError::AccessDenied) } /// Reads a file from a `FileId`. /// /// If the ID represents stdin it will read from standard input, /// otherwise it gets the file path of the ID and reads the file from disk. fn read( id: FileId, project_root: &Path, package_storage: &PackageStorage, ) -> FileResult> { if id == *STDIN_ID { read_from_stdin() } else { read_from_disk(&system_path(project_root, id, package_storage)?) } } /// Read a file from disk. fn read_from_disk(path: &Path) -> FileResult> { 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) } } /// Read from stdin. fn read_from_stdin() -> FileResult> { let mut buf = Vec::new(); let result = io::stdin().read_to_end(&mut buf); match result { Ok(_) => (), Err(err) if err.kind() == io::ErrorKind::BrokenPipe => (), Err(err) => return Err(FileError::from_io(err, Path::new(""))), } Ok(buf) } /// Decode UTF-8 with an optional BOM. fn decode_utf8(buf: &[u8]) -> FileResult<&str> { // Remove UTF-8 BOM. Ok(std::str::from_utf8(buf.strip_prefix(b"\xef\xbb\xbf").unwrap_or(buf))?) } /// The current date and time. enum Now { /// The date and time if the environment `SOURCE_DATE_EPOCH` is set. /// Used for reproducible builds. Fixed(DateTime), /// The current date and time if the time is not externally fixed. System(OnceLock>), } /// An error that occurs during world construction. #[derive(Debug)] pub enum WorldCreationError { /// The input file does not appear to exist. InputNotFound(PathBuf), /// The input file is not contained within the root folder. InputOutsideRoot, /// The root directory does not appear to exist. RootNotFound(PathBuf), /// Another type of I/O error. Io(io::Error), } impl fmt::Display for WorldCreationError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { WorldCreationError::InputNotFound(path) => { write!(f, "input file not found (searched at {})", path.display()) } WorldCreationError::InputOutsideRoot => { write!(f, "source file must be contained in project root") } WorldCreationError::RootNotFound(path) => { write!(f, "root directory not found (searched at {})", path.display()) } WorldCreationError::Io(err) => write!(f, "{err}"), } } } impl From for EcoString { fn from(err: WorldCreationError) -> Self { eco_format!("{err}") } }