1use 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#[derive(derive_more::Debug, Copy, Clone)]
31pub struct IconCache<'a> {
32 #[debug(skip)]
34 pub bytes: &'a [u8],
35 pub header: &'a raw::Header,
37 pub hash: &'a raw::Hash,
39 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 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 if offset.is_null() {
115 return None;
116 }
117
118 offset.at(self.bytes).ok()
119 }
120}
121
122#[derive(derive_more::Debug, Copy, Clone)]
124pub struct DirectoryList<'a> {
125 #[debug(skip)]
126 #[allow(unused)] bytes: &'a [u8],
128 pub raw_list: &'a raw::DirectoryList,
129}
130
131impl<'a> DirectoryList<'a> {
132 #[inline(always)]
134 pub fn len(&self) -> u32 {
135 self.raw_list.n_directories.get()
136 }
137
138 pub fn is_empty(&self) -> bool {
140 self.len() == 0
141 }
142
143 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 pub fn iter(&self) -> impl Iterator<Item = &'a Path> {
156 (0..self.len()).filter_map(|idx| self.dir(idx))
157 }
158}
159
160#[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 pub fn len(&self) -> u32 {
184 self.raw_list.n_images.get()
185 }
186
187 pub fn is_empty(&self) -> bool {
189 self.len() == 0
190 }
191
192 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 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 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: (), 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 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}