@@ -309,6 +309,7 @@ fn justify(vm: &mut VM, recv: Val, pos: &[Val], right: bool) -> Result<(), VmErr
309309 match ( cs. next ( ) , cs. next ( ) ) { ( Some ( c) , None ) => c, _ => return Err ( cold_type ( "The fill character must be exactly one character long" ) ) }
310310 } else { ' ' } ;
311311 let pad = width. saturating_sub ( s. chars ( ) . count ( ) ) ;
312+ vm. charge_steps ( pad) ?; // Charge the actual O(pad) fill so a huge `width` hits the op budget, not a native spin.
312313 let fills: String = iter:: repeat_n ( fill, pad) . collect ( ) ;
313314 let out = if right { fills + & s } else { s + & fills } ;
314315 vm. alloc_and_push_str ( out)
@@ -323,7 +324,14 @@ pub fn expandtabs(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> {
323324 let mut col = 0usize ;
324325 for c in s. chars ( ) {
325326 match c {
326- '\t' => { let n = if ts == 0 { 0 } else { ts - ( col % ts) } ; for _ in 0 ..n { out. push ( ' ' ) ; } col += n; }
327+ // Guard each tab's expansion against the heap limit and op budget so a huge `ts` can't spin natively.
328+ '\t' => {
329+ let n = if ts == 0 { 0 } else { ts - ( col % ts) } ;
330+ if out. len ( ) . saturating_add ( n) > vm. heap . limit ( ) { return Err ( cold_heap ( ) ) ; }
331+ vm. charge_steps ( n) ?;
332+ for _ in 0 ..n { out. push ( ' ' ) ; }
333+ col += n;
334+ }
327335 '\n' | '\r' => { out. push ( c) ; col = 0 ; }
328336 _ => { out. push ( c) ; col += 1 ; }
329337 }
@@ -434,6 +442,8 @@ pub fn center(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> {
434442 let width = pos[ 0 ] . as_int ( ) . max ( 0 ) as usize ;
435443 // User-controlled width drives the output size; cap it so a huge value errors instead of aborting in the allocator.
436444 if width > vm. heap . limit ( ) { return Err ( cold_heap ( ) ) ; }
445+ // Charge the O(width) fill so a huge `width` hits the op budget, not a native spin.
446+ vm. charge_steps ( width) ?;
437447 let fill = if pos. len ( ) > 1 {
438448 let f = val_to_str ( vm, pos[ 1 ] ) ?;
439449 let mut cs = f. chars ( ) ;
@@ -456,6 +466,7 @@ pub fn zfill(vm: &mut VM, recv: Val, pos: &[Val]) -> Result<(), VmErr> {
456466 let width = pos[ 0 ] . as_int ( ) . max ( 0 ) as usize ;
457467 // User-controlled width drives the output size; cap it so a huge value errors instead of aborting in the allocator.
458468 if width > vm. heap . limit ( ) { return Err ( cold_heap ( ) ) ; }
469+ vm. charge_steps ( width) ?; // Charge the O(width) fill so a huge `width` hits the op budget, not a native spin.
459470 let nchars = s. chars ( ) . count ( ) ;
460471 let out = if nchars >= width {
461472 s
0 commit comments