Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_austenite/src/vfs.rs

12.2 KiB, 26 runs

created by r1870400018:40271, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1//! The one seam through which the assembler reads a document's files.
2//!
3//! A book compile follows a Typst root's `#include` chain and reads its `config.typ`, `terms.typ`,
4//! bibliography and figure assets. On a native build those come off the real filesystem; a
5//! `wasm32-unknown-unknown` build has no filesystem, so its files are injected here as a `path -> bytes`
6//! map before the same assembler runs. Every library file read routes through this module, so the
7//! assembler is written once and reads either source without a `#cfg` at each call.
8//!
9//! A path under a package's root, `@preview/cetz:0.3.4/src/lib.typ`, is served by the package layer
10//! ([`crate::eval::package`]) from what the host supplied, in memory or on disc, and never from the source
11//! map or the project's filesystem, so a package's files and a project's cannot shadow one another.
12//!
13//! The contract that keeps a native build byte-for-byte what it was: with no map installed -- the native
14//! default -- a read is exactly the `std::fs` call it replaced, and every predicate (`exists`, `is_file`,
15//! `canonicalize`) falls through to the real filesystem unchanged. A map is installed only by the wasm
16//! surface ([`crate::wasm`]), and only there does a read resolve from injected bytes. The global mirrors
17//! the crate's other assembly-time singletons -- the image base directory and the term dictionary --
18//! since the reader sets one file at a time and threads no source map of its own.
19
20use crate::eval::package::{
21 self,
22 PackageSpec,
23};
24
25use oxedyne_fe2o3_core::prelude::*;
26
27use std::collections::HashMap;
28use std::io;
29use std::path::{
30 Component,
31 Path,
32 PathBuf,
33};
34use std::sync::RwLock;
35
36// The injected source map. `None` on the native default path, where every read and predicate falls
37// through to `std::fs`; `Some` once the wasm surface installs a document's files, under which reads
38// resolve from the map and the real filesystem is never touched.
39static SOURCES: RwLock<Option<HashMap<PathBuf, Vec<u8>>>> = RwLock::new(None);
40
41/// Installs an injected source map, so every subsequent read resolves from `files` rather than the real
42/// filesystem. Keys are normalised on install and again on lookup, so a `dir.join("../x")` and a direct
43/// `x` resolve alike. The wasm compile surface calls this before it runs the assembler and clears it
44/// after, so one process can compile many documents in turn.
45pub fn install(files: HashMap<PathBuf, Vec<u8>>) -> Outcome<()> {
46 let mut norm: HashMap<PathBuf, Vec<u8>> = HashMap::with_capacity(files.len());
47 for (k, v) in files {
48 norm.insert(normalise(&k), v);
49 }
50 let mut guard = lock_write!(SOURCES, "While installing the source map");
51 *guard = Some(norm);
52 Ok(())
53}
54
55/// Removes any installed source map, restoring the native filesystem behaviour.
56pub fn clear() -> Outcome<()> {
57 let mut guard = lock_write!(SOURCES, "While clearing the source map");
58 *guard = None;
59 Ok(())
60}
61
62/// Is a source map installed? True only under a wasm compile; false on the native default path, where a
63/// poisoned lock also reads as absent so a read still falls through to `std::fs`.
64pub fn is_installed() -> bool {
65 match SOURCES.read() {
66 Ok(guard) => guard.is_some(),
67 Err(_) => false,
68 }
69}
70
71/// Reads a file's bytes: from the installed source map where one holds the path, else from the real
72/// filesystem. With no map installed this is exactly [`std::fs::read`], so a native compile is unchanged.
73pub fn read(path: &Path) -> io::Result<Vec<u8>> {
74 if let Some(r) = package::read(path) {
75 return r;
76 }
77 match SOURCES.read() {
78 Ok(guard) => match guard.as_ref() {
79 Some(map) => match map.get(&normalise(path)) {
80 Some(bytes) => Ok(bytes.clone()),
81 None => native_read(path),
82 },
83 None => native_read(path),
84 },
85 Err(_) => native_read(path),
86 }
87}
88
89/// Reads a file as UTF-8 text, the drop-in for [`std::fs::read_to_string`]: invalid UTF-8 is the same
90/// `InvalidData` error the standard call raises, so a caller's error handling is unchanged.
91pub fn read_to_string(path: &Path) -> io::Result<String> {
92 let bytes = ok!(read(path));
93 match String::from_utf8(bytes) {
94 Ok(s) => Ok(s),
95 Err(e) => Err(io::Error::new(io::ErrorKind::InvalidData, e)),
96 }
97}
98
99/// Did a [`read_to_string`] fail because the file is not valid UTF-8 text, rather than because it could not
100/// be read at all? The remedy differs -- re-save the file, not supply it -- so a caller tags the two apart.
101pub fn is_not_utf8(e: &io::Error) -> bool {
102 e.kind() == io::ErrorKind::InvalidData
103 && e.get_ref().map_or(false, |inner| inner.is::<std::string::FromUtf8Error>())
104}
105
106/// Writes a file's bytes: into the installed source map where one is present (so a later read in the same
107/// compile sees it), else to the real filesystem. With no map installed this is exactly [`std::fs::write`].
108pub fn write(path: &Path, contents: &[u8]) -> io::Result<()> {
109 if PackageSpec::of_path(path).is_some() {
110 return Err(io::Error::new(io::ErrorKind::PermissionDenied, fmt!("{:?} is in a package, which is read-only.", path)));
111 }
112 if is_installed() {
113 let mut guard = match SOURCES.write() {
114 Ok(g) => g,
115 Err(_) => return Err(io::Error::new(io::ErrorKind::Other, "the source map lock is poisoned")),
116 };
117 if let Some(map) = guard.as_mut() {
118 map.insert(normalise(path), contents.to_vec());
119 }
120 Ok(())
121 } else {
122 native_write(path, contents)
123 }
124}
125
126/// Does a path resolve to a file? A map lookup where one is installed, falling through to the real
127/// filesystem; exactly [`Path::exists`] on the native default path.
128pub fn exists(path: &Path) -> bool {
129 if let (Some(file), Some(dir)) = (package::is_file(path), package::is_dir(path)) {
130 return file || dir;
131 }
132 match SOURCES.read() {
133 Ok(guard) => match guard.as_ref() {
134 Some(map) => map.contains_key(&normalise(path)) || native_exists(path),
135 None => native_exists(path),
136 },
137 Err(_) => native_exists(path),
138 }
139}
140
141/// Is a path a readable file? For the injected map a key is a file, so this matches [`exists`] there;
142/// exactly [`Path::is_file`] on the native default path.
143pub fn is_file(path: &Path) -> bool {
144 if let Some(file) = package::is_file(path) {
145 return file;
146 }
147 match SOURCES.read() {
148 Ok(guard) => match guard.as_ref() {
149 Some(map) => map.contains_key(&normalise(path)) || native_is_file(path),
150 None => native_is_file(path),
151 },
152 Err(_) => native_is_file(path),
153 }
154}
155
156/// Is a path a directory? A package path asks the package it lies in; under an installed map a directory is
157/// any proper prefix of a key, or a real directory; exactly [`Path::is_dir`] on the native default path.
158pub fn is_dir(path: &Path) -> bool {
159 if let Some(dir) = package::is_dir(path) {
160 return dir;
161 }
162 match SOURCES.read() {
163 Ok(guard) => match guard.as_ref() {
164 Some(map) => {
165 let root = normalise(path);
166 map.keys().any(|k| k.starts_with(&root) && k != &root) || native_is_dir(path)
167 },
168 None => native_is_dir(path),
169 },
170 Err(_) => native_is_dir(path),
171 }
172}
173
174/// Every file beneath `dir`, at any depth, in a stable (sorted) order: the injected map's keys under that
175/// directory where a map is installed, together with whatever the real filesystem holds there. A directory
176/// that does not exist lists nothing, so a caller scanning an optional directory needs no existence check.
177pub fn list_files(dir: &Path) -> Vec<PathBuf> {
178 if let Some(files) = package::list(dir) {
179 return files;
180 }
181 let root = normalise(dir);
182 let mut out: Vec<PathBuf> = Vec::new();
183 if let Ok(guard) = SOURCES.read() {
184 if let Some(map) = guard.as_ref() {
185 for k in map.keys() {
186 if k.starts_with(&root) && k != &root {
187 out.push(k.clone());
188 }
189 }
190 }
191 }
192 native_list(dir, &mut out, LIST_DEPTH);
193 out.sort();
194 out.dedup();
195 out
196}
197
198/// Resolves a path to an absolute, canonical form. Under an installed map there is no filesystem to walk,
199/// so the path is normalised lexically (`.` and `..` folded); exactly [`std::fs::canonicalize`] on the
200/// native default path.
201pub fn canonicalize(path: &Path) -> io::Result<PathBuf> {
202 if PackageSpec::of_path(path).is_some() {
203 return Ok(normalise(path));
204 }
205 match SOURCES.read() {
206 Ok(guard) => match guard.as_ref() {
207 Some(_) => Ok(normalise(path)),
208 None => native_canonicalize(path),
209 },
210 Err(_) => native_canonicalize(path),
211 }
212}
213
214/// Folds `.` and `..` out of a path lexically, without touching the filesystem, so an injected map keys
215/// consistently whether a file is named directly or reached through a `join("..")`.
216fn normalise(path: &Path) -> PathBuf {
217 let mut out = PathBuf::new();
218 for comp in path.components() {
219 match comp {
220 Component::CurDir => {},
221 Component::ParentDir => { out.pop(); },
222 other => out.push(other.as_os_str()),
223 }
224 }
225 out
226}
227
228// ┌───────────────────────────────────────────────────────────────────────────┐
229// │ NATIVE FALL-THROUGH │
230// └───────────────────────────────────────────────────────────────────────────┘
231// On every target but wasm the fall-through is the real filesystem, so a native build with no map
232// installed behaves exactly as the direct `std::fs` calls these replaced. On wasm there is no
233// filesystem: a path absent from the injected map cannot be read, and a missing entry names itself and
234// the map so a diagnostic carries the file rather than a bare "not found".
235
236#[cfg(not(target_arch = "wasm32"))]
237fn native_read(path: &Path) -> io::Result<Vec<u8>> {
238 std::fs::read(path)
239}
240
241#[cfg(target_arch = "wasm32")]
242fn native_read(path: &Path) -> io::Result<Vec<u8>> {
243 Err(io::Error::new(
244 io::ErrorKind::NotFound,
245 fmt!("{:?} is not in the injected source map, and the wasm target has no filesystem.", path)))
246}
247
248#[cfg(not(target_arch = "wasm32"))]
249fn native_write(path: &Path, contents: &[u8]) -> io::Result<()> {
250 std::fs::write(path, contents)
251}
252
253#[cfg(target_arch = "wasm32")]
254fn native_write(path: &Path, _contents: &[u8]) -> io::Result<()> {
255 Err(io::Error::new(
256 io::ErrorKind::Unsupported,
257 fmt!("cannot write {:?}: the wasm target has no filesystem and no source map is installed.", path)))
258}
259
260#[cfg(not(target_arch = "wasm32"))]
261fn native_exists(path: &Path) -> bool {
262 path.exists()
263}
264
265#[cfg(target_arch = "wasm32")]
266fn native_exists(_path: &Path) -> bool {
267 false
268}
269
270#[cfg(not(target_arch = "wasm32"))]
271fn native_is_file(path: &Path) -> bool {
272 path.is_file()
273}
274
275#[cfg(target_arch = "wasm32")]
276fn native_is_file(_path: &Path) -> bool {
277 false
278}
279
280#[cfg(not(target_arch = "wasm32"))]
281fn native_is_dir(path: &Path) -> bool {
282 path.is_dir()
283}
284
285#[cfg(target_arch = "wasm32")]
286fn native_is_dir(_path: &Path) -> bool {
287 false
288}
289
290// How deep [`list_files`] descends: deep enough for any font tree, shallow enough that a symlink cycle
291// ends rather than recursing without bound.
292const LIST_DEPTH: u32 = 8;
293
294#[cfg(not(target_arch = "wasm32"))]
295fn native_list(dir: &Path, out: &mut Vec<PathBuf>, depth: u32) {
296 if depth == 0 {
297 return;
298 }
299 let entries = match std::fs::read_dir(dir) {
300 Ok(e) => e,
301 Err(_) => return,
302 };
303 for entry in entries.flatten() {
304 let path = entry.path();
305 if path.is_dir() {
306 native_list(&path, out, depth - 1);
307 } else if path.is_file() {
308 out.push(path);
309 }
310 }
311}
312
313#[cfg(target_arch = "wasm32")]
314fn native_list(_dir: &Path, _out: &mut Vec<PathBuf>, _depth: u32) {}
315
316#[cfg(not(target_arch = "wasm32"))]
317fn native_canonicalize(path: &Path) -> io::Result<PathBuf> {
318 std::fs::canonicalize(path)
319}
320
321#[cfg(target_arch = "wasm32")]
322fn native_canonicalize(path: &Path) -> io::Result<PathBuf> {
323 Ok(normalise(path))
324}
325
326#[cfg(test)]
327mod tests {
328 use super::*;
329
330 /// A path with `.` and `..` components folds to the same key whether it was named directly or reached
331 /// through a join, so an injected map keyed on one form is found by the other.
332 #[test]
333 fn normalise_folds_dot_and_parent() {
334 assert_eq!(normalise(Path::new("a/b/../c/./d")), PathBuf::from("a/c/d"));
335 assert_eq!(normalise(Path::new("./x")), PathBuf::from("x"));
336 }
337
338 /// With no map installed the module reports itself absent, so every read falls through to `std::fs`
339 /// and a native compile is unchanged.
340 #[test]
341 fn not_installed_by_default() {
342 assert!(!is_installed());
343 }
344}