icon_cache/
file.rs

1//! Load icon caches from a file path in a safe manner
2
3use crate::IconCache;
4use file_lock::FileLock;
5use memmap2::Mmap;
6use std::error::Error;
7use std::ops::Deref;
8use std::os::fd::AsRawFd;
9use std::path::Path;
10
11/// Reexports `file_lock` and `memmap2`, which are used in the [OwnedIconCache] type.
12pub mod reexports {
13    pub use file_lock;
14    pub use memmap2;
15}
16
17/// Provides access to an [IconCache] constructed from a file that is guaranteed not to be modified.
18///
19/// `OwnedIconCache` holds a lock on the cache file and creates a memory-mapped region with the file's
20/// contents inside. It does not copy the file contents.
21///
22/// To access the icon cache, use [OwnedIconCache::icon_cache]
23#[derive(Debug)]
24pub struct OwnedIconCache {
25    pub lock: FileLock,
26    pub memmap: Mmap,
27}
28
29impl OwnedIconCache {
30    /// Open and lock a file. This call may block waiting to acquire a lock if an exclusive lock
31    /// is already held.
32    ///
33    /// If this behaviour is undesirable, use [open_non_blocking](Self::open_non_blocking) instead.
34    pub fn open(path: impl AsRef<Path>) -> std::io::Result<Self> {
35        Self::create(path, true)
36    }
37
38    /// Open and lock a file, returning an error if an exclusive lock on the file was already held
39    /// by another process.
40    pub fn open_non_blocking(path: impl AsRef<Path>) -> std::io::Result<Self> {
41        Self::create(path, false)
42    }
43
44    /// Access the icon cache held by this `OwnedIconCache`.
45    ///
46    /// Returns an error if the cache could not be parsed.
47    pub fn icon_cache<'a>(&'a self) -> Result<IconCache<'a>, Box<dyn Error + 'a>> {
48        let bytes = self.memmap.deref();
49        
50        IconCache::new_from_bytes(bytes)
51    }
52
53    fn create(path: impl AsRef<Path>, blocking: bool) -> std::io::Result<Self> {
54        let path = path.as_ref();
55        let options = file_lock::FileOptions::new().read(true).write(false); // we explicitly do NOT want to write to the cache!
56        let lock = FileLock::lock(path, blocking, options)?;
57        
58        Self::from_lock(lock)
59    }
60
61    /// Create a `OwnedIconCache` from a locked file
62    pub fn from_lock(lock: FileLock) -> std::io::Result<Self> {
63        let fd = lock.file.as_raw_fd();
64        // SAFETY: we hold `lock`, which claims that `fd` will not change (unless done by us, which we won't)
65        // throughout the lifetime of the lock
66        let memmap = unsafe { Mmap::map(fd)? };
67
68        Ok(Self { lock, memmap })
69    }
70}
71
72#[cfg(test)]
73mod tests {
74    use crate::file::OwnedIconCache;
75    use crate::raw;
76    use crate::raw::Offset;
77    use std::error::Error;
78    use std::ops::Deref;
79    use std::sync::LazyLock;
80    use zerocopy::U16;
81
82    use mktemp::Temp;
83
84    static SAMPLE_INDEX_FILE: &[u8] = include_bytes!("../assets/icon-theme.cache");
85    static TEMP_FILE: LazyLock<Temp> = LazyLock::new(|| create_test_cache().unwrap());
86
87    fn create_test_cache() -> std::io::Result<Temp> {
88        let temp = Temp::new_file()?;
89
90        std::fs::write(temp.as_path(), SAMPLE_INDEX_FILE)?;
91
92        Ok(temp)
93    }
94
95    #[test]
96    fn open_test_file() -> std::io::Result<()> {
97        let path = TEMP_FILE.as_path();
98        let _file = OwnedIconCache::open_non_blocking(path)?;
99
100        Ok(())
101    }
102
103    #[test]
104    fn mmap_correct() -> Result<(), Box<dyn Error>> {
105        let path = TEMP_FILE.as_path();
106        let file = OwnedIconCache::open_non_blocking(path)?;
107
108        assert_eq!(file.memmap.deref(), SAMPLE_INDEX_FILE);
109
110        let icon_cache = file.icon_cache().unwrap();
111
112        assert_eq!(
113            icon_cache.header,
114            &raw::Header {
115                major_version: U16::new(1),
116                minor_version: U16::new(0),
117                hash: Offset::new(12),
118                directory_list: Offset::new(35812)
119            }
120        );
121        
122        assert_eq!(
123            icon_cache.hash.n_buckets.get() as usize,
124            icon_cache.hash.icon.len(),
125            "claimed hash len is the same as parsed len"
126        );
127
128        assert_eq!(
129            icon_cache.directory_list.raw_list.n_directories.get() as usize,
130            icon_cache.directory_list.raw_list.directory.len(),
131            "claimed directory list len is the same as parsed len"
132        );
133
134        Ok(())
135    }
136}