perf: drop ini-core crate for simpler parser using memchr and bstr

This commit is contained in:
Michael Aaron Murphy 2025-12-01 15:19:01 +01:00
parent 44edef9673
commit 8b045f90c7
No known key found for this signature in database
GPG key ID: B2732D4240C9212C
6 changed files with 413 additions and 94 deletions

View file

@ -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"

View file

@ -133,7 +133,7 @@ pub fn default_theme_gtk() -> Option<String> {
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<u64>,
theme: &str,
theme: &[u8],
) -> Option<PathBuf> {
THEMES
.get(theme)?

View file

@ -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,

View file

@ -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<BTreeMap<String, Vec<Theme>>> = LazyLock::new(get_all_themes);
pub static THEMES: LazyLock<BTreeMap<Vec<u8>, Vec<Theme>>> = LazyLock::new(get_all_themes);
#[inline]
pub fn read_ini_theme(path: &Path) -> std::io::Result<Mmap> {
@ -33,15 +34,14 @@ impl Theme {
force_svg: bool,
) -> Option<PathBuf> {
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<Item = &'a str> + '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<Item = &'a str> + '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<String, Vec<Theme>> {
let mut icon_themes = BTreeMap::<_, Vec<_>>::new();
pub(super) fn get_all_themes() -> BTreeMap<Vec<u8>, Vec<Theme>> {
let mut icon_themes = BTreeMap::<Vec<u8>, 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<String, Vec<Theme>> {
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<String, Vec<Theme>> {
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",

View file

@ -1,42 +1,11 @@
use crate::theme::Theme;
use crate::theme::directories::{Directory, DirectoryType};
fn icon_theme_section(file: &str) -> impl Iterator<Item = (&str, &str)> + '_ {
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<Item = DirectorySection<'_>> {
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<Item = Directory<'a>> + '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<Item = &'a str> {
pub fn inherits<'a>(&self, file: &'a [u8]) -> impl Iterator<Item = &'a [u8]> {
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<Item = DirectorySection<'_>> {
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<Item = (&[u8], &[u8])> + '_ {
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);

View file

@ -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]