@@ -198,9 +198,8 @@ pub(crate) fn traverse_with_callbacks<'a, P: ColorPainter>(
198198 if let Some ( rect) = clipbox {
199199 state. painter . push_clip_box ( rect) ;
200200 }
201-
202- let result = traverse_with_callbacks (
203- & state. resolve_paint ( & base_glyph) ?,
201+ let result = traverse_unresolved_paint (
202+ & base_glyph,
204203 state,
205204 & mut cycle_guard,
206205 recurse_depth + 1 ,
@@ -236,12 +235,7 @@ pub(crate) fn traverse_with_callbacks<'a, P: ColorPainter>(
236235 . ok_or ( ReadError :: MalformedData ( "expected a transform paint" ) ) ?
237236 . 0 ,
238237 ) ;
239- let result = traverse_with_callbacks (
240- & state. resolve_paint ( next_paint) ?,
241- state,
242- decycler,
243- recurse_depth + 1 ,
244- ) ;
238+ let result = traverse_unresolved_paint ( next_paint, state, decycler, recurse_depth + 1 ) ;
245239 state. painter . pop_transform ( ) ;
246240 result
247241 }
@@ -251,27 +245,31 @@ pub(crate) fn traverse_with_callbacks<'a, P: ColorPainter>(
251245 backdrop_paint,
252246 } => {
253247 state. painter . push_layer ( CompositeMode :: SrcOver ) ;
254- let mut result = traverse_with_callbacks (
255- & state. resolve_paint ( backdrop_paint) ?,
256- state,
257- decycler,
258- recurse_depth + 1 ,
259- ) ;
260- result?;
248+ let mut result =
249+ traverse_unresolved_paint ( backdrop_paint, state, decycler, recurse_depth + 1 ) ;
250+ if result. is_err ( ) {
251+ state. painter . pop_layer_with_mode ( CompositeMode :: SrcOver ) ;
252+ return result;
253+ }
261254 state. painter . push_layer ( * mode) ;
262- result = traverse_with_callbacks (
263- & state. resolve_paint ( source_paint) ?,
264- state,
265- decycler,
266- recurse_depth + 1 ,
267- ) ;
255+ result = traverse_unresolved_paint ( source_paint, state, decycler, recurse_depth + 1 ) ;
268256 state. painter . pop_layer_with_mode ( * mode) ;
269257 state. painter . pop_layer_with_mode ( CompositeMode :: SrcOver ) ;
270258 result
271259 }
272260 }
273261}
274262
263+ fn traverse_unresolved_paint < ' a , P : ColorPainter > (
264+ paint : & Paint < ' a > ,
265+ state : & mut TraversalState < ' a , P > ,
266+ decycler : & mut PaintDecycler ,
267+ recurse_depth : usize ,
268+ ) -> Result < ( ) , PaintError > {
269+ let resolved_paint = state. resolve_paint ( paint) ?;
270+ traverse_with_callbacks ( & resolved_paint, state, decycler, recurse_depth)
271+ }
272+
275273pub ( crate ) fn traverse_v0_range (
276274 range : & Range < usize > ,
277275 instance : & ColrInstance ,
@@ -304,7 +302,10 @@ mod tests {
304302 MetadataProvider ,
305303 } ;
306304 use raw:: types:: GlyphId ;
307- use read_fonts:: { types:: BoundingBox , FontRef , TableProvider } ;
305+ use read_fonts:: {
306+ types:: { BoundingBox , GlyphId16 } ,
307+ FontRef , TableProvider ,
308+ } ;
308309
309310 #[ test]
310311 fn clipbox_test ( ) {
@@ -394,6 +395,117 @@ mod tests {
394395 }
395396 }
396397
398+ #[ derive( Default ) ]
399+ struct StackTrackingPainter {
400+ transform_pushes : usize ,
401+ transform_pops : usize ,
402+ clip_pushes : usize ,
403+ clip_pops : usize ,
404+ layer_pushes : usize ,
405+ layer_pops : usize ,
406+ }
407+
408+ impl ColorPainter for StackTrackingPainter {
409+ fn push_transform ( & mut self , _transform : Transform ) {
410+ self . transform_pushes += 1 ;
411+ }
412+
413+ fn pop_transform ( & mut self ) {
414+ self . transform_pops += 1 ;
415+ }
416+
417+ fn push_clip_glyph ( & mut self , _glyph_id : GlyphId ) {
418+ self . clip_pushes += 1 ;
419+ }
420+
421+ fn push_clip_box ( & mut self , _clip_box : BoundingBox < f32 > ) {
422+ self . clip_pushes += 1 ;
423+ }
424+
425+ fn pop_clip ( & mut self ) {
426+ self . clip_pops += 1 ;
427+ }
428+
429+ fn fill ( & mut self , _brush : Brush < ' _ > ) {
430+ // nop
431+ }
432+
433+ fn push_layer ( & mut self , _composite_mode : CompositeMode ) {
434+ self . layer_pushes += 1 ;
435+ }
436+
437+ fn pop_layer ( & mut self ) {
438+ self . layer_pops += 1 ;
439+ }
440+ }
441+
442+ #[ test]
443+ fn transform_error_unwinds_transform_stack ( ) {
444+ let colr_font = font_test_data:: COLRV0V1_VARIABLE ;
445+ let font = FontRef :: new ( colr_font) . unwrap ( ) ;
446+ let glyph_id = font. charmap ( ) . map ( CLIPBOX [ 0 ] ) . unwrap ( ) ;
447+ let mut painter = StackTrackingPainter :: default ( ) ;
448+ let instance = ColrInstance :: new ( font. colr ( ) . unwrap ( ) , & [ ] ) ;
449+ let inner_paint = instance. v1_base_glyph ( glyph_id) . unwrap ( ) . unwrap ( ) . 0 ;
450+ let mut state = TraversalState :: new ( instance, & mut painter) ;
451+ let mut decycler = PaintDecycler :: new ( ) ;
452+ state. nodes_left = 0 ;
453+ let paint = ResolvedPaint :: Transform {
454+ xx : 1.0 ,
455+ yx : 0.0 ,
456+ xy : 0.0 ,
457+ yy : 1.0 ,
458+ dx : 0.0 ,
459+ dy : 0.0 ,
460+ paint : inner_paint,
461+ } ;
462+ let result = traverse_with_callbacks ( & paint, & mut state, & mut decycler, 0 ) ;
463+ assert ! ( matches!( result, Err ( PaintError :: DepthLimitExceeded ) ) ) ;
464+ assert_eq ! ( painter. transform_pushes, painter. transform_pops) ;
465+ assert_ne ! ( painter. transform_pushes, 0 ) ;
466+ }
467+
468+ #[ test]
469+ fn composite_error_unwinds_layer_stack ( ) {
470+ let colr_font = font_test_data:: COLRV0V1_VARIABLE ;
471+ let font = FontRef :: new ( colr_font) . unwrap ( ) ;
472+ let glyph_id = font. charmap ( ) . map ( CLIPBOX [ 0 ] ) . unwrap ( ) ;
473+ let mut painter = StackTrackingPainter :: default ( ) ;
474+ let instance = ColrInstance :: new ( font. colr ( ) . unwrap ( ) , & [ ] ) ;
475+ let inner_paint = instance. v1_base_glyph ( glyph_id) . unwrap ( ) . unwrap ( ) . 0 ;
476+ let mut state = TraversalState :: new ( instance, & mut painter) ;
477+ let mut decycler = PaintDecycler :: new ( ) ;
478+ state. nodes_left = 0 ;
479+ let paint = ResolvedPaint :: Composite {
480+ source_paint : inner_paint. clone ( ) ,
481+ mode : CompositeMode :: SrcOver ,
482+ backdrop_paint : inner_paint,
483+ } ;
484+ let result = traverse_with_callbacks ( & paint, & mut state, & mut decycler, 0 ) ;
485+ assert ! ( matches!( result, Err ( PaintError :: DepthLimitExceeded ) ) ) ;
486+ assert_eq ! ( painter. layer_pushes, painter. layer_pops) ;
487+ assert_ne ! ( painter. layer_pushes, 0 ) ;
488+ }
489+
490+ #[ test]
491+ fn clipbox_error_unwinds_clip_stack ( ) {
492+ let colr_font = font_test_data:: COLRV0V1_VARIABLE ;
493+ let font = FontRef :: new ( colr_font) . unwrap ( ) ;
494+ let glyph_id = font. charmap ( ) . map ( CLIPBOX [ 0 ] ) . unwrap ( ) ;
495+ let mut painter = StackTrackingPainter :: default ( ) ;
496+ let instance = ColrInstance :: new ( font. colr ( ) . unwrap ( ) , & [ ] ) ;
497+ let mut state = TraversalState :: new ( instance, & mut painter) ;
498+ let mut decycler = PaintDecycler :: new ( ) ;
499+ state. nodes_left = 0 ;
500+ let paint = ResolvedPaint :: ColrGlyph {
501+ glyph_id : GlyphId16 :: new ( glyph_id. to_u32 ( ) as u16 ) ,
502+ } ;
503+ let result = traverse_with_callbacks ( & paint, & mut state, & mut decycler, 0 ) ;
504+ assert ! ( matches!( result, Err ( PaintError :: DepthLimitExceeded ) ) ) ;
505+ assert_eq ! ( painter. clip_pushes, painter. clip_pops) ;
506+ assert_ne ! ( painter. clip_pushes, 0 ) ;
507+ }
508+
397509 #[ test]
398510 fn no_panic_on_empty_colorline ( ) {
399511 // Minimized test case from <https://issues.oss-fuzz.com/issues/375768991>.
0 commit comments