@@ -296,4 +296,101 @@ mod tests {
296296 // expected_col_count=1 → None (need at least 2 to have a recoverable second col)
297297 assert ! ( try_carve_first_col_missing( & data, 0 , "t" , 1 ) . is_none( ) ) ;
298298 }
299+
300+ #[ test]
301+ fn test_carve_freeblock_fallback_first_col_missing ( ) {
302+ // Covers line 57: matches.push(m) for FirstColMissing in the fallback path.
303+ // Normal fails because header_len is huge (> data.len()), both ColumnsOnly and
304+ // FirstColMissing are tried. expected_col_count=2 so FirstColMissing can succeed.
305+ let data = vec ! [ 0xFF , 0xFF , 0xFF , 0xFF , 0x01 , 0x63 ] ;
306+ let matches = carve_freeblock ( & data, 0 , "t" , 2 ) ;
307+ assert ! ( matches. len( ) >= 1 ) ;
308+ let has_first_col_missing = matches. iter ( ) . any ( |m| m. mode == CarveMode :: FirstColMissing ) ;
309+ assert ! ( has_first_col_missing) ;
310+ }
311+
312+ #[ test]
313+ fn test_carve_normal_truncated_value_falls_back_to_null ( ) {
314+ // Covers line 102: None => values.push(SqlValue::Null) in try_carve_normal.
315+ // header_len=2, st=6 (8-byte int), but only 1 value byte follows → decode fails → Null.
316+ let data = vec ! [ 0x02 , 0x06 , 0x00 ] ;
317+ let matches = carve_freeblock ( & data, 0 , "t" , 1 ) ;
318+ assert ! ( !matches. is_empty( ) ) ;
319+ assert ! ( matches[ 0 ] . record. values. contains( & SqlValue :: Null ) ) ;
320+ }
321+
322+ #[ test]
323+ fn test_carve_normal_empty_serial_types_via_2byte_varint ( ) {
324+ // Covers line 87 (end of serial types loop) and line 91 (empty serial_types return None).
325+ // header_len=2 encoded as a 2-byte varint (0x80, 0x02). hl_consumed=2, header_end=2.
326+ // Since pos(2) == header_end(2), the serial types loop doesn't execute → empty → None.
327+ // Normal fails, then fallbacks are tried. We just verify it doesn't panic.
328+ let data = vec ! [ 0x80 , 0x02 , 0x00 , 0x00 ] ;
329+ let _matches = carve_freeblock ( & data, 0 , "t" , 1 ) ;
330+ // Normal returns None due to empty serial_types (lines 87, 91 covered).
331+ }
332+
333+ #[ test]
334+ fn test_carve_columns_only_empty_serial_types_via_zero_cols ( ) {
335+ // Covers line 140: ColumnsOnly with expected_col_count=0.
336+ // The while loop condition `serial_types.len() < 0` is always false → loop never enters
337+ // → serial_types is empty → return None (line 140).
338+ // Normal with expected_col_count=0: the serial types loop enters, pushes at least one,
339+ // then breaks (since 1 >= 0). So Normal succeeds. But we need Normal to fail first.
340+ // Use data where Normal's header_len is too large.
341+ // data = [0x10, 0x01, 0x2A] → header_len=16 > data.len()=3 → Normal fails.
342+ // ColumnsOnly: expected_col_count=0 → loop doesn't enter → empty → line 140 → None.
343+ // FirstColMissing: data.len() < 5 → None.
344+ let matches = carve_freeblock ( & [ 0x10 , 0x01 , 0x2A ] , 0 , "t" , 0 ) ;
345+ assert ! ( matches. is_empty( ) ) ;
346+ }
347+
348+ #[ test]
349+ fn test_carve_columns_only_decode_null_fallback ( ) {
350+ // Covers line 151: ColumnsOnly succeeds at reading serial types but decode fails.
351+ // Normal must fail first. Use data where header_len is large.
352+ // data = [0x10, 0x06]: Normal reads header_len=16 > data.len()=2 → None.
353+ // ColumnsOnly: st1 = read_varint(&data, 0) = (0x10=16, 1). expected_col_count=1 → done.
354+ // data_pos=1, decode_serial_type(16, data, 1): st=16 → blob len=(16-12)/2=2, need data[1..3],
355+ // but data.len()=2 → data[1..3] fails → None → push Null. Line 151 covered.
356+ // But data.len()=2, pos+3=3>2 so Normal loop never enters.
357+ // Actually need 3+ bytes for the while loop: data = [0x10, 0x06, 0x00].
358+ // Normal: header_len=16 > 3 → fails (header_end > data.len()).
359+ // Actually, Normal's while loop checks pos+3 <= data.len(). pos=0, 0+3=3<=3 → enters.
360+ // try_carve_normal: read_varint=16, header_end=16 > 3 → return None.
361+ // matches empty. ColumnsOnly: read_varint(0)=16, serial_types=[16], expected=1 → stop.
362+ // data_pos=1. decode(16, data, 1): blob len=2, data[1..3]=[0x06, 0x00] → Ok!
363+ // So it succeeds. That covers ColumnsOnly but not line 151.
364+ // Need decode to fail: st=6 (8-byte int), data has < 8 bytes after serial type area.
365+ // data = [0x10, 0x06, 0x00]: ColumnsOnly reads st=16, then at pos=1 reads st=6,
366+ // expected_col_count=2 → has 2 serial types. data_pos=2.
367+ // decode(16, data, 2): blob len=2, data[2..4]? data.len()=3, only 1 byte → None → Null (line 151!)
368+ let data = vec ! [ 0x10 , 0x06 , 0x00 ] ;
369+ let matches = carve_freeblock ( & data, 0 , "t" , 2 ) ;
370+ // Should find ColumnsOnly match with at least one Null
371+ let co = matches. iter ( ) . find ( |m| m. mode == CarveMode :: ColumnsOnly ) ;
372+ assert ! ( co. is_some( ) ) ;
373+ assert ! ( co. unwrap( ) . record. values. contains( & SqlValue :: Null ) ) ;
374+ }
375+
376+ #[ test]
377+ fn test_carve_first_col_missing_empty_serial_types ( ) {
378+ // Covers line 193: return None when serial_types is empty in FirstColMissing.
379+ // After 4 skip bytes, byte 4 is a truncated continuation varint → read_varint fails.
380+ let data = vec ! [ 0x00 , 0x00 , 0x00 , 0x00 , 0xFF ] ;
381+ let result = try_carve_first_col_missing ( & data, 0 , "t" , 2 ) ;
382+ assert ! ( result. is_none( ) ) ;
383+ }
384+
385+ #[ test]
386+ fn test_carve_first_col_missing_truncated_value ( ) {
387+ // Covers line 204: decode fails → push Null in FirstColMissing.
388+ // After 4 skip bytes: st=6 (8-byte int) but only 1 byte of value data.
389+ let data = vec ! [ 0x00 , 0x00 , 0x00 , 0x00 , 0x06 , 0x00 ] ;
390+ let result = try_carve_first_col_missing ( & data, 0 , "t" , 2 ) ;
391+ assert ! ( result. is_some( ) ) ;
392+ let m = result. unwrap ( ) ;
393+ assert_eq ! ( m. record. values[ 0 ] , SqlValue :: Null ) ; // destroyed first column
394+ assert_eq ! ( m. record. values[ 1 ] , SqlValue :: Null ) ; // truncated decode
395+ }
299396}
0 commit comments