gtk_icon_cache/
lib.rs

1//!
2//! This crate provide a reader for gtk-icon-cache file.
3//!
4//! ```
5//! use gtk_icon_cache::*;
6//!
7//! let path = "test/caches/icon-theme.cache";
8//! let icon_cache = GtkIconCache::with_file_path(path).unwrap();
9//!
10//! // lookup for `firefox`
11//! let dirs = icon_cache.lookup("firefox").unwrap();
12//!
13//! // icon should be found in apps/64
14//! assert!(dirs.contains(&&"apps/64".to_string()));
15//! ```
16//!
17//! _See_:
18//! - [GTK icon-cache specific](https://github.com/GNOME/gtk/blob/master/docs/iconcache.txt)
19//! - [Qt icon loader](https://codereview.qt-project.org/#/c/125379/9/src/gui/image/qiconloader.cpp)
20//!
21
22extern crate memmap;
23#[macro_use]
24extern crate log;
25
26use memmap::Mmap;
27
28use std::io::{ErrorKind, Result, Error};
29use std::num::Wrapping;
30use std::fs::File;
31use std::path::Path;
32use std::collections::{HashMap, HashSet};
33use std::sync::Arc;
34
35///
36/// GtkIconCache
37///
38#[derive(Debug, Clone)]
39pub struct GtkIconCache {
40    hash_offset: usize,
41    directory_list_offset: usize,
42
43    n_buckets: usize,
44
45    dir_names: HashMap<usize, String>,
46    file_mmap: Arc<Mmap>,
47}
48
49impl GtkIconCache {
50    ///
51    /// Create with a cache file.
52    ///
53    /// * `path` - Cache file path.
54    ///
55    pub fn with_file_path<T: AsRef<Path>>(path: T) -> Result<Self> {
56        // read data
57        let f = File::open(&path.as_ref())?;
58        let _last_modified = f.metadata().and_then(|x| x.modified()).ok();
59        let mmap = unsafe { Mmap::map(&f)? };
60
61        let r = Self {
62            hash_offset: 0,
63            directory_list_offset: 0,
64
65            n_buckets: 0,
66
67            dir_names: HashMap::new(),
68            file_mmap: Arc::new(mmap),
69        };
70
71        match r.load_cache() {
72            Some(cache) => Ok(cache),
73            _ => Err(Error::new(ErrorKind::Other, "cache load failed.")),
74        }
75    }
76
77    fn load_cache(mut self) -> Option<Self> {
78
79        let major_version = self.read_card16_from(0)?;
80        let minor_version = self.read_card16_from(2)?;
81
82        self.hash_offset = self.read_card32_from(4)?;
83        self.directory_list_offset = self.read_card32_from(8)?;
84        self.n_buckets = self.read_card32_from(self.hash_offset)?;
85
86        if major_version != 1usize && minor_version != 0usize {
87            return None;
88        }
89
90        let n_directorys = self.read_card32_from(self.directory_list_offset)?;
91
92        // dump directories
93        for i in 0..n_directorys {
94            let offset = self.read_card32_from(self.directory_list_offset + 4 + 4 * i)?;
95            if let Some(dir) = self.read_cstring_from(offset as usize) {
96                self.dir_names.insert(offset, dir);
97            }
98        }
99
100        trace!("{:#?}", self);
101
102        Some(self)
103    }
104
105    fn read_card16_from(&self, offset: usize) -> Option<usize> {
106        let m = &self.file_mmap;
107
108        if offset < self.file_mmap.len() - 2 {
109            Some((m[offset    ] as usize) << 8 |
110                 (m[offset + 1] as usize))
111        } else {
112            None
113        }
114    }
115
116    fn read_card32_from(&self, offset: usize) -> Option<usize> {
117        let m = &self.file_mmap;
118
119        if offset > 0 && offset < self.file_mmap.len() - 4 {
120            Some((m[offset    ] as usize) << 24 |
121                 (m[offset + 1] as usize) << 16 |
122                 (m[offset + 2] as usize) <<  8 |
123                 (m[offset + 3] as usize))
124        } else {
125            None
126        }
127    }
128
129    fn read_cstring_from(&self, offset: usize) -> Option<String> {
130        let mut terminate = offset;
131
132        while self.file_mmap[terminate] != b'\0' { terminate += 1; }
133
134        if terminate == offset { return None; }
135
136        Some(String::from_utf8_lossy(&self.file_mmap[offset..terminate]).to_string())
137    }
138
139    ///
140    /// Look up an icon.
141    ///
142    /// * `name` - icon name.
143    ///
144    pub fn lookup<T: AsRef<str>>(&self, name: T) -> Option<Vec<&String>> {
145        let icon_hash = icon_name_hash(name.as_ref());
146        let bucket_index = icon_hash % self.n_buckets;
147
148        let mut bucket_offset = self.read_card32_from(self.hash_offset + 4 + bucket_index * 4)?;
149        while let Some(bucket_name_offset) = self.read_card32_from(bucket_offset + 4) {
150            // read bucket name
151            if let Some(cache) = self.read_cstring_from(bucket_name_offset) {
152                if cache == name.as_ref() {
153                    let list_offset = self.read_card32_from(bucket_offset + 8)?;
154                    let list_len = self.read_card32_from(list_offset)?;
155
156                    let mut r = HashSet::with_capacity(list_len);
157                    // read cached dirs
158                    for i in 0..list_len {
159                        if let Some(dir_index) = self.read_card16_from(list_offset + 4 + 8 * i) {
160                            if let Some(offset) = self.read_card32_from(self.directory_list_offset + 4 + dir_index * 4) {
161                                r.insert(offset);
162                            }
163                        }
164                    }
165
166                    let ref dir_names = self.dir_names;
167                    return Some(r.iter().map(|x| dir_names.get(&x).unwrap()).collect())
168                }
169            }
170
171            // find in next
172            bucket_offset = self.read_card32_from(bucket_offset)?;
173        }
174
175        // not found
176        None
177    }
178}
179
180fn icon_name_hash<T: AsRef<str>>(name: T) -> usize {
181
182    let name = name.as_ref().as_bytes();
183
184    name.iter()
185        .fold(Wrapping(0u32), |r, &c| (r << 5) - r + Wrapping(c as u32)).0
186        as usize
187}
188
189#[cfg(test)]
190mod test {
191
192    use GtkIconCache;
193    use icon_name_hash;
194
195    #[test]
196    fn test_icon_cache() {
197        let path = "test/caches/icon-theme.cache";
198        let icon_cache = GtkIconCache::with_file_path(path).unwrap();
199
200        let icon_name = "web-browser";
201        let icon_hash = icon_name_hash(icon_name);
202
203        assert_eq!(icon_hash, 2769241519);
204        assert_eq!(icon_cache.hash_offset, 12);
205
206        println!("=> {:?}", icon_cache.lookup(icon_name));
207    }
208
209    #[test]
210    fn test_cache_test1() {
211        let path = "test/caches/test1.cache";
212        let icon_cache = GtkIconCache::with_file_path(path).unwrap();
213
214        let dirs = icon_cache.lookup("test").unwrap();
215        assert!(dirs.contains(&&"apps/32".to_string()));
216        assert!(dirs.contains(&&"apps/48".to_string()));
217
218        let dirs = icon_cache.lookup("deepin-deb-installer").unwrap();
219        assert!(dirs.contains(&&"apps/16".to_string()));
220        assert!(dirs.contains(&&"apps/32".to_string()));
221        assert!(dirs.contains(&&"apps/48".to_string()));
222        assert!(dirs.contains(&&"apps/scalable".to_string()));
223    }
224
225    #[test]
226    fn test_icon_name_hash() {
227        assert_eq!(icon_name_hash("deepin-deb-installer"), 1927089920);
228    }
229}