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 } ;
54use std:: path:: { Path , PathBuf } ;
65use 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.
1312const 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 ) ]
1716pub 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.
219242fn 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> {
249272const FSTYPE_RESTRICTED : u16 = 0x0002 ;
250273const 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