Compare commits

...

11 Commits

Author SHA1 Message Date
Carlos Fernandez
1789dc6d4c test(rust): Add comprehensive FFI integration tests for CEA-708 decoder
Add 18 new tests covering the FFI boundary between C and Rust code:

Lifecycle tests:
- ccxr_dtvcc_init creates valid context
- ccxr_dtvcc_init with null returns null
- ccxr_dtvcc_free with null is safe (no crash)
- ccxr_dtvcc_is_active with null returns zero
- Complete lifecycle test (init -> set_encoder -> process -> flush -> free)

Encoder tests:
- ccxr_dtvcc_set_encoder with valid context
- ccxr_dtvcc_set_encoder with null context is safe

Process data tests:
- ccxr_dtvcc_process_data packet start sets state correctly
- ccxr_dtvcc_process_data with null is safe
- ccxr_dtvcc_process_data state persists across calls (key fix verification)

ccxr_process_cc_data FFI entry point tests:
- Returns error with null context
- Returns error with null data
- Returns error with zero count
- Returns error with null dtvcc_rust pointer
- Processes valid CEA-708 data correctly
- Skips invalid CC pairs

Flush tests:
- ccxr_flush_active_decoders with null is safe
- ccxr_flush_active_decoders with valid context

These tests specifically cover the FFI gap identified where:
- The Rust-enabled path (dtvcc=NULL, dtvcc_rust set) wasn't tested
- Null pointer edge cases at the C→Rust boundary weren't verified
- State persistence across FFI calls wasn't validated

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-31 20:59:16 +01:00
Carlos Fernandez
e45b2528b7 fix(rust): Check dtvcc_rust instead of dtvcc in ccxr_process_cc_data
When Rust CEA-708 decoder is enabled, dec_ctx.dtvcc is set to NULL
and dec_ctx.dtvcc_rust holds the actual DtvccRust context. The null
check was incorrectly checking dtvcc, causing the function to return
early and skip all CEA-708 data processing.

This fixes tests 21, 31, 32, 105, 137, 141-149 which were failing
with exit code 10 (EXIT_NO_CAPTIONS) because no captions were being
extracted from CEA-708 streams.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-31 20:51:55 +01:00
Carlos
a2a844da5c fix(rust): remove double-increment of cb_708 counter
The cb_708 counter was being incremented twice for each CEA-708 data block:
1. In do_cb_dtvcc_rust() in Rust (src/rust/src/lib.rs)
2. In do_cb() in C (src/lib_ccx/ccx_decoders_common.c)

Since FTS calculation uses cb_708 (fts = fts_now + fts_global + cb_708 * 1001 / 30),
the double-increment caused timestamps to advance ~2x as fast as expected,
resulting in incorrect milliseconds in start timestamps.

This fix removes the increment from the Rust code since the C code already
handles it in do_cb().

Fixes timestamp issues reported in PR #1782 tests where start times like
00:00:20,688 were incorrectly output as 00:00:20,737.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-31 20:51:55 +01:00
Carlos
85263161cc chore: Remove plan file from repo and add plans/ to .gitignore
- Move PLAN_PR1618_REIMPLEMENTATION.md to local plans/ folder
- Add plans/ to .gitignore to keep plans local

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-31 20:51:55 +01:00
Carlos
473b3b4c46 fix(rust): Use persistent DtvccRust context in ccxr_process_cc_data
The ccxr_process_cc_data function was still accessing dec_ctx.dtvcc
(which is NULL when Rust is enabled), causing a null pointer panic.

Changed to use dec_ctx.dtvcc_rust (the persistent DtvccRust context)
instead, which fixes the crash when processing CEA-708 data.

Added do_cb_dtvcc_rust() function that works with DtvccRust instead
of the old Dtvcc struct.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-31 20:51:55 +01:00
Carlos
a6596ad65b style(c): Fix clang-format issues in Phase 3 code
- Remove extra space before comment in ccx_decoders_common.c
- Fix comment indentation in mp4.c

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-31 20:51:55 +01:00
Carlos
2180ab5bfa feat(c): Use Rust CEA-708 decoder in C code (Phase 3)
- init_cc_decode(): Initialize dtvcc_rust via ccxr_dtvcc_init()
- dinit_cc_decode(): Free dtvcc_rust via ccxr_dtvcc_free()
- flush_cc_decode(): Flush via ccxr_flush_active_decoders()
- general_loop.c: Set encoder via ccxr_dtvcc_set_encoder() (3 locations)
- mp4.c: Use ccxr_dtvcc_set_encoder() and ccxr_dtvcc_process_data()
- Add ccxr_dtvcc_is_active() declaration to ccx_dtvcc.h
- Fix clippy warnings in tv_screen.rs (unused assignments)
- All changes guarded with #ifndef DISABLE_RUST
- Update implementation plan to mark Phase 3 complete

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-31 20:51:55 +01:00
Carlos
ad3b1decc6 feat(c): Add C header declarations for Rust CEA-708 FFI (Phase 2)
- Add void *dtvcc_rust field to lib_cc_decode struct
- Declare ccxr_dtvcc_init, ccxr_dtvcc_free, ccxr_dtvcc_process_data in ccx_dtvcc.h
- Declare ccxr_dtvcc_set_encoder in lib_ccx.h
- Declare ccxr_flush_active_decoders in ccx_decoders_common.h
- All declarations guarded with #ifndef DISABLE_RUST
- Update implementation plan to mark Phase 2 complete

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-31 20:51:55 +01:00
Carlos
693c3aaba7 fix(rust): Address PR review - use existing DTVCC_MAX_SERVICES constant
- Remove duplicate CCX_DTVCC_MAX_SERVICES constant from decoder/mod.rs
- Import existing DTVCC_MAX_SERVICES from lib_ccxr::common
- Fix clippy uninlined_format_args warnings in avc/core.rs and decoder/mod.rs

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-31 20:51:55 +01:00
Carlos
e53921ef2c style(rust): Apply cargo fmt formatting
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-31 20:51:55 +01:00
Carlos
0f01679ca3 feat(rust): Add persistent DtvccRust context for CEA-708 decoder (Phase 1)
This is Phase 1 of the fix for issue #1499. It adds the Rust-side
infrastructure for a persistent CEA-708 decoder context without
modifying any C code, ensuring backward compatibility.

Problem:
The current Rust CEA-708 decoder creates a new Dtvcc struct on every
call to ccxr_process_cc_data(), causing all state to be reset. This
breaks stateful caption processing.

Solution:
Add a new DtvccRust struct that:
- Owns its decoder state (rather than borrowing from C)
- Persists across processing calls
- Is managed via FFI functions callable from C

Changes:
- Add DtvccRust struct in decoder/mod.rs with owned decoders
- Add CCX_DTVCC_MAX_SERVICES constant (63)
- Add FFI functions in lib.rs:
  - ccxr_dtvcc_init(): Create persistent context
  - ccxr_dtvcc_free(): Free context and all owned memory
  - ccxr_dtvcc_set_encoder(): Set encoder (not available at init)
  - ccxr_dtvcc_process_data(): Process CC data
  - ccxr_flush_active_decoders(): Flush all active decoders
  - ccxr_dtvcc_is_active(): Check if context is active
- Add unit tests for DtvccRust
- Use heap allocation for large structs to avoid stack overflow

The existing Dtvcc struct and ccxr_process_cc_data() remain unchanged
for backward compatibility. Phase 2-3 will add C header declarations
and modify C code to use the new functions.

Fixes: #1499 (partial)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-31 20:51:55 +01:00
12 changed files with 1124 additions and 30 deletions

3
.gitignore vendored
View File

@@ -145,6 +145,9 @@ bazel*
#Intellij IDEs
.idea/
# Plans (local only)
plans/
# Rust build and MakeFiles (and CMake files)
src/rust/CMakeFiles/
src/rust/CMakeCache.txt

View File

@@ -224,7 +224,12 @@ int do_cb(struct lib_cc_decode *ctx, unsigned char *cc_block, struct cc_subtitle
void dinit_cc_decode(struct lib_cc_decode **ctx)
{
struct lib_cc_decode *lctx = *ctx;
#ifndef DISABLE_RUST
ccxr_dtvcc_free(lctx->dtvcc_rust);
lctx->dtvcc_rust = NULL;
#else
dtvcc_free(&lctx->dtvcc);
#endif
dinit_avc(&lctx->avc_ctx);
ccx_decoder_608_dinit_library(&lctx->context_cc608_field_1);
ccx_decoder_608_dinit_library(&lctx->context_cc608_field_2);
@@ -294,10 +299,16 @@ struct lib_cc_decode *init_cc_decode(struct ccx_decoders_common_settings_t *sett
ctx->no_rollup = setting->no_rollup;
ctx->noscte20 = setting->noscte20;
#ifndef DISABLE_RUST
ctx->dtvcc_rust = ccxr_dtvcc_init(setting->settings_dtvcc);
ctx->dtvcc = NULL; // Not used when Rust is enabled
#else
ctx->dtvcc = dtvcc_init(setting->settings_dtvcc);
if (!ctx->dtvcc)
fatal(EXIT_NOT_ENOUGH_MEMORY, "In init_cc_decode: Out of memory initializing dtvcc.");
ctx->dtvcc->is_active = setting->settings_dtvcc->enabled;
ctx->dtvcc_rust = NULL;
#endif
if (setting->codec == CCX_CODEC_ATSC_CC)
{
@@ -477,6 +488,13 @@ void flush_cc_decode(struct lib_cc_decode *ctx, struct cc_subtitle *sub)
}
}
}
#ifndef DISABLE_RUST
if (ccxr_dtvcc_is_active(ctx->dtvcc_rust))
{
ctx->current_field = 3;
ccxr_flush_active_decoders(ctx->dtvcc_rust);
}
#else
if (ctx->dtvcc->is_active)
{
for (int i = 0; i < CCX_DTVCC_MAX_SERVICES; i++)
@@ -491,6 +509,7 @@ void flush_cc_decode(struct lib_cc_decode *ctx, struct cc_subtitle *sub)
}
}
}
#endif
}
struct encoder_ctx *copy_encoder_context(struct encoder_ctx *ctx)
{

View File

@@ -32,4 +32,10 @@ struct cc_subtitle *copy_subtitle(struct cc_subtitle *sub);
void free_encoder_context(struct encoder_ctx *ctx);
void free_decoder_context(struct lib_cc_decode *ctx);
void free_subtitle(struct cc_subtitle *sub);
#ifndef DISABLE_RUST
// Rust FFI function to flush active CEA-708 service decoders
extern void ccxr_flush_active_decoders(void *dtvcc_rust);
#endif
#endif

View File

@@ -208,6 +208,7 @@ struct lib_cc_decode
int false_pict_header;
dtvcc_ctx *dtvcc;
void *dtvcc_rust; // Persistent Rust CEA-708 decoder context
int current_field;
// Analyse/use the picture information
int maxtref; // Use to remember the temporal reference number

View File

@@ -10,4 +10,13 @@ void dtvcc_process_data(struct dtvcc_ctx *dtvcc,
dtvcc_ctx *dtvcc_init(ccx_decoder_dtvcc_settings *opts);
void dtvcc_free(dtvcc_ctx **);
#ifndef DISABLE_RUST
// Rust FFI functions for persistent CEA-708 decoder
extern void *ccxr_dtvcc_init(struct ccx_decoder_dtvcc_settings *settings_dtvcc);
extern void ccxr_dtvcc_free(void *dtvcc_rust);
extern void ccxr_dtvcc_process_data(void *dtvcc_rust, const unsigned char cc_valid,
const unsigned char cc_type, const unsigned char data1, const unsigned char data2);
extern int ccxr_dtvcc_is_active(void *dtvcc_rust);
#endif
#endif // CCEXTRACTOR_CCX_DTVCC_H

View File

@@ -1050,7 +1050,11 @@ int process_non_multiprogram_general_loop(struct lib_ccx_ctx *ctx,
cinfo = get_cinfo(ctx->demux_ctx, pid);
*enc_ctx = update_encoder_list_cinfo(ctx, cinfo);
*dec_ctx = update_decoder_list_cinfo(ctx, cinfo);
#ifndef DISABLE_RUST
ccxr_dtvcc_set_encoder((*dec_ctx)->dtvcc_rust, *enc_ctx);
#else
(*dec_ctx)->dtvcc->encoder = (void *)(*enc_ctx);
#endif
if ((*dec_ctx)->timing->min_pts == 0x01FFFFFFFFLL) // if we didn't set the min_pts of the program
{
@@ -1274,7 +1278,11 @@ int general_loop(struct lib_ccx_ctx *ctx)
enc_ctx = update_encoder_list_cinfo(ctx, cinfo);
dec_ctx = update_decoder_list_cinfo(ctx, cinfo);
#ifndef DISABLE_RUST
ccxr_dtvcc_set_encoder(dec_ctx->dtvcc_rust, enc_ctx);
#else
dec_ctx->dtvcc->encoder = (void *)enc_ctx; // WARN: otherwise cea-708 will not work
#endif
if (dec_ctx->timing->min_pts == 0x01FFFFFFFFLL) // if we didn't set the min_pts of the program
{
@@ -1484,7 +1492,11 @@ int rcwt_loop(struct lib_ccx_ctx *ctx)
}
dec_ctx = update_decoder_list(ctx);
#ifndef DISABLE_RUST
ccxr_dtvcc_set_encoder(dec_ctx->dtvcc_rust, enc_ctx);
#else
dec_ctx->dtvcc->encoder = (void *)enc_ctx; // WARN: otherwise cea-708 will not work
#endif
if (parsebuf[6] == 0 && parsebuf[7] == 2)
{
dec_ctx->codec = CCX_CODEC_TELETEXT;

View File

@@ -341,4 +341,9 @@ int process_non_multiprogram_general_loop(struct lib_ccx_ctx *ctx,
void segment_output_file(struct lib_ccx_ctx *ctx, struct lib_cc_decode *dec_ctx);
int decode_vbi(struct lib_cc_decode *dec_ctx, uint8_t field, unsigned char *buffer, size_t len, struct cc_subtitle *sub);
#ifndef DISABLE_RUST
// Rust FFI function to set encoder on persistent CEA-708 decoder
void ccxr_dtvcc_set_encoder(void *dtvcc_rust, struct encoder_ctx *encoder);
#endif
#endif

View File

@@ -749,7 +749,11 @@ static int process_clcp(struct lib_ccx_ctx *ctx, struct encoder_ctx *enc_ctx,
dbg_print(CCX_DMT_PARSE, "MP4-708: atom skipped (cc_type < 2)\n");
continue;
}
#ifndef DISABLE_RUST
ccxr_dtvcc_process_data(dec_ctx->dtvcc_rust, cc_valid, cc_type, temp[2], temp[3]);
#else
dtvcc_process_data(dec_ctx->dtvcc, (unsigned char *)temp);
#endif
cb_708++;
}
if (ctx->write_format == CCX_OF_MCC)
@@ -887,8 +891,12 @@ int processmp4(struct lib_ccx_ctx *ctx, struct ccx_s_mp4Cfg *cfg, char *file)
if (enc_ctx)
enc_ctx->timing = dec_ctx->timing;
// WARN: otherwise cea-708 will not work
// WARN: otherwise cea-708 will not work
#ifndef DISABLE_RUST
ccxr_dtvcc_set_encoder(dec_ctx->dtvcc_rust, enc_ctx);
#else
dec_ctx->dtvcc->encoder = (void *)enc_ctx;
#endif
memset(&dec_sub, 0, sizeof(dec_sub));
mprint("Opening \'%s\': ", file);

22
src/rust/Cargo.lock generated
View File

@@ -129,26 +129,6 @@ dependencies = [
"syn 2.0.111",
]
[[package]]
name = "bindgen"
version = "0.72.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895"
dependencies = [
"bitflags 2.10.0",
"cexpr",
"clang-sys",
"itertools",
"log",
"prettyplease",
"proc-macro2",
"quote",
"regex",
"rustc-hash 2.1.1",
"shlex",
"syn 2.0.111",
]
[[package]]
name = "bitflags"
version = "1.3.2"
@@ -171,7 +151,7 @@ checksum = "276a59bf2b2c967788139340c9f0c5b12d7fd6630315c15c217e559de85d2609"
name = "ccx_rust"
version = "0.1.0"
dependencies = [
"bindgen 0.72.1",
"bindgen 0.64.0",
"cfg-if",
"clap",
"encoding_rs",

View File

@@ -455,7 +455,7 @@ pub fn hex_dump(data: &[u8]) {
// Print hex bytes
for byte in chunk {
print!("{:02X} ", byte);
print!("{byte:02X} ");
}
// Pad if less than 16 bytes

View File

@@ -10,7 +10,10 @@ mod timing;
mod tv_screen;
mod window;
use log::debug as log_debug;
use lib_ccxr::{
common::DTVCC_MAX_SERVICES,
debug, fatal,
util::log::{DebugMessageFlag, ExitCause},
};
@@ -208,6 +211,359 @@ impl<'a> Dtvcc<'a> {
}
}
// =============================================================================
// DtvccRust: Persistent CEA-708 decoder context for Rust-owned state
// =============================================================================
//
// This struct is designed to be created once and persist throughout the program's
// lifetime, solving the issue where state was being reset on each call.
// See: https://github.com/CCExtractor/ccextractor/issues/1499
/// Persistent CEA-708 decoder context that owns its data.
///
/// Unlike `Dtvcc` which borrows from C structures, `DtvccRust` owns all its
/// decoder state and is designed to persist across multiple processing calls.
/// This is created once via `ccxr_dtvcc_init()` and freed via `ccxr_dtvcc_free()`.
pub struct DtvccRust {
pub is_active: bool,
pub active_services_count: u8,
pub services_active: Vec<i32>,
pub report_enabled: bool,
pub report: *mut ccx_decoder_dtvcc_report,
pub decoders: [Option<Box<dtvcc_service_decoder>>; DTVCC_MAX_SERVICES],
pub packet: Vec<u8>,
pub packet_length: u8,
pub is_header_parsed: bool,
pub last_sequence: i32,
pub encoder: *mut encoder_ctx,
pub no_rollup: bool,
pub timing: *mut ccx_common_timing_ctx,
}
impl DtvccRust {
/// Create a new persistent dtvcc context from settings.
///
/// This closely follows `dtvcc_init` at `src/lib_ccx/ccx_dtvcc.c:82`
///
/// # Safety
/// The following pointers in `opts` must not be null:
/// - `opts.report`
/// - `opts.timing`
pub fn new(opts: &ccx_decoder_dtvcc_settings) -> Self {
let is_active = is_true(opts.enabled);
let active_services_count = opts.active_services_count as u8;
let services_active = opts.services_enabled.to_vec();
let report_enabled = is_true(opts.print_file_reports);
// Reset the report counter
if !opts.report.is_null() {
unsafe {
(*opts.report).reset_count = 0;
}
}
// Initialize packet state (equivalent to dtvcc_clear_packet)
let packet_length = 0;
let is_header_parsed = false;
let packet = vec![0u8; CCX_DTVCC_MAX_PACKET_LENGTH as usize];
let last_sequence = CCX_DTVCC_NO_LAST_SEQUENCE;
let no_rollup = is_true(opts.no_rollup);
// Initialize decoders - only for active services
// Note: dtvcc_service_decoder is a large struct, so we must allocate it
// directly on the heap to avoid stack overflow.
let decoders = {
const INIT: Option<Box<dtvcc_service_decoder>> = None;
let mut decoders = [INIT; DTVCC_MAX_SERVICES];
for (i, d) in decoders.iter_mut().enumerate() {
if i >= opts.services_enabled.len() || !is_true(opts.services_enabled[i]) {
continue;
}
// Create owned tv_screen on the heap using zeroed allocation
// to avoid stack overflow (dtvcc_tv_screen is also large)
let tv_layout = std::alloc::Layout::new::<dtvcc_tv_screen>();
let tv_ptr = unsafe { std::alloc::alloc_zeroed(tv_layout) } as *mut dtvcc_tv_screen;
if tv_ptr.is_null() {
panic!("Failed to allocate dtvcc_tv_screen");
}
let mut tv_screen = unsafe { Box::from_raw(tv_ptr) };
tv_screen.cc_count = 0;
tv_screen.service_number = i as i32 + 1;
// Allocate decoder directly on heap using zeroed memory to avoid
// stack overflow (dtvcc_service_decoder is very large)
let decoder_layout = std::alloc::Layout::new::<dtvcc_service_decoder>();
let decoder_ptr = unsafe { std::alloc::alloc_zeroed(decoder_layout) }
as *mut dtvcc_service_decoder;
if decoder_ptr.is_null() {
panic!("Failed to allocate dtvcc_service_decoder");
}
let mut decoder = unsafe { Box::from_raw(decoder_ptr) };
// Set the tv pointer
decoder.tv = Box::into_raw(tv_screen);
// Initialize windows
for window in decoder.windows.iter_mut() {
window.memory_reserved = 0;
}
// Call reset handler
decoder.handle_reset();
*d = Some(decoder);
}
decoders
};
// Encoder is set later via set_encoder()
let encoder = std::ptr::null_mut();
DtvccRust {
is_active,
active_services_count,
services_active,
report_enabled,
report: opts.report,
decoders,
packet,
packet_length,
is_header_parsed,
last_sequence,
no_rollup,
timing: opts.timing,
encoder,
}
}
/// Set the encoder for this context.
///
/// The encoder is typically not available at initialization time,
/// so it must be set separately before processing.
pub fn set_encoder(&mut self, encoder: *mut encoder_ctx) {
self.encoder = encoder;
}
/// Process cc data and add it to the dtvcc packet.
///
/// This is the main entry point for CEA-708 data processing.
pub fn process_cc_data(&mut self, cc_valid: u8, cc_type: u8, data1: u8, data2: u8) {
if !self.is_active && !self.report_enabled {
return;
}
match cc_type {
// type 0 and 1 are for CEA 608 data and are handled before calling this function
// valid types for CEA 708 data are only 2 and 3
2 => {
log_debug!("dtvcc_process_data: DTVCC Channel Packet Data");
if cc_valid == 1 && self.is_header_parsed {
if self.packet_length > 253 {
log_debug!("dtvcc_process_data: Warning: Legal packet size exceeded (1), data not added.");
} else {
self.add_data_to_packet(data1, data2);
let mut max_len = self.packet[0] & 0x3F;
if max_len == 0 {
// This is well defined in EIA-708; no magic.
max_len = 128;
} else {
max_len *= 2;
}
// If packet is complete then process the packet
if self.packet_length >= max_len {
self.process_current_packet(max_len);
}
}
}
}
3 => {
log_debug!("dtvcc_process_data: DTVCC Channel Packet Start");
if cc_valid == 1 {
if self.packet_length > (CCX_DTVCC_MAX_PACKET_LENGTH - 1) {
log_debug!("dtvcc_process_data: Warning: Legal packet size exceeded (2), data not added.");
} else {
if self.is_header_parsed {
log_debug!("dtvcc_process_data: Warning: Incorrect packet length specified. Packet will be skipped.");
self.clear_packet();
}
self.add_data_to_packet(data1, data2);
self.is_header_parsed = true;
}
}
}
_ => fatal!(cause = ExitCause::Bug;
"dtvcc_process_data: shouldn't be here - cc_type: {}",
cc_type
),
}
}
/// Add data to the packet
fn add_data_to_packet(&mut self, data1: u8, data2: u8) {
self.packet[self.packet_length as usize] = data1;
self.packet_length += 1;
self.packet[self.packet_length as usize] = data2;
self.packet_length += 1;
}
/// Process current packet into service blocks
fn process_current_packet(&mut self, len: u8) {
let seq = (self.packet[0] & 0xC0) >> 6;
log_debug!("dtvcc_process_current_packet: Sequence: {seq}, packet length: {len}");
if self.packet_length == 0 {
return;
}
// Check if current sequence is correct
// Sequence number is a 2 bit rolling sequence from (0-3)
if self.last_sequence != CCX_DTVCC_NO_LAST_SEQUENCE
&& (self.last_sequence + 1) % 4 != seq as i32
{
log_debug!(
"dtvcc_process_current_packet: Unexpected sequence number, it is {} but should be {}",
seq, (self.last_sequence + 1) % 4
);
}
self.last_sequence = seq as i32;
let mut pos: u8 = 1;
while pos < len {
let mut service_number = (self.packet[pos as usize] & 0xE0) >> 5; // 3 more significant bits
let block_length = self.packet[pos as usize] & 0x1F; // 5 less significant bits
log_debug!("dtvcc_process_current_packet: Standard header Service number: {service_number}, Block length: {block_length}");
if service_number == 7 {
// There is an extended header
// CEA-708-E 6.2.2 Extended Service Block Header
pos += 1;
service_number = self.packet[pos as usize] & 0x3F; // 6 more significant bits
if service_number > 7 {
log_debug!("dtvcc_process_current_packet: Illegal service number in extended header: {service_number}");
}
}
pos += 1;
if service_number == 0 && block_length != 0 {
// Illegal, but specs say what to do...
pos = len; // Move to end
break;
}
if block_length != 0 && !self.report.is_null() {
unsafe {
(*self.report).services[service_number as usize] = 1;
}
}
if service_number > 0 && is_true(self.services_active[(service_number - 1) as usize]) {
if let Some(decoder) = &mut self.decoders[(service_number - 1) as usize] {
// Get encoder and timing references
if !self.encoder.is_null() && !self.timing.is_null() {
let encoder = unsafe { &mut *self.encoder };
let timing = unsafe { &mut *self.timing };
decoder.process_service_block(
&self.packet[pos as usize..(pos + block_length) as usize],
encoder,
timing,
self.no_rollup,
);
}
}
}
pos += block_length // Skip data
}
self.clear_packet();
if len < 128 && self.packet[pos as usize] != 0 {
// Null header is mandatory if there is room
log_debug!(
"dtvcc_process_current_packet: Warning: Null header expected but not found."
);
}
}
/// Clear current packet
fn clear_packet(&mut self) {
self.packet_length = 0;
self.is_header_parsed = false;
self.packet.iter_mut().for_each(|x| *x = 0);
}
/// Flush all active service decoders.
///
/// This writes out any pending caption data from all active services.
/// Called when processing is complete or when switching contexts.
pub fn flush_active_decoders(&mut self) {
if !self.is_active {
return;
}
for i in 0..DTVCC_MAX_SERVICES {
if i >= self.services_active.len() || !is_true(self.services_active[i]) {
continue;
}
if let Some(decoder) = &mut self.decoders[i] {
if decoder.cc_count > 0 {
// Flush this decoder
self.flush_decoder(i);
}
}
}
}
/// Flush a specific service decoder by index.
fn flush_decoder(&mut self, service_index: usize) {
log_debug!(
"dtvcc_decoder_flush: Flushing decoder for service {}",
service_index + 1
);
// Need encoder and timing to flush
if self.encoder.is_null() || self.timing.is_null() {
log_debug!("dtvcc_decoder_flush: Cannot flush - encoder or timing is null");
return;
}
if let Some(decoder) = &mut self.decoders[service_index] {
let timing = unsafe { &mut *self.timing };
let encoder = unsafe { &mut *self.encoder };
let mut screen_content_changed = false;
// Process all visible windows
for i in 0..CCX_DTVCC_MAX_WINDOWS {
let window = &mut decoder.windows[i as usize];
if is_true(window.visible) {
screen_content_changed = true;
window.update_time_hide(timing);
// Copy window content to screen
decoder.copy_to_screen(&decoder.windows[i as usize]);
decoder.windows[i as usize].visible = 0;
}
}
if screen_content_changed {
decoder.screen_print(encoder, timing);
}
decoder.flush(encoder);
}
}
}
const CCX_DTVCC_MAX_WINDOWS: u8 = 8;
/// A single character symbol
///
/// sym stores the symbol
@@ -361,4 +717,130 @@ pub mod test {
assert_eq!(decoder.report.services[8], 1);
assert_eq!(decoder.packet_length, 0); // due to `clear_packet()` fn call
}
// =========================================================================
// Tests for DtvccRust (persistent CEA-708 decoder)
// =========================================================================
/// Helper function to create a test ccx_decoder_dtvcc_settings
/// Uses heap allocation to avoid stack overflow with large structs
pub fn create_test_dtvcc_settings() -> Box<ccx_decoder_dtvcc_settings> {
let mut settings = get_zero_allocated_obj::<ccx_decoder_dtvcc_settings>();
// Initialize required pointers using heap allocation
let report = get_zero_allocated_obj::<ccx_decoder_dtvcc_report>();
settings.report = Box::into_raw(report);
let timing = get_zero_allocated_obj::<ccx_common_timing_ctx>();
settings.timing = Box::into_raw(timing);
// Enable the decoder and first service
settings.enabled = 1;
settings.active_services_count = 1;
settings.services_enabled[0] = 1;
settings
}
#[test]
fn test_dtvcc_rust_new() {
let settings = create_test_dtvcc_settings();
let dtvcc = DtvccRust::new(&settings);
// Verify basic initialization
assert!(dtvcc.is_active);
assert_eq!(dtvcc.active_services_count, 1);
assert_eq!(dtvcc.packet_length, 0);
assert!(!dtvcc.is_header_parsed);
assert_eq!(dtvcc.last_sequence, CCX_DTVCC_NO_LAST_SEQUENCE);
// Verify encoder is initially null (set later)
assert!(dtvcc.encoder.is_null());
// Verify first decoder is created (service 0 is enabled)
assert!(dtvcc.decoders[0].is_some());
// Verify other decoders are not created
assert!(dtvcc.decoders[1].is_none());
}
#[test]
fn test_dtvcc_rust_set_encoder() {
let settings = create_test_dtvcc_settings();
let mut dtvcc = DtvccRust::new(&settings);
// Initially null
assert!(dtvcc.encoder.is_null());
// Create an encoder and set it
let mut encoder = Box::new(encoder_ctx::default());
let encoder_ptr = &mut *encoder as *mut encoder_ctx;
dtvcc.set_encoder(encoder_ptr);
// Verify encoder is set
assert!(!dtvcc.encoder.is_null());
assert_eq!(dtvcc.encoder, encoder_ptr);
}
#[test]
fn test_dtvcc_rust_process_cc_data() {
let settings = create_test_dtvcc_settings();
let mut dtvcc = DtvccRust::new(&settings);
// Process cc_type = 3 (packet start) - should set is_header_parsed
dtvcc.process_cc_data(1, 3, 0xC2, 0x00);
assert!(dtvcc.is_header_parsed);
assert_eq!(dtvcc.packet_length, 2);
assert_eq!(dtvcc.packet[0], 0xC2);
assert_eq!(dtvcc.packet[1], 0x00);
}
#[test]
fn test_dtvcc_rust_clear_packet() {
let settings = create_test_dtvcc_settings();
let mut dtvcc = DtvccRust::new(&settings);
// Add some data
dtvcc.process_cc_data(1, 3, 0xC2, 0x00);
assert!(dtvcc.is_header_parsed);
assert_eq!(dtvcc.packet_length, 2);
// Process more data that triggers clear (when packet is malformed)
// Simulate by directly testing the packet processing
dtvcc.is_header_parsed = true;
dtvcc.packet[0] = 0x02; // Very short packet length (2*1 = 2 bytes)
dtvcc.packet_length = 2;
// This should process and clear the packet
dtvcc.process_cc_data(1, 2, 0x00, 0x00);
// After processing a complete packet, it should be cleared
assert_eq!(dtvcc.packet_length, 0);
assert!(!dtvcc.is_header_parsed);
}
#[test]
fn test_dtvcc_rust_state_persistence() {
// This test verifies the key fix: state persists across calls
let settings = create_test_dtvcc_settings();
let mut dtvcc = DtvccRust::new(&settings);
// First call: start a packet
dtvcc.process_cc_data(1, 3, 0xC4, 0x00); // Packet with length 4*2=8 bytes
assert!(dtvcc.is_header_parsed);
assert_eq!(dtvcc.packet_length, 2);
// Second call: add more data (this is where the old code would fail)
dtvcc.process_cc_data(1, 2, 0x21, 0x00);
assert_eq!(dtvcc.packet_length, 4);
// Third call: add more data
dtvcc.process_cc_data(1, 2, 0x00, 0x00);
assert_eq!(dtvcc.packet_length, 6);
// State is preserved across all calls!
assert!(dtvcc.is_header_parsed);
assert_eq!(dtvcc.last_sequence, CCX_DTVCC_NO_LAST_SEQUENCE); // Not processed yet
}
}

View File

@@ -38,7 +38,7 @@ use bindings::*;
use cfg_if::cfg_if;
use clap::{error::ErrorKind, Parser};
use common::{copy_from_rust, CType, CType2};
use decoder::Dtvcc;
use decoder::{Dtvcc, DtvccRust};
use lib_ccxr::{common::Options, teletext::TeletextConfig, util::log::ExitCause};
use parser::OptionsExt;
use utils::is_true;
@@ -210,11 +210,175 @@ pub extern "C" fn ccxr_init_logger() {
.init();
}
// =============================================================================
// FFI functions for persistent DtvccRust context
// =============================================================================
//
// These functions provide a C-compatible interface for managing the persistent
// Rust CEA-708 decoder context. They are designed to be called from C code
// and will be used in Phase 2-3 of the implementation.
// See: https://github.com/CCExtractor/ccextractor/issues/1499
/// Create a new persistent DtvccRust context.
///
/// This function allocates and initializes a new `DtvccRust` struct on the heap
/// and returns an opaque pointer to it. The context persists until freed with
/// `ccxr_dtvcc_free()`.
///
/// # Safety
/// - `opts_ptr` must be a valid pointer to `ccx_decoder_dtvcc_settings`
/// - `opts.report` and `opts.timing` must not be null
/// - The returned pointer must be freed with `ccxr_dtvcc_free()` when done
///
/// # Returns
/// An opaque pointer to the DtvccRust context, or null if opts_ptr is null.
#[no_mangle]
pub unsafe extern "C" fn ccxr_dtvcc_init(
opts_ptr: *const ccx_decoder_dtvcc_settings,
) -> *mut std::ffi::c_void {
if opts_ptr.is_null() {
return std::ptr::null_mut();
}
let opts = &*opts_ptr;
let dtvcc = Box::new(DtvccRust::new(opts));
Box::into_raw(dtvcc) as *mut std::ffi::c_void
}
/// Free a DtvccRust context.
///
/// This function properly frees all memory associated with the DtvccRust context,
/// including owned decoders and their tv_screens.
///
/// # Safety
/// - `dtvcc_ptr` must be a valid pointer returned by `ccxr_dtvcc_init()`
/// - `dtvcc_ptr` must not be used after this call
/// - It is safe to call with a null pointer (no-op)
#[no_mangle]
pub extern "C" fn ccxr_dtvcc_free(dtvcc_ptr: *mut std::ffi::c_void) {
if dtvcc_ptr.is_null() {
return;
}
let dtvcc = unsafe { Box::from_raw(dtvcc_ptr as *mut DtvccRust) };
// Free owned decoders and their tv_screens
for (i, decoder_opt) in dtvcc.decoders.iter().enumerate() {
if i >= dtvcc.services_active.len() || !is_true(dtvcc.services_active[i]) {
continue;
}
if let Some(decoder) = decoder_opt {
// Free windows rows if memory was reserved
for window in decoder.windows.iter() {
if is_true(window.memory_reserved) {
for row_ptr in window.rows.iter() {
if !row_ptr.is_null() {
unsafe {
drop(Box::from_raw(*row_ptr));
}
}
}
}
}
// Free the tv_screen
if !decoder.tv.is_null() {
unsafe {
drop(Box::from_raw(decoder.tv));
}
}
}
}
// The Box containing dtvcc will be dropped here, freeing the DtvccRust struct
drop(dtvcc);
}
/// Set the encoder for a DtvccRust context.
///
/// The encoder is typically not available at initialization time, so it must
/// be set separately before processing begins.
///
/// # Safety
/// - `dtvcc_ptr` must be a valid pointer returned by `ccxr_dtvcc_init()`
/// - `encoder` can be null (processing will skip service blocks if so)
#[no_mangle]
pub extern "C" fn ccxr_dtvcc_set_encoder(
dtvcc_ptr: *mut std::ffi::c_void,
encoder: *mut encoder_ctx,
) {
if dtvcc_ptr.is_null() {
return;
}
let dtvcc = unsafe { &mut *(dtvcc_ptr as *mut DtvccRust) };
dtvcc.set_encoder(encoder);
}
/// Process CEA-708 CC data using the persistent DtvccRust context.
///
/// This function processes a single CC data unit (cc_valid, cc_type, data1, data2)
/// using the persistent context, maintaining state across calls.
///
/// # Safety
/// - `dtvcc_ptr` must be a valid pointer returned by `ccxr_dtvcc_init()`
#[no_mangle]
pub extern "C" fn ccxr_dtvcc_process_data(
dtvcc_ptr: *mut std::ffi::c_void,
cc_valid: u8,
cc_type: u8,
data1: u8,
data2: u8,
) {
if dtvcc_ptr.is_null() {
return;
}
let dtvcc = unsafe { &mut *(dtvcc_ptr as *mut DtvccRust) };
dtvcc.process_cc_data(cc_valid, cc_type, data1, data2);
}
/// Flush all active service decoders in the DtvccRust context.
///
/// This writes out any pending caption data from all active services.
/// Should be called when processing is complete or when switching contexts.
///
/// # Safety
/// - `dtvcc_ptr` must be a valid pointer returned by `ccxr_dtvcc_init()`
/// - It is safe to call with a null pointer (no-op)
#[no_mangle]
pub extern "C" fn ccxr_flush_active_decoders(dtvcc_ptr: *mut std::ffi::c_void) {
if dtvcc_ptr.is_null() {
return;
}
let dtvcc = unsafe { &mut *(dtvcc_ptr as *mut DtvccRust) };
dtvcc.flush_active_decoders();
}
/// Check if the DtvccRust context is active.
///
/// # Safety
/// - `dtvcc_ptr` must be a valid pointer returned by `ccxr_dtvcc_init()`
///
/// # Returns
/// 1 if active, 0 if not active or if pointer is null.
#[no_mangle]
pub extern "C" fn ccxr_dtvcc_is_active(dtvcc_ptr: *mut std::ffi::c_void) -> i32 {
if dtvcc_ptr.is_null() {
return 0;
}
let dtvcc = unsafe { &*(dtvcc_ptr as *mut DtvccRust) };
if dtvcc.is_active {
1
} else {
0
}
}
/// Process cc_data
///
/// # Safety
/// dec_ctx should not be a null pointer
/// data should point to cc_data of length cc_count
/// dec_ctx.dtvcc_rust must point to a valid DtvccRust instance
#[no_mangle]
extern "C" fn ccxr_process_cc_data(
dec_ctx: *mut lib_cc_decode,
@@ -228,8 +392,9 @@ extern "C" fn ccxr_process_cc_data(
let dec_ctx = unsafe { &mut *dec_ctx };
// Check dtvcc pointer before dereferencing
if dec_ctx.dtvcc.is_null() {
// Check dtvcc_rust pointer before dereferencing (not dtvcc!)
// When Rust is enabled, dtvcc is NULL and dtvcc_rust holds the actual context
if dec_ctx.dtvcc_rust.is_null() {
return -1;
}
@@ -237,13 +402,19 @@ extern "C" fn ccxr_process_cc_data(
let mut cc_data: Vec<u8> = (0..cc_count * 3)
.map(|x| unsafe { *data.add(x as usize) })
.collect();
let dtvcc_ctx = unsafe { &mut *dec_ctx.dtvcc };
let mut dtvcc = Dtvcc::new(dtvcc_ctx);
// Use the persistent DtvccRust context from dtvcc_rust
let dtvcc_rust = dec_ctx.dtvcc_rust as *mut DtvccRust;
if dtvcc_rust.is_null() {
warn!("ccxr_process_cc_data: dtvcc_rust is null");
return ret;
}
let dtvcc = unsafe { &mut *dtvcc_rust };
for cc_block in cc_data.chunks_exact_mut(3) {
if !validate_cc_pair(cc_block) {
continue;
}
let success = do_cb_dtvcc(dec_ctx, &mut dtvcc, cc_block);
let success = do_cb_dtvcc_rust(dec_ctx, dtvcc, cc_block);
if success {
ret = 0;
}
@@ -310,7 +481,7 @@ pub fn verify_parity(data: u8) -> bool {
/// This isn't related to the "solid blank" character - it's just that the mask happens to have the same value.
const PARITY_BIT_MASK: u8 = 0x7F;
/// Process CC data according to its type
/// Process CC data according to its type (using Dtvcc)
pub fn do_cb_dtvcc(ctx: &mut lib_cc_decode, dtvcc: &mut Dtvcc, cc_block: &[u8]) -> bool {
let cc_valid = (cc_block[0] & 4) >> 2;
let cc_type = cc_block[0] & 3;
@@ -361,6 +532,59 @@ pub fn do_cb_dtvcc(ctx: &mut lib_cc_decode, dtvcc: &mut Dtvcc, cc_block: &[u8])
true
}
/// Process CC data according to its type (using DtvccRust - persistent context)
pub fn do_cb_dtvcc_rust(ctx: &mut lib_cc_decode, dtvcc: &mut DtvccRust, cc_block: &[u8]) -> bool {
let cc_valid = (cc_block[0] & 4) >> 2;
let cc_type = cc_block[0] & 3;
let mut timeok = true;
if ctx.write_format != ccx_output_format::CCX_OF_DVDRAW
&& ctx.write_format != ccx_output_format::CCX_OF_RAW
&& (cc_block[0] == 0xFA || cc_block[0] == 0xFC || cc_block[0] == 0xFD)
&& (cc_block[1] & PARITY_BIT_MASK) == 0
&& (cc_block[2] & PARITY_BIT_MASK) == 0
{
return true;
}
if cc_valid == 1 || cc_type == 3 {
ctx.cc_stats[cc_type as usize] += 1;
match cc_type {
// Type 0 and 1 are for CEA-608 data. Handled by C code, do nothing
0 | 1 => {}
// Type 2 and 3 are for CEA-708 data.
2 | 3 => {
let current_time = if ctx.timing.is_null() {
0
} else {
unsafe { (*ctx.timing).get_fts(ctx.current_field as u8) }
};
ctx.current_field = 3;
// Check whether current time is within start and end bounds
if is_true(ctx.extraction_start.set)
&& current_time < ctx.extraction_start.time_in_ms
{
timeok = false;
}
if is_true(ctx.extraction_end.set) && current_time > ctx.extraction_end.time_in_ms {
timeok = false;
ctx.processed_enough = 1;
}
if timeok && ctx.write_format != ccx_output_format::CCX_OF_RAW {
dtvcc.process_cc_data(cc_valid, cc_type, cc_block[1], cc_block[2]);
}
// Note: cb_708 is incremented by the C code in do_cb(), not here.
// Previously incrementing here caused a double-increment bug that
// resulted in incorrect start timestamps.
}
_ => warn!("Invalid cc_type"),
}
}
true
}
/// Close a Windows handle by wrapping it in a File and dropping it.
///
/// # Safety
@@ -654,4 +878,349 @@ mod test {
// Double dash alone (end of options marker)
assert_eq!(normalize_legacy_option("--".to_string()), "--".to_string());
}
// =========================================================================
// FFI Integration Tests
// =========================================================================
//
// These tests verify the FFI boundary - the extern "C" functions that are
// called from C code. They test the actual C→Rust call path with realistic
// struct states.
mod ffi_integration_tests {
use super::*;
use crate::decoder::test::create_test_dtvcc_settings;
use crate::utils::get_zero_allocated_obj;
/// Helper to create a lib_cc_decode struct configured for Rust-enabled path
/// (dtvcc=NULL, dtvcc_rust=valid pointer)
fn create_rust_enabled_decode_ctx() -> (Box<lib_cc_decode>, *mut std::ffi::c_void) {
let mut ctx = get_zero_allocated_obj::<lib_cc_decode>();
// Create the DtvccRust context via the FFI function
let settings = create_test_dtvcc_settings();
let dtvcc_rust = unsafe { ccxr_dtvcc_init(settings.as_ref()) };
// Set up the timing context (required for processing)
let timing = get_zero_allocated_obj::<ccx_common_timing_ctx>();
ctx.timing = Box::into_raw(timing);
// Simulate Rust-enabled mode: dtvcc is NULL, dtvcc_rust is set
ctx.dtvcc = std::ptr::null_mut();
ctx.dtvcc_rust = dtvcc_rust;
(ctx, dtvcc_rust)
}
// -----------------------------------------------------------------
// ccxr_dtvcc_init / ccxr_dtvcc_free lifecycle tests
// -----------------------------------------------------------------
#[test]
fn test_ffi_dtvcc_init_creates_valid_context() {
let settings = create_test_dtvcc_settings();
let dtvcc_ptr = unsafe { ccxr_dtvcc_init(settings.as_ref()) };
// Should return a valid (non-null) pointer
assert!(!dtvcc_ptr.is_null());
// Verify we can check if it's active
let is_active = unsafe { ccxr_dtvcc_is_active(dtvcc_ptr) };
assert_eq!(is_active, 1);
// Clean up
ccxr_dtvcc_free(dtvcc_ptr);
}
#[test]
fn test_ffi_dtvcc_init_with_null_returns_null() {
let dtvcc_ptr = unsafe { ccxr_dtvcc_init(std::ptr::null()) };
assert!(dtvcc_ptr.is_null());
}
#[test]
fn test_ffi_dtvcc_free_with_null_is_safe() {
// Should not crash when called with null
ccxr_dtvcc_free(std::ptr::null_mut());
}
#[test]
fn test_ffi_dtvcc_is_active_with_null_returns_zero() {
let result = unsafe { ccxr_dtvcc_is_active(std::ptr::null_mut()) };
assert_eq!(result, 0);
}
// -----------------------------------------------------------------
// ccxr_dtvcc_set_encoder tests
// -----------------------------------------------------------------
#[test]
fn test_ffi_set_encoder_with_valid_context() {
let settings = create_test_dtvcc_settings();
let dtvcc_ptr = unsafe { ccxr_dtvcc_init(settings.as_ref()) };
// Create an encoder
let encoder = Box::new(encoder_ctx::default());
let encoder_ptr = Box::into_raw(encoder);
// Set the encoder
unsafe { ccxr_dtvcc_set_encoder(dtvcc_ptr, encoder_ptr) };
// Verify by checking the internal state
let dtvcc = unsafe { &*(dtvcc_ptr as *mut DtvccRust) };
assert_eq!(dtvcc.encoder, encoder_ptr);
// Clean up
ccxr_dtvcc_free(dtvcc_ptr);
unsafe { drop(Box::from_raw(encoder_ptr)) };
}
#[test]
fn test_ffi_set_encoder_with_null_context_is_safe() {
let encoder = Box::new(encoder_ctx::default());
let encoder_ptr = Box::into_raw(encoder);
// Should not crash
unsafe { ccxr_dtvcc_set_encoder(std::ptr::null_mut(), encoder_ptr) };
// Clean up
unsafe { drop(Box::from_raw(encoder_ptr)) };
}
// -----------------------------------------------------------------
// ccxr_dtvcc_process_data tests
// -----------------------------------------------------------------
#[test]
fn test_ffi_process_data_packet_start() {
let settings = create_test_dtvcc_settings();
let dtvcc_ptr = unsafe { ccxr_dtvcc_init(settings.as_ref()) };
// Process a packet start (cc_type = 3)
unsafe { ccxr_dtvcc_process_data(dtvcc_ptr, 1, 3, 0xC2, 0x00) };
// Verify state changed
let dtvcc = unsafe { &*(dtvcc_ptr as *mut DtvccRust) };
assert!(dtvcc.is_header_parsed);
assert_eq!(dtvcc.packet_length, 2);
// Clean up
ccxr_dtvcc_free(dtvcc_ptr);
}
#[test]
fn test_ffi_process_data_with_null_is_safe() {
// Should not crash
unsafe { ccxr_dtvcc_process_data(std::ptr::null_mut(), 1, 3, 0xC2, 0x00) };
}
#[test]
fn test_ffi_process_data_state_persists_across_calls() {
// This is THE key test - verifying the fix for issue #1499
let settings = create_test_dtvcc_settings();
let dtvcc_ptr = unsafe { ccxr_dtvcc_init(settings.as_ref()) };
// First call: start a packet (packet length = 8 bytes)
unsafe { ccxr_dtvcc_process_data(dtvcc_ptr, 1, 3, 0xC4, 0x00) };
let dtvcc = unsafe { &*(dtvcc_ptr as *mut DtvccRust) };
assert!(dtvcc.is_header_parsed);
assert_eq!(dtvcc.packet_length, 2);
// Second call: add more data (cc_type = 2)
unsafe { ccxr_dtvcc_process_data(dtvcc_ptr, 1, 2, 0x21, 0x00) };
let dtvcc = unsafe { &*(dtvcc_ptr as *mut DtvccRust) };
assert_eq!(dtvcc.packet_length, 4);
// Third call: add more data
unsafe { ccxr_dtvcc_process_data(dtvcc_ptr, 1, 2, 0x00, 0x00) };
let dtvcc = unsafe { &*(dtvcc_ptr as *mut DtvccRust) };
assert_eq!(dtvcc.packet_length, 6);
// State persisted across all calls!
assert!(dtvcc.is_header_parsed);
// Clean up
ccxr_dtvcc_free(dtvcc_ptr);
}
// -----------------------------------------------------------------
// ccxr_process_cc_data tests (the main FFI entry point from C)
// -----------------------------------------------------------------
#[test]
fn test_ffi_ccxr_process_cc_data_with_null_ctx_returns_error() {
let data: [u8; 3] = [0x97, 0x1F, 0x3C];
let result = unsafe { ccxr_process_cc_data(std::ptr::null_mut(), data.as_ptr(), 1) };
assert_eq!(result, -1);
}
#[test]
fn test_ffi_ccxr_process_cc_data_with_null_data_returns_error() {
let (ctx, dtvcc_ptr) = create_rust_enabled_decode_ctx();
let result =
unsafe { ccxr_process_cc_data(Box::into_raw(ctx), std::ptr::null(), 1) };
assert_eq!(result, -1);
// Clean up
ccxr_dtvcc_free(dtvcc_ptr);
}
#[test]
fn test_ffi_ccxr_process_cc_data_with_zero_count_returns_error() {
let (ctx, dtvcc_ptr) = create_rust_enabled_decode_ctx();
let data: [u8; 3] = [0x97, 0x1F, 0x3C];
let result =
unsafe { ccxr_process_cc_data(Box::into_raw(ctx), data.as_ptr(), 0) };
assert_eq!(result, -1);
// Clean up
ccxr_dtvcc_free(dtvcc_ptr);
}
#[test]
fn test_ffi_ccxr_process_cc_data_with_null_dtvcc_rust_returns_error() {
let mut ctx = get_zero_allocated_obj::<lib_cc_decode>();
// Set up timing but leave dtvcc_rust as null
let timing = get_zero_allocated_obj::<ccx_common_timing_ctx>();
ctx.timing = Box::into_raw(timing);
ctx.dtvcc_rust = std::ptr::null_mut();
let data: [u8; 3] = [0x97, 0x1F, 0x3C];
let result =
unsafe { ccxr_process_cc_data(Box::into_raw(ctx), data.as_ptr(), 1) };
assert_eq!(result, -1);
}
#[test]
fn test_ffi_ccxr_process_cc_data_processes_708_data() {
let (ctx, dtvcc_ptr) = create_rust_enabled_decode_ctx();
// Set an encoder so processing can complete
let encoder = get_zero_allocated_obj::<encoder_ctx>();
ccxr_dtvcc_set_encoder(dtvcc_ptr, Box::into_raw(encoder));
// CEA-708 packet start (cc_type=3, cc_valid=1)
// Header byte breakdown: cc_valid is bit 2, cc_type is bits 0-1
// For cc_type=3 (bits 0-1 = 11) and cc_valid=1 (bit 2 = 1):
// 0xFF = 11111111 -> cc_valid=1, cc_type=3
let data: [u8; 3] = [0xFF, 0xC2, 0x00];
let ctx_ptr = Box::into_raw(ctx);
let result = ccxr_process_cc_data(ctx_ptr, data.as_ptr(), 1);
// Should return 0 (success) for valid 708 data
assert_eq!(result, 0);
// Verify the context was updated
let ctx = unsafe { &*ctx_ptr };
assert_eq!(ctx.cc_stats[3], 1); // cc_type 3 was processed
assert_eq!(ctx.current_field, 3); // Field set to 3 for 708 data
// Clean up
ccxr_dtvcc_free(dtvcc_ptr);
}
#[test]
fn test_ffi_ccxr_process_cc_data_skips_invalid_pairs() {
let (ctx, dtvcc_ptr) = create_rust_enabled_decode_ctx();
// Invalid pair (cc_valid = 0)
// Header byte: 0xF9 = 11111001 -> cc_valid=0, cc_type=1
let data: [u8; 3] = [0xF9, 0x00, 0x00];
let ctx_ptr = Box::into_raw(ctx);
let result = unsafe { ccxr_process_cc_data(ctx_ptr, data.as_ptr(), 1) };
// Should return -1 (no valid data processed)
assert_eq!(result, -1);
// Clean up
ccxr_dtvcc_free(dtvcc_ptr);
}
// -----------------------------------------------------------------
// ccxr_flush_active_decoders tests
// -----------------------------------------------------------------
#[test]
fn test_ffi_flush_with_null_is_safe() {
// Should not crash
unsafe { ccxr_flush_active_decoders(std::ptr::null_mut()) };
}
#[test]
fn test_ffi_flush_with_valid_context() {
let settings = create_test_dtvcc_settings();
let dtvcc_ptr = unsafe { ccxr_dtvcc_init(settings.as_ref()) };
// Set an encoder (required for flushing)
let encoder = Box::new(encoder_ctx::default());
unsafe { ccxr_dtvcc_set_encoder(dtvcc_ptr, Box::into_raw(encoder)) };
// Should not crash
unsafe { ccxr_flush_active_decoders(dtvcc_ptr) };
// Clean up
ccxr_dtvcc_free(dtvcc_ptr);
}
// -----------------------------------------------------------------
// Full lifecycle integration test
// -----------------------------------------------------------------
#[test]
fn test_ffi_complete_lifecycle() {
// This test simulates the complete lifecycle as it would be used from C code:
// 1. Initialize context
// 2. Set encoder
// 3. Process multiple CC data packets
// 4. Flush
// 5. Free
// 1. Initialize
let settings = create_test_dtvcc_settings();
let dtvcc_ptr = unsafe { ccxr_dtvcc_init(settings.as_ref()) };
assert!(!dtvcc_ptr.is_null());
// Verify initial state
let dtvcc = unsafe { &*(dtvcc_ptr as *mut DtvccRust) };
assert!(dtvcc.is_active, "DtvccRust should be active after init");
assert_eq!(dtvcc.packet_length, 0);
assert!(!dtvcc.is_header_parsed);
// 2. Set encoder
let encoder = get_zero_allocated_obj::<encoder_ctx>();
let encoder_ptr = Box::into_raw(encoder);
ccxr_dtvcc_set_encoder(dtvcc_ptr, encoder_ptr);
// 3. Process a packet start (cc_type=3) - this should set is_header_parsed
// Use 0xC4 for packet header: 0xC4 & 0x3F = 4, so max_len = 4*2 = 8 bytes
// This way the packet won't be completed until we've added enough data
ccxr_dtvcc_process_data(dtvcc_ptr, 1, 3, 0xC4, 0x00);
// Verify packet start was processed
let dtvcc = unsafe { &*(dtvcc_ptr as *mut DtvccRust) };
assert!(
dtvcc.is_header_parsed,
"is_header_parsed should be true after packet start"
);
assert_eq!(dtvcc.packet_length, 2, "packet_length should be 2");
// Process packet data (cc_type=2) - packet is not complete yet (need 8 bytes)
ccxr_dtvcc_process_data(dtvcc_ptr, 1, 2, 0x21, 0x00);
let dtvcc = unsafe { &*(dtvcc_ptr as *mut DtvccRust) };
assert_eq!(dtvcc.packet_length, 4, "packet_length should be 4");
// Add more data
ccxr_dtvcc_process_data(dtvcc_ptr, 1, 2, 0x00, 0x00);
let dtvcc = unsafe { &*(dtvcc_ptr as *mut DtvccRust) };
assert_eq!(dtvcc.packet_length, 6, "packet_length should be 6");
// 4. Flush
ccxr_flush_active_decoders(dtvcc_ptr);
// 5. Free
ccxr_dtvcc_free(dtvcc_ptr);
unsafe { drop(Box::from_raw(encoder_ptr)) };
}
}
}