Skip to main content

rustial_engine/io/
disk_cache.rs

1//! Optional on-disk tile cache using a flat-file directory layout.
2//!
3//! Stores tiles as individual files under `{base_dir}/{z}/{x}/{y}.bin`.
4//! Feature-gated behind the `disk-cache` feature flag.
5//!
6//! # On-disk format
7//!
8//! Each file is a minimal binary envelope:
9//!
10//! ```text
11//! [u8 magic 'R']['M'][u8 version][u8 kind][u32 width LE][u32 height LE][RGBA8 data...]
12//! ```
13//!
14//! - **Magic** (`RM`) guards against reading arbitrary files.
15//! - **Version** (`1`) allows future format changes without silent corruption.
16//! - **Kind** (`0` = Raster RGBA8) supports future tile data variants.
17//! - Width / height / pixel data are identical to [`DecodedImage`](crate::tile_source::DecodedImage).
18//!
19//! # Thread safety
20//!
21//! All methods use only local `std::fs` calls with no interior mutability,
22//! so `&DiskCache` is safe to share across threads. File-system atomicity
23//! is achieved by writing to a temporary file and renaming, which is
24//! atomic on all major platforms.
25//!
26//! # Eviction
27//!
28//! This cache is **write-only** -- it does not enforce a maximum size.
29//! Call [`evict_older_than`](DiskCache::evict_older_than) periodically
30//! (e.g. at startup) to prune stale entries, or use
31//! [`clear`](DiskCache::clear) to wipe everything.
32
33use crate::tile_source::TileData;
34use rustial_math::TileId;
35use std::io;
36use std::path::{Path, PathBuf};
37use std::time::SystemTime;
38use thiserror::Error;
39
40// ---------------------------------------------------------------------------
41// Binary format constants
42// ---------------------------------------------------------------------------
43
44/// Two-byte magic header identifying a rustial cache file.
45const MAGIC: [u8; 2] = [b'R', b'M'];
46
47/// Current format version.  Bump when the on-disk layout changes.
48const VERSION: u8 = 1;
49
50/// Kind tag for [`TileData::Raster`].
51const KIND_RASTER: u8 = 0;
52
53/// Total header size: magic (2) + version (1) + kind (1) + width (4) + height (4).
54const HEADER_LEN: usize = 12;
55
56// ---------------------------------------------------------------------------
57// Error type
58// ---------------------------------------------------------------------------
59
60/// Errors from disk cache operations.
61#[derive(Debug, Error)]
62pub enum DiskCacheError {
63    /// An I/O error occurred reading or writing a cache file.
64    #[error("disk cache I/O error: {0}")]
65    Io(#[from] io::Error),
66    /// The cached data could not be decoded (corrupt file, wrong version, etc.).
67    #[error("disk cache decode error: {0}")]
68    Decode(String),
69}
70
71// ---------------------------------------------------------------------------
72// Internal helper types
73// ---------------------------------------------------------------------------
74
75/// Metadata for a single cache file, used by [`DiskCache::evict_to_size`].
76struct CacheFileInfo {
77    path: PathBuf,
78    size: u64,
79    modified: SystemTime,
80}
81
82// ---------------------------------------------------------------------------
83// DiskCache
84// ---------------------------------------------------------------------------
85
86/// A flat-file on-disk tile cache.
87///
88/// Directory layout: `{base_dir}/{z}/{x}/{y}.bin`.
89///
90/// # Example
91///
92/// ```rust,no_run
93/// use rustial_engine::DiskCache;
94/// use rustial_math::TileId;
95///
96/// let cache = DiskCache::new("./tile_cache").unwrap();
97/// let tile = TileId::new(10, 512, 340);
98/// if let Some(data) = cache.get(&tile).unwrap() {
99///     // use cached tile
100/// }
101/// ```
102#[derive(Debug)]
103pub struct DiskCache {
104    base_dir: PathBuf,
105}
106
107impl DiskCache {
108    /// Create (or open) a disk cache rooted at `base_dir`.
109    ///
110    /// The directory tree is created on demand; this call only ensures
111    /// the root directory exists.
112    pub fn new(base_dir: impl Into<PathBuf>) -> Result<Self, DiskCacheError> {
113        let base_dir = base_dir.into();
114        std::fs::create_dir_all(&base_dir)?;
115        Ok(Self { base_dir })
116    }
117
118    /// Load a tile from cache.
119    ///
120    /// Returns `Ok(None)` if the tile is not cached.
121    pub fn get(&self, id: &TileId) -> Result<Option<TileData>, DiskCacheError> {
122        let path = self.tile_path(id);
123        match std::fs::read(&path) {
124            Ok(bytes) => decode_tile_data(&bytes).map(Some),
125            Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(None),
126            Err(e) => Err(DiskCacheError::Io(e)),
127        }
128    }
129
130    /// Store a tile in the cache.
131    ///
132    /// Writes to a temporary file first, then renames atomically to
133    /// prevent readers from seeing a partial write.
134    pub fn put(&self, id: &TileId, data: &TileData) -> Result<(), DiskCacheError> {
135        let bytes = encode_tile_data(data);
136        if bytes.is_empty() {
137            // Encoding not supported for this tile variant (e.g. Vector).
138            return Err(DiskCacheError::Decode(
139                "disk-cache serialisation not supported for this tile data kind".into(),
140            ));
141        }
142
143        let path = self.tile_path(id);
144        if let Some(parent) = path.parent() {
145            std::fs::create_dir_all(parent)?;
146        }
147
148        // Write to a sibling temp file, then rename.
149        let tmp = path.with_extension("tmp");
150        std::fs::write(&tmp, bytes)?;
151        std::fs::rename(&tmp, &path)?;
152        Ok(())
153    }
154
155    /// Check whether a tile is present in the cache.
156    pub fn contains(&self, id: &TileId) -> bool {
157        self.tile_path(id).exists()
158    }
159
160    /// Remove a single tile from the cache.
161    ///
162    /// Returns `true` if the file was deleted, `false` if it was not cached.
163    pub fn remove(&self, id: &TileId) -> Result<bool, DiskCacheError> {
164        let path = self.tile_path(id);
165        match std::fs::remove_file(&path) {
166            Ok(()) => Ok(true),
167            Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(false),
168            Err(e) => Err(DiskCacheError::Io(e)),
169        }
170    }
171
172    /// Delete **all** cached tiles.
173    ///
174    /// Removes the entire directory tree under [`base_dir`](Self::base_dir)
175    /// and recreates the root.
176    pub fn clear(&self) -> Result<(), DiskCacheError> {
177        if self.base_dir.exists() {
178            std::fs::remove_dir_all(&self.base_dir)?;
179        }
180        std::fs::create_dir_all(&self.base_dir)?;
181        Ok(())
182    }
183
184    /// Remove cached tiles whose last-modified time is older than `max_age`.
185    ///
186    /// Walks the directory tree, deletes matching `.bin` files, and
187    /// removes any empty parent directories left behind.
188    ///
189    /// Returns the number of files deleted.
190    pub fn evict_older_than(
191        &self,
192        max_age: std::time::Duration,
193    ) -> Result<usize, DiskCacheError> {
194        let cutoff = SystemTime::now()
195            .checked_sub(max_age)
196            .unwrap_or(SystemTime::UNIX_EPOCH);
197
198        let mut removed = 0usize;
199        self.walk_and_evict(&self.base_dir, cutoff, &mut removed)?;
200        Ok(removed)
201    }
202
203    /// Remove cached tiles until the total on-disk size is at or below
204    /// `max_bytes`.
205    ///
206    /// Files are sorted oldest-first (by last-modified time) and deleted
207    /// in that order until the total size drops below the limit.
208    ///
209    /// Returns the number of files deleted.
210    pub fn evict_to_size(&self, max_bytes: u64) -> Result<usize, DiskCacheError> {
211        let mut entries = Vec::new();
212        self.walk_file_info(&self.base_dir, &mut entries)?;
213
214        // Sort oldest-first (smallest modified time first).
215        entries.sort_by_key(|e| e.modified);
216
217        let total: u64 = entries.iter().map(|e| e.size).sum();
218        if total <= max_bytes {
219            return Ok(0);
220        }
221
222        let mut current = total;
223        let mut removed = 0usize;
224        for entry in &entries {
225            if current <= max_bytes {
226                break;
227            }
228            if std::fs::remove_file(&entry.path).is_ok() {
229                current = current.saturating_sub(entry.size);
230                removed += 1;
231                // Try to remove empty parent directories.
232                if let Some(parent) = entry.path.parent() {
233                    let _ = std::fs::remove_dir(parent);
234                }
235            }
236        }
237        Ok(removed)
238    }
239
240    /// Count the number of cached tiles (walks the directory tree).
241    pub fn len(&self) -> Result<usize, DiskCacheError> {
242        let mut count = 0usize;
243        self.walk_count(&self.base_dir, &mut count)?;
244        Ok(count)
245    }
246
247    /// Whether the cache directory is empty (no `.bin` files).
248    pub fn is_empty(&self) -> Result<bool, DiskCacheError> {
249        Ok(self.len()? == 0)
250    }
251
252    /// The root directory of this cache.
253    pub fn base_dir(&self) -> &Path {
254        &self.base_dir
255    }
256
257    // -- private helpers --------------------------------------------------
258
259    fn tile_path(&self, id: &TileId) -> PathBuf {
260        self.base_dir
261            .join(id.zoom.to_string())
262            .join(id.x.to_string())
263            .join(format!("{}.bin", id.y))
264    }
265
266    fn walk_and_evict(
267        &self,
268        dir: &Path,
269        cutoff: SystemTime,
270        removed: &mut usize,
271    ) -> Result<(), DiskCacheError> {
272        let entries = match std::fs::read_dir(dir) {
273            Ok(e) => e,
274            Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(()),
275            Err(e) => return Err(DiskCacheError::Io(e)),
276        };
277
278        for entry in entries {
279            let entry = entry?;
280            let ft = entry.file_type()?;
281
282            if ft.is_dir() {
283                self.walk_and_evict(&entry.path(), cutoff, removed)?;
284                // Remove the directory if it is now empty.
285                let _ = std::fs::remove_dir(entry.path());
286            } else if ft.is_file() {
287                if let Some(ext) = entry.path().extension() {
288                    if ext == "bin" {
289                        let modified = entry
290                            .metadata()
291                            .and_then(|m| m.modified())
292                            .unwrap_or(SystemTime::UNIX_EPOCH);
293                        if modified < cutoff {
294                            std::fs::remove_file(entry.path())?;
295                            *removed += 1;
296                        }
297                    }
298                }
299            }
300        }
301        Ok(())
302    }
303
304    fn walk_file_info(
305        &self,
306        dir: &Path,
307        out: &mut Vec<CacheFileInfo>,
308    ) -> Result<(), DiskCacheError> {
309        let entries = match std::fs::read_dir(dir) {
310            Ok(e) => e,
311            Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(()),
312            Err(e) => return Err(DiskCacheError::Io(e)),
313        };
314
315        for entry in entries {
316            let entry = entry?;
317            let ft = entry.file_type()?;
318            if ft.is_dir() {
319                self.walk_file_info(&entry.path(), out)?;
320            } else if ft.is_file() {
321                if let Some(ext) = entry.path().extension() {
322                    if ext == "bin" {
323                        let meta = entry.metadata()?;
324                        out.push(CacheFileInfo {
325                            path: entry.path(),
326                            size: meta.len(),
327                            modified: meta.modified().unwrap_or(SystemTime::UNIX_EPOCH),
328                        });
329                    }
330                }
331            }
332        }
333        Ok(())
334    }
335
336    fn walk_count(&self, dir: &Path, count: &mut usize) -> Result<(), DiskCacheError> {
337        let entries = match std::fs::read_dir(dir) {
338            Ok(e) => e,
339            Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(()),
340            Err(e) => return Err(DiskCacheError::Io(e)),
341        };
342
343        for entry in entries {
344            let entry = entry?;
345            let ft = entry.file_type()?;
346            if ft.is_dir() {
347                self.walk_count(&entry.path(), count)?;
348            } else if ft.is_file() {
349                if let Some(ext) = entry.path().extension() {
350                    if ext == "bin" {
351                        *count += 1;
352                    }
353                }
354            }
355        }
356        Ok(())
357    }
358}
359
360// ---------------------------------------------------------------------------
361// Encode / Decode
362// ---------------------------------------------------------------------------
363
364fn encode_tile_data(data: &TileData) -> Vec<u8> {
365    match data {
366        TileData::Raster(img) => {
367            let mut buf = Vec::with_capacity(HEADER_LEN + img.data.len());
368            buf.extend_from_slice(&MAGIC);
369            buf.push(VERSION);
370            buf.push(KIND_RASTER);
371            buf.extend_from_slice(&img.width.to_le_bytes());
372            buf.extend_from_slice(&img.height.to_le_bytes());
373            buf.extend_from_slice(&img.data);
374            buf
375        }
376        TileData::Vector(_) | TileData::RawVector(_) => {
377            // Vector tile disk-cache serialisation is not yet implemented.
378            // Return a zero-length buffer; callers should check before writing.
379            Vec::new()
380        }
381    }
382}
383
384fn decode_tile_data(bytes: &[u8]) -> Result<TileData, DiskCacheError> {
385    if bytes.len() < HEADER_LEN {
386        return Err(DiskCacheError::Decode(format![
387            "file too short ({} bytes, need at least {HEADER_LEN})",
388            bytes.len()
389        ]));
390    }
391
392    // Validate magic bytes.
393    if bytes[0..2] != MAGIC {
394        return Err(DiskCacheError::Decode(format![
395            "invalid magic: expected {:?}, got {:?}",
396            MAGIC,
397            &bytes[0..2]
398        ]));
399    }
400
401    // Validate version.
402    let version = bytes[2];
403    if version != VERSION {
404        return Err(DiskCacheError::Decode(format![
405            "unsupported version {version} (expected {VERSION})"
406        ]));
407    }
408
409    let kind = bytes[3];
410    match kind {
411        KIND_RASTER => {
412            let width = u32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]);
413            let height = u32::from_le_bytes([bytes[8], bytes[9], bytes[10], bytes[11]]);
414            let pixel_data = &bytes[HEADER_LEN..];
415
416            let expected_len = (width as usize)
417                .checked_mul(height as usize)
418                .and_then(|n| n.checked_mul(4))
419                .ok_or_else(|| {
420                    DiskCacheError::Decode(format![
421                        "dimensions overflow: {width}x{height}"
422                    ])
423                })?;
424
425            if pixel_data.len() != expected_len {
426                return Err(DiskCacheError::Decode(format![
427                    "expected {expected_len} bytes of pixel data for {width}x{height}, got {}",
428                    pixel_data.len()
429                ]));
430            }
431
432            Ok(TileData::Raster(crate::tile_source::DecodedImage {
433                width,
434                height,
435                data: std::sync::Arc::new(pixel_data.to_vec()),
436            }))
437        }
438        _ => Err(DiskCacheError::Decode(format![
439            "unknown tile kind tag: {kind}"
440        ])),
441    }
442}
443
444// ---------------------------------------------------------------------------
445// Tests
446// ---------------------------------------------------------------------------
447
448#[cfg(test)]
449mod tests {
450    use super::*;
451    use crate::tile_source::DecodedImage;
452
453    /// Create a unique temp directory for each test.
454    fn temp_cache(name: &str) -> (DiskCache, PathBuf) {
455        let dir = std::env::temp_dir()
456            .join("rustial_disk_cache_test")
457            .join(name);
458        let _ = std::fs::remove_dir_all(&dir);
459        let cache = DiskCache::new(&dir).expect("create cache");
460        (cache, dir)
461    }
462
463    fn sample_tile() -> TileData {
464        TileData::Raster(DecodedImage {
465            width: 2,
466            height: 2,
467            data: vec![255u8; 16].into(), // 2x2 RGBA
468        })
469    }
470
471    // -- roundtrip ---------------------------------------------------------
472
473    #[test]
474    fn roundtrip_raster_tile() {
475        let (cache, dir) = temp_cache("roundtrip");
476        let id = TileId::new(5, 10, 15);
477        let data = sample_tile();
478
479        assert!(!cache.contains(&id));
480        cache.put(&id, &data).expect("put");
481        assert!(cache.contains(&id));
482
483        match cache.get(&id).expect("get").expect("some") {
484            TileData::Raster(img) => {
485                assert_eq!(img.width, 2);
486                assert_eq!(img.height, 2);
487                assert_eq!(img.data.len(), 16);
488                assert!(img.data.iter().all(|&b| b == 255));
489            }
490            TileData::Vector(_) | TileData::RawVector(_) => panic!("expected Raster, got Vector"),
491        }
492
493        assert!(cache.remove(&id).expect("remove"));
494        assert!(!cache.contains(&id));
495        let _ = std::fs::remove_dir_all(&dir);
496    }
497
498    // -- miss returns None -------------------------------------------------
499
500    #[test]
501    fn get_nonexistent_returns_none() {
502        let (cache, dir) = temp_cache("miss");
503        assert!(cache.get(&TileId::new(0, 0, 0)).expect("get").is_none());
504        let _ = std::fs::remove_dir_all(&dir);
505    }
506
507    // -- remove nonexistent returns false ----------------------------------
508
509    #[test]
510    fn remove_nonexistent_returns_false() {
511        let (cache, dir) = temp_cache("remove_miss");
512        assert!(!cache.remove(&TileId::new(0, 0, 0)).expect("remove"));
513        let _ = std::fs::remove_dir_all(&dir);
514    }
515
516    // -- decode validation -------------------------------------------------
517
518    #[test]
519    fn decode_too_short() {
520        assert!(decode_tile_data(&[0, 1, 2]).is_err());
521    }
522
523    #[test]
524    fn decode_bad_magic() {
525        let mut data = vec![b'X', b'Y', VERSION, KIND_RASTER];
526        data.extend_from_slice(&1u32.to_le_bytes()); // width
527        data.extend_from_slice(&1u32.to_le_bytes()); // height
528        data.extend_from_slice(&[0u8; 4]); // 1x1 RGBA
529        assert!(decode_tile_data(&data).is_err());
530    }
531
532    #[test]
533    fn decode_wrong_version() {
534        let mut data = vec![b'R', b'M', 99, KIND_RASTER];
535        data.extend_from_slice(&1u32.to_le_bytes());
536        data.extend_from_slice(&1u32.to_le_bytes());
537        data.extend_from_slice(&[0u8; 4]);
538        assert!(decode_tile_data(&data).is_err());
539    }
540
541    #[test]
542    fn decode_unknown_kind() {
543        let mut data = vec![b'R', b'M', VERSION, 77];
544        data.extend_from_slice(&1u32.to_le_bytes());
545        data.extend_from_slice(&1u32.to_le_bytes());
546        data.extend_from_slice(&[0u8; 4]);
547        assert!(decode_tile_data(&data).is_err());
548    }
549
550    #[test]
551    fn decode_wrong_pixel_length() {
552        let mut data = vec![b'R', b'M', VERSION, KIND_RASTER];
553        data.extend_from_slice(&2u32.to_le_bytes()); // width=2
554        data.extend_from_slice(&2u32.to_le_bytes()); // height=2
555        data.extend_from_slice(&[0u8; 4]); // only 4 bytes, need 16
556        assert!(decode_tile_data(&data).is_err());
557    }
558
559    #[test]
560    fn decode_overflow_dimensions() {
561        let mut data = vec![b'R', b'M', VERSION, KIND_RASTER];
562        data.extend_from_slice(&u32::MAX.to_le_bytes()); // width
563        data.extend_from_slice(&u32::MAX.to_le_bytes()); // height
564        // No pixel data -- the overflow check fires first.
565        assert!(decode_tile_data(&data).is_err());
566    }
567
568    // -- clear -------------------------------------------------------------
569
570    #[test]
571    fn clear_removes_all_tiles() {
572        let (cache, dir) = temp_cache("clear");
573        let data = sample_tile();
574        cache.put(&TileId::new(1, 0, 0), &data).expect("put");
575        cache.put(&TileId::new(2, 1, 1), &data).expect("put");
576        assert_eq!(cache.len().expect("len"), 2);
577
578        cache.clear().expect("clear");
579        assert!(cache.is_empty().expect("is_empty"));
580        let _ = std::fs::remove_dir_all(&dir);
581    }
582
583    // -- len / is_empty ----------------------------------------------------
584
585    #[test]
586    fn len_counts_tiles() {
587        let (cache, dir) = temp_cache("len");
588        assert!(cache.is_empty().expect("empty"));
589
590        let data = sample_tile();
591        cache.put(&TileId::new(0, 0, 0), &data).expect("put");
592        cache.put(&TileId::new(1, 0, 0), &data).expect("put");
593        assert_eq!(cache.len().expect("len"), 2);
594        let _ = std::fs::remove_dir_all(&dir);
595    }
596
597    // -- eviction ----------------------------------------------------------
598
599    #[test]
600    fn evict_removes_old_tiles() {
601        let (cache, dir) = temp_cache("evict");
602        let data = sample_tile();
603        cache.put(&TileId::new(0, 0, 0), &data).expect("put");
604
605        // Evict tiles older than 0 seconds (i.e. everything).
606        let removed = cache
607            .evict_older_than(std::time::Duration::ZERO)
608            .expect("evict");
609        assert_eq!(removed, 1);
610        assert!(cache.is_empty().expect("is_empty"));
611        let _ = std::fs::remove_dir_all(&dir);
612    }
613
614    #[test]
615    fn evict_keeps_recent_tiles() {
616        let (cache, dir) = temp_cache("evict_keep");
617        let data = sample_tile();
618        cache.put(&TileId::new(0, 0, 0), &data).expect("put");
619
620        // Evict tiles older than 1 hour -- the tile we just wrote stays.
621        let removed = cache
622            .evict_older_than(std::time::Duration::from_secs(3600))
623            .expect("evict");
624        assert_eq!(removed, 0);
625        assert_eq!(cache.len().expect("len"), 1);
626        let _ = std::fs::remove_dir_all(&dir);
627    }
628
629    #[test]
630    fn evict_to_size_removes_excess_files() {
631        let (cache, dir) = temp_cache("evict_to_size");
632        let data = sample_tile();
633        cache.put(&TileId::new(0, 0, 0), &data).expect("put");
634        cache.put(&TileId::new(1, 0, 0), &data).expect("put");
635        cache.put(&TileId::new(2, 0, 0), &data).expect("put");
636        assert_eq!(cache.len().expect("len"), 3);
637
638        // Evict to a size limit just above the current size -- no files removed.
639        let removed = cache
640            .evict_to_size(64)
641            .expect("evict_to_size");
642        assert_eq!(removed, 0);
643        assert_eq!(cache.len().expect("len"), 3);
644
645        // Evict to a size limit below the size of the largest single tile.
646        let removed = cache
647            .evict_to_size(15)
648            .expect("evict_to_size");
649        assert_eq!(removed, 1);
650        assert_eq!(cache.len().expect("len"), 2);
651
652        // Clean up the remaining tiles.
653        let _ = std::fs::remove_dir_all(&dir);
654    }
655
656    #[test]
657    fn evict_to_size_removes_oldest() {
658        let (cache, dir) = temp_cache("evict_size");
659        let data = sample_tile();
660
661        // Each sample_tile is 2x2 RGBA = 16 bytes of pixel data + 12 byte header = 28 bytes on disk.
662        cache.put(&TileId::new(0, 0, 0), &data).expect("put");
663        // Brief sleep so modified times differ.
664        std::thread::sleep(std::time::Duration::from_millis(50));
665        cache.put(&TileId::new(1, 0, 0), &data).expect("put");
666
667        assert_eq!(cache.len().expect("len"), 2);
668
669        // Evict to a size that fits only one file.
670        let removed = cache.evict_to_size(30).expect("evict");
671        assert_eq!(removed, 1);
672        assert_eq!(cache.len().expect("len"), 1);
673        // The older tile (zoom 0) should have been evicted.
674        assert!(!cache.contains(&TileId::new(0, 0, 0)));
675        assert!(cache.contains(&TileId::new(1, 0, 0)));
676
677        let _ = std::fs::remove_dir_all(&dir);
678    }
679
680    #[test]
681    fn evict_to_size_noop_when_under_limit() {
682        let (cache, dir) = temp_cache("evict_size_noop");
683        let data = sample_tile();
684        cache.put(&TileId::new(0, 0, 0), &data).expect("put");
685
686        let removed = cache.evict_to_size(1_000_000).expect("evict");
687        assert_eq!(removed, 0);
688        assert_eq!(cache.len().expect("len"), 1);
689        let _ = std::fs::remove_dir_all(&dir);
690    }
691}