@@ -303,7 +303,17 @@ impl NodeGraphExecutor {
303303 . map_err ( |e| e. to_string ( ) ) ?;
304304
305305 if let Some ( animation) = export_config. animation {
306- // Allocate an export ID and accumulator, then queue one execution per frame
306+ // Defense-in-depth: the dialog already validates these, but reject non-finite/non-positive values here
307+ // too so a corrupt message can't reach `Duration::from_secs_f64` (which panics on NaN/negative/huge).
308+ if !animation. fps . is_finite ( ) || animation. fps <= 0. || !animation. start_seconds . is_finite ( ) {
309+ return Err ( "Animation export rejected: fps and start time must be finite, with fps > 0" . to_string ( ) ) ;
310+ }
311+
312+ // Allocate an export ID and accumulator, then queue one execution per frame.
313+ // TODO: All encoded frames are held in `pending_animation_exports` until the last frame arrives, so peak
314+ // memory grows with `total_frames * encoded_frame_size`. For long/high-resolution animations, this could
315+ // exhaust memory. A bounded cap is enforced upstream in the export dialog (ANIMATION_EXPORT_MAX_FRAMES),
316+ // but a true fix would stream frames out incrementally instead of accumulating.
307317 let export_id = self . next_animation_export_id ;
308318 self . next_animation_export_id = self . next_animation_export_id . wrapping_add ( 1 ) ;
309319
@@ -325,7 +335,10 @@ impl NodeGraphExecutor {
325335
326336 for frame_index in 0 ..animation. total_frames {
327337 let frame_seconds = animation. frame_time_seconds ( frame_index) ;
328- let animation_time = Duration :: from_secs_f64 ( frame_seconds. max ( 0. ) ) ;
338+ // `Duration::from_secs_f64` panics on negative/NaN/huge values; clamp defensively (we've already
339+ // validated `fps`/`start_seconds` above, but a far-future `start_seconds` could still overflow).
340+ let safe_seconds = if frame_seconds. is_finite ( ) { frame_seconds. clamp ( 0. , 1e9 ) } else { 0. } ;
341+ let animation_time = Duration :: from_secs_f64 ( safe_seconds) ;
329342 let timing = TimingInformation { time : frame_seconds, animation_time } ;
330343
331344 let frame_render_config = RenderConfig { time : timing, ..base_render_config } ;
@@ -635,9 +648,11 @@ impl NodeGraphExecutor {
635648
636649 // All frames received: drain the accumulator and emit a single message
637650 let accumulator = self . pending_animation_exports . remove ( & animation_frame. export_id ) . expect ( "Accumulator was present" ) ;
638- let base_name = match ( accumulator. artboard_name , accumulator. artboard_count ) {
639- ( Some ( artboard_name) , count) if count > 1 => format ! ( "{} - {}" , accumulator. name, artboard_name) ,
640- _ => accumulator. name ,
651+ // Sanitize before the name reaches filesystem joins or zip entry names downstream.
652+ let safe_doc_name = crate :: messages:: frontend:: utility_types:: sanitize_filename_component ( & accumulator. name ) ;
653+ let base_name = match ( accumulator. artboard_name . as_deref ( ) , accumulator. artboard_count ) {
654+ ( Some ( artboard_name) , count) if count > 1 => format ! ( "{safe_doc_name} - {}" , crate :: messages:: frontend:: utility_types:: sanitize_filename_component( artboard_name) ) ,
655+ _ => safe_doc_name,
641656 } ;
642657 let frames: Vec < _ > = accumulator
643658 . frames
0 commit comments