diff --git a/Cargo.toml b/Cargo.toml index e14a113..0de8cb6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,12 +10,14 @@ readme = "README.md" keywords = ["icons", "gui", "freedesktop"] [dependencies] +bstr = "1.12.1" +btoi = "0.5.0" dirs = "6.0" -thiserror = "2.0" -xdg = "3.0" -tracing = "0.1.41" -ini_core = "0.2.0" +memchr = "2.7.6" memmap2 = "0.9" +thiserror = "2.0" +tracing = "0.1.41" +xdg = "3.0" [dev-dependencies] speculoos = "0.13.0" diff --git a/src/lib.rs b/src/lib.rs index 551cc0d..bcf21e5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -133,7 +133,7 @@ pub fn default_theme_gtk() -> Option { if gsettings.status.success() { let name = String::from_utf8(gsettings.stdout).ok()?; let name = name.trim().trim_matches('\''); - THEMES.get(name).and_then(|themes| { + THEMES.get(name.as_bytes()).and_then(|themes| { themes.first().and_then(|path| { let file = std::fs::File::open(&path.index) .and_then(|file| unsafe { Mmap::map(&file) }) @@ -323,8 +323,8 @@ impl<'a> LookupBuilder<'a> { // Then lookup in the given theme THEMES - .get(self.theme) - .or_else(|| THEMES.get("hicolor")) + .get(self.theme.as_bytes()) + .or_else(|| THEMES.get("hicolor".as_bytes())) .and_then(|icon_themes| { let icon = icon_themes .iter() @@ -337,13 +337,13 @@ impl<'a> LookupBuilder<'a> { }) }) // Search the cosmic icon theme - .or_else(|| self.search_inherited_theme(searched_themes, "Cosmic")) + .or_else(|| self.search_inherited_theme(searched_themes, "Cosmic".as_bytes())) // Search the hicolor icon theme if it was not previously searched - .or_else(|| self.search_inherited_theme(searched_themes, "hicolor")) + .or_else(|| self.search_inherited_theme(searched_themes, "hicolor".as_bytes())) // GNOME applications may rely on the gnome theme - .or_else(|| self.search_inherited_theme(searched_themes, "gnome")) + .or_else(|| self.search_inherited_theme(searched_themes, "gnome".as_bytes())) // Ubuntu applications may require Yaru - .or_else(|| self.search_inherited_theme(searched_themes, "Yaru")) + .or_else(|| self.search_inherited_theme(searched_themes, "Yaru".as_bytes())) .or_else(|| { for theme_base_dir in BASE_PATHS.iter() { let mut path = theme_base_dir.clone(); @@ -430,13 +430,9 @@ impl<'a> LookupBuilder<'a> { return None; }; - let Ok(file) = std::str::from_utf8(file.as_ref()) else { - return None; - }; - // Search all inherited themes that we haven't already searched return theme - .inherits(file) + .inherits(file.as_ref()) .into_iter() .find_map(|parent| self.search_inherited_theme(searched_themes, parent)); } @@ -448,7 +444,7 @@ impl<'a> LookupBuilder<'a> { fn search_inherited_theme( &self, searched_themes: &mut Vec, - theme: &str, + theme: &[u8], ) -> Option { THEMES .get(theme)? diff --git a/src/theme/directories.rs b/src/theme/directories.rs index 33135a1..bdd3744 100644 --- a/src/theme/directories.rs +++ b/src/theme/directories.rs @@ -72,9 +72,9 @@ impl Default for DirectoryType { } } -impl From<&str> for DirectoryType { - fn from(value: &str) -> Self { - match value.as_bytes()[0] { +impl From<&[u8]> for DirectoryType { + fn from(value: &[u8]) -> Self { + match value[0] { b'F' => DirectoryType::Fixed, b'S' => DirectoryType::Scalable, _ => DirectoryType::Threshold, diff --git a/src/theme/mod.rs b/src/theme/mod.rs index 7af9c80..d23d3ea 100644 --- a/src/theme/mod.rs +++ b/src/theme/mod.rs @@ -3,6 +3,7 @@ use memmap2::Mmap; pub(crate) use paths::BASE_PATHS; use std::collections::BTreeMap; use std::ops::ControlFlow; +use std::os::unix::ffi::OsStrExt; use std::path::{Path, PathBuf}; use std::sync::LazyLock; @@ -10,7 +11,7 @@ mod directories; mod parse; mod paths; -pub static THEMES: LazyLock>> = LazyLock::new(get_all_themes); +pub static THEMES: LazyLock, Vec>> = LazyLock::new(get_all_themes); #[inline] pub fn read_ini_theme(path: &Path) -> std::io::Result { @@ -33,15 +34,14 @@ impl Theme { force_svg: bool, ) -> Option { let file = read_ini_theme(&self.index).ok()?; - let file = std::str::from_utf8(file.as_ref()).ok()?; - self.try_get_icon_exact_size(file, name, size, scale, force_svg) - .or_else(|| self.try_get_icon_closest_size(file, name, size, scale, force_svg)) + self.try_get_icon_exact_size(file.as_ref(), name, size, scale, force_svg) + .or_else(|| self.try_get_icon_closest_size(file.as_ref(), name, size, scale, force_svg)) } #[inline] fn try_get_icon_exact_size( &self, - file: &str, + file: &[u8], name: &str, size: u16, scale: u16, @@ -53,7 +53,7 @@ impl Theme { #[inline] fn match_size<'a>( &'a self, - file: &'a str, + file: &'a [u8], size: u16, scale: u16, ) -> impl Iterator + 'a { @@ -65,7 +65,7 @@ impl Theme { #[inline] fn try_get_icon_closest_size( &self, - file: &str, + file: &[u8], name: &str, size: u16, scale: u16, @@ -104,7 +104,7 @@ impl Theme { fn closest_match_size<'a>( &'a self, - file: &'a str, + file: &'a [u8], size: u16, scale: u16, ) -> impl Iterator + 'a { @@ -155,8 +155,8 @@ fn try_build_ext(path: &mut PathBuf, name_buf: &mut String, name: &str, ext: &'s } // Iter through the base paths and get all theme directories -pub(super) fn get_all_themes() -> BTreeMap> { - let mut icon_themes = BTreeMap::<_, Vec<_>>::new(); +pub(super) fn get_all_themes() -> BTreeMap, Vec> { + let mut icon_themes = BTreeMap::, Vec<_>>::new(); let mut found_indices = BTreeMap::new(); let mut to_revisit = Vec::new(); @@ -176,8 +176,10 @@ pub(super) fn get_all_themes() -> BTreeMap> { if fallback_index.is_none() { found_indices.insert(name.clone(), theme.index.clone()); } - let name = name.to_string_lossy().to_string(); - icon_themes.entry(name).or_default().push(theme); + icon_themes + .entry(name.as_bytes().to_owned()) + .or_default() + .push(theme); } else if entry.path().is_dir() { to_revisit.push(entry); } @@ -188,9 +190,10 @@ pub(super) fn get_all_themes() -> BTreeMap> { let name = entry.file_name(); let fallback_index = found_indices.get(&name); if let Some(theme) = Theme::from_path(entry.path(), fallback_index) { - if let Some(name) = name.to_str() { - icon_themes.entry(name.to_owned()).or_default().push(theme); - } + icon_themes + .entry(name.as_bytes().to_owned()) + .or_default() + .push(theme); } } @@ -230,24 +233,22 @@ mod test { #[test] fn get_one_icon() { - let themes = THEMES.get("Adwaita").unwrap(); + let themes = THEMES.get(&b"Adwaita"[..]).unwrap(); println!( "{:?}", themes.iter().find_map(|t| { let file = super::read_ini_theme(&t.index).ok()?; - let file = std::str::from_utf8(file.as_ref()).ok()?; - t.try_get_icon_exact_size(file, "edit-delete-symbolic", 24, 1, false) + t.try_get_icon_exact_size(file.as_ref(), "edit-delete-symbolic", 24, 1, false) }) ); } #[test] fn should_get_png_first() { - let themes = THEMES.get("hicolor").unwrap(); + let themes = THEMES.get(&b"hicolor"[..]).unwrap(); let icon = themes.iter().find_map(|t| { let file = super::read_ini_theme(&t.index).ok()?; - let file = std::str::from_utf8(file.as_ref()).ok()?; - t.try_get_icon_exact_size(file, "blueman", 24, 1, true) + t.try_get_icon_exact_size(file.as_ref(), "blueman", 24, 1, true) }); assert_that!(icon).is_some().is_equal_to(PathBuf::from( "/usr/share/icons/hicolor/22x22/apps/blueman.png", @@ -256,11 +257,10 @@ mod test { #[test] fn should_get_svg_first() { - let themes = THEMES.get("hicolor").unwrap(); + let themes = THEMES.get(&b"hicolor"[..]).unwrap(); let icon = themes.iter().find_map(|t| { let file = super::read_ini_theme(&t.index).ok()?; - let file = std::str::from_utf8(file.as_ref()).ok()?; - t.try_get_icon_exact_size(file, "blueman", 24, 1, false) + t.try_get_icon_exact_size(file.as_ref(), "blueman", 24, 1, false) }); assert_that!(icon).is_some().is_equal_to(PathBuf::from( "/usr/share/icons/hicolor/22x22/apps/blueman.png", diff --git a/src/theme/parse.rs b/src/theme/parse.rs index 25787f4..73f3376 100644 --- a/src/theme/parse.rs +++ b/src/theme/parse.rs @@ -1,42 +1,11 @@ use crate::theme::Theme; use crate::theme::directories::{Directory, DirectoryType}; - -fn icon_theme_section(file: &str) -> impl Iterator + '_ { - ini_core::Parser::new(file) - .skip_while(|item| *item != ini_core::Item::Section("Icon Theme")) - .take_while(|item| match item { - ini_core::Item::Section(value) => *value == "Icon Theme", - _ => true, - }) - .filter_map(|item| { - if let ini_core::Item::Property(key, value) = item { - Some((key, value?)) - } else { - None - } - }) -} - -#[derive(Debug)] -enum DirectorySection<'a> { - Property(&'a str, &'a str), - EndSection, - Section(&'a str), -} - -fn sections(file: &str) -> impl Iterator> { - ini_core::Parser::new(file).filter_map(move |item| match item { - ini_core::Item::Property(key, Some(value)) => Some(DirectorySection::Property(key, value)), - ini_core::Item::Section(section) => Some(DirectorySection::Section(section)), - ini_core::Item::SectionEnd => Some(DirectorySection::EndSection), - _ => None, - }) -} +use bstr::{BStr, ByteSlice}; impl Theme { pub(super) fn get_all_directories<'a>( &'a self, - file: &'a str, + file: &'a [u8], ) -> impl Iterator> + 'a { let mut iterator = sections(file); @@ -59,19 +28,19 @@ impl Theme { } match key { - "Size" => size = str::parse(value).ok(), - "Scale" => scale = str::parse(value).ok(), + b"Size" => size = btoi::btoi(value).ok(), + b"Scale" => scale = btoi::btoi(value).ok(), // "Context" => context = Some(value), - "Type" => dtype = DirectoryType::from(value), - "MaxSize" => max_size = str::parse(value).ok(), - "MinSize" => min_size = str::parse(value).ok(), - "Threshold" => threshold = str::parse(value).ok(), + b"Type" => dtype = DirectoryType::from(value), + b"MaxSize" => max_size = btoi::btoi(value).ok(), + b"MinSize" => min_size = btoi::btoi(value).ok(), + b"Threshold" => threshold = btoi::btoi(value).ok(), _ => (), } } DirectorySection::Section(new_name) => { - name = new_name; + name = std::str::from_utf8(new_name).unwrap_or(""); size = None; max_size = None; min_size = None; @@ -105,28 +74,380 @@ impl Theme { }) } - pub fn inherits<'a>(&self, file: &'a str) -> impl Iterator { + pub fn inherits<'a>(&self, file: &'a [u8]) -> impl Iterator { icon_theme_section(file) - .find(|&(key, _)| key == "Inherits") + .find(|&(key, _)| key == b"Inherits") .into_iter() .flat_map(|(_, parents)| { - parents - .split(',') + BStr::new(parents) + .split(|&char| char == b',') // Filtering out 'hicolor' since we are going to fallback there anyway - .filter(|parent| parent != &"hicolor") + .filter(|parent| parent != &b"hicolor") }) } } +#[derive(Debug)] +enum DirectorySection<'a> { + Property(&'a [u8], &'a [u8]), + EndSection, + Section(&'a [u8]), +} + +fn sections(file: &[u8]) -> impl Iterator> { + let mut finished = false; + let mut table_found = false; + let mut section: &[u8] = b""; + let mut prev = 0; + let mut line_indices = memchr::memchr_iter(b'\n', file); + + std::iter::from_fn(move || { + if finished { + return None; + } + + if !section.is_empty() { + let new_section = section; + section = b""; + return Some(DirectorySection::Section(new_section)); + } + + loop { + let line_pos = match line_indices.next() { + Some(pos) => pos, + None => { + let value = if !finished { + Some(DirectorySection::EndSection) + } else { + None + }; + finished = true; + return value; + } + }; + + let line = BStr::new(&file[prev..line_pos]).trim_ascii(); + prev = line_pos + 1; + + if line.is_empty() { + continue; + } + + if line[0] == b'[' { + section = &line[1..line.len() - 1]; + if table_found { + return Some(DirectorySection::EndSection); + } else { + table_found = true; + return Some(DirectorySection::Section(section)); + } + } + + if let Some((key, value)) = memchr::memchr(b'=', line).map(|pos| unsafe { + // Position was already validated by memchr. + line.split_at_unchecked(pos) + }) { + return Some(DirectorySection::Property(key, &value[1..])); + } + } + }) +} + +fn icon_theme_section(file: &[u8]) -> impl Iterator + '_ { + let mut found_table = false; + let mut prev = 0; + let mut line_indices = memchr::memchr_iter(b'\n', file); + + std::iter::from_fn(move || { + loop { + let line_pos = line_indices.next()?; + let line = BStr::new(&file[prev..line_pos]).trim_ascii(); + prev = line_pos + 1; + + if line.is_empty() { + continue; + } + + if line[0] == b'[' { + if found_table { + return None; + } else { + let section = &line[1..line.len() - 1]; + found_table = section == b"Icon Theme"; + } + } + + if let Some((key, value)) = memchr::memchr(b'=', line).map(|pos| unsafe { + // Position was already validated by memchr. + line.split_at_unchecked(pos) + }) { + return Some((key, &value[1..])); + } + } + }) +} + #[cfg(test)] -#[cfg(feature = "local_tests")] + mod test { - use crate::THEMES; - use speculoos::prelude::*; + const ADWAITA_INDEX: &str = "[Icon Theme] +Name=Adwaita\u{0020} +Comment=The Only One +Example=folder +Inherits=hicolor + +# KDE Specific Stuff +DisplayDepth=32 + +# Directory list +Directories=16x16/actions,16x16/apps,16x16/categories,16x16/devices,16x16/emblems,16x16/emotes,16x16/legacy,16x16/mimetypes,16x16/places,16x16/status,16x16/ui,scalable/devices,scalable/mimetypes,scalable/places,scalable/status,scalable/actions,scalable/apps,scalable/categories,scalable/emblems,scalable/emotes,scalable/legacy,scalable/ui,symbolic-up-to-32/status,symbolic/actions,symbolic/apps,symbolic/categories,symbolic/devices,symbolic/emblems,symbolic/emotes,symbolic/mimetypes,symbolic/places,symbolic/status,symbolic/legacy,symbolic/ui, + +[16x16/actions] +Context=Actions +Size=16 +Type=Fixed + +[16x16/apps] +Context=Applications +Size=16 +Type=Fixed + +[16x16/categories] +Context=Categories +Size=16 +Type=Fixed + +[16x16/devices] +Context=Devices +Size=16 +Type=Fixed + +[16x16/emblems] +Context=Emblems +Size=16 +Type=Fixed + +[16x16/emotes] +Context=Emotes +Size=16 +Type=Fixed + +[16x16/legacy] +Context=Legacy +Size=16 +Type=Fixed + +[16x16/mimetypes] +Context=MimeTypes +Size=16 +Type=Fixed + +[16x16/places] +Context=Places +Size=16 +Type=Fixed + +[16x16/status] +Context=Status +Size=16 +Type=Fixed + +[16x16/ui] +Context=UI +Size=16 +Type=Fixed + +[scalable/devices] +Context=Devices +Size=128 +MinSize=8 +MaxSize=512 +Type=Scalable + +[scalable/mimetypes] +Context=MimeTypes +Size=128 +MinSize=8 +MaxSize=512 +Type=Scalable + +[scalable/places] +Context=Places +Size=128 +MinSize=8 +MaxSize=512 +Type=Scalable + +[scalable/status] +Context=Status +Size=128 +MinSize=8 +MaxSize=512 +Type=Scalable + +[scalable/actions] +Context=Actions +Size=128 +MinSize=8 +MaxSize=512 +Type=Scalable + +[scalable/apps] +Context=Applications +Size=128 +MinSize=8 +MaxSize=512 +Type=Scalable + +[scalable/categories] +Context=Categories +Size=128 +MinSize=8 +MaxSize=512 +Type=Scalable + +[scalable/emblems] +Context=Emblems +Size=128 +MinSize=8 +MaxSize=512 +Type=Scalable + +[scalable/emotes] +Context=Emotes +Size=128 +MinSize=8 +MaxSize=512 +Type=Scalable + +[scalable/legacy] +Context=Legacy +Size=128 +MinSize=8 +MaxSize=512 +Type=Scalable + +[scalable/ui] +Context=UI +Size=128 +MinSize=8 +MaxSize=512 +Type=Scalable + +[symbolic-up-to-32/status] +Context=Status +Size=16 +MinSize=16 +MaxSize=32 +Type=Scalable + +[symbolic/actions] +Context=Actions +Size=16 +MinSize=8 +MaxSize=512 +Type=Scalable + +[symbolic/apps] +Context=Applications +Size=16 +MinSize=8 +MaxSize=512 +Type=Scalable + +[symbolic/categories] +Context=Categories +Size=16 +MinSize=8 +MaxSize=512 +Type=Scalable + +[symbolic/devices] +Context=Devices +Size=16 +MinSize=8 +MaxSize=512 +Type=Scalable + +[symbolic/emblems] +Context=Emblems +Size=16 +MinSize=8 +MaxSize=512 +Type=Scalable + +[symbolic/emotes] +Context=Emotes +Size=16 +MinSize=8 +MaxSize=512 +Type=Scalable + +[symbolic/mimetypes] +Context=MimeTypes +Size=16 +MinSize=8 +MaxSize=512 +Type=Scalable + +[symbolic/places] +Context=Places +Size=16 +MinSize=8 +MaxSize=512 +Type=Scalable + +[symbolic/status] +Context=Status +Size=16 +MinSize=8 +MaxSize=512 +Type=Scalable + +[symbolic/legacy] +Context=Legacy +Size=16 +MinSize=8 +MaxSize=512 +Type=Scalable + +[symbolic/ui] +Context=UI +Size=16 +MinSize=8 +MaxSize=512 +Type=Scalable"; #[test] + fn icon_theme_section() { + let mut iterator = super::icon_theme_section(ADWAITA_INDEX.as_bytes()); + + let (key, value) = iterator.next().unwrap(); + assert_eq!(key, b"Name"); + assert_eq!(value, b"Adwaita"); + let (key, value) = iterator.next().unwrap(); + assert_eq!(key, b"Comment"); + assert_eq!(value, b"The Only One"); + let (key, value) = iterator.next().unwrap(); + assert_eq!(key, b"Example"); + assert_eq!(value, b"folder"); + let (key, value) = iterator.next().unwrap(); + assert_eq!(key, b"Inherits"); + assert_eq!(value, b"hicolor"); + let (key, value) = iterator.next().unwrap(); + assert_eq!(key, b"DisplayDepth"); + assert_eq!(value, b"32"); + let (key, value) = iterator.next().unwrap(); + assert_eq!(key, b"Directories"); + assert_eq!(value, b"16x16/actions,16x16/apps,16x16/categories,16x16/devices,16x16/emblems,16x16/emotes,16x16/legacy,16x16/mimetypes,16x16/places,16x16/status,16x16/ui,scalable/devices,scalable/mimetypes,scalable/places,scalable/status,scalable/actions,scalable/apps,scalable/categories,scalable/emblems,scalable/emotes,scalable/legacy,scalable/ui,symbolic-up-to-32/status,symbolic/actions,symbolic/apps,symbolic/categories,symbolic/devices,symbolic/emblems,symbolic/emotes,symbolic/mimetypes,symbolic/places,symbolic/status,symbolic/legacy,symbolic/ui,"); + assert_eq!(iterator.next(), None); + } + + #[test] + #[cfg(feature = "local_tests")] fn should_get_theme_parents() { - for theme in THEMES.get("Arc").unwrap() { + use speculoos::prelude::*; + for theme in crate::THEMES.get("Arc").unwrap() { let file = crate::theme::read_ini_theme(&theme.index).ok().unwrap(); let file = std::str::from_utf8(file.as_ref()).ok().unwrap(); let parents = theme.inherits(file); diff --git a/src/theme/paths.rs b/src/theme/paths.rs index c446c58..0c846a6 100644 --- a/src/theme/paths.rs +++ b/src/theme/paths.rs @@ -41,7 +41,7 @@ mod test { #[test] fn should_get_all_themes() { let themes = get_all_themes(); - assert_that!(themes.get("hicolor")).is_some(); + assert_that!(themes.get(&b"hicolor"[..])).is_some(); } #[test]