@@ -878,4 +878,349 @@ mod test {
878878 // Double dash alone (end of options marker)
879879 assert_eq ! ( normalize_legacy_option( "--" . to_string( ) ) , "--" . to_string( ) ) ;
880880 }
881+
882+ // =========================================================================
883+ // FFI Integration Tests
884+ // =========================================================================
885+ //
886+ // These tests verify the FFI boundary - the extern "C" functions that are
887+ // called from C code. They test the actual C→Rust call path with realistic
888+ // struct states.
889+
890+ mod ffi_integration_tests {
891+ use super :: * ;
892+ use crate :: decoder:: test:: create_test_dtvcc_settings;
893+ use crate :: utils:: get_zero_allocated_obj;
894+
895+ /// Helper to create a lib_cc_decode struct configured for Rust-enabled path
896+ /// (dtvcc=NULL, dtvcc_rust=valid pointer)
897+ fn create_rust_enabled_decode_ctx ( ) -> ( Box < lib_cc_decode > , * mut std:: ffi:: c_void ) {
898+ let mut ctx = get_zero_allocated_obj :: < lib_cc_decode > ( ) ;
899+
900+ // Create the DtvccRust context via the FFI function
901+ let settings = create_test_dtvcc_settings ( ) ;
902+ let dtvcc_rust = unsafe { ccxr_dtvcc_init ( settings. as_ref ( ) ) } ;
903+
904+ // Set up the timing context (required for processing)
905+ let timing = get_zero_allocated_obj :: < ccx_common_timing_ctx > ( ) ;
906+ ctx. timing = Box :: into_raw ( timing) ;
907+
908+ // Simulate Rust-enabled mode: dtvcc is NULL, dtvcc_rust is set
909+ ctx. dtvcc = std:: ptr:: null_mut ( ) ;
910+ ctx. dtvcc_rust = dtvcc_rust;
911+
912+ ( ctx, dtvcc_rust)
913+ }
914+
915+ // -----------------------------------------------------------------
916+ // ccxr_dtvcc_init / ccxr_dtvcc_free lifecycle tests
917+ // -----------------------------------------------------------------
918+
919+ #[ test]
920+ fn test_ffi_dtvcc_init_creates_valid_context ( ) {
921+ let settings = create_test_dtvcc_settings ( ) ;
922+ let dtvcc_ptr = unsafe { ccxr_dtvcc_init ( settings. as_ref ( ) ) } ;
923+
924+ // Should return a valid (non-null) pointer
925+ assert ! ( !dtvcc_ptr. is_null( ) ) ;
926+
927+ // Verify we can check if it's active
928+ let is_active = unsafe { ccxr_dtvcc_is_active ( dtvcc_ptr) } ;
929+ assert_eq ! ( is_active, 1 ) ;
930+
931+ // Clean up
932+ ccxr_dtvcc_free ( dtvcc_ptr) ;
933+ }
934+
935+ #[ test]
936+ fn test_ffi_dtvcc_init_with_null_returns_null ( ) {
937+ let dtvcc_ptr = unsafe { ccxr_dtvcc_init ( std:: ptr:: null ( ) ) } ;
938+ assert ! ( dtvcc_ptr. is_null( ) ) ;
939+ }
940+
941+ #[ test]
942+ fn test_ffi_dtvcc_free_with_null_is_safe ( ) {
943+ // Should not crash when called with null
944+ ccxr_dtvcc_free ( std:: ptr:: null_mut ( ) ) ;
945+ }
946+
947+ #[ test]
948+ fn test_ffi_dtvcc_is_active_with_null_returns_zero ( ) {
949+ let result = unsafe { ccxr_dtvcc_is_active ( std:: ptr:: null_mut ( ) ) } ;
950+ assert_eq ! ( result, 0 ) ;
951+ }
952+
953+ // -----------------------------------------------------------------
954+ // ccxr_dtvcc_set_encoder tests
955+ // -----------------------------------------------------------------
956+
957+ #[ test]
958+ fn test_ffi_set_encoder_with_valid_context ( ) {
959+ let settings = create_test_dtvcc_settings ( ) ;
960+ let dtvcc_ptr = unsafe { ccxr_dtvcc_init ( settings. as_ref ( ) ) } ;
961+
962+ // Create an encoder
963+ let encoder = Box :: new ( encoder_ctx:: default ( ) ) ;
964+ let encoder_ptr = Box :: into_raw ( encoder) ;
965+
966+ // Set the encoder
967+ unsafe { ccxr_dtvcc_set_encoder ( dtvcc_ptr, encoder_ptr) } ;
968+
969+ // Verify by checking the internal state
970+ let dtvcc = unsafe { & * ( dtvcc_ptr as * mut DtvccRust ) } ;
971+ assert_eq ! ( dtvcc. encoder, encoder_ptr) ;
972+
973+ // Clean up
974+ ccxr_dtvcc_free ( dtvcc_ptr) ;
975+ unsafe { drop ( Box :: from_raw ( encoder_ptr) ) } ;
976+ }
977+
978+ #[ test]
979+ fn test_ffi_set_encoder_with_null_context_is_safe ( ) {
980+ let encoder = Box :: new ( encoder_ctx:: default ( ) ) ;
981+ let encoder_ptr = Box :: into_raw ( encoder) ;
982+
983+ // Should not crash
984+ unsafe { ccxr_dtvcc_set_encoder ( std:: ptr:: null_mut ( ) , encoder_ptr) } ;
985+
986+ // Clean up
987+ unsafe { drop ( Box :: from_raw ( encoder_ptr) ) } ;
988+ }
989+
990+ // -----------------------------------------------------------------
991+ // ccxr_dtvcc_process_data tests
992+ // -----------------------------------------------------------------
993+
994+ #[ test]
995+ fn test_ffi_process_data_packet_start ( ) {
996+ let settings = create_test_dtvcc_settings ( ) ;
997+ let dtvcc_ptr = unsafe { ccxr_dtvcc_init ( settings. as_ref ( ) ) } ;
998+
999+ // Process a packet start (cc_type = 3)
1000+ unsafe { ccxr_dtvcc_process_data ( dtvcc_ptr, 1 , 3 , 0xC2 , 0x00 ) } ;
1001+
1002+ // Verify state changed
1003+ let dtvcc = unsafe { & * ( dtvcc_ptr as * mut DtvccRust ) } ;
1004+ assert ! ( dtvcc. is_header_parsed) ;
1005+ assert_eq ! ( dtvcc. packet_length, 2 ) ;
1006+
1007+ // Clean up
1008+ ccxr_dtvcc_free ( dtvcc_ptr) ;
1009+ }
1010+
1011+ #[ test]
1012+ fn test_ffi_process_data_with_null_is_safe ( ) {
1013+ // Should not crash
1014+ unsafe { ccxr_dtvcc_process_data ( std:: ptr:: null_mut ( ) , 1 , 3 , 0xC2 , 0x00 ) } ;
1015+ }
1016+
1017+ #[ test]
1018+ fn test_ffi_process_data_state_persists_across_calls ( ) {
1019+ // This is THE key test - verifying the fix for issue #1499
1020+ let settings = create_test_dtvcc_settings ( ) ;
1021+ let dtvcc_ptr = unsafe { ccxr_dtvcc_init ( settings. as_ref ( ) ) } ;
1022+
1023+ // First call: start a packet (packet length = 8 bytes)
1024+ unsafe { ccxr_dtvcc_process_data ( dtvcc_ptr, 1 , 3 , 0xC4 , 0x00 ) } ;
1025+ let dtvcc = unsafe { & * ( dtvcc_ptr as * mut DtvccRust ) } ;
1026+ assert ! ( dtvcc. is_header_parsed) ;
1027+ assert_eq ! ( dtvcc. packet_length, 2 ) ;
1028+
1029+ // Second call: add more data (cc_type = 2)
1030+ unsafe { ccxr_dtvcc_process_data ( dtvcc_ptr, 1 , 2 , 0x21 , 0x00 ) } ;
1031+ let dtvcc = unsafe { & * ( dtvcc_ptr as * mut DtvccRust ) } ;
1032+ assert_eq ! ( dtvcc. packet_length, 4 ) ;
1033+
1034+ // Third call: add more data
1035+ unsafe { ccxr_dtvcc_process_data ( dtvcc_ptr, 1 , 2 , 0x00 , 0x00 ) } ;
1036+ let dtvcc = unsafe { & * ( dtvcc_ptr as * mut DtvccRust ) } ;
1037+ assert_eq ! ( dtvcc. packet_length, 6 ) ;
1038+
1039+ // State persisted across all calls!
1040+ assert ! ( dtvcc. is_header_parsed) ;
1041+
1042+ // Clean up
1043+ ccxr_dtvcc_free ( dtvcc_ptr) ;
1044+ }
1045+
1046+ // -----------------------------------------------------------------
1047+ // ccxr_process_cc_data tests (the main FFI entry point from C)
1048+ // -----------------------------------------------------------------
1049+
1050+ #[ test]
1051+ fn test_ffi_ccxr_process_cc_data_with_null_ctx_returns_error ( ) {
1052+ let data: [ u8 ; 3 ] = [ 0x97 , 0x1F , 0x3C ] ;
1053+ let result = unsafe { ccxr_process_cc_data ( std:: ptr:: null_mut ( ) , data. as_ptr ( ) , 1 ) } ;
1054+ assert_eq ! ( result, -1 ) ;
1055+ }
1056+
1057+ #[ test]
1058+ fn test_ffi_ccxr_process_cc_data_with_null_data_returns_error ( ) {
1059+ let ( ctx, dtvcc_ptr) = create_rust_enabled_decode_ctx ( ) ;
1060+ let result =
1061+ unsafe { ccxr_process_cc_data ( Box :: into_raw ( ctx) , std:: ptr:: null ( ) , 1 ) } ;
1062+ assert_eq ! ( result, -1 ) ;
1063+
1064+ // Clean up
1065+ ccxr_dtvcc_free ( dtvcc_ptr) ;
1066+ }
1067+
1068+ #[ test]
1069+ fn test_ffi_ccxr_process_cc_data_with_zero_count_returns_error ( ) {
1070+ let ( ctx, dtvcc_ptr) = create_rust_enabled_decode_ctx ( ) ;
1071+ let data: [ u8 ; 3 ] = [ 0x97 , 0x1F , 0x3C ] ;
1072+ let result =
1073+ unsafe { ccxr_process_cc_data ( Box :: into_raw ( ctx) , data. as_ptr ( ) , 0 ) } ;
1074+ assert_eq ! ( result, -1 ) ;
1075+
1076+ // Clean up
1077+ ccxr_dtvcc_free ( dtvcc_ptr) ;
1078+ }
1079+
1080+ #[ test]
1081+ fn test_ffi_ccxr_process_cc_data_with_null_dtvcc_rust_returns_error ( ) {
1082+ let mut ctx = get_zero_allocated_obj :: < lib_cc_decode > ( ) ;
1083+
1084+ // Set up timing but leave dtvcc_rust as null
1085+ let timing = get_zero_allocated_obj :: < ccx_common_timing_ctx > ( ) ;
1086+ ctx. timing = Box :: into_raw ( timing) ;
1087+ ctx. dtvcc_rust = std:: ptr:: null_mut ( ) ;
1088+
1089+ let data: [ u8 ; 3 ] = [ 0x97 , 0x1F , 0x3C ] ;
1090+ let result =
1091+ unsafe { ccxr_process_cc_data ( Box :: into_raw ( ctx) , data. as_ptr ( ) , 1 ) } ;
1092+ assert_eq ! ( result, -1 ) ;
1093+ }
1094+
1095+ #[ test]
1096+ fn test_ffi_ccxr_process_cc_data_processes_708_data ( ) {
1097+ let ( ctx, dtvcc_ptr) = create_rust_enabled_decode_ctx ( ) ;
1098+
1099+ // Set an encoder so processing can complete
1100+ let encoder = get_zero_allocated_obj :: < encoder_ctx > ( ) ;
1101+ ccxr_dtvcc_set_encoder ( dtvcc_ptr, Box :: into_raw ( encoder) ) ;
1102+
1103+ // CEA-708 packet start (cc_type=3, cc_valid=1)
1104+ // Header byte breakdown: cc_valid is bit 2, cc_type is bits 0-1
1105+ // For cc_type=3 (bits 0-1 = 11) and cc_valid=1 (bit 2 = 1):
1106+ // 0xFF = 11111111 -> cc_valid=1, cc_type=3
1107+ let data: [ u8 ; 3 ] = [ 0xFF , 0xC2 , 0x00 ] ;
1108+ let ctx_ptr = Box :: into_raw ( ctx) ;
1109+ let result = ccxr_process_cc_data ( ctx_ptr, data. as_ptr ( ) , 1 ) ;
1110+
1111+ // Should return 0 (success) for valid 708 data
1112+ assert_eq ! ( result, 0 ) ;
1113+
1114+ // Verify the context was updated
1115+ let ctx = unsafe { & * ctx_ptr } ;
1116+ assert_eq ! ( ctx. cc_stats[ 3 ] , 1 ) ; // cc_type 3 was processed
1117+ assert_eq ! ( ctx. current_field, 3 ) ; // Field set to 3 for 708 data
1118+
1119+ // Clean up
1120+ ccxr_dtvcc_free ( dtvcc_ptr) ;
1121+ }
1122+
1123+ #[ test]
1124+ fn test_ffi_ccxr_process_cc_data_skips_invalid_pairs ( ) {
1125+ let ( ctx, dtvcc_ptr) = create_rust_enabled_decode_ctx ( ) ;
1126+
1127+ // Invalid pair (cc_valid = 0)
1128+ // Header byte: 0xF9 = 11111001 -> cc_valid=0, cc_type=1
1129+ let data: [ u8 ; 3 ] = [ 0xF9 , 0x00 , 0x00 ] ;
1130+ let ctx_ptr = Box :: into_raw ( ctx) ;
1131+ let result = unsafe { ccxr_process_cc_data ( ctx_ptr, data. as_ptr ( ) , 1 ) } ;
1132+
1133+ // Should return -1 (no valid data processed)
1134+ assert_eq ! ( result, -1 ) ;
1135+
1136+ // Clean up
1137+ ccxr_dtvcc_free ( dtvcc_ptr) ;
1138+ }
1139+
1140+ // -----------------------------------------------------------------
1141+ // ccxr_flush_active_decoders tests
1142+ // -----------------------------------------------------------------
1143+
1144+ #[ test]
1145+ fn test_ffi_flush_with_null_is_safe ( ) {
1146+ // Should not crash
1147+ unsafe { ccxr_flush_active_decoders ( std:: ptr:: null_mut ( ) ) } ;
1148+ }
1149+
1150+ #[ test]
1151+ fn test_ffi_flush_with_valid_context ( ) {
1152+ let settings = create_test_dtvcc_settings ( ) ;
1153+ let dtvcc_ptr = unsafe { ccxr_dtvcc_init ( settings. as_ref ( ) ) } ;
1154+
1155+ // Set an encoder (required for flushing)
1156+ let encoder = Box :: new ( encoder_ctx:: default ( ) ) ;
1157+ unsafe { ccxr_dtvcc_set_encoder ( dtvcc_ptr, Box :: into_raw ( encoder) ) } ;
1158+
1159+ // Should not crash
1160+ unsafe { ccxr_flush_active_decoders ( dtvcc_ptr) } ;
1161+
1162+ // Clean up
1163+ ccxr_dtvcc_free ( dtvcc_ptr) ;
1164+ }
1165+
1166+ // -----------------------------------------------------------------
1167+ // Full lifecycle integration test
1168+ // -----------------------------------------------------------------
1169+
1170+ #[ test]
1171+ fn test_ffi_complete_lifecycle ( ) {
1172+ // This test simulates the complete lifecycle as it would be used from C code:
1173+ // 1. Initialize context
1174+ // 2. Set encoder
1175+ // 3. Process multiple CC data packets
1176+ // 4. Flush
1177+ // 5. Free
1178+
1179+ // 1. Initialize
1180+ let settings = create_test_dtvcc_settings ( ) ;
1181+ let dtvcc_ptr = unsafe { ccxr_dtvcc_init ( settings. as_ref ( ) ) } ;
1182+ assert ! ( !dtvcc_ptr. is_null( ) ) ;
1183+
1184+ // Verify initial state
1185+ let dtvcc = unsafe { & * ( dtvcc_ptr as * mut DtvccRust ) } ;
1186+ assert ! ( dtvcc. is_active, "DtvccRust should be active after init" ) ;
1187+ assert_eq ! ( dtvcc. packet_length, 0 ) ;
1188+ assert ! ( !dtvcc. is_header_parsed) ;
1189+
1190+ // 2. Set encoder
1191+ let encoder = get_zero_allocated_obj :: < encoder_ctx > ( ) ;
1192+ let encoder_ptr = Box :: into_raw ( encoder) ;
1193+ ccxr_dtvcc_set_encoder ( dtvcc_ptr, encoder_ptr) ;
1194+
1195+ // 3. Process a packet start (cc_type=3) - this should set is_header_parsed
1196+ // Use 0xC4 for packet header: 0xC4 & 0x3F = 4, so max_len = 4*2 = 8 bytes
1197+ // This way the packet won't be completed until we've added enough data
1198+ ccxr_dtvcc_process_data ( dtvcc_ptr, 1 , 3 , 0xC4 , 0x00 ) ;
1199+
1200+ // Verify packet start was processed
1201+ let dtvcc = unsafe { & * ( dtvcc_ptr as * mut DtvccRust ) } ;
1202+ assert ! (
1203+ dtvcc. is_header_parsed,
1204+ "is_header_parsed should be true after packet start"
1205+ ) ;
1206+ assert_eq ! ( dtvcc. packet_length, 2 , "packet_length should be 2" ) ;
1207+
1208+ // Process packet data (cc_type=2) - packet is not complete yet (need 8 bytes)
1209+ ccxr_dtvcc_process_data ( dtvcc_ptr, 1 , 2 , 0x21 , 0x00 ) ;
1210+ let dtvcc = unsafe { & * ( dtvcc_ptr as * mut DtvccRust ) } ;
1211+ assert_eq ! ( dtvcc. packet_length, 4 , "packet_length should be 4" ) ;
1212+
1213+ // Add more data
1214+ ccxr_dtvcc_process_data ( dtvcc_ptr, 1 , 2 , 0x00 , 0x00 ) ;
1215+ let dtvcc = unsafe { & * ( dtvcc_ptr as * mut DtvccRust ) } ;
1216+ assert_eq ! ( dtvcc. packet_length, 6 , "packet_length should be 6" ) ;
1217+
1218+ // 4. Flush
1219+ ccxr_flush_active_decoders ( dtvcc_ptr) ;
1220+
1221+ // 5. Free
1222+ ccxr_dtvcc_free ( dtvcc_ptr) ;
1223+ unsafe { drop ( Box :: from_raw ( encoder_ptr) ) } ;
1224+ }
1225+ }
8811226}
0 commit comments