@@ -342,6 +342,183 @@ static void test_cobs_matches_python(void) {
342342 ASSERT (mock_tx [i ] != 0x00 , "python compat: no internal zeros" );
343343}
344344
345+ /* ---------- Regression tests ---------- */
346+
347+ /*
348+ * Bug: cobs_decode() stripped trailing 0x00 bytes. When a packet's CRC32
349+ * had 0x00 as the MSB, the decoded output was 1 byte short → CRC mismatch.
350+ * This caused ~1/256 packets to fail deterministically based on data content.
351+ * Root cause of ALL flash write failures before the fix.
352+ */
353+ static void test_cobs_trailing_zero_preserved (void ) {
354+ /* Build payloads where CRC32 MSB is 0x00 (trailing zero after LE encode).
355+ * The COBS roundtrip must preserve the trailing zero. */
356+ for (int trial = 0 ; trial < 1000 ; trial ++ ) {
357+ /* Construct a packet: [cmd] [data...] [crc32 LE] */
358+ uint8_t payload [64 ];
359+ uint32_t plen = 5 + (trial % 20 ); /* varying lengths */
360+ payload [0 ] = 0x82 ; /* RSP_DATA */
361+ for (uint32_t i = 1 ; i < plen ; i ++ )
362+ payload [i ] = (uint8_t )((trial * 7 + i * 13 ) & 0xFF );
363+
364+ /* Compute CRC and append */
365+ uint32_t c = crc32 (0 , payload , plen );
366+ payload [plen + 0 ] = (c >> 0 ) & 0xFF ;
367+ payload [plen + 1 ] = (c >> 8 ) & 0xFF ;
368+ payload [plen + 2 ] = (c >> 16 ) & 0xFF ;
369+ payload [plen + 3 ] = (c >> 24 ) & 0xFF ;
370+ uint32_t total = plen + 4 ;
371+
372+ /* COBS encode → decode roundtrip */
373+ uint8_t enc [256 ], dec [256 ];
374+ uint32_t enc_len = cobs_encode (payload , total , enc );
375+ uint32_t dec_len = cobs_decode (enc , enc_len , dec );
376+
377+ ASSERT (dec_len == total , "cobs trailing zero: length preserved" );
378+ ASSERT (memcmp (payload , dec , total ) == 0 ,
379+ "cobs trailing zero: data preserved" );
380+ }
381+ }
382+
383+ /*
384+ * Bug: CRC32 extraction used `cp[3] << 24` where cp[3] >= 128.
385+ * uint8_t promotes to int, and left-shifting a signed int into the sign
386+ * bit is undefined behavior. With ASAN this manifests as a runtime error.
387+ * Fix: cast to (uint32_t) before shifting.
388+ */
389+ static void test_crc32_high_byte_shift (void ) {
390+ /* Test CRC32 values where the MSB is >= 0x80 (signed bit set).
391+ * These exercise the (uint32_t) cast in CRC extraction. */
392+ uint8_t data_a [] = {0x03 , 0x00 , 0x00 }; /* CMD_WRITE + padding */
393+ uint32_t c = crc32 (0 , data_a , 3 );
394+
395+ /* Verify the CRC packs/unpacks correctly through all 4 bytes */
396+ uint8_t packed [4 ];
397+ packed [0 ] = (c >> 0 ) & 0xFF ;
398+ packed [1 ] = (c >> 8 ) & 0xFF ;
399+ packed [2 ] = (c >> 16 ) & 0xFF ;
400+ packed [3 ] = (c >> 24 ) & 0xFF ;
401+
402+ uint32_t unpacked = (uint32_t )packed [0 ]
403+ | ((uint32_t )packed [1 ] << 8 )
404+ | ((uint32_t )packed [2 ] << 16 )
405+ | ((uint32_t )packed [3 ] << 24 );
406+ ASSERT (unpacked == c , "crc32 high byte: pack/unpack roundtrip" );
407+
408+ /* Test with every possible MSB value */
409+ for (int msb = 0 ; msb < 256 ; msb ++ ) {
410+ packed [3 ] = (uint8_t )msb ;
411+ uint32_t val = (uint32_t )packed [0 ]
412+ | ((uint32_t )packed [1 ] << 8 )
413+ | ((uint32_t )packed [2 ] << 16 )
414+ | ((uint32_t )packed [3 ] << 24 );
415+ ASSERT ((val >> 24 ) == (uint32_t )msb , "crc32 high byte shift" );
416+ }
417+ }
418+
419+ /*
420+ * End-to-end: for ALL possible payload data (varying byte values that
421+ * produce different CRC patterns), verify proto_send → proto_recv roundtrip.
422+ * This catches the COBS trailing zero bug: ~1/256 CRC values have MSB=0x00.
423+ */
424+ static void test_cobs_roundtrip_all_crc_patterns (void ) {
425+ /* Send 256 different 8-byte payloads through proto_send/recv.
426+ * Each produces a different CRC32, covering all possible MSB values. */
427+ for (int i = 0 ; i < 256 ; i ++ ) {
428+ mock_reset ();
429+
430+ uint8_t data [8 ];
431+ for (int j = 0 ; j < 8 ; j ++ )
432+ data [j ] = (uint8_t )((i * 37 + j * 53 ) & 0xFF );
433+
434+ proto_send (RSP_DATA , data , 8 );
435+
436+ memcpy (mock_rx , mock_tx , mock_tx_len );
437+ mock_rx_len = mock_tx_len ;
438+ mock_rx_pos = 0 ;
439+
440+ uint8_t recv_buf [MAX_PAYLOAD + 16 ];
441+ uint32_t recv_len = 0 ;
442+ uint8_t cmd = proto_recv (recv_buf , & recv_len , 1000 );
443+
444+ ASSERT (cmd == RSP_DATA , "crc pattern roundtrip: cmd" );
445+ ASSERT (recv_len == 8 , "crc pattern roundtrip: len" );
446+ ASSERT (memcmp (recv_buf , data , 8 ) == 0 ,
447+ "crc pattern roundtrip: data" );
448+ }
449+ }
450+
451+ /*
452+ * page_is_ff helper: verify it correctly identifies all-0xFF pages
453+ * and rejects pages with even a single non-0xFF byte.
454+ */
455+ static int page_is_ff_test (const uint8_t * data , uint32_t len ) {
456+ const uint32_t * w = (const uint32_t * )data ;
457+ for (uint32_t i = 0 ; i < len / 4 ; i ++ )
458+ if (w [i ] != 0xFFFFFFFF ) return 0 ;
459+ return 1 ;
460+ }
461+
462+ static void test_page_is_ff (void ) {
463+ uint8_t page [256 ];
464+
465+ /* All 0xFF → true */
466+ memset (page , 0xFF , 256 );
467+ ASSERT (page_is_ff_test (page , 256 ) == 1 , "page_is_ff: all FF" );
468+
469+ /* All 0x00 → false */
470+ memset (page , 0x00 , 256 );
471+ ASSERT (page_is_ff_test (page , 256 ) == 0 , "page_is_ff: all 00" );
472+
473+ /* Single non-FF byte at each position */
474+ for (int pos = 0 ; pos < 256 ; pos ++ ) {
475+ memset (page , 0xFF , 256 );
476+ page [pos ] = 0xFE ;
477+ ASSERT (page_is_ff_test (page , 256 ) == 0 ,
478+ "page_is_ff: single non-FF byte" );
479+ }
480+
481+ /* Random data → false */
482+ for (int i = 0 ; i < 256 ; i ++ ) page [i ] = (uint8_t )i ;
483+ ASSERT (page_is_ff_test (page , 256 ) == 0 , "page_is_ff: random data" );
484+ }
485+
486+ /*
487+ * Sector bitmap: verify bit indexing matches the host Python implementation.
488+ * Bit N of bitmap = sector N. LSB-first within each byte.
489+ */
490+ static void test_sector_bitmap (void ) {
491+ uint8_t bitmap [32 ];
492+ memset (bitmap , 0 , 32 );
493+
494+ /* Set specific sectors */
495+ bitmap [0 ] |= (1 << 0 ); /* sector 0 */
496+ bitmap [0 ] |= (1 << 7 ); /* sector 7 */
497+ bitmap [1 ] |= (1 << 0 ); /* sector 8 */
498+ bitmap [31 ] |= (1 << 7 ); /* sector 255 */
499+
500+ /* Check sector_has_data equivalent */
501+ ASSERT ((bitmap [0 / 8 ] & (1 << (0 % 8 ))) != 0 , "bitmap: sector 0 set" );
502+ ASSERT ((bitmap [7 / 8 ] & (1 << (7 % 8 ))) != 0 , "bitmap: sector 7 set" );
503+ ASSERT ((bitmap [8 / 8 ] & (1 << (8 % 8 ))) != 0 , "bitmap: sector 8 set" );
504+ ASSERT ((bitmap [255 / 8 ] & (1 << (255 % 8 ))) != 0 , "bitmap: sector 255 set" );
505+
506+ /* Check unset sectors */
507+ ASSERT ((bitmap [1 / 8 ] & (1 << (1 % 8 ))) == 0 , "bitmap: sector 1 unset" );
508+ ASSERT ((bitmap [128 / 8 ] & (1 << (128 % 8 ))) == 0 , "bitmap: sector 128 unset" );
509+ ASSERT ((bitmap [254 / 8 ] & (1 << (254 % 8 ))) == 0 , "bitmap: sector 254 unset" );
510+
511+ /* All-ones bitmap */
512+ memset (bitmap , 0xFF , 32 );
513+ for (int s = 0 ; s < 256 ; s ++ )
514+ ASSERT ((bitmap [s / 8 ] & (1 << (s % 8 ))) != 0 , "bitmap: all set" );
515+
516+ /* All-zeros bitmap */
517+ memset (bitmap , 0 , 32 );
518+ for (int s = 0 ; s < 256 ; s ++ )
519+ ASSERT ((bitmap [s / 8 ] & (1 << (s % 8 ))) == 0 , "bitmap: all clear" );
520+ }
521+
345522/* ---------- main ---------- */
346523
347524int main (void ) {
@@ -375,6 +552,13 @@ int main(void) {
375552 printf ("Cross-compatibility:\n" );
376553 test_cobs_matches_python ();
377554
555+ printf ("Regression:\n" );
556+ test_cobs_trailing_zero_preserved ();
557+ test_crc32_high_byte_shift ();
558+ test_cobs_roundtrip_all_crc_patterns ();
559+ test_page_is_ff ();
560+ test_sector_bitmap ();
561+
378562 printf ("\n%d/%d tests passed\n" , tests_passed , tests_run );
379563 return tests_passed == tests_run ? 0 : 1 ;
380564}
0 commit comments