typst_batch/
file.rs

1//! File caching with fingerprint-based invalidation.
2//!
3//! Files are cached globally to enable reuse across compilations.
4//! Fingerprint-based invalidation ensures changed files are re-read.
5//!
6//! # Caching Strategy
7//!
8//! ```text
9//! GLOBAL_FILE_CACHE (shared across all compilations)
10//! └── FxHashMap<FileId, FileSlot>
11//!     └── FileSlot
12//!         ├── source: SlotCell<Source>  ─┐
13//!         └── file: SlotCell<Bytes>     ─┼── Fingerprint-based invalidation
14//!
15//! Access Flow:
16//! 1. If accessed=true && data.is_some() → return cached (fast path)
17//! 2. Load file, compute fingerprint
18//! 3. If fingerprint unchanged → return cached
19//! 4. Otherwise → recompute and cache
20//! ```
21//!
22//! # Virtual Data Extension
23//!
24//! This module provides basic file caching. The main application (tola) extends
25//! this with virtual data support for `/_data/*.json` files via the
26//! [`VirtualDataProvider`] trait.
27
28use std::cell::RefCell;
29use std::fs;
30use std::io::{self, Read};
31use std::mem;
32use std::path::Path;
33use std::sync::LazyLock;
34
35use parking_lot::RwLock;
36use rustc_hash::FxHashMap;
37use typst::diag::{FileError, FileResult};
38use typst::foundations::Bytes;
39use typst::syntax::{FileId, Source, VirtualPath};
40use typst_kit::download::{DownloadState, Progress};
41
42use crate::config::package_storage;
43
44// =============================================================================
45// Constants
46// =============================================================================
47
48/// Virtual `FileId` for stdin input.
49pub static STDIN_ID: LazyLock<FileId> =
50    LazyLock::new(|| FileId::new_fake(VirtualPath::new("<stdin>")));
51
52/// Virtual `FileId` for empty/no input.
53pub static EMPTY_ID: LazyLock<FileId> =
54    LazyLock::new(|| FileId::new_fake(VirtualPath::new("<empty>")));
55
56// =============================================================================
57// FileId Helper Functions
58// =============================================================================
59
60/// Create a `FileId` for a project file.
61///
62/// This is the standard way to create file IDs for files within your project.
63/// The path should be root-relative (e.g., `/content/post.typ`).
64///
65/// # Arguments
66///
67/// * `path` - Path relative to project root, with leading `/`
68///
69/// # Example
70///
71/// ```ignore
72/// use typst_batch::file_id;
73///
74/// let id = file_id("/content/post.typ");
75/// ```
76pub fn file_id(path: impl AsRef<Path>) -> FileId {
77    FileId::new(None, VirtualPath::new(path.as_ref()))
78}
79
80/// Create a `FileId` for a file from its absolute path within a project root.
81///
82/// Returns `None` if the file is outside the root directory.
83///
84/// # Arguments
85///
86/// * `file_path` - Absolute path to the file
87/// * `root` - Absolute path to the project root
88///
89/// # Example
90///
91/// ```ignore
92/// use typst_batch::file_id_from_path;
93/// use std::path::Path;
94///
95/// let id = file_id_from_path(
96///     Path::new("/project/content/post.typ"),
97///     Path::new("/project"),
98/// );
99/// ```
100pub fn file_id_from_path(file_path: &Path, root: &Path) -> Option<FileId> {
101    VirtualPath::within_root(file_path, root).map(|vpath| FileId::new(None, vpath))
102}
103
104/// Create a virtual/fake `FileId` for dynamically generated content.
105///
106/// Use this for content that doesn't correspond to a real file on disk.
107/// Each call creates a unique ID that won't conflict with real file IDs.
108///
109/// # Arguments
110///
111/// * `name` - A descriptive name for the virtual file
112///
113/// # Example
114///
115/// ```ignore
116/// use typst_batch::virtual_file_id;
117///
118/// let id = virtual_file_id("<generated-data>");
119/// ```
120pub fn virtual_file_id(name: &str) -> FileId {
121    FileId::new_fake(VirtualPath::new(name))
122}
123
124// =============================================================================
125// Virtual File System
126// =============================================================================
127
128/// Trait for providing virtual files that don't exist on disk.
129///
130/// This is the primary extension point for batch compilation scenarios.
131/// Implement this trait to inject dynamically generated content into
132/// Typst's file system.
133///
134/// # Use Cases
135///
136/// - **Data injection**: Provide `/_data/posts.json` with blog post metadata
137/// - **Configuration**: Inject site configuration without physical files
138/// - **Template variables**: Provide computed values accessible via `json()`
139/// - **Asset manifests**: Generate asset URLs at compile time
140///
141/// # Example
142///
143/// ```ignore
144/// use typst_batch::{VirtualFileSystem, set_virtual_fs};
145/// use std::path::Path;
146///
147/// struct MyVirtualFS {
148///     site_config: String,
149/// }
150///
151/// impl VirtualFileSystem for MyVirtualFS {
152///     fn read(&self, path: &Path) -> Option<Vec<u8>> {
153///         match path.to_str()? {
154///             "/_data/site.json" => Some(self.site_config.as_bytes().to_vec()),
155///             "/_data/build-time.txt" => {
156///                 Some(chrono::Utc::now().to_rfc3339().into_bytes())
157///             }
158///             _ => None, // Fall back to real filesystem
159///         }
160///     }
161/// }
162///
163/// set_virtual_fs(MyVirtualFS { site_config: r#"{"title":"My Blog"}"#.into() });
164/// ```
165///
166/// # Thread Safety
167///
168/// Implementations must be `Send + Sync` as compilation may run in parallel.
169/// The provider is called for each file access, so implementations should be
170/// efficient (consider caching expensive computations).
171///
172/// # Note on Global State
173///
174/// Due to Typst's `World` trait design, the virtual file system must be
175/// registered globally via [`set_virtual_fs`]. This is a limitation of
176/// the underlying architecture, not a design choice.
177pub trait VirtualFileSystem: Send + Sync {
178    /// Read a virtual file by path.
179    ///
180    /// Return `Some(bytes)` to provide virtual content, or `None` to fall
181    /// back to the real filesystem.
182    ///
183    /// The path is root-relative (e.g., `/_data/config.json` or `/assets/style.css`).
184    fn read(&self, path: &Path) -> Option<Vec<u8>>;
185}
186
187/// No-op virtual file system (all files from real filesystem).
188pub struct NoVirtualFS;
189
190impl VirtualFileSystem for NoVirtualFS {
191    fn read(&self, _path: &Path) -> Option<Vec<u8>> {
192        None
193    }
194}
195
196/// A simple map-based virtual file system.
197///
198/// This provides a convenient way to inject virtual files without implementing
199/// the [`VirtualFileSystem`] trait manually.
200///
201/// # Example
202///
203/// ```ignore
204/// use typst_batch::{MapVirtualFS, set_virtual_fs};
205///
206/// let mut vfs = MapVirtualFS::new();
207/// vfs.insert("/_data/site.json", r#"{"title":"My Blog"}"#);
208/// vfs.insert("/_data/posts.json", serde_json::to_string(&posts)?);
209///
210/// set_virtual_fs(vfs);
211/// ```
212#[derive(Default, Clone)]
213pub struct MapVirtualFS {
214    files: FxHashMap<String, Vec<u8>>,
215}
216
217impl MapVirtualFS {
218    /// Create a new empty virtual file system.
219    pub fn new() -> Self {
220        Self::default()
221    }
222
223    /// Insert a virtual file with string content.
224    ///
225    /// The path should be root-relative (e.g., `/_data/config.json`).
226    pub fn insert(&mut self, path: impl Into<String>, content: impl AsRef<str>) {
227        self.files
228            .insert(path.into(), content.as_ref().as_bytes().to_vec());
229    }
230
231    /// Insert a virtual file with binary content.
232    pub fn insert_bytes(&mut self, path: impl Into<String>, content: impl Into<Vec<u8>>) {
233        self.files.insert(path.into(), content.into());
234    }
235
236    /// Check if a path exists in the virtual file system.
237    pub fn contains(&self, path: &str) -> bool {
238        self.files.contains_key(path)
239    }
240
241    /// Remove a virtual file.
242    pub fn remove(&mut self, path: &str) -> Option<Vec<u8>> {
243        self.files.remove(path)
244    }
245
246    /// Get the number of virtual files.
247    pub fn len(&self) -> usize {
248        self.files.len()
249    }
250
251    /// Check if the virtual file system is empty.
252    pub fn is_empty(&self) -> bool {
253        self.files.is_empty()
254    }
255
256    /// Iterate over all virtual file paths.
257    pub fn paths(&self) -> impl Iterator<Item = &str> {
258        self.files.keys().map(String::as_str)
259    }
260}
261
262impl VirtualFileSystem for MapVirtualFS {
263    fn read(&self, path: &Path) -> Option<Vec<u8>> {
264        let path_str = path.to_str()?;
265        self.files.get(path_str).cloned()
266    }
267}
268
269// Legacy type alias for backward compatibility
270#[doc(hidden)]
271pub type VirtualDataProvider = dyn VirtualFileSystem;
272
273// =============================================================================
274// Global Virtual File System
275// =============================================================================
276
277/// Global virtual file system instance.
278///
279/// This allows the main application to register a custom virtual file system
280/// that will be used by all file access operations during compilation.
281static GLOBAL_VIRTUAL_FS: LazyLock<RwLock<Box<dyn VirtualFileSystem>>> =
282    LazyLock::new(|| RwLock::new(Box::new(NoVirtualFS)));
283
284/// Set the global virtual file system.
285///
286/// Call this at application startup to enable virtual files.
287/// The virtual file system will be consulted for every file access.
288/// Return `Some(bytes)` from your implementation to provide virtual content,
289/// or `None` to fall back to the real filesystem.
290///
291/// # Example
292///
293/// ```ignore
294/// use typst_batch::{VirtualFileSystem, set_virtual_fs};
295/// use std::path::Path;
296///
297/// struct SiteData {
298///     posts: Vec<Post>,
299/// }
300///
301/// impl VirtualFileSystem for SiteData {
302///     fn read(&self, path: &Path) -> Option<Vec<u8>> {
303///         if path == Path::new("/_data/posts.json") {
304///             Some(serde_json::to_vec(&self.posts).unwrap())
305///         } else {
306///             None
307///         }
308///     }
309/// }
310///
311/// set_virtual_fs(SiteData { posts: vec![...] });
312/// ```
313pub fn set_virtual_fs<V: VirtualFileSystem + 'static>(fs: V) {
314    *GLOBAL_VIRTUAL_FS.write() = Box::new(fs);
315}
316
317/// Read a file, checking virtual file system first.
318///
319/// Returns virtual content if the VFS provides it, otherwise returns `None`
320/// to indicate the real filesystem should be used.
321pub fn read_virtual(path: &Path) -> Option<Vec<u8>> {
322    GLOBAL_VIRTUAL_FS.read().read(path)
323}
324
325/// Check if a path has virtual content available.
326pub fn is_virtual_path(path: &Path) -> bool {
327    GLOBAL_VIRTUAL_FS.read().read(path).is_some()
328}
329
330// Legacy function alias for backward compatibility
331#[doc(hidden)]
332pub fn set_virtual_provider<V: VirtualFileSystem + 'static>(provider: V) {
333    set_virtual_fs(provider);
334}
335
336// =============================================================================
337// Global File Cache
338// =============================================================================
339
340/// Global shared file cache - reused across all compilations.
341pub static GLOBAL_FILE_CACHE: LazyLock<RwLock<FxHashMap<FileId, FileSlot>>> =
342    LazyLock::new(|| RwLock::new(FxHashMap::default()));
343
344// =============================================================================
345// Thread-Local Access Tracking
346// =============================================================================
347
348thread_local! {
349    /// Thread-local set of accessed file IDs for the current compilation.
350    /// This avoids race conditions when compiling files in parallel.
351    static ACCESSED_FILES: RefCell<rustc_hash::FxHashSet<FileId>> =
352        RefCell::new(rustc_hash::FxHashSet::default());
353}
354
355/// Clear the thread-local accessed files set and reset global cache access flags.
356///
357/// Call at the start of each file compilation.
358pub fn reset_access_flags() {
359    // Reset thread-local tracking
360    ACCESSED_FILES.with(|files| files.borrow_mut().clear());
361
362    // Reset global cache access flags for fingerprint re-checking
363    for slot in GLOBAL_FILE_CACHE.write().values_mut() {
364        slot.reset_access();
365    }
366}
367
368/// Record a file access in the thread-local set.
369pub fn record_file_access(id: FileId) {
370    ACCESSED_FILES.with(|files| {
371        files.borrow_mut().insert(id);
372    });
373}
374
375/// Get all files accessed during the current compilation.
376///
377/// Returns a list of `FileId`s that were accessed since last `reset_access_flags()`.
378/// Thread-safe: each thread has its own tracking.
379pub fn get_accessed_files() -> Vec<FileId> {
380    ACCESSED_FILES.with(|files| files.borrow().iter().copied().collect())
381}
382
383/// Clear the global file cache.
384///
385/// Call when template/dependency files change to ensure fresh data is loaded.
386/// This also clears the comemo cache.
387pub fn clear_file_cache() {
388    GLOBAL_FILE_CACHE.write().clear();
389    typst::comemo::evict(0);
390}
391
392// =============================================================================
393// SlotCell - Fingerprint-based Caching
394// =============================================================================
395
396/// Lazily processes data for a file with fingerprint-based caching.
397pub struct SlotCell<T> {
398    data: Option<FileResult<T>>,
399    fingerprint: u128,
400    /// Whether this cell has been accessed in the current compilation.
401    pub accessed: bool,
402}
403
404impl<T: Clone> Default for SlotCell<T> {
405    fn default() -> Self {
406        Self::new()
407    }
408}
409
410impl<T: Clone> SlotCell<T> {
411    /// Create a new empty slot cell.
412    pub const fn new() -> Self {
413        Self {
414            data: None,
415            fingerprint: 0,
416            accessed: false,
417        }
418    }
419
420    /// Reset the access flag for a new compilation.
421    pub const fn reset_access(&mut self) {
422        self.accessed = false;
423    }
424
425    /// Get or initialize cached data using fingerprint-based invalidation.
426    pub fn get_or_init(
427        &mut self,
428        load: impl FnOnce() -> FileResult<Vec<u8>>,
429        process: impl FnOnce(Vec<u8>, Option<T>) -> FileResult<T>,
430    ) -> FileResult<T> {
431        // Fast path: already accessed in this compilation
432        if mem::replace(&mut self.accessed, true)
433            && let Some(data) = &self.data
434        {
435            return data.clone();
436        }
437
438        let result = load();
439        let fingerprint = typst::utils::hash128(&result);
440
441        // Fingerprint unchanged: reuse previous result
442        if mem::replace(&mut self.fingerprint, fingerprint) == fingerprint
443            && let Some(data) = &self.data
444        {
445            return data.clone();
446        }
447
448        // Process and cache new data
449        let prev = self.data.take().and_then(Result::ok);
450        let value = result.and_then(|data| process(data, prev));
451        self.data = Some(value.clone());
452        value
453    }
454}
455
456// =============================================================================
457// File Reading
458// =============================================================================
459
460/// Read file content from a `FileId`.
461///
462/// Handles special cases:
463/// - `EMPTY_ID`: Returns empty bytes
464/// - `STDIN_ID`: Reads from stdin
465/// - Package files: Downloads package if needed
466pub fn read(id: FileId, project_root: &Path) -> FileResult<Vec<u8>> {
467    read_with_virtual(id, project_root, &NoVirtualFS)
468}
469
470/// Read file content using the global virtual file system.
471///
472/// This function uses the globally registered virtual file system.
473/// Call [`set_virtual_fs`] at startup to register your VFS.
474pub fn read_with_global_virtual(id: FileId, project_root: &Path) -> FileResult<Vec<u8>> {
475    // Handle virtual file IDs first (don't need provider)
476    if id == *EMPTY_ID {
477        return Ok(Vec::new());
478    }
479    if id == *STDIN_ID {
480        return read_stdin();
481    }
482
483    // Check global virtual provider
484    let vpath = id.vpath().as_rooted_path();
485    if is_virtual_path(vpath) {
486        record_file_access(id);
487        return read_virtual(vpath).ok_or_else(|| FileError::NotFound(vpath.to_path_buf()));
488    }
489
490    // Resolve path and read from disk
491    let path = resolve_path(project_root, id)?;
492    read_disk(&path)
493}
494
495/// Read file content with virtual data support.
496///
497/// Like [`read`], but also handles virtual data files via the provider.
498pub fn read_with_virtual<V: VirtualFileSystem>(
499    id: FileId,
500    project_root: &Path,
501    virtual_fs: &V,
502) -> FileResult<Vec<u8>> {
503    // Handle virtual file IDs
504    if id == *EMPTY_ID {
505        return Ok(Vec::new());
506    }
507    if id == *STDIN_ID {
508        return read_stdin();
509    }
510
511    // Handle virtual data files (e.g., /_data/*.json)
512    let vpath = id.vpath().as_rooted_path();
513    if let Some(data) = virtual_fs.read(vpath) {
514        record_file_access(id);
515        return Ok(data);
516    }
517
518    // Resolve path and read from disk
519    let path = resolve_path(project_root, id)?;
520    read_disk(&path)
521}
522
523/// Decode bytes as UTF-8, stripping BOM if present.
524pub fn decode_utf8(buf: &[u8]) -> FileResult<&str> {
525    let buf = buf.strip_prefix(b"\xef\xbb\xbf").unwrap_or(buf);
526    std::str::from_utf8(buf).map_err(|_| FileError::InvalidUtf8)
527}
528
529/// Resolve file path, downloading package if needed.
530fn resolve_path(project_root: &Path, id: FileId) -> FileResult<std::path::PathBuf> {
531    let root = id
532        .package()
533        .map(|spec| package_storage().prepare_package(spec, &mut SilentProgress))
534        .transpose()?
535        .unwrap_or_else(|| project_root.to_path_buf());
536
537    id.vpath().resolve(&root).ok_or(FileError::AccessDenied)
538}
539
540/// Read file from disk.
541fn read_disk(path: &Path) -> FileResult<Vec<u8>> {
542    let map_err = |e| FileError::from_io(e, path);
543    fs::metadata(path).map_err(map_err).and_then(|m| {
544        if m.is_dir() {
545            Err(FileError::IsDirectory)
546        } else {
547            fs::read(path).map_err(map_err)
548        }
549    })
550}
551
552/// Read all data from stdin.
553fn read_stdin() -> FileResult<Vec<u8>> {
554    let mut buf = Vec::new();
555    io::stdin()
556        .read_to_end(&mut buf)
557        .or_else(|e| {
558            if e.kind() == io::ErrorKind::BrokenPipe {
559                Ok(0)
560            } else {
561                Err(FileError::from_io(e, Path::new("<stdin>")))
562            }
563        })?;
564    Ok(buf)
565}
566
567/// No-op progress reporter for silent package downloads.
568struct SilentProgress;
569
570impl Progress for SilentProgress {
571    fn print_start(&mut self) {}
572    fn print_progress(&mut self, _: &DownloadState) {}
573    fn print_finish(&mut self, _: &DownloadState) {}
574}
575
576// =============================================================================
577// FileSlot - Per-file Caching
578// =============================================================================
579
580/// Holds cached data for a file ID.
581pub struct FileSlot {
582    id: FileId,
583    source: SlotCell<Source>,
584    file: SlotCell<Bytes>,
585}
586
587impl FileSlot {
588    /// Create a new file slot for the given ID.
589    pub const fn new(id: FileId) -> Self {
590        Self {
591            id,
592            source: SlotCell::new(),
593            file: SlotCell::new(),
594        }
595    }
596
597    /// Reset access flags for a new compilation.
598    pub const fn reset_access(&mut self) {
599        self.source.reset_access();
600        self.file.reset_access();
601    }
602
603    /// Retrieve parsed source for this file (no virtual data).
604    pub fn source(&mut self, project_root: &Path) -> FileResult<Source> {
605        self.source_with_virtual(project_root, &NoVirtualFS)
606    }
607
608    /// Retrieve parsed source using the global virtual file system.
609    ///
610    /// This uses the VFS registered via [`set_virtual_fs`].
611    pub fn source_with_global_virtual(&mut self, project_root: &Path) -> FileResult<Source> {
612        record_file_access(self.id);
613        self.source.get_or_init(
614            || read_with_global_virtual(self.id, project_root),
615            |data, prev| {
616                let text = decode_utf8(&data)?;
617                match prev {
618                    Some(mut src) => {
619                        src.replace(text);
620                        Ok(src)
621                    }
622                    None => Ok(Source::new(self.id, text.into())),
623                }
624            },
625        )
626    }
627
628    /// Retrieve parsed source with virtual file system support.
629    pub fn source_with_virtual<V: VirtualFileSystem>(
630        &mut self,
631        project_root: &Path,
632        virtual_fs: &V,
633    ) -> FileResult<Source> {
634        record_file_access(self.id);
635        self.source.get_or_init(
636            || read_with_virtual(self.id, project_root, virtual_fs),
637            |data, prev| {
638                let text = decode_utf8(&data)?;
639                match prev {
640                    Some(mut src) => {
641                        src.replace(text);
642                        Ok(src)
643                    }
644                    None => Ok(Source::new(self.id, text.into())),
645                }
646            },
647        )
648    }
649
650    /// Retrieve raw bytes for this file (no virtual data).
651    pub fn file(&mut self, project_root: &Path) -> FileResult<Bytes> {
652        self.file_with_virtual(project_root, &NoVirtualFS)
653    }
654
655    /// Retrieve raw bytes using the global virtual file system.
656    ///
657    /// This uses the VFS registered via [`set_virtual_fs`].
658    pub fn file_with_global_virtual(&mut self, project_root: &Path) -> FileResult<Bytes> {
659        record_file_access(self.id);
660        self.file.get_or_init(
661            || read_with_global_virtual(self.id, project_root),
662            |data, _| Ok(Bytes::new(data)),
663        )
664    }
665
666    /// Retrieve raw bytes with virtual file system support.
667    pub fn file_with_virtual<V: VirtualFileSystem>(
668        &mut self,
669        project_root: &Path,
670        virtual_fs: &V,
671    ) -> FileResult<Bytes> {
672        record_file_access(self.id);
673        self.file.get_or_init(
674            || read_with_virtual(self.id, project_root, virtual_fs),
675            |data, _| Ok(Bytes::new(data)),
676        )
677    }
678}
679
680// =============================================================================
681// Tests
682// =============================================================================
683
684#[cfg(test)]
685mod tests {
686    use super::*;
687    use std::fs;
688    use tempfile::TempDir;
689
690    #[test]
691    fn test_decode_utf8_valid() {
692        let text = "Hello, 世界!";
693        assert_eq!(decode_utf8(text.as_bytes()).unwrap(), text);
694    }
695
696    #[test]
697    fn test_decode_utf8_strips_bom() {
698        let mut bytes = vec![0xef, 0xbb, 0xbf];
699        bytes.extend_from_slice(b"Hello");
700        assert_eq!(decode_utf8(&bytes).unwrap(), "Hello");
701    }
702
703    #[test]
704    fn test_decode_utf8_invalid() {
705        let invalid = vec![0xff, 0xfe];
706        assert!(decode_utf8(&invalid).is_err());
707    }
708
709    #[test]
710    fn test_read_disk() {
711        let dir = TempDir::new().unwrap();
712        let path = dir.path().join("test.txt");
713        fs::write(&path, "test content").unwrap();
714
715        assert_eq!(read_disk(&path).unwrap(), b"test content");
716    }
717
718    #[test]
719    fn test_read_disk_directory() {
720        let dir = TempDir::new().unwrap();
721        assert!(read_disk(dir.path()).is_err());
722    }
723
724    #[test]
725    fn test_read_disk_nonexistent() {
726        assert!(read_disk(Path::new("/nonexistent/file.txt")).is_err());
727    }
728
729    #[test]
730    fn test_slot_cell_fingerprint() {
731        let mut slot: SlotCell<String> = SlotCell::new();
732
733        let result1 = slot.get_or_init(
734            || Ok(b"hello".to_vec()),
735            |data, _| Ok(String::from_utf8(data).unwrap()),
736        );
737        assert_eq!(result1.unwrap(), "hello");
738
739        slot.accessed = false;
740        let result2 = slot.get_or_init(
741            || Ok(b"hello".to_vec()),
742            |_, _| panic!("Should not reprocess"),
743        );
744        assert_eq!(result2.unwrap(), "hello");
745    }
746
747    #[test]
748    fn test_file_slot_caching() {
749        let dir = TempDir::new().unwrap();
750        let path = dir.path().join("test.typ");
751        fs::write(&path, "= Hello").unwrap();
752
753        let vpath = VirtualPath::new("test.typ");
754        let id = FileId::new(None, vpath);
755        let mut slot = FileSlot::new(id);
756
757        let result1 = slot.file(dir.path());
758        let result2 = slot.file(dir.path());
759
760        assert!(result1.is_ok());
761        assert_eq!(result1.unwrap(), result2.unwrap());
762    }
763
764    #[test]
765    fn test_empty_id() {
766        let dir = TempDir::new().unwrap();
767        let result = read(*EMPTY_ID, dir.path());
768        assert!(result.is_ok());
769        assert!(result.unwrap().is_empty());
770    }
771}