Skip to content

Commit f5c545f

Browse files
committed
perf(fonts): the font scan reads only the tables it needs
1 parent 989f808 commit f5c545f

1 file changed

Lines changed: 113 additions & 46 deletions

File tree

‎src-tauri/src/system_fonts.rs‎

Lines changed: 113 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
//! A path leaves this module only through the listing, and the read serves only what the listing
2-
//! named. The tables are read here, not by a font crate: the parser crate is unmaintained
3-
//! (RUSTSEC-2026-0192) and three tables suffice.
4-
use std::io::Read;
1+
//! A path leaves this module only through the listing, and the read serves only what the listing named.
2+
//! The name and OS/2 tables are parsed here instead of with ttf-parser, which is unmaintained under RUSTSEC-2026-0192.
3+
use std::io::{Cursor, Read, Seek, SeekFrom};
54
use std::path::{Path, PathBuf};
65
use std::sync::Mutex;
76

@@ -12,7 +11,7 @@ use crate::transport::blocking;
1211
/// The font cache's own cap, so the listing only offers rows the cache will take.
1312
const MAX_FONT_BYTES: u64 = 4 * 1024 * 1024;
1413

15-
/// The scan reads every font file, so one session keeps its result.
14+
/// The scan opens every font file, so one session keeps its result.
1615
#[derive(Default)]
1716
pub struct SystemFonts {
1817
listing: Mutex<Option<Vec<SystemFont>>>,
@@ -84,7 +83,7 @@ fn read_listed(allowed: bool, path: &str) -> Result<Vec<u8>, String> {
8483
if bytes.len() as u64 > MAX_FONT_BYTES {
8584
return Err("too large".to_string());
8685
}
87-
match read_face(&bytes) {
86+
match read_face(&mut Cursor::new(&bytes)) {
8887
Some(face) if !face.restricted => Ok(bytes),
8988
Some(_) => Err("restricted".to_string()),
9089
None => Err("not a font".to_string()),
@@ -157,16 +156,16 @@ fn scan() -> Vec<SystemFont> {
157156
}
158157
let mut out = Vec::new();
159158
for path in files {
160-
let Ok(meta) = std::fs::metadata(&path) else {
159+
let Ok(mut file) = std::fs::File::open(&path) else {
160+
continue;
161+
};
162+
let Ok(meta) = file.metadata() else {
161163
continue;
162164
};
163165
if meta.len() > MAX_FONT_BYTES {
164166
continue;
165167
}
166-
let Ok(bytes) = std::fs::read(&path) else {
167-
continue;
168-
};
169-
let Some(face) = read_face(&bytes) else {
168+
let Some(face) = read_face(&mut file) else {
170169
continue;
171170
};
172171
out.push(SystemFont {
@@ -202,20 +201,44 @@ fn u32_at(b: &[u8], at: usize) -> Option<u32> {
202201
.map(|s| u32::from_be_bytes([s[0], s[1], s[2], s[3]]))
203202
}
204203

205-
fn table<'a>(bytes: &'a [u8], tag: &[u8; 4]) -> Option<&'a [u8]> {
206-
let count = u16_at(bytes, 4)? as usize;
207-
(0..count).find_map(|i| {
208-
let at = 12 + i * 16;
209-
if bytes.get(at..at + 4)? != tag {
204+
struct Directory(Vec<u8>);
205+
206+
impl Directory {
207+
fn read<R: Read + Seek>(src: &mut R) -> Option<Self> {
208+
let mut head = [0u8; 12];
209+
src.read_exact(&mut head).ok()?;
210+
let version = &head[..4];
211+
if version != [0, 1, 0, 0] && version != b"OTTO" && version != b"true" {
210212
return None;
211213
}
212-
let offset = u32_at(bytes, at + 8)? as usize;
213-
let length = u32_at(bytes, at + 12)? as usize;
214-
bytes.get(offset..offset.checked_add(length)?)
215-
})
214+
let entries = read_exact_vec(src, u16_at(&head, 4)? as u64 * 16)?;
215+
Some(Self(entries))
216+
}
217+
218+
fn entry(&self, tag: &[u8; 4]) -> Option<(u64, u64)> {
219+
self.0.chunks_exact(16).find_map(|e| {
220+
if &e[..4] != tag {
221+
return None;
222+
}
223+
Some((u32_at(e, 8)? as u64, u32_at(e, 12)? as u64))
224+
})
225+
}
226+
227+
fn table<R: Read + Seek>(&self, src: &mut R, tag: &[u8; 4]) -> Option<Vec<u8>> {
228+
let (offset, length) = self.entry(tag)?;
229+
src.seek(SeekFrom::Start(offset)).ok()?;
230+
read_exact_vec(src, length)
231+
}
232+
}
233+
234+
/// Fails unless the whole length arrives, so a length the file declares never sizes an allocation.
235+
fn read_exact_vec<R: Read>(src: &mut R, length: u64) -> Option<Vec<u8>> {
236+
let mut bytes = Vec::new();
237+
src.by_ref().take(length).read_to_end(&mut bytes).ok()?;
238+
(bytes.len() as u64 == length).then_some(bytes)
216239
}
217240

218-
/// Windows Unicode names first, Macintosh Roman as the fallback the oldest fonts carry.
241+
/// Unicode names win, Macintosh Roman is the fallback the oldest fonts carry.
219242
fn name_string(name: &[u8], id: u16) -> Option<String> {
220243
let count = u16_at(name, 2)? as usize;
221244
let strings = u16_at(name, 4)? as usize;
@@ -249,24 +272,22 @@ fn name_string(name: &[u8], id: u16) -> Option<String> {
249272
const FSTYPE_RESTRICTED: u16 = 0x0002;
250273
const FSTYPE_BITMAP_ONLY: u16 = 0x0200;
251274

252-
fn read_face(bytes: &[u8]) -> Option<Face> {
253-
let version = bytes.get(..4)?;
254-
if version != [0, 1, 0, 0] && version != b"OTTO" && version != b"true" {
255-
return None;
256-
}
257-
let name = table(bytes, b"name")?;
258-
let family = name_string(name, 16).or_else(|| name_string(name, 1))?;
259-
let style = name_string(name, 17)
260-
.or_else(|| name_string(name, 2))
275+
fn read_face<R: Read + Seek>(src: &mut R) -> Option<Face> {
276+
let dir = Directory::read(src)?;
277+
let name = dir.table(src, b"name")?;
278+
let family = name_string(&name, 16).or_else(|| name_string(&name, 1))?;
279+
let style = name_string(&name, 17)
280+
.or_else(|| name_string(&name, 2))
261281
.unwrap_or_else(|| "Regular".to_string());
262-
let fs_type = table(bytes, b"OS/2")
263-
.and_then(|t| u16_at(t, 8))
282+
let fs_type = dir
283+
.table(src, b"OS/2")
284+
.and_then(|t| u16_at(&t, 8))
264285
.unwrap_or(0);
265286
Some(Face {
266287
family,
267288
style,
268289
restricted: fs_type & (FSTYPE_RESTRICTED | FSTYPE_BITMAP_ONLY) != 0,
269-
variable: table(bytes, b"fvar").is_some(),
290+
variable: dir.entry(b"fvar").is_some(),
270291
})
271292
}
272293

@@ -306,7 +327,11 @@ mod tests {
306327
name.extend(&sty);
307328
let mut os2 = vec![0u8; 78];
308329
os2[8..10].copy_from_slice(&fs_type.to_be_bytes());
309-
let mut tables: Vec<(&[u8; 4], Vec<u8>)> = vec![(b"OS/2", os2), (b"name", name)];
330+
let mut tables: Vec<(&[u8; 4], Vec<u8>)> = vec![
331+
(b"glyf", vec![0u8; 64 * 1024]),
332+
(b"OS/2", os2),
333+
(b"name", name),
334+
];
310335
if variable {
311336
tables.push((b"fvar", vec![0u8; 16]));
312337
}
@@ -328,36 +353,78 @@ mod tests {
328353
out
329354
}
330355

356+
struct Counting<R>(R, usize);
357+
358+
impl<R: Read> Read for Counting<R> {
359+
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
360+
let n = self.0.read(buf)?;
361+
self.1 += n;
362+
Ok(n)
363+
}
364+
}
365+
366+
impl<R: Seek> Seek for Counting<R> {
367+
fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
368+
self.0.seek(pos)
369+
}
370+
}
371+
372+
fn parse(bytes: &[u8]) -> Option<Face> {
373+
read_face(&mut Cursor::new(bytes))
374+
}
375+
376+
#[test]
377+
fn reads_none_of_the_glyph_table() {
378+
let bytes = font("A", "B", 0, true);
379+
let mut src = Counting(Cursor::new(&bytes), 0);
380+
read_face(&mut src).unwrap();
381+
assert!(src.1 < 1024, "read {} of {} bytes", src.1, bytes.len());
382+
}
383+
384+
#[test]
385+
fn a_variable_font_needs_no_fvar_read() {
386+
let mut bytes = font("A", "B", 0, true);
387+
let fvar = 12 + 3 * 16;
388+
bytes[fvar + 8..fvar + 12].copy_from_slice(&u32::MAX.to_be_bytes());
389+
assert!(parse(&bytes).unwrap().variable);
390+
}
391+
331392
#[test]
332393
fn reads_names_and_flags_from_the_tables() {
333-
let face = read_face(&font("Noto Sans", "Bold Italic", 0, true)).unwrap();
394+
let face = parse(&font("Noto Sans", "Bold Italic", 0, true)).unwrap();
334395
assert_eq!(
335396
(face.family.as_str(), face.style.as_str()),
336397
("Noto Sans", "Bold Italic")
337398
);
338399
assert!(!face.restricted);
339400
assert!(face.variable);
340401
assert!(
341-
read_face(&font("A", "B", FSTYPE_RESTRICTED, false))
402+
parse(&font("A", "B", FSTYPE_RESTRICTED, false))
342403
.unwrap()
343404
.restricted
344405
);
345406
assert!(
346-
read_face(&font("A", "B", FSTYPE_BITMAP_ONLY, false))
407+
parse(&font("A", "B", FSTYPE_BITMAP_ONLY, false))
347408
.unwrap()
348409
.restricted
349410
);
350-
assert!(
351-
!read_face(&font("A", "B", 0x0008, false))
352-
.unwrap()
353-
.restricted
354-
);
355-
assert!(read_face(b"not a font at all").is_none());
356-
assert!(read_face(&[]).is_none());
411+
assert!(!parse(&font("A", "B", 0x0008, false)).unwrap().restricted);
412+
}
413+
414+
#[test]
415+
fn rejects_a_foreign_or_truncated_file() {
416+
assert!(parse(b"not a font at all").is_none());
417+
assert!(parse(&[]).is_none());
418+
let mut short_directory = font("A", "B", 0, false);
419+
short_directory.truncate(12 + 10);
420+
assert!(parse(&short_directory).is_none());
421+
let mut short_table = font("A", "B", 0, false);
422+
short_table.truncate(12 + 3 * 16 + 10);
423+
assert!(parse(&short_table).is_none());
357424
}
358425

359426
#[test]
360-
fn serves_the_listed_spelling_only() {
427+
fn serves_only_the_listed_spelling_of_an_unrestricted_font() {
361428
let fonts = [listed("C:/Windows/Fonts/arial.ttf", false)];
362429
assert!(allows(&fonts, "C:/Windows/Fonts/arial.ttf"));
363430
assert!(!allows(&fonts, "C:/Windows/Fonts/../Fonts/arial.ttf"));
@@ -370,7 +437,7 @@ mod tests {
370437
}
371438

372439
#[test]
373-
fn reads_only_when_allowed_and_within_the_cap() {
440+
fn reads_only_an_allowed_unrestricted_font_within_the_cap() {
374441
let dir = std::env::temp_dir().join(format!("zplab-font-{}", std::process::id()));
375442
std::fs::create_dir_all(&dir).unwrap();
376443
let file = dir.join("a.ttf");

0 commit comments

Comments
 (0)