use std::fs;
use std::mem;
use std::path::{Path, PathBuf};
use std::str;
use std::str::Utf8Error;
use std::sync::Arc;
use parking_lot::Mutex;
use rustc_hash::FxHashMap;
use typst_library::diag::{FileError, FileResult};
use typst_library::foundations::Bytes;
use typst_syntax::{FileId, Source, VirtualPath};
#[cfg(feature = "system-files")]
use {crate::packages::SystemPackages, typst_syntax::VirtualRoot};
#[derive(Default)]
pub struct FileStore<L> {
loader: L,
slots: Mutex<FxHashMap<FileId, FileSlot>>,
}
impl<L> FileStore<L>
where
L: FileLoader,
{
pub fn new(loader: L) -> Self {
Self { loader, slots: Mutex::new(FxHashMap::default()) }
}
pub fn loader(&self) -> &L {
&self.loader
}
pub fn loader_mut(&mut self) -> &mut L {
&mut self.loader
}
pub fn into_loader(self) -> L {
self.loader
}
pub fn source(&self, id: FileId) -> FileResult<Source> {
self.slot(id, |slot| slot.source(&self.loader, id))
}
pub fn file(&self, id: FileId) -> FileResult<Bytes> {
self.slot(id, |slot| slot.file(&self.loader, id))
}
pub fn dependencies(&mut self) -> (&L, impl Iterator<Item = FileId> + '_) {
let iter = self
.slots
.get_mut()
.iter()
.filter(|(_, slot)| slot.accessed())
.map(|(&id, _)| id);
(&self.loader, iter)
}
pub fn reset(&mut self) {
#[allow(clippy::iter_over_hash_type, reason = "order does not matter")]
for slot in self.slots.get_mut().values_mut() {
slot.reset();
}
}
fn slot<F, T>(&self, id: FileId, f: F) -> FileResult<T>
where
F: FnOnce(&mut FileSlot) -> FileResult<T>,
{
let mut map = self.slots.lock();
f(map.entry(id).or_default())
}
}
enum FileSlot {
Empty(Stale<Source>),
Loaded(FileResult<Bytes>, Stale<Source>),
Parsed(Result<Source, Utf8Error>, Bytes),
}
type Stale<T> = Option<T>;
impl FileSlot {
fn accessed(&self) -> bool {
!matches!(self, Self::Empty(_))
}
fn reset(&mut self) {
let stale = match mem::take(self) {
Self::Parsed(Ok(source), _) => Some(source),
_ => None,
};
*self = Self::Empty(stale);
}
fn file(&mut self, loader: &impl FileLoader, id: FileId) -> FileResult<Bytes> {
match self {
Self::Empty(stale) => {
let result = loader.load(id);
*self = Self::Loaded(result.clone(), mem::take(stale));
result
}
Self::Loaded(result, _) => result.clone(),
Self::Parsed(_, bytes) => Ok(bytes.clone()),
}
}
fn source(&mut self, loader: &impl FileLoader, id: FileId) -> FileResult<Source> {
let (bytes, stale) = match self {
Self::Empty(stale) => match loader.load(id) {
Ok(bytes) => (bytes, mem::take(stale)),
Err(err) => {
*self = Self::Loaded(Err(err.clone()), mem::take(stale));
return Err(err);
}
},
Self::Loaded(Ok(_), _) => match mem::take(self) {
Self::Loaded(Ok(bytes), stale) => (bytes, stale),
_ => unreachable!(),
},
Self::Loaded(Err(err), _) => return Err(err.clone()),
Self::Parsed(source, _) => return Ok(source.clone()?),
};
const UTF8_BOM: &[u8] = b"\xef\xbb\xbf";
let without_bom = bytes.strip_prefix(UTF8_BOM);
let (result, bytes) = if let Some(mut source) = stale {
let result = str::from_utf8(without_bom.unwrap_or(&bytes)).map(|new| {
source.replace(new);
source
});
(result, bytes)
} else if let Some(rest) = without_bom {
(str::from_utf8(rest).map(|text| Source::new(id, text.into())), bytes)
} else {
match bytes.into_string().map(|text| Source::new(id, text)) {
Ok(source) => (Ok(source.clone()), Bytes::from_string(source)),
Err(err) => (Err(err.error), err.bytes),
}
};
*self = Self::Parsed(result.clone(), bytes);
Ok(result?)
}
}
impl Default for FileSlot {
fn default() -> Self {
Self::Empty(None)
}
}
pub trait FileLoader {
fn load(&self, id: FileId) -> FileResult<Bytes>;
}
impl<F: FileLoader> FileLoader for Box<F> {
fn load(&self, id: FileId) -> FileResult<Bytes> {
(**self).load(id)
}
}
impl<F: FileLoader> FileLoader for Arc<F> {
fn load(&self, id: FileId) -> FileResult<Bytes> {
(**self).load(id)
}
}
#[cfg(feature = "system-files")]
#[derive(Debug)]
pub struct SystemFiles {
project: FsRoot,
packages: SystemPackages,
}
#[cfg(feature = "system-files")]
impl SystemFiles {
pub fn new(project: FsRoot, packages: SystemPackages) -> Self {
Self { project, packages }
}
pub fn resolve(&self, id: FileId) -> FileResult<PathBuf> {
self.root(id)?.resolve(id.vpath())
}
pub fn root(&self, id: FileId) -> FileResult<FsRoot> {
Ok(match id.root() {
VirtualRoot::Project => self.project.clone(),
VirtualRoot::Package(spec) => self.packages.obtain(spec)?,
})
}
}
#[cfg(feature = "system-files")]
impl FileLoader for SystemFiles {
fn load(&self, id: FileId) -> FileResult<Bytes> {
self.root(id)?.load(id.vpath())
}
}
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct FsRoot(PathBuf);
impl FsRoot {
pub fn new(root: PathBuf) -> Self {
Self(root)
}
pub fn path(&self) -> &Path {
&self.0
}
pub fn resolve(&self, path: &VirtualPath) -> FileResult<PathBuf> {
path.realize(&self.0).map_err(Into::into)
}
pub fn load(&self, path: &VirtualPath) -> FileResult<Bytes> {
let path = self.resolve(path)?;
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(Bytes::new).map_err(f)
}
}
}
#[cfg(test)]
mod tests {
use typst_syntax::{RootedPath, VirtualRoot};
use super::*;
#[test]
fn test_file_store_source_via_file() {
let store = FileStore::new(TestLoader(1));
store.file(id("a.typ")).must_be(A_TEXT);
store.source(id("a.typ")).must_be(A_TEXT);
}
#[test]
fn test_file_store_bom() {
let store = FileStore::new(TestLoader(1));
store.file(id("b.typ")).must_be(B_DATA);
store.source(id("b.typ")).must_be(B_TEXT);
}
#[test]
fn test_file_store_storage_reuse() {
let store = FileStore::new(TestLoader(1));
let a_source = store.source(id("a.typ")).unwrap();
let a_file = store.file(id("a.typ")).unwrap();
a_file.must_be(A_TEXT);
a_source.must_be(A_TEXT);
assert!(std::ptr::eq(a_file.as_slice().as_ptr(), a_source.text().as_ptr()));
}
#[test]
fn test_file_store_cycles() {
let mut store = FileStore::new(TestLoader(1));
let deps = |store: &mut FileStore<TestLoader>| {
let (_, iter) = store.dependencies();
let mut vec = iter
.map(|id| id.get().vpath().get_without_slash())
.collect::<Vec<_>>();
vec.sort();
vec
};
store.source(id("a.typ")).must_be(A_TEXT);
store.source(id("d.typ")).must_be("1");
assert_eq!(store.file(id("e.bin")), Err(FileError::NotFound("e.bin".into())));
assert_eq!(deps(&mut store), ["a.typ", "d.typ", "e.bin"]);
store.loader_mut().0 = 5;
store.reset();
store.source(id("d.typ")).must_be("5");
store.file(id("e.bin")).must_be(E_TEXT);
assert_eq!(deps(&mut store), ["d.typ", "e.bin"]);
}
const A_TEXT: &str = "Hello from A";
const B_DATA: &[u8] = b"\xef\xbb\xbfHello from B";
const B_TEXT: &str = "Hello from B";
const C_DATA: &[u8] = b"a\xFF\xFF\xFFb";
const E_TEXT: &str = "A secret";
struct TestLoader(usize);
impl FileLoader for TestLoader {
fn load(&self, id: FileId) -> FileResult<Bytes> {
Ok(match id.vpath().get_without_slash() {
"a.typ" => Bytes::new(Vec::from(A_TEXT)),
"b.typ" => Bytes::new(B_DATA),
"c.bin" => Bytes::new(C_DATA),
"d.typ" => Bytes::from_string(format!("{}", self.0)),
"e.bin" if self.0 > 3 => Bytes::from_string(E_TEXT),
path => return Err(FileError::NotFound(path.into())),
})
}
}
fn id(path: &str) -> FileId {
RootedPath::new(VirtualRoot::Project, VirtualPath::new(path).unwrap()).intern()
}
trait OutputExt {
fn must_be(&self, data: impl AsRef<[u8]>);
}
impl OutputExt for Source {
#[track_caller]
fn must_be(&self, data: impl AsRef<[u8]>) {
assert_eq!(self.text().as_bytes(), data.as_ref());
}
}
impl OutputExt for Bytes {
#[track_caller]
fn must_be(&self, data: impl AsRef<[u8]>) {
assert_eq!(self.as_slice(), data.as_ref());
}
}
impl<T: OutputExt> OutputExt for FileResult<T> {
#[track_caller]
fn must_be(&self, data: impl AsRef<[u8]>) {
self.as_ref().unwrap().must_be(data);
}
}
}