Icon themes: parse index.theme once and refuse gzip icons (#11227)

This commit is contained in:
Dylan M. Taylor authored and GitHub committed 2026-11-10 17:57:02 -05:00
1 parent bbf3ad7fd0
commit 73eb06adce
1 file changed
+34 -12
+34 -12
View File
@@ -45,21 +45,28 @@ pub fn base_dirs(var: &dyn Fn(&str) -> Option<String>) -> Vec<PathBuf> {
dirs
}
/// One `[section]` of an `index.theme`, as (key, value) pairs.
fn section<'a>(index: &'a str, name: &str) -> Vec<(&'a str, &'a str)> {
let mut inside = false;
let mut pairs = vec![];
/// Every `[section]` of an `index.theme` as (key, value) pairs, in one pass; the first section
/// of a name wins. (Looking each directory's section up by rescanning the file multiplied its
/// size by the number of directories.)
fn sections(index: &str) -> std::collections::HashMap<&str, Vec<(&str, &str)>> {
let mut all = std::collections::HashMap::new();
let mut current: Option<Vec<(&str, &str)>> = None;
let mut name = "";
for line in index.lines().map(str::trim) {
if let Some(s) = line.strip_prefix('[').and_then(|l| l.strip_suffix(']')) {
if inside {
break;
if let Some(done) = current.take() {
all.entry(name).or_insert(done);
}
inside = s == name;
} else if inside && let Some((k, v)) = line.split_once('=') {
name = s;
current = Some(vec![]);
} else if let (Some(pairs), Some((k, v))) = (current.as_mut(), line.split_once('=')) {
pairs.push((k.trim(), v.trim()));
}
}
pairs
if let Some(done) = current {
all.entry(name).or_insert(done);
}
all
}
fn list(value: Option<&str>) -> impl Iterator<Item = &str> {
@@ -156,13 +163,14 @@ impl IconTheme {
// The first index wins; a theme can still be spread over several base directories.
let Some(index) = roots.iter().find_map(|r| read_small(&r.join("index.theme"))) else { continue };
let index = String::from_utf8_lossy(&index);
let head = section(&index, "Icon Theme");
let all = sections(&index);
let head = all.get("Icon Theme").map_or(&[][..], Vec::as_slice);
let get = |k: &str| head.iter().find(|(key, _)| *key == k).map(|(_, v)| *v);
let mut subdirs: Vec<((i64, i64), &str)> = list(get("Directories"))
.chain(list(get("ScaledDirectories")))
.filter(|d| !d.split('/').any(|part| part == ".." || part.is_empty()))
.take(MAX_DIRS)
.map(|d| (distance(&section(&index, d)), d))
.map(|d| (distance(all.get(d).map_or(&[][..], Vec::as_slice)), d))
.collect();
subdirs.sort_by_key(|(key, _)| *key);
// Not checked for existence: a theme lists hundreds, and the search stops at a hit.
@@ -193,7 +201,8 @@ impl IconTheme {
/// unreadable SVG or an absurd size.
pub fn rasterize_mask(svg: &[u8], px: u32) -> Option<Vec<u8>> {
use resvg::{tiny_skia, usvg};
if !(1..=256).contains(&px) {
// usvg inflates gzip (`.svgz`) input without a decoded-size limit, and icons are plain `.svg`.
if !(1..=256).contains(&px) || svg.starts_with(&[0x1f, 0x8b]) {
return None;
}
// Glyphs are paths: no `<image>` is loaded, so an icon can't make us read another file
@@ -287,6 +296,19 @@ mod tests {
let _ = std::fs::remove_dir_all(base);
}
#[test]
fn sections_are_read_once_and_the_first_of_a_name_wins() {
let all = sections("stray=1\n[A]\nSize=16\n[B]\nSize=22\n[A]\nSize=48\n");
assert_eq!(all["A"], vec![("Size", "16")]);
assert_eq!(all["B"], vec![("Size", "22")]);
assert!(!all.contains_key(""));
}
#[test]
fn a_gzip_icon_is_refused() {
assert_eq!(rasterize_mask(&[0x1f, 0x8b, 8, 0, 0, 0, 0, 0], 16), None);
}
#[test]
fn distance_follows_the_spec() {
assert_eq!(distance(&[("Size", "16"), ("Type", "Fixed")]), (0, 1));