icon_cache/
lib.rs

1//! Complete and user-friendly zero-copy wrappers for the GTK icon cache which is present on most
2//! linux systems.
3//!
4//! GTK's icon cache maintains a hash-indexed map from icon names (e.g. `open-menu`) to a list of
5//! images representing that icon, each in a different directory, usually denoting that icon's size,
6//! whether it's scalable, etc.
7//!
8//! This crate provides a safe wrapper around this cache and is designed for use with `mmap`.
9//! To get started, look at [IconCache].
10//!
11//! # Feature flags
12//!
13//! * `file`: Enables the [mod@file] module to safely open and memory-map cache files. Enabled by default.
14
15use std::error::Error;
16use std::ffi::CStr;
17use std::path::Path;
18use zerocopy::{network_endian, FromBytes};
19
20#[cfg(feature = "file")]
21pub mod file;
22pub mod raw;
23
24/// Thin wrapper around an in-memory icon cache.
25///
26/// This is `icon-cache`'s main entrypoint. To look up an icon, use the [icon](IconCache::icon) function.
27///
28/// `IconCache`'s fields may be interesting for advanced uses, but if all you need is to look up
29/// icons—use [icon](IconCache::icon).
30#[derive(derive_more::Debug, Copy, Clone)]
31pub struct IconCache<'a> {
32    /// The raw bytes representing the cache
33    #[debug(skip)]
34    pub bytes: &'a [u8],
35    /// Cache header file: contains version and hash & directory list offsets
36    pub header: &'a raw::Header,
37    /// Internal hash table storing mapping from icon names to icon information
38    pub hash: &'a raw::Hash,
39    /// List of directories within the theme, relative to the theme's root
40    pub directory_list: DirectoryList<'a>,
41}
42
43impl<'a> IconCache<'a> {
44    pub fn new_from_bytes(bytes: &'a [u8]) -> Result<Self, Box<dyn Error + 'a>> {
45        let (header, _) = raw::Header::ref_from_prefix(bytes)?;
46        
47        let hash_offset = header.hash.offset.get() as usize;
48        let dir_list_offset = header.directory_list.offset.get() as usize;
49        
50        let (hash_len, _) = network_endian::U32::read_from_prefix(&bytes[hash_offset..])?;
51        let (dir_len, _) = network_endian::U32::read_from_prefix(&bytes[dir_list_offset..])?;
52        
53        let (hash, _) = raw::Hash::ref_from_prefix_with_elems(&bytes[hash_offset..], hash_len.get() as usize)?;
54        let (directory_list, _) = raw::DirectoryList::ref_from_prefix_with_elems(&bytes[dir_list_offset..], dir_len.get() as usize)?;
55
56        let directory_list = DirectoryList {
57            bytes,
58            raw_list: directory_list,
59        };
60        
61        Ok(IconCache {
62            bytes,
63            header,
64            hash,
65            directory_list,
66        })
67    }
68
69    /// Look up an icon by name in the cache. `icon_name` accepts any type that turns into a byte
70    /// slice: typically `str` suffices.
71    ///
72    /// Returns `None` if no icon by that name exists within the icon theme, or if parsing failed.
73    pub fn icon(&self, icon_name: impl AsRef<[u8]>) -> Option<Icon<'a>> {
74        let icon_name = icon_name.as_ref();
75        let hash = icon_str_hash(icon_name);
76        let n_buckets = self.hash.n_buckets.get();
77        let bucket = hash % n_buckets;
78
79        let icons = self.icon_chain(bucket)?.iter(self.bytes);
80
81        for icon in icons {
82            let Ok(name) = icon.name.str_at(self.bytes) else {
83                continue;
84            };
85
86            if name.to_bytes() == icon_name {
87                return Some(Icon {
88                    name,
89                    image_list: ImageList::from_icon(icon, self.bytes)?,
90                });
91            }
92        }
93
94        None
95    }
96
97    pub fn iter(&self) -> impl Iterator<Item = Icon<'a>> {
98        (0..self.hash.n_buckets.get())
99            .filter_map(|bucket| self.icon_chain(bucket))
100            .flat_map(|chain| chain.iter(self.bytes))
101            .filter_map(|icon| {
102                Some(Icon {
103                    name: icon.name.str_at(self.bytes).ok()?,
104                    image_list: ImageList::from_icon(icon, self.bytes)?,
105                })
106            })
107    }
108
109    fn icon_chain(&self, bucket: u32) -> Option<&'a raw::Icon> {
110        debug_assert!(bucket < self.hash.n_buckets.get());
111
112        let offset = self.hash.icon[bucket as usize];
113        // A bucket may be empty!
114        if offset.is_null() {
115            return None;
116        }
117
118        offset.at(self.bytes).ok()
119    }
120}
121
122/// List of directories in the icon theme with paths relative to the root of the icon theme.
123#[derive(derive_more::Debug, Copy, Clone)]
124pub struct DirectoryList<'a> {
125    #[debug(skip)]
126    #[allow(unused)] // clippy thinks this is unused but it isn't? maybe because of the Debug
127    bytes: &'a [u8],
128    pub raw_list: &'a raw::DirectoryList,
129}
130
131impl<'a> DirectoryList<'a> {
132    /// Returns the amount of directories in this list
133    #[inline(always)]
134    pub fn len(&self) -> u32 {
135        self.raw_list.n_directories.get()
136    }
137
138    /// Returns `true` if the list is empty
139    pub fn is_empty(&self) -> bool {
140        self.len() == 0
141    }
142
143    /// Access a directory by its index in the list.
144    ///
145    /// Returns `None` if the index larger than the length of the list.
146    pub fn dir(&self, idx: u32) -> Option<&'a Path> {
147        if idx >= self.len() {
148            return None;
149        }
150
151        self.raw_list.directory[idx as usize].path_at(self.bytes)
152    }
153
154    /// Returns an iterator over the directory list
155    pub fn iter(&self) -> impl Iterator<Item = &'a Path> {
156        (0..self.len()).filter_map(|idx| self.dir(idx))
157    }
158}
159
160/// An icon, identified by its name, and the list of images provided by the icon theme for this icon.
161#[derive(Debug, Copy, Clone)]
162pub struct Icon<'a> {
163    pub name: &'a CStr,
164    pub image_list: ImageList<'a>,
165}
166
167#[derive(derive_more::Debug, Copy, Clone)]
168pub struct ImageList<'a> {
169    #[debug(skip)]
170    bytes: &'a [u8],
171    pub raw_list: &'a raw::ImageList,
172}
173
174impl<'a> ImageList<'a> {
175    fn from_icon(icon: &raw::Icon, bytes: &'a [u8]) -> Option<ImageList<'a>> {
176        Some(Self {
177            bytes,
178            raw_list: icon.image_list.at(bytes).ok()?,
179        })
180    }
181
182    /// Returns the amount of images in this list
183    pub fn len(&self) -> u32 {
184        self.raw_list.n_images.get()
185    }
186
187    /// Returns `true` if the list is empty
188    pub fn is_empty(&self) -> bool {
189        self.len() == 0
190    }
191
192    /// Access an image by its index in the list.
193    ///
194    /// Returns `None` if the index larger than the length of the list, or if the image data
195    /// failed to parse.
196    pub fn image(&self, idx: u32) -> Option<Image<'a>> {
197        if idx >= self.len() {
198            return None;
199        }
200
201        let raw_image = &self.raw_list.images[idx as usize];
202
203        // TODO: how does the overhead of re-interpreting the header and directory list here over
204        // passing those down from the cache struct, or alternatively re-introducing the ref to cache?
205        let (header, _) = raw::Header::ref_from_prefix(self.bytes).ok()?;
206        let directory_list = header.directory_list.at(self.bytes).ok()?;
207        let directory = directory_list.directory[raw_image.directory_index.get() as usize]
208            .path_at(self.bytes)?;
209
210        let icon_flags = raw_image.icon_flags;
211
212        let mut image_data = None;
213
214        if raw_image.image_data.offset != 0 {
215            let &raw::ImageData {
216                image_pixel_data,
217                image_meta_data,
218                image_pixel_data_length,
219                image_pixel_data_type,
220            } = raw_image.image_data.at(self.bytes).ok()?;
221
222            image_data = Some(ImageData {
223                image_pixel_data: *image_pixel_data.at(self.bytes).ok()?,
224                image_meta_data: image_meta_data.at(self.bytes).ok()?,
225                image_pixel_data_type: *image_pixel_data_type.at(self.bytes).ok()?,
226                image_pixel_data_length: *image_pixel_data_length.at(self.bytes).ok()?,
227            });
228        }
229
230        Some(Image {
231            directory,
232            icon_flags,
233            image_data,
234        })
235    }
236
237    /// Returns an iterator over the image list
238    pub fn iter(&self) -> impl Iterator<Item = Image<'a>> {
239        (0..self.len()).filter_map(|idx| self.image(idx))
240    }
241}
242
243#[derive(derive_more::Debug, Copy, Clone)]
244pub struct Image<'a> {
245    pub directory: &'a Path,
246    pub icon_flags: raw::Flags,
247    pub image_data: Option<ImageData<'a>>,
248}
249
250#[derive(derive_more::Debug, Copy, Clone)]
251pub struct ImageData<'a> {
252    pub image_pixel_data: (), // TODO: what type is this?
253    pub image_meta_data: &'a raw::MetaData,
254    pub image_pixel_data_type: (),
255    pub image_pixel_data_length: (),
256}
257
258fn icon_str_hash(key: impl AsRef<[u8]>) -> u32 {
259    let bytes = key.as_ref();
260
261    if bytes.is_empty() {
262        return 0;
263    }
264
265    let mut h: u32 = bytes[0] as u32;
266    for &p in &bytes[1..] {
267        h = (h << 5).overflowing_sub(h).0.overflowing_add(p as u32).0;
268    }
269
270    h
271}
272
273#[cfg(test)]
274mod tests {
275    use super::*;
276    use crate::raw::Offset;
277    use zerocopy::network_endian::U16;
278
279    // The included sample cache file was generated using the gtk-update-icon-cache utility
280    // from my system-installed hicolor theme.
281    static SAMPLE_INDEX_FILE: &[u8] = include_bytes!("../assets/icon-theme.cache");
282
283    #[test]
284    fn test_find_specific_icon() -> Result<(), Box<dyn Error>> {
285        let cache = IconCache::new_from_bytes(SAMPLE_INDEX_FILE)?;
286
287        assert_eq!(
288            cache.header,
289            &raw::Header {
290                major_version: U16::new(1),
291                minor_version: U16::new(0),
292                hash: Offset::new(12),
293                directory_list: Offset::new(35812)
294            }
295        );
296
297        assert_eq!(cache.hash.n_buckets, 251);
298
299        let icon = cache.icon("mpv").unwrap();
300
301        assert_eq!(icon.name.to_str(), Ok("mpv"));
302        assert_eq!(icon.image_list.len(), 5);
303
304        let image = &icon.image_list.image(0).unwrap();
305
306        assert_eq!(image.directory.to_str(), Some("scalable/apps"));
307        assert_eq!(
308            image.icon_flags,
309            raw::Flags::new(raw::Flags::HAS_SUFFIX_SVG)
310        );
311        assert!(image.image_data.is_none());
312
313        Ok(())
314    }
315
316    #[test]
317    fn test_icon_iter() -> Result<(), Box<dyn Error>> {
318        let cache = IconCache::new_from_bytes(SAMPLE_INDEX_FILE)?;
319
320        assert_eq!(cache.iter().count(), 563);
321
322        Ok(())
323    }
324
325    #[test]
326    fn test_image_list_iter() -> Result<(), Box<dyn Error>> {
327        let cache = IconCache::new_from_bytes(SAMPLE_INDEX_FILE)?;
328        let icon = cache.icon("mpv").unwrap();
329
330        let count = icon.image_list.iter().count();
331        assert_eq!(count, 5);
332
333        Ok(())
334    }
335
336    #[test]
337    fn test_directory_list_iter() -> Result<(), Box<dyn Error>> {
338        let cache = IconCache::new_from_bytes(SAMPLE_INDEX_FILE)?;
339        let dir_list = cache.directory_list;
340
341        assert_eq!(dir_list.len(), 59);
342
343        assert!(!cache.directory_list.is_empty());
344        assert_eq!(cache.directory_list.iter().count(), 59);
345
346        Ok(())
347    }
348
349    #[test]
350    fn icon_str_hash_empty() {
351        assert_eq!(icon_str_hash(""), 0);
352    }
353
354    #[test]
355    fn icon_str_hash_hello_world() {
356        assert_eq!(icon_str_hash("hello world"), 1794106052);
357    }
358
359    #[test]
360    fn icon_str_hash_sym() {
361        assert_eq!(icon_str_hash("preferences-other-symbolic") % 251, 243);
362    }
363
364    #[test]
365    fn image_size_correct() {
366        assert_eq!(size_of::<raw::Image>(), 8);
367    }
368}