diff --git a/src/lib_ccx/ccx_decoders_xds.c b/src/lib_ccx/ccx_decoders_xds.c index 529a32df4..0c3b88269 100644 --- a/src/lib_ccx/ccx_decoders_xds.c +++ b/src/lib_ccx/ccx_decoders_xds.c @@ -4,6 +4,18 @@ #include "ccx_common_common.h" #include "utility.h" +extern void ccxr_process_xds_bytes( + struct ccx_decoders_xds_context *ctx, + unsigned char hi, + int lo); + +extern void ccxr_do_end_of_xds( + struct cc_subtitle *sub, + struct ccx_decoders_xds_context *ctx, + unsigned char expected_checksum); + +extern void ccxr_set_ts_start_of_xds(long long value); + LLONG ts_start_of_xds = -1; // Time at which we switched to XDS mode, =-1 hasn't happened yet static const char *XDSclasses[] = @@ -238,6 +250,9 @@ void clear_xds_buffer(struct ccx_decoders_xds_context *ctx, int num) void process_xds_bytes(struct ccx_decoders_xds_context *ctx, const unsigned char hi, int lo) { + ccxr_process_xds_bytes(ctx, hi, lo); + return; // use the rust implementation + int is_new; if (!ctx) return; @@ -890,6 +905,10 @@ int xds_do_misc(struct ccx_decoders_xds_context *ctx) void do_end_of_xds(struct cc_subtitle *sub, struct ccx_decoders_xds_context *ctx, unsigned char expected_checksum) { + ccxr_set_ts_start_of_xds(ts_start_of_xds); + ccxr_do_end_of_xds(sub, ctx, expected_checksum); + return; // use the rust implementation + int cs = 0; int i; diff --git a/src/lib_ccx/lib_ccx.h b/src/lib_ccx/lib_ccx.h index 4a07a4f0d..5e68efab3 100644 --- a/src/lib_ccx/lib_ccx.h +++ b/src/lib_ccx/lib_ccx.h @@ -21,6 +21,8 @@ #include "avc_functions.h" #include "teletext.h" +#include "ccx_decoders_xds.h" + #ifdef WITH_LIBCURL #include #endif diff --git a/src/rust/build.rs b/src/rust/build.rs index 6cce8fc38..23bca6d63 100644 --- a/src/rust/build.rs +++ b/src/rust/build.rs @@ -63,6 +63,7 @@ fn main() { "ccx_encoding_type", "ccx_decoder_608_settings", "ccx_decoder_608_report", + "ccx_decoders_xds_context", "ccx_gxf", "MXFContext", "demuxer_data", diff --git a/src/rust/lib_ccxr/src/util/log.rs b/src/rust/lib_ccxr/src/util/log.rs index 155541e49..8830d39cb 100644 --- a/src/rust/lib_ccxr/src/util/log.rs +++ b/src/rust/lib_ccxr/src/util/log.rs @@ -264,6 +264,11 @@ impl<'a> CCExtractorLogger { self.debug_mask.is_debug_mode() } + /// Check if a specific debug flag is set in the current debug mask + pub fn has_debug_flag(&self, flag: DebugMessageFlag) -> bool { + self.debug_mask.mask().intersects(flag) + } + /// Returns the currently set target for logging messages. pub fn target(&self) -> OutputTarget { self.target diff --git a/src/rust/src/lib.rs b/src/rust/src/lib.rs index f4b838cda..330e561c1 100644 --- a/src/rust/src/lib.rs +++ b/src/rust/src/lib.rs @@ -29,6 +29,7 @@ pub mod libccxr_exports; pub mod parser; pub mod track_lister; pub mod utils; +pub mod xds; #[cfg(windows)] use std::os::windows::io::{FromRawHandle, RawHandle}; diff --git a/src/rust/src/libccxr_exports/time.rs b/src/rust/src/libccxr_exports/time.rs index 82df6838c..8eaeff59f 100644 --- a/src/rust/src/libccxr_exports/time.rs +++ b/src/rust/src/libccxr_exports/time.rs @@ -138,7 +138,7 @@ pub unsafe extern "C" fn ccxr_millis_to_time( /// # Safety /// /// `ctx` should not be null. -unsafe fn generate_timing_context(ctx: *const ccx_common_timing_ctx) -> TimingContext { +pub unsafe fn generate_timing_context(ctx: *const ccx_common_timing_ctx) -> TimingContext { let pts_set = match (*ctx).pts_set { 0 => PtsSet::No, 1 => PtsSet::Received, @@ -206,7 +206,7 @@ unsafe fn generate_timing_context(ctx: *const ccx_common_timing_ctx) -> TimingCo /// # Safety /// /// `ctx` should not be null. -unsafe fn write_back_to_common_timing_ctx( +pub unsafe fn write_back_to_common_timing_ctx( ctx: *mut ccx_common_timing_ctx, timing_ctx: &TimingContext, ) { @@ -275,7 +275,7 @@ unsafe fn write_back_to_common_timing_ctx( /// # Safety /// /// All the static variables should be initialized and in valid state. -unsafe fn apply_timing_info() { +pub(crate) unsafe fn apply_timing_info() { let Ok(mut timing_info) = GLOBAL_TIMING_INFO.write() else { // RwLock is poisoned, skip updating return; diff --git a/src/rust/src/xds/constants.rs b/src/rust/src/xds/constants.rs new file mode 100644 index 000000000..0b71d54a3 --- /dev/null +++ b/src/rust/src/xds/constants.rs @@ -0,0 +1,534 @@ +//! XDS constants, enums, and static data +//! +//! # Conversion Guide +//! +//! | C (ccx_decoders_xds.c/.h) | Rust (constants.rs) | +//! |------------------------------------|--------------------------------------------------| +//! | `XDS_CLASS_*` defines | [`XdsClass`] enum | +//! | `XDS_TYPE_*` defines | [`XdsType`], [`XdsCurrentFutureType`], etc. | +//! | `xds_class` (int field) | [`XdsClass::from_c_int`], [`XdsClass::to_c_int`] | +//! | `xds_type` (int field) | [`XdsType::from_c_int`], [`XdsType::to_c_int`] | +//! | `xds_program_type[]` array | [`XDS_PROGRAM_TYPES`] | +//! | `xds_classes[]` array | [`XDS_CLASSES`] | +//! | `ts_start_of_xds` global | [`TS_START_OF_XDS`] | +//! | `cur_xds_packet_type` (int match) | [`XdsPacketType`] enum | + +use std::fmt; +use std::os::raw::c_int; +use std::sync::atomic::AtomicI64; +use std::sync::Mutex; + +/// XDS write errors +#[derive(Debug)] +pub enum XdsError { + /// string contains a null byte + InvalidString, + /// alloc/realloc failed + MemoryAllocation, +} + +impl fmt::Display for XdsError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + XdsError::InvalidString => write!(f, "XDS string contains interior null byte"), + XdsError::MemoryAllocation => write!(f, "XDS memory allocation failed"), + } + } +} + +pub const NUM_BYTES_PER_PACKET: usize = 35; // Class + type (repeated for convenience) + data + zero +pub const NUM_XDS_BUFFERS: usize = 9; // CEA recommends no more than one level of interleaving. Play it safe + +pub static XDS_CLASSES: [&str; 8] = [ + "Current", + "Future", + "Channel", + "Miscellaneous", + "Public service", + "Reserved", + "Private data", + "End", +]; + +pub static XDS_PROGRAM_TYPES: [&str; 96] = [ + "Education", + "Entertainment", + "Movie", + "News", + "Religious", + "Sports", + "Other", + "Action", + "Advertisement", + "Animated", + "Anthology", + "Automobile", + "Awards", + "Baseball", + "Basketball", + "Bulletin", + "Business", + "Classical", + "College", + "Combat", + "Comedy", + "Commentary", + "Concert", + "Consumer", + "Contemporary", + "Crime", + "Dance", + "Documentary", + "Drama", + "Elementary", + "Erotica", + "Exercise", + "Fantasy", + "Farm", + "Fashion", + "Fiction", + "Food", + "Football", + "Foreign", + "Fund-Raiser", + "Game/Quiz", + "Garden", + "Golf", + "Government", + "Health", + "High_School", + "History", + "Hobby", + "Hockey", + "Home", + "Horror", + "Information", + "Instruction", + "International", + "Interview", + "Language", + "Legal", + "Live", + "Local", + "Math", + "Medical", + "Meeting", + "Military", + "Mini-Series", + "Music", + "Mystery", + "National", + "Nature", + "Police", + "Politics", + "Premiere", + "Pre-Recorded", + "Product", + "Professional", + "Public", + "Racing", + "Reading", + "Repair", + "Repeat", + "Review", + "Romance", + "Science", + "Series", + "Service", + "Shopping", + "Soap_Opera", + "Special", + "Suspense", + "Talk", + "Technical", + "Tennis", + "Travel", + "Variety", + "Video", + "Weather", + "Western", +]; + +#[derive(Debug, Clone, Copy, PartialEq)] +#[repr(u8)] +pub enum XdsClass { + Current = 0, + Future = 1, + Channel = 2, + Misc = 3, + Public = 4, + Reserved = 5, + Private = 6, + End = 7, + OutOfBand = 0x40, +} + +impl XdsClass { + pub fn from_c_int(value: c_int) -> Option { + match value { + 0 => Some(XdsClass::Current), + 1 => Some(XdsClass::Future), + 2 => Some(XdsClass::Channel), + 3 => Some(XdsClass::Misc), + 4 => Some(XdsClass::Public), + 5 => Some(XdsClass::Reserved), + 6 => Some(XdsClass::Private), + 7 => Some(XdsClass::End), + 0x40 => Some(XdsClass::OutOfBand), + _ => None, + } + } + + pub fn to_c_int(&self) -> c_int { + match self { + XdsClass::Current => 0, + XdsClass::Future => 1, + XdsClass::Channel => 2, + XdsClass::Misc => 3, + XdsClass::Public => 4, + XdsClass::Reserved => 5, + XdsClass::Private => 6, + XdsClass::End => 7, + XdsClass::OutOfBand => 0x40, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq)] +#[repr(u8)] +pub enum XdsCurrentFutureType { + PinStartTime = 1, + LengthAndCurrentTime = 2, + ProgramName = 3, + ProgramType = 4, + ContentAdvisory = 5, + AudioServices = 6, + Cgms = 8, + AspectRatioInfo = 9, + ProgramDesc1 = 0x10, + ProgramDesc2 = 0x11, + ProgramDesc3 = 0x12, + ProgramDesc4 = 0x13, + ProgramDesc5 = 0x14, + ProgramDesc6 = 0x15, + ProgramDesc7 = 0x16, + ProgramDesc8 = 0x17, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +#[repr(u8)] +pub enum XdsChannelType { + NetworkName = 1, + CallLettersAndChannel = 2, + Tsid = 4, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +#[repr(u8)] +pub enum XdsMiscType { + TimeOfDay = 1, + LocalTimeZone = 4, + OutOfBandChannelNumber = 0x40, +} + +#[derive(Debug, Clone, Copy, PartialEq)] +#[repr(u8)] +pub enum XdsType { + CurrentFuture(XdsCurrentFutureType), + Misc(XdsMiscType), + Channel(XdsChannelType), +} + +impl XdsType { + pub fn from_c_int(class: Option, type_value: c_int) -> Option { + match class? { + XdsClass::Current | XdsClass::Future => match type_value { + 1 => Some(XdsType::CurrentFuture(XdsCurrentFutureType::PinStartTime)), + 2 => Some(XdsType::CurrentFuture( + XdsCurrentFutureType::LengthAndCurrentTime, + )), + 3 => Some(XdsType::CurrentFuture(XdsCurrentFutureType::ProgramName)), + 4 => Some(XdsType::CurrentFuture(XdsCurrentFutureType::ProgramType)), + 5 => Some(XdsType::CurrentFuture( + XdsCurrentFutureType::ContentAdvisory, + )), + 6 => Some(XdsType::CurrentFuture(XdsCurrentFutureType::AudioServices)), + 8 => Some(XdsType::CurrentFuture(XdsCurrentFutureType::Cgms)), + 9 => Some(XdsType::CurrentFuture( + XdsCurrentFutureType::AspectRatioInfo, + )), + 0x10 => Some(XdsType::CurrentFuture(XdsCurrentFutureType::ProgramDesc1)), + 0x11 => Some(XdsType::CurrentFuture(XdsCurrentFutureType::ProgramDesc2)), + 0x12 => Some(XdsType::CurrentFuture(XdsCurrentFutureType::ProgramDesc3)), + 0x13 => Some(XdsType::CurrentFuture(XdsCurrentFutureType::ProgramDesc4)), + 0x14 => Some(XdsType::CurrentFuture(XdsCurrentFutureType::ProgramDesc5)), + 0x15 => Some(XdsType::CurrentFuture(XdsCurrentFutureType::ProgramDesc6)), + 0x16 => Some(XdsType::CurrentFuture(XdsCurrentFutureType::ProgramDesc7)), + 0x17 => Some(XdsType::CurrentFuture(XdsCurrentFutureType::ProgramDesc8)), + _ => None, + }, + XdsClass::Channel => match type_value { + 1 => Some(XdsType::Channel(XdsChannelType::NetworkName)), + 2 => Some(XdsType::Channel(XdsChannelType::CallLettersAndChannel)), + 4 => Some(XdsType::Channel(XdsChannelType::Tsid)), + _ => None, + }, + XdsClass::Misc => match type_value { + 1 => Some(XdsType::Misc(XdsMiscType::TimeOfDay)), + 4 => Some(XdsType::Misc(XdsMiscType::LocalTimeZone)), + 0x40 => Some(XdsType::Misc(XdsMiscType::OutOfBandChannelNumber)), + _ => None, + }, + XdsClass::Public + | XdsClass::Reserved + | XdsClass::Private + | XdsClass::End + | XdsClass::OutOfBand => None, + } + } + + pub fn to_c_int(&self) -> c_int { + match self { + XdsType::CurrentFuture(t) => match t { + XdsCurrentFutureType::PinStartTime => 1, + XdsCurrentFutureType::LengthAndCurrentTime => 2, + XdsCurrentFutureType::ProgramName => 3, + XdsCurrentFutureType::ProgramType => 4, + XdsCurrentFutureType::ContentAdvisory => 5, + XdsCurrentFutureType::AudioServices => 6, + XdsCurrentFutureType::Cgms => 8, + XdsCurrentFutureType::AspectRatioInfo => 9, + XdsCurrentFutureType::ProgramDesc1 => 0x10, + XdsCurrentFutureType::ProgramDesc2 => 0x11, + XdsCurrentFutureType::ProgramDesc3 => 0x12, + XdsCurrentFutureType::ProgramDesc4 => 0x13, + XdsCurrentFutureType::ProgramDesc5 => 0x14, + XdsCurrentFutureType::ProgramDesc6 => 0x15, + XdsCurrentFutureType::ProgramDesc7 => 0x16, + XdsCurrentFutureType::ProgramDesc8 => 0x17, + }, + XdsType::Channel(t) => match t { + XdsChannelType::NetworkName => 1, + XdsChannelType::CallLettersAndChannel => 2, + XdsChannelType::Tsid => 4, + }, + XdsType::Misc(t) => match t { + XdsMiscType::TimeOfDay => 1, + XdsMiscType::LocalTimeZone => 4, + XdsMiscType::OutOfBandChannelNumber => 0x40, + }, + } + } +} + +pub static TS_START_OF_XDS: AtomicI64 = AtomicI64::new(-1); // Time at which we switched to XDS mode, =-1 hasn't happened yet + +/// XDS packet type codes used in `cur_xds_packet_type` matching +/// +/// Maps integer constants from the C `XDS_TYPE_*` defines to a Rust enum +#[derive(Debug, Clone, Copy, PartialEq)] +#[repr(i32)] +pub enum XdsPacketType { + PinStartTime = 1, + LengthAndCurrentTime = 2, + ProgramName = 3, + ProgramType = 4, + ContentAdvisory = 5, + AudioServices = 6, + Cgms = 8, + AspectRatioInfo = 9, + ProgramDesc1 = 0x10, + ProgramDesc2 = 0x11, + ProgramDesc3 = 0x12, + ProgramDesc4 = 0x13, + ProgramDesc5 = 0x14, + ProgramDesc6 = 0x15, + ProgramDesc7 = 0x16, + ProgramDesc8 = 0x17, + // channel class types + NetworkName = 0x101, + CallLettersAndChannel = 0x102, + Tsid = 0x104, + // misc class types + TimeOfDay = 0x201, + LocalTimeZone = 0x204, +} + +impl XdsPacketType { + /// Convert a raw `cur_xds_packet_type` integer (Current/Future class) to enum. + pub fn from_current_future(value: i32) -> Option { + match value { + 1 => Some(Self::PinStartTime), + 2 => Some(Self::LengthAndCurrentTime), + 3 => Some(Self::ProgramName), + 4 => Some(Self::ProgramType), + 5 => Some(Self::ContentAdvisory), + 6 => Some(Self::AudioServices), + 8 => Some(Self::Cgms), + 9 => Some(Self::AspectRatioInfo), + 0x10..=0x17 => Some(match value { + 0x10 => Self::ProgramDesc1, + 0x11 => Self::ProgramDesc2, + 0x12 => Self::ProgramDesc3, + 0x13 => Self::ProgramDesc4, + 0x14 => Self::ProgramDesc5, + 0x15 => Self::ProgramDesc6, + 0x16 => Self::ProgramDesc7, + 0x17 => Self::ProgramDesc8, + _ => unreachable!(), + }), + _ => None, + } + } + + /// Convert a raw `cur_xds_packet_type` integer (Channel class) to enum. + pub fn from_channel(value: i32) -> Option { + match value { + 1 => Some(Self::NetworkName), + 2 => Some(Self::CallLettersAndChannel), + 4 => Some(Self::Tsid), + _ => None, + } + } + + /// Convert a raw `cur_xds_packet_type` integer (Misc class) to enum. + pub fn from_misc(value: i32) -> Option { + match value { + 1 => Some(Self::TimeOfDay), + 4 => Some(Self::LocalTimeZone), + _ => None, + } + } + + /// Returns true if this is a program description type (0x10-0x17). + pub fn is_program_desc(&self) -> bool { + matches!( + self, + Self::ProgramDesc1 + | Self::ProgramDesc2 + | Self::ProgramDesc3 + | Self::ProgramDesc4 + | Self::ProgramDesc5 + | Self::ProgramDesc6 + | Self::ProgramDesc7 + | Self::ProgramDesc8 + ) + } +} + +/// State for Content Advisory caching +pub(crate) struct ContentAdvisoryState { + pub last_c1: u32, + pub last_c2: u32, + pub age: String, + pub content: String, + pub rating: String, +} + +impl Default for ContentAdvisoryState { + fn default() -> Self { + ContentAdvisoryState { + last_c1: u32::MAX, + last_c2: u32::MAX, + age: String::new(), + content: String::new(), + rating: String::new(), + } + } +} + +/// State for CGMS (Copy Generation Management System) caching +pub(crate) struct CgmsState { + pub last_c1: u32, + pub last_c2: u32, + pub copy_permitted: String, + pub aps: String, + pub rcd: String, +} + +impl Default for CgmsState { + fn default() -> Self { + CgmsState { + last_c1: u32::MAX, + last_c2: u32::MAX, + copy_permitted: String::new(), + aps: String::new(), + rcd: String::new(), + } + } +} + +/// Cached state for Content Advisory to detect changes +pub(crate) static CONTENT_ADVISORY_STATE: Mutex = + Mutex::new(ContentAdvisoryState { + last_c1: u32::MAX, + last_c2: u32::MAX, + age: String::new(), + content: String::new(), + rating: String::new(), + }); + +/// US TV Parental Guidelines age ratings +pub(crate) const US_TV_AGE_TEXT: [&str; 8] = [ + "None", + "TV-Y (All Children)", + "TV-Y7 (Older Children)", + "TV-G (General Audience)", + "TV-PG (Parental Guidance Suggested)", + "TV-14 (Parents Strongly Cautioned)", + "TV-MA (Mature Audience Only)", + "None", +]; + +/// MPA rating text +pub(crate) const MPA_RATING_TEXT: [&str; 8] = + ["N/A", "G", "PG", "PG-13", "R", "NC-17", "X", "Not Rated"]; + +/// Canadian English Language Rating +pub(crate) const CANADIAN_ENGLISH_RATING_TEXT: [&str; 8] = [ + "Exempt", + "Children", + "Children eight years and older", + "General programming suitable for all audiences", + "Parental Guidance", + "Viewers 14 years and older", + "Adult Programming", + "[undefined]", +]; + +/// Canadian French Language Rating +pub(crate) const CANADIAN_FRENCH_RATING_TEXT: [&str; 8] = [ + "Exempt?es", + "G?n?ral", + "G?n?ral - D?conseill? aux jeunes enfants", + "Cette ?mission peut ne pas convenir aux enfants de moins de 13 ans", + "Cette ?mission ne convient pas aux moins de 16 ans", + "Cette ?mission est r?serv?e aux adultes", + "[invalid]", + "[invalid]", +]; + +/// Cached state for Copy Generation Management System (CGMS) to detect changes +pub(crate) static CGMS_STATE: Mutex = Mutex::new(CgmsState { + last_c1: u32::MAX, + last_c2: u32::MAX, + copy_permitted: String::new(), + aps: String::new(), + rcd: String::new(), +}); + +/// Copy Generation Management System text descriptions +pub(crate) const COPY_TEXT: [&str; 4] = [ + "Copy permitted (no restrictions)", + "No more copies (one generation copy has been made)", + "One generation of copies can be made", + "No copying is permitted", +]; + +/// APS (Analog Protection System) mode descriptions +pub(crate) const APS_TEXT: [&str; 4] = [ + "No APS", + "PSP On; Split Burst Off", + "PSP On; 2 line Split Burst On", + "PSP On; 4 line Split Burst On", +]; diff --git a/src/rust/src/xds/handlers.rs b/src/rust/src/xds/handlers.rs new file mode 100644 index 000000000..df22a1c07 --- /dev/null +++ b/src/rust/src/xds/handlers.rs @@ -0,0 +1,1299 @@ +//! XDS (Extended Data Services) handler functions for processing extended data packets. +//! +//! This module provides functions for handling XDS packets including content advisory, +//! copy generation management, program information, channel data, and miscellaneous metadata. +//! +//! # Conversion Guide +//! +//! | C (ccx_decoders_xds.c) | Rust (handlers.rs) | +//! |-------------------------------------------|-------------------------------------------------------| +//! | `write_xds_string` | [`write_xds_string`] | +//! | `xdsprint` | [`xdsprint`] | +//! | `xds_do_copy_generation_management_system`| [`xds_do_copy_generation_management_system`] | +//! | `xds_do_content_advisory` | [`xds_do_content_advisory`] | +//! | `xds_do_current_and_future` | [`xds_do_current_and_future`] | +//! | `xds_do_channel` | [`xds_do_channel`] | +//! | `xds_do_misc` | [`xds_do_misc`] | +//! | `xds_do_private_data` | [`xds_do_private_data`] | +//! | `do_end_of_xds` | [`do_end_of_xds`] | +//! | `xds_debug_test` | [`xds_debug_test`] | +//! | `xds_cea608_test` | [`xds_cea608_test`] | +//! | `cc_subtitle` struct | [`cc_subtitle`] (C binding) | +//! | `eia608_screen` struct | [`eia608_screen`] (C binding) | + +#[allow(clippy::all)] +pub mod bindings { + include!(concat!(env!("OUT_DIR"), "/bindings.rs")); +} + +use std::alloc::{self, Layout}; +use std::ffi::CString; +use std::mem::{align_of, size_of}; +use std::os::raw::c_void; +use std::ptr::null_mut; +use std::sync::atomic::Ordering; + +use crate::bindings::{cc_subtitle, eia608_screen}; + +use lib_ccxr::debug; +use lib_ccxr::info; +use lib_ccxr::time::c_functions::get_fts; +use lib_ccxr::time::CaptionField; +use lib_ccxr::util::log::{hex_dump, send_gui, DebugMessageFlag, GuiXdsMessage}; + +use crate::xds::constants::*; +use crate::xds::types::*; + +// --- helper func --- + +/// Helper function to copy String into i8 array +fn string_to_i8_array(s: &str, arr: &mut [i8]) { + let bytes = s.as_bytes(); + let len = std::cmp::min(bytes.len(), arr.len() - 1); + for i in 0..len { + arr[i] = bytes[i] as i8; + } + if len < arr.len() { + arr[len] = 0; + } +} + +/// Helper function to convert i8 array to String +fn i8_array_to_string(arr: &[i8]) -> String { + let bytes: Vec = arr + .iter() + .take_while(|&&b| b != 0) + .map(|&b| b as u8) + .collect(); + String::from_utf8_lossy(&bytes).to_string() +} + +// --- core XDS handlers --- + +/// Writes an XDS string to the subtitle output buffer. +/// +/// # Safety +/// - The caller must ensure `sub` and `ctx` remain valid for the duration of the call. +/// - The returned `xds_str` pointer in the screen data must be freed with `CString::from_raw`. +pub unsafe fn write_xds_string( + sub: &mut cc_subtitle, + ctx: &mut CcxDecodersXdsContext, + p: String, + ts_start_of_xds: i64, +) -> Result<(), XdsError> { + let c_str = CString::new(p).map_err(|_| XdsError::InvalidString)?; + let len = c_str.as_bytes().len(); // length w/o null terminator - matching C's vsnprintf return + let alloc_len = len + 1; // allocate space for null terminator + + let new_size = (sub.nb_data + 1) as usize * size_of::(); + let screen_align = align_of::(); + let new_data = unsafe { + if sub.data.is_null() { + let layout = Layout::from_size_align_unchecked(new_size, screen_align); + alloc::alloc(layout) as *mut eia608_screen + } else { + let old_size = sub.nb_data as usize * size_of::(); + let old_layout = Layout::from_size_align_unchecked(old_size, screen_align); + alloc::realloc(sub.data as *mut u8, old_layout, new_size) as *mut eia608_screen + } + }; + if new_data.is_null() { + if !sub.data.is_null() && sub.nb_data > 0 { + let old_size = sub.nb_data as usize * size_of::(); + let old_layout = Layout::from_size_align_unchecked(old_size, screen_align); + alloc::dealloc(sub.data as *mut u8, old_layout); + sub.data = null_mut(); + } + sub.nb_data = 0; + info!("No Memory left"); + return Err(XdsError::MemoryAllocation); + } + sub.data = new_data as *mut c_void; + sub.datatype = 0; + let data_element = &mut *new_data.add(sub.nb_data as usize); + + let str_layout = + Layout::from_size_align(alloc_len, 1).map_err(|_| XdsError::MemoryAllocation)?; + let ptr = unsafe { alloc::alloc(str_layout) as *mut i8 }; + if ptr.is_null() { + return Err(XdsError::MemoryAllocation); + } + unsafe { + std::ptr::copy_nonoverlapping(c_str.as_ptr(), ptr, alloc_len); + } + + data_element.format = 2; + data_element.start_time = ts_start_of_xds; + if let Some(timing) = ctx.timing.as_mut() { + data_element.end_time = get_fts(timing, CaptionField::Field2).millis(); + } + data_element.xds_str = ptr; + data_element.xds_len = len; + data_element.cur_xds_packet_class = ctx.cur_xds_packet_class; + sub.nb_data += 1; + sub.type_ = 1; + sub.got_output = 1; + Ok(()) +} + +/// Prints an XDS message to the subtitle output if XDS file writing is enabled. +/// +/// # Safety +/// - Same safety requirements as `write_xds_string`. +/// - Relies on the global `TS_START_OF_XDS` atomic value being correctly set. +pub unsafe fn xdsprint( + sub: &mut cc_subtitle, + ctx: &mut CcxDecodersXdsContext, + message: String, +) -> Result<(), XdsError> { + if !ctx.xds_write_to_file { + return Ok(()); + } + + write_xds_string(sub, ctx, message, TS_START_OF_XDS.load(Ordering::SeqCst)) +} + +/// Utility methods for XDS buffer management and process_xds_bytes (function). +impl CcxDecodersXdsContext<'_> { + /// Count how many XDS buffers are currently in use + fn how_many_used(&self) -> usize { + self.xds_buffers + .iter() + .filter(|buf| buf.in_use != 0) + .count() + } +} + +/// XDS byte processing and packet handling. +impl CcxDecodersXdsContext<'_> { + pub(crate) fn process_xds_bytes(&mut self, hi: u8, lo: u8) { + if (0x01..=0x0f).contains(&hi) { + let xds_class = ((hi - 1) / 2) as i32; // Start codes 1 and 2 are "class type" 0, 3-4 are 2, and so on. + let is_new = !hi.is_multiple_of(2); // Start codes are even + + debug!( + msg_type = DebugMessageFlag::DECODER_XDS; + "XDS Start: {}.{} Is new: {} | Class: {} ({}), Used buffers: {}", + hi, + lo, + is_new, + xds_class, + XDS_CLASSES.get(xds_class as usize).unwrap_or(&"Unknown"), + self.how_many_used() + ); + + let mut first_free_buf = -1i32; + let mut matching_buf = -1i32; + + for i in 0..NUM_XDS_BUFFERS { + if self.xds_buffers[i].in_use != 0 + && self.xds_buffers[i] + .xds_class + .map(|c| c.to_c_int()) + .unwrap_or(-1) + == xds_class + && self.xds_buffers[i] + .xds_type + .map(|t| t.to_c_int()) + .unwrap_or(-1) + == lo as i32 + { + matching_buf = i as i32; + break; + } + if first_free_buf == -1 && self.xds_buffers[i].in_use == 0 { + first_free_buf = i as i32; + } + } + + /* Here, 3 possibilities: + 1) We already had a buffer for this class/type and matching_buf points to it + 2) We didn't have a buffer for this class/type and first_free_buf points to an unused one + 3) All buffers are full and we will have to skip this packet. + */ + if matching_buf == -1 && first_free_buf == -1 { + info!( + "Note: All XDS buffers full (bug or suicidal stream). Ignoring this one ({},{}).", + xds_class, lo + ); + self.cur_xds_buffer_idx = -1; + return; + } + + self.cur_xds_buffer_idx = if matching_buf != -1 { + matching_buf + } else { + first_free_buf + }; + let idx = self.cur_xds_buffer_idx as usize; + + if is_new || self.xds_buffers[idx].in_use == 0 { + // Whatever we had before we discard; must belong to an interrupted packet + self.xds_buffers[idx].xds_class = XdsClass::from_c_int(xds_class); + self.xds_buffers[idx].xds_type = + XdsType::from_c_int(self.xds_buffers[idx].xds_class, lo as i32); + self.xds_buffers[idx].used_bytes = 0; + self.xds_buffers[idx].in_use = 1; + self.xds_buffers[idx].bytes = [0; NUM_BYTES_PER_PACKET]; + } + + if !is_new { + // Continue codes aren't added to packet. + return; + } + } else { + // Informational: 00, or 0x20-0x7F, so 01-0x1f forbidden + debug!( + msg_type = DebugMessageFlag::DECODER_XDS; + "XDS: {:02X}.{:02X} ({}, {})", + hi, + lo, + hi as char, + lo as char + ); + + if (hi > 0 && hi <= 0x1f) || (lo > 0 && lo <= 0x1f) { + info!("\rNote: Illegal XDS data"); + return; + } + } + + if self.cur_xds_buffer_idx >= 0 { + let idx = self.cur_xds_buffer_idx as usize; + if idx < NUM_XDS_BUFFERS && (self.xds_buffers[idx].used_bytes as usize) <= 32 { + let pos = self.xds_buffers[idx].used_bytes as usize; + self.xds_buffers[idx].bytes[pos] = hi; + self.xds_buffers[idx].bytes[pos + 1] = lo; + self.xds_buffers[idx].used_bytes += 2; + if (self.xds_buffers[idx].used_bytes as usize) < NUM_BYTES_PER_PACKET { + self.xds_buffers[idx].bytes[self.xds_buffers[idx].used_bytes as usize] = 0; + } + } + } + } +} + +/// Decodes and outputs Copy Generation Management System (CGMS) data from XDS packets. +/// +/// # Safety +/// - `sub` must be a valid, non-null pointer to a `cc_subtitle` struct +/// - `ctx` must be a valid, non-null pointer to a `ccx_decoders_xds_context` struct +/// - Both pointers must remain valid for the duration of the call +/// - The caller must ensure proper synchronization if accessed from multiple threads +pub unsafe fn xds_do_copy_generation_management_system( + sub: &mut crate::bindings::cc_subtitle, + ctx: &mut CcxDecodersXdsContext, + c1: u32, + c2: u32, +) { + // Extract bit fields from c1 + let c1_6 = (c1 & 0x40) >> 6; + // let _unused1 = (c1 & 0x20) >> 5; // unused in original + let cgms_a_b4 = (c1 & 0x10) >> 4; + let cgms_a_b3 = (c1 & 0x08) >> 3; + let aps_b2 = (c1 & 0x04) >> 2; + let aps_b1 = (c1 & 0x02) >> 1; + // let _asb_0 = c1 & 0x01; // unused in original + + // Extract bit fields from c2 + let c2_6 = (c2 & 0x40) >> 6; + // let _c2_5 = (c2 & 0x20) >> 5; // unused + // let _c2_4 = (c2 & 0x10) >> 4; // unused + // let _c2_3 = (c2 & 0x08) >> 3; // unused + // let _c2_2 = (c2 & 0x04) >> 2; // unused + // let _c2_1 = (c2 & 0x02) >> 1; // unused + let rcd0 = c2 & 0x01; + + // These bits must be high per specs + if c1_6 == 0 || c2_6 == 0 { + return; + } + + let mut state = match CGMS_STATE.lock() { + Ok(s) => s, + Err(_) => return, + }; + + let changed = if state.last_c1 != c1 || state.last_c2 != c2 { + state.last_c1 = c1; + state.last_c2 = c2; + + // Decode CGMS-A copy protection + let copy_idx = (cgms_a_b4 * 2 + cgms_a_b3) as usize; + state.copy_permitted = format!("CGMS: {}", COPY_TEXT[copy_idx]); + + // Decode APS (Analog Protection System) + let aps_idx = (aps_b2 * 2 + aps_b1) as usize; + state.aps = format!("APS: {}", APS_TEXT[aps_idx]); + + // Decode RCD (Redistribution Control Descriptor) + state.rcd = format!("Redistribution Control Descriptor: {}", rcd0); + + true + } else { + false + }; + + // Output via xdsprint + let _ = xdsprint(sub, ctx, state.copy_permitted.clone()); + let _ = xdsprint(sub, ctx, state.aps.clone()); + let _ = xdsprint(sub, ctx, state.rcd.clone()); + + // Log if changed + if changed { + info!("\rXDS: {}\n", state.copy_permitted); + info!("\rXDS: {}\n", state.aps); + info!("\rXDS: {}\n", state.rcd); + } + + // Debug output (always, when debug mask matches) + debug!(msg_type = DebugMessageFlag::DECODER_XDS; "\rXDS: {}\n", state.copy_permitted); + debug!(msg_type = DebugMessageFlag::DECODER_XDS; "\rXDS: {}\n", state.aps); + debug!(msg_type = DebugMessageFlag::DECODER_XDS; "\rXDS: {}\n", state.rcd); +} + +/// Handles content advisory/rating information (US TV, MPA, Canadian ratings) +/// +/// # Safety +/// - `sub` must be a valid, non-null pointer to a `cc_subtitle` struct +/// - `ctx` must be a valid, non-null pointer to a `ccx_decoders_xds_context` struct +/// - Both pointers must remain valid for the duration of the call +/// - The caller must ensure proper synchronization if accessed from multiple threads +pub unsafe fn xds_do_content_advisory( + sub: &mut crate::bindings::cc_subtitle, + ctx: &mut CcxDecodersXdsContext, + c1: u32, + c2: u32, +) { + // Extract bit fields from c1 (insane encoding per original comment) + let c1_6 = (c1 & 0x40) >> 6; + let da2 = (c1 & 0x20) >> 5; + let a1 = (c1 & 0x10) >> 4; + let a0 = (c1 & 0x08) >> 3; + let r2 = (c1 & 0x04) >> 2; + let r1 = (c1 & 0x02) >> 1; + let r0 = c1 & 0x01; + + // Extract bit fields from c2 + let c2_6 = (c2 & 0x40) >> 6; + let fv = (c2 & 0x20) >> 5; + let s = (c2 & 0x10) >> 4; + let la3 = (c2 & 0x08) >> 3; + let g2 = (c2 & 0x04) >> 2; + let g1 = (c2 & 0x02) >> 1; + let g0 = c2 & 0x01; + + // These bits must be high per specs + if c1_6 == 0 || c2_6 == 0 { + return; + } + + let mut state = match CONTENT_ADVISORY_STATE.lock() { + Ok(s) => s, + Err(_) => return, + }; + + let mut changed = false; + let mut supported = false; + + if state.last_c1 != c1 || state.last_c2 != c2 { + changed = true; + state.last_c1 = c1; + state.last_c2 = c2; + + // Bits a1 and a0 determine the encoding + if a1 == 0 && a0 != 0 { + // US TV parental guidelines + let age_idx = (g2 * 4 + g1 * 2 + g0) as usize; + state.age = format!( + "ContentAdvisory: US TV Parental Guidelines. Age Rating: {}", + US_TV_AGE_TEXT[age_idx] + ); + + // Build content string + let mut content_parts = Vec::new(); + + if g2 == 0 && g1 != 0 && g0 == 0 { + // For TV-Y7 (Older children), the Violence bit is "fantasy violence" + if fv != 0 { + content_parts.push("[Fantasy Violence] "); + } + } else { + // For all others, is real + if fv != 0 { + content_parts.push("[Violence] "); + } + } + + if s != 0 { + content_parts.push("[Sexual Situations] "); + } + if la3 != 0 { + content_parts.push("[Adult Language] "); + } + if da2 != 0 { + content_parts.push("[Sexually Suggestive Dialog] "); + } + + state.content = content_parts.join(""); // "" used instead of " " : to keep xds terminal(stdout) output same as that of c code + supported = true; + } + + if a0 == 0 { + // MPA + let rating_idx = (r2 * 4 + r1 * 2 + r0) as usize; + state.rating = format!( + "ContentAdvisory: MPA Rating: {}", + MPA_RATING_TEXT[rating_idx] + ); + supported = true; + } + + if a0 != 0 && a1 != 0 && da2 == 0 && la3 == 0 { + // Canadian English Language Rating + let rating_idx = (g2 * 4 + g1 * 2 + g0) as usize; + state.rating = format!( + "ContentAdvisory: Canadian English Rating: {}", + CANADIAN_ENGLISH_RATING_TEXT[rating_idx] + ); + supported = true; + } + + if a0 != 0 && a1 != 0 && da2 != 0 && la3 == 0 { + // Canadian French Language Rating + let rating_idx = (g2 * 4 + g1 * 2 + g0) as usize; + state.rating = format!( + "ContentAdvisory: Canadian French Rating: {}", + CANADIAN_FRENCH_RATING_TEXT[rating_idx] + ); + supported = true; + } + } + + // Output based on encoding type + // US TV parental guidelines + if a1 == 0 && a0 != 0 { + let _ = xdsprint(sub, ctx, state.age.clone()); + if !state.content.is_empty() { + let _ = xdsprint(sub, ctx, state.content.clone()); + } + + if changed { + info!("\rXDS: {}\n ", state.age); + if !state.content.is_empty() { + info!("\rXDS: {}\n ", state.content); + } + } + + debug!(msg_type = DebugMessageFlag::DECODER_XDS; "\rXDS: {}\n", state.age); + if !state.content.is_empty() { + debug!(msg_type = DebugMessageFlag::DECODER_XDS; "\rXDS: {}\n", state.content); + } + } + + // MPA, Canadian English, or Canadian French + if a0 == 0 || (a1 != 0 && la3 == 0) { + let _ = xdsprint(sub, ctx, state.rating.clone()); + + if changed { + info!("\rXDS: {}\n ", state.rating); + } + + debug!(msg_type = DebugMessageFlag::DECODER_XDS; "\rXDS: {}\n", state.rating); + } + + if changed && !supported { + info!("XDS: Unsupported ContentAdvisory encoding, please submit sample.\n"); + } +} + +/// Handles current and future program info (PIN, length, name, type, descriptions) +/// +/// # Safety +/// - `sub` must be a valid, non-null pointer to a `cc_subtitle` struct +/// - `ctx` must be a valid, non-null pointer to a `ccx_decoders_xds_context` struct +/// - `ctx.cur_xds_payload` must point to valid memory with at least `ctx.cur_xds_payload_length` bytes +/// - Both pointers must remain valid for the duration of the call +pub unsafe fn xds_do_current_and_future( + sub: &mut crate::bindings::cc_subtitle, + ctx: &mut CcxDecodersXdsContext, +) -> i32 { + let mut was_proc = 0; + + // Safety check for payload + if ctx.cur_xds_payload.is_null() { + return -1; // CCX_EINVAL equivalent + } + + let payload = + std::slice::from_raw_parts(ctx.cur_xds_payload, ctx.cur_xds_payload_length as usize); + + let packet_type = XdsPacketType::from_current_future(ctx.cur_xds_packet_type); + + match packet_type { + // XDS_TYPE_PIN_START_TIME = 1 + Some(XdsPacketType::PinStartTime) => { + was_proc = 1; + if ctx.cur_xds_payload_length < 7 { + // We need 4 data bytes + return was_proc; + } + + let min = (payload[2] & 0x3f) as i32; // 6 bits + let hour = (payload[3] & 0x1f) as i32; // 5 bits + let date = (payload[4] & 0x1f) as i32; // 5 bits + let month = (payload[5] & 0x0f) as i32; // 4 bits + + if ctx.current_xds_min != min + || ctx.current_xds_hour != hour + || ctx.current_xds_date != date + || ctx.current_xds_month != month + { + ctx.xds_start_time_shown = 0; + ctx.current_xds_min = min; + ctx.current_xds_hour = hour; + ctx.current_xds_date = date; + ctx.current_xds_month = month; + } + + // XDS_CLASS_CURRENT = 0 + let class_str = if ctx.cur_xds_packet_class == 0 { + "Current" + } else { + "Future" + }; + + debug!( + msg_type = DebugMessageFlag::DECODER_XDS; + "PIN (Start Time): {} {:02}-{:02} {:02}:{:02}\n", + class_str, date, month, hour, min + ); + + let pin_msg = format!( + "PIN (Start Time): {} {:02}-{:02} {:02}:{:02}\n", + class_str, date, month, hour, min + ); + let _ = xdsprint(sub, ctx, pin_msg); + + // XDS_CLASS_CURRENT = 0 + if ctx.xds_start_time_shown == 0 && ctx.cur_xds_packet_class == 0 { + info!("\rXDS: Program changed.\n"); + info!( + "XDS program start time (DD/MM HH:MM) {:02}-{:02} {:02}:{:02}\n", + date, month, hour, min + ); + send_gui(GuiXdsMessage::ProgramIdNr { + minute: ctx.current_xds_min as u8, + hour: ctx.current_xds_hour as u8, + date: ctx.current_xds_date as u8, + month: ctx.current_xds_month as u8, + }); + ctx.xds_start_time_shown = 1; + } + } + + // XDS_TYPE_LENGH_AND_CURRENT_TIME = 2 + Some(XdsPacketType::LengthAndCurrentTime) => { + was_proc = 1; + if ctx.cur_xds_payload_length < 5 { + // We need 2 data bytes + return was_proc; + } + + let min = (payload[2] & 0x3f) as i32; // 6 bits + let hour = (payload[3] & 0x1f) as i32; // 5 bits + + if ctx.xds_program_length_shown == 0 { + info!("\rXDS: Program length (HH:MM): {:02}:{:02} ", hour, min); + } else { + debug!( + msg_type = DebugMessageFlag::DECODER_XDS; + "\rXDS: Program length (HH:MM): {:02}:{:02} ", hour, min + ); + } + + let length_msg = format!("Program length (HH:MM): {:02}:{:02} ", hour, min); + let _ = xdsprint(sub, ctx, length_msg); + + if ctx.cur_xds_payload_length > 6 { + // Next two bytes (optional) available + let el_min = (payload[4] & 0x3f) as i32; // 6 bits + let el_hour = (payload[5] & 0x1f) as i32; // 5 bits + + if ctx.xds_program_length_shown == 0 { + info!("Elapsed (HH:MM): {:02}:{:02}", el_hour, el_min); + } else { + debug!( + msg_type = DebugMessageFlag::DECODER_XDS; + "Elapsed (HH:MM): {:02}:{:02}", el_hour, el_min + ); + } + + let elapsed_msg = format!("Elapsed (HH:MM): {:02}:{:02}", el_hour, el_min); + let _ = xdsprint(sub, ctx, elapsed_msg); + } + + if ctx.cur_xds_payload_length > 8 { + // Next two bytes (optional) available + let el_sec = (payload[6] & 0x3f) as i32; // 6 bits + + if ctx.xds_program_length_shown == 0 { + debug!( + msg_type = DebugMessageFlag::DECODER_XDS; + ":{:02}", el_sec + ); + } + + let elapsed_sec_msg = format!("Elapsed (SS) :{:02}", el_sec); + let _ = xdsprint(sub, ctx, elapsed_sec_msg); + } + + if ctx.xds_program_length_shown == 0 { + info!("\n"); + } else { + debug!(msg_type = DebugMessageFlag::DECODER_XDS; "\n"); + } + + ctx.xds_program_length_shown = 1; + } + + // XDS_TYPE_PROGRAM_NAME = 3 + Some(XdsPacketType::ProgramName) => { + was_proc = 1; + + // Extract program name from payload (bytes 2 to payload_length - 2) + let name_end = (ctx.cur_xds_payload_length - 1) as usize; + let name_slice = &payload[2..name_end]; + let name_bytes = match name_slice.iter().position(|&b| b == 0) { + Some(pos) => &name_slice[..pos], + None => name_slice, + }; + + let xds_program_name = String::from_utf8_lossy(name_bytes).to_string(); + + debug!( + msg_type = DebugMessageFlag::DECODER_XDS; + "\rXDS Program name: {}\n", xds_program_name + ); + + let name_msg = format!("Program name: {}", xds_program_name); + let _ = xdsprint(sub, ctx, name_msg); + + // Convert current_xds_program_name from i8 array to String for comparison + let current_name = i8_array_to_string(&ctx.current_xds_program_name); + + // XDS_CLASS_CURRENT = 0 + if ctx.cur_xds_packet_class == 0 && xds_program_name != current_name { + info!("\rXDS Notice: Program is now {}\n", xds_program_name); + string_to_i8_array(&xds_program_name, &mut ctx.current_xds_program_name); + send_gui(GuiXdsMessage::ProgramName(&xds_program_name)); + } + } + + // XDS_TYPE_PROGRAM_TYPE = 4 + Some(XdsPacketType::ProgramType) => { + was_proc = 1; + if ctx.cur_xds_payload_length < 5 { + // We need 2 data bytes + return was_proc; + } + + if ctx.current_program_type_reported != 0 { + // Check if we should do it again + + let should_report = payload + .iter() + .zip(ctx.current_xds_program_type.iter()) + .take(33.min(ctx.cur_xds_payload_length as usize)) + .any(|(p, c)| *p != *c as u8); + + if should_report { + ctx.current_program_type_reported = 0; + } + } + + // Copy payload to current_xds_program_type + for (dst, src) in ctx + .current_xds_program_type + .iter_mut() + .zip(payload.iter()) + .take(std::cmp::min(ctx.cur_xds_payload_length as usize, 32)) + { + *dst = *src as i8; + } + if (ctx.cur_xds_payload_length as usize) < 33 { + ctx.current_xds_program_type[ctx.cur_xds_payload_length as usize] = 0; + } + + if ctx.current_program_type_reported == 0 { + info!("\rXDS Program Type: "); + } + + let mut type_str = String::new(); + + for &byte in payload + .iter() + .take((ctx.cur_xds_payload_length - 1) as usize) + .skip(2) + { + if byte == 0 { + continue; + } + + if ctx.current_program_type_reported == 0 { + info!("[{:02X}-", byte); + } + + if (0x20..0x7F).contains(&byte) { + let type_idx = (byte - 0x20) as usize; + if type_idx < XDS_PROGRAM_TYPES.len() { + type_str.push_str(&format!("[{}] ", XDS_PROGRAM_TYPES[type_idx])); + } + } + + if ctx.current_program_type_reported == 0 { + if (0x20..0x7F).contains(&byte) { + let type_idx = (byte - 0x20) as usize; + if type_idx < XDS_PROGRAM_TYPES.len() { + info!("{}", XDS_PROGRAM_TYPES[type_idx]); + } + } else { + info!("ILLEGAL VALUE"); + } + info!("] "); + } + } + + let type_msg = format!("Program type {}", type_str); + let _ = xdsprint(sub, ctx, type_msg); + + if ctx.current_program_type_reported == 0 { + info!("\n"); + } + + ctx.current_program_type_reported = 1; + } + + // XDS_TYPE_CONTENT_ADVISORY = 5 + Some(XdsPacketType::ContentAdvisory) => { + was_proc = 1; + if ctx.cur_xds_payload_length < 5 { + // We need 2 data bytes + return was_proc; + } + xds_do_content_advisory(sub, ctx, payload[2] as u32, payload[3] as u32); + } + + // XDS_TYPE_AUDIO_SERVICES = 6 + Some(XdsPacketType::AudioServices) => { + was_proc = 1; + // No sample available - nothing to do + } + + // XDS_TYPE_CGMS = 8 + Some(XdsPacketType::Cgms) => { + was_proc = 1; + xds_do_copy_generation_management_system( + sub, + ctx, + payload[2] as u32, + payload[3] as u32, + ); + } + + // XDS_TYPE_ASPECT_RATIO_INFO = 9 + Some(XdsPacketType::AspectRatioInfo) => { + was_proc = 1; + if ctx.cur_xds_payload_length < 5 { + // We need 2 data bytes + return was_proc; + } + + // Bit 6 must be 1 (note: C code checks bit 5, 0x20 = bit 5) + if (payload[2] & 0x20) == 0 || (payload[3] & 0x20) == 0 { + return was_proc; + } + + // CEA-608-B: The starting line is computed by adding 22 to the decimal number + // represented by bits S0 to S5. The ending line is computing by subtracting + // the decimal number represented by bits E0 to E5 from 262 + let ar_start = ((payload[2] & 0x1F) as u32) + 22; + let ar_end = 262 - ((payload[3] & 0x1F) as u32); + let active_picture_height = ar_end - ar_start; + let aspect_ratio = 320.0 / active_picture_height as f32; + + if ar_start != ctx.current_ar_start { + ctx.current_ar_start = ar_start; + ctx.current_ar_end = ar_end; + info!( + "\rXDS Notice: Aspect ratio info, start line={}, end line={}\n", + ar_start, ar_end + ); + info!( + "\rXDS Notice: Aspect ratio info, active picture height={}, ratio={}\n", + active_picture_height, aspect_ratio + ); + } else { + debug!( + msg_type = DebugMessageFlag::DECODER_XDS; + "\rXDS Notice: Aspect ratio info, start line={}, end line={}\n", + ar_start, ar_end + ); + debug!( + msg_type = DebugMessageFlag::DECODER_XDS; + "\rXDS Notice: Aspect ratio info, active picture height={}, ratio={}\n", + active_picture_height, aspect_ratio + ); + } + } + + // XDS_TYPE_PROGRAM_DESC_1 through XDS_TYPE_PROGRAM_DESC_8 = 0x10-0x17 + Some(ptype) if ptype.is_program_desc() => { + was_proc = 1; + + // Extract description from payload + let desc_end = (ctx.cur_xds_payload_length - 1) as usize; + let desc_slice = &payload[2..desc_end]; + let desc_bytes = match desc_slice.iter().position(|&b| b == 0) { + Some(pos) => &desc_slice[..pos], + None => desc_slice, + }; + + let xds_desc = String::from_utf8_lossy(desc_bytes).to_string(); + + if !xds_desc.is_empty() { + let line_num = ctx.cur_xds_packet_type - 0x10; // XDS_TYPE_PROGRAM_DESC_1 = 0x10 + + // Get current description for this line + let current_desc = if (line_num as usize) < 8 { + i8_array_to_string(&ctx.xds_program_description[line_num as usize]) + } else { + String::new() + }; + + let changed = xds_desc != current_desc; + + if changed { + info!("\rXDS description line {}: {}\n", line_num, xds_desc); + if (line_num as usize) < 8 { + string_to_i8_array( + &xds_desc, + &mut ctx.xds_program_description[line_num as usize], + ); + } + } else { + debug!( + msg_type = DebugMessageFlag::DECODER_XDS; + "\rXDS description line {}: {}\n", line_num, xds_desc + ); + } + + let desc_msg = format!("XDS description line {}: {}", line_num, xds_desc); + let _ = xdsprint(sub, ctx, desc_msg); + + send_gui(GuiXdsMessage::ProgramDescription { + line_num, + desc: &xds_desc, + }); + } + } + + _ => { + // Unknown packet type + } + } + + was_proc +} + +/// Processes channel-related XDS data (network name, call letters, TSID) +/// +/// # Safety +/// - `sub` must be a valid, non-null pointer to a `cc_subtitle` struct +/// - `ctx` must be a valid, non-null pointer to a `ccx_decoders_xds_context` struct +/// - `ctx.cur_xds_payload` must point to valid memory with at least `ctx.cur_xds_payload_length` bytes +/// - Both pointers must remain valid for the duration of the call +pub unsafe fn xds_do_channel( + sub: &mut crate::bindings::cc_subtitle, + ctx: &mut CcxDecodersXdsContext, +) -> i32 { + let mut was_proc = 0; + + // Safety check for payload + if ctx.cur_xds_payload.is_null() { + return -1; // CCX_EINVAL equivalent + } + + let payload = + std::slice::from_raw_parts(ctx.cur_xds_payload, ctx.cur_xds_payload_length as usize); + + let packet_type = XdsPacketType::from_channel(ctx.cur_xds_packet_type); + + match packet_type { + // XDS_TYPE_NETWORK_NAME = 1 + Some(XdsPacketType::NetworkName) => { + was_proc = 1; + + // Extract network name from payload (bytes 2 to payload_length - 2) + let name_end = (ctx.cur_xds_payload_length - 1) as usize; + let name_slice = &payload[2..name_end]; + let name_bytes = match name_slice.iter().position(|&b| b == 0) { + Some(pos) => &name_slice[..pos], + None => name_slice, + }; + + let xds_network_name = String::from_utf8_lossy(name_bytes).to_string(); + + debug!( + msg_type = DebugMessageFlag::DECODER_XDS; + "XDS Network name: {}\n", xds_network_name + ); + + let network_msg = format!("Network: {}", xds_network_name); + let _ = xdsprint(sub, ctx, network_msg); + + // Convert current_xds_network_name from i8 array to String for comparison + let current_name = i8_array_to_string(&ctx.current_xds_network_name); + + if xds_network_name != current_name { + // Change of station + info!("XDS Notice: Network is now {}\n", xds_network_name); + string_to_i8_array(&xds_network_name, &mut ctx.current_xds_network_name); + } + } + + // XDS_TYPE_CALL_LETTERS_AND_CHANNEL = 2 + Some(XdsPacketType::CallLettersAndChannel) => { + was_proc = 1; + + // We need 4-6 data bytes (payload_length 7 or 9) + if ctx.cur_xds_payload_length != 7 && ctx.cur_xds_payload_length != 9 { + return was_proc; + } + + // Extract call letters from payload (bytes 2 to payload_length - 2) + let letters_end = (ctx.cur_xds_payload_length - 1) as usize; + let letters_slice = &payload[2..letters_end]; + let letters_bytes = match letters_slice.iter().position(|&b| b == 0) { + Some(pos) => &letters_slice[..pos], + None => letters_slice, + }; + + let xds_call_letters = String::from_utf8_lossy(letters_bytes).to_string(); + + debug!( + msg_type = DebugMessageFlag::DECODER_XDS; + "XDS Network call letters: {}\n", xds_call_letters + ); + + let letters_msg = format!("Call Letters: {}", xds_call_letters); + let _ = xdsprint(sub, ctx, letters_msg); + + // Convert current_xds_call_letters from i8 array to String for comparison + let current_letters = i8_array_to_string(&ctx.current_xds_call_letters); + + if xds_call_letters != current_letters { + // Change of station + info!( + "XDS Notice: Network call letters now {}\n", + xds_call_letters + ); + string_to_i8_array(&xds_call_letters, &mut ctx.current_xds_call_letters); + send_gui(GuiXdsMessage::CallLetters(&xds_call_letters)); + } + } + + // XDS_TYPE_TSID = 4 + Some(XdsPacketType::Tsid) => { + // According to CEA-608, data here (4 bytes) are used to identify the + // "originating analog licensee". No interesting data for us. + was_proc = 1; + + if ctx.cur_xds_payload_length < 7 { + // We need 4 data bytes + return was_proc; + } + + // Only low 4 bits from each byte + let b1 = (payload[2] & 0x10) as u32; + let b2 = (payload[3] & 0x10) as u32; + let b3 = (payload[4] & 0x10) as u32; + let b4 = (payload[5] & 0x10) as u32; + let tsid = (b4 << 12) | (b3 << 8) | (b2 << 4) | b1; + + if tsid != 0 { + let tsid_msg = format!("TSID: {}", tsid); + let _ = xdsprint(sub, ctx, tsid_msg); + } + } + + _ => { + // Unknown packet type + } + } + + was_proc +} + +/// Processes XDS Private Data class packets. +/// +/// # Safety +/// - `sub` must be a valid, non-null pointer to a `cc_subtitle` struct +/// - `ctx` must be a valid, non-null pointer to a `ccx_decoders_xds_context` struct +/// - `ctx.cur_xds_payload` must point to valid memory with at least `ctx.cur_xds_payload_length` bytes +/// - Both pointers must remain valid for the duration of the call +pub unsafe fn xds_do_private_data( + sub: &mut crate::bindings::cc_subtitle, + ctx: &mut CcxDecodersXdsContext, +) -> i32 { + // Safety check for payload + if ctx.cur_xds_payload.is_null() { + return -1; // CCX_EINVAL equivalent + } + + let payload = + std::slice::from_raw_parts(ctx.cur_xds_payload, ctx.cur_xds_payload_length as usize); + + // Build hex string from payload bytes (bytes 2 to payload_length - 2) + let hex_str: String = payload[2..(ctx.cur_xds_payload_length - 1) as usize] + .iter() + .map(|b| format!("{:02X} ", b)) + .collect(); + + let _ = xdsprint(sub, ctx, hex_str); + + 1 +} + +/// Processes XDS Miscellaneous class packets. +/// +/// # Safety +/// - `ctx` must be a valid, non-null pointer to a `ccx_decoders_xds_context` struct +/// - `ctx.cur_xds_payload` must point to valid memory with at least `ctx.cur_xds_payload_length` bytes +/// - The pointer must remain valid for the duration of the call +pub unsafe fn xds_do_misc(ctx: &mut CcxDecodersXdsContext) -> i32 { + let was_proc; + + // Safety check for payload + if ctx.cur_xds_payload.is_null() { + return -1; // CCX_EINVAL equivalent + } + + let payload = + std::slice::from_raw_parts(ctx.cur_xds_payload, ctx.cur_xds_payload_length as usize); + + let packet_type = XdsPacketType::from_misc(ctx.cur_xds_packet_type); + + match packet_type { + // XDS_TYPE_TIME_OF_DAY = 1 + Some(XdsPacketType::TimeOfDay) => { + was_proc = 1; + if ctx.cur_xds_payload_length < 9 { + // We need 6 data bytes + return was_proc; + } + + let min = (payload[2] & 0x3f) as i32; // 6 bits + let hour = (payload[3] & 0x1f) as i32; // 5 bits + let date = (payload[4] & 0x1f) as i32; // 5 bits + let month = (payload[5] & 0x0f) as i32; // 4 bits + let reset_seconds = (payload[5] & 0x20) != 0; + let day_of_week = (payload[6] & 0x07) as i32; + let year = ((payload[7] & 0x3f) as i32) + 1990; + + debug!( + msg_type = DebugMessageFlag::DECODER_XDS; + "Time of day: (YYYY/MM/DD) {:04}/{:02}/{:02} (HH:SS) {:02}:{:02} DoW: {} Reset seconds: {}\n", + year, month, date, hour, min, day_of_week, if reset_seconds { 1 } else { 0 } + ); + } + + // XDS_TYPE_LOCAL_TIME_ZONE = 4 + Some(XdsPacketType::LocalTimeZone) => { + was_proc = 1; + if ctx.cur_xds_payload_length < 5 { + // We need 2 data bytes + return was_proc; + } + + // let b6 = (payload[2] & 0x40) >> 6; // Bit 6 should always be 1 + let dst = (payload[2] & 0x20) >> 5; // Daylight Saving Time + let hour = (payload[2] & 0x1f) as i32; // 5 bits + + debug!( + msg_type = DebugMessageFlag::DECODER_XDS; + "Local Time Zone: {:02} DST: {}\n", + hour, dst + ); + } + + _ => { + was_proc = 0; + } + } + + was_proc +} + +/// Processes the end of an XDS packet, validates checksum, and dispatches to appropriate handler. +/// +/// # Safety +/// This function is marked unsafe because it dereferences raw pointers from +/// the C bindings in `cc_subtitle`. The caller must ensure: +/// * `sub` points to a valid, properly initialized `cc_subtitle` structure +/// * The `data` field in `sub` is either null or points to valid memory +/// * No other code is concurrently modifying the subtitle structure +pub unsafe fn do_end_of_xds( + sub: &mut crate::bindings::cc_subtitle, + ctx: &mut CcxDecodersXdsContext, + expected_checksum: u8, +) { + // Check if buffer index is valid and in use + if ctx.cur_xds_buffer_idx == -1 { + return; + } + + let idx = ctx.cur_xds_buffer_idx as usize; + if idx >= ctx.xds_buffers.len() || ctx.xds_buffers[idx].in_use == 0 { + return; + } + + // Set up context from buffer + ctx.cur_xds_packet_class = ctx.xds_buffers[idx] + .xds_class + .map(|c| c as i32) + .unwrap_or(-1); + ctx.cur_xds_payload = ctx.xds_buffers[idx].bytes.as_mut_ptr(); + ctx.cur_xds_payload_length = ctx.xds_buffers[idx].used_bytes as i32; + + // Get packet type from payload[1] + if ctx.cur_xds_payload_length >= 2 { + ctx.cur_xds_packet_type = ctx.xds_buffers[idx].bytes[1] as i32; + } + + // Add the end byte (0x0F) to the packet + if (ctx.cur_xds_payload_length as usize) < NUM_BYTES_PER_PACKET { + ctx.xds_buffers[idx].bytes[ctx.cur_xds_payload_length as usize] = 0x0F; + ctx.cur_xds_payload_length += 1; + ctx.xds_buffers[idx].used_bytes = ctx.cur_xds_payload_length as u8; + } + + // Calculate checksum + let mut cs: i32 = 0; + for i in 0..ctx.cur_xds_payload_length as usize { + let byte = ctx.xds_buffers[idx].bytes[i]; + cs = (cs + byte as i32) & 0x7f; // Keep 7 bits only + let c = byte & 0x7F; + let display_char = if c >= 0x20 { c as char } else { '?' }; + debug!( + msg_type = DebugMessageFlag::DECODER_XDS; + "{:02X} - {} cs: {:02X}\n", c, display_char, cs + ); + } + cs = (128 - cs) & 0x7F; // Convert to 2's complement & discard high-order bit + + // Get class name for debug output + let class_name = if (ctx.cur_xds_packet_class as usize) < XDS_CLASSES.len() { + XDS_CLASSES[ctx.cur_xds_packet_class as usize] + } else { + "Unknown" + }; + + debug!( + msg_type = DebugMessageFlag::DECODER_XDS; + "End of XDS. Class={} ({}), size={} Checksum OK: {} Used buffers: {}\n", + ctx.cur_xds_packet_class, + class_name, + ctx. cur_xds_payload_length, + cs == expected_checksum as i32, + ctx.xds_buffers. iter().filter(|b| b.in_use != 0).count() + ); + + // Validate checksum and minimum length + if cs != expected_checksum as i32 { + debug!( + msg_type = DebugMessageFlag::DECODER_XDS; + "Expected checksum: {:02X} Calculated: {:02X}\n", expected_checksum, cs + ); + ctx.xds_buffers[idx] = XdsBuffer::default(); // clear_xds_buffer + return; // Bad packets ignored as per specs + } + + let mut was_proc = 0; + + // Check if bit 6 is set for out-of-band data + if (ctx.cur_xds_packet_type & 0x40) != 0 { + ctx.cur_xds_packet_class = XdsClass::OutOfBand as i32; + } + + // Convert cur_xds_packet_class to XdsClass for matching + let xds_class = match ctx.cur_xds_packet_class { + x if x == XdsClass::Current as i32 => Some(XdsClass::Current), + x if x == XdsClass::Future as i32 => Some(XdsClass::Future), + x if x == XdsClass::Channel as i32 => Some(XdsClass::Channel), + x if x == XdsClass::Misc as i32 => Some(XdsClass::Misc), + x if x == XdsClass::Private as i32 => Some(XdsClass::Private), + x if x == XdsClass::OutOfBand as i32 => Some(XdsClass::OutOfBand), + _ => None, + }; + + match xds_class { + Some(XdsClass::Future) => { + // Info on future program + // Check if debug mask includes DECODER_XDS + if let Some(logger) = lib_ccxr::util::log::logger() { + if !logger.has_debug_flag(DebugMessageFlag::DECODER_XDS) { + // Don't bother processing something we don't need + was_proc = 1; + } else { + // Fall through to current processing + was_proc = xds_do_current_and_future(sub, ctx); + } + } else { + was_proc = 1; + } + } + + Some(XdsClass::Current) => { + // Info on current program + was_proc = xds_do_current_and_future(sub, ctx); + } + + Some(XdsClass::Channel) => { + was_proc = xds_do_channel(sub, ctx); + } + + Some(XdsClass::Misc) => { + was_proc = xds_do_misc(ctx); + } + + Some(XdsClass::Private) => { + // CEA-608: The Private Data Class is for use in any closed system + // for whatever that system wishes. It shall not be defined by this + // standard now or in the future. + was_proc = xds_do_private_data(sub, ctx); + } + + Some(XdsClass::OutOfBand) => { + debug!( + msg_type = DebugMessageFlag::DECODER_XDS; + "Out-of-band data, ignored." + ); + was_proc = 1; + } + + _ => { + // Unknown class + } + } + + if was_proc == 0 { + info!("Note: We found a currently unsupported XDS packet.\n"); + // Dump the payload for debugging + let payload_slice = &ctx.xds_buffers[idx].bytes[0..ctx.cur_xds_payload_length as usize]; + hex_dump(DebugMessageFlag::DECODER_XDS, payload_slice, false); + } + + // Clear the buffer + ctx.xds_buffers[idx] = XdsBuffer::default(); +} diff --git a/src/rust/src/xds/mod.rs b/src/rust/src/xds/mod.rs new file mode 100644 index 000000000..37c4de0ec --- /dev/null +++ b/src/rust/src/xds/mod.rs @@ -0,0 +1,108 @@ +//! XDS (Extended Data Services) decoder module for processing CEA-608 extended data. +//! +//! This module provides Rust implementations for decoding XDS packets embedded in +//! closed caption streams, including program information, content ratings, network +//! identification, and time-of-day data. +//! +//! # Submodules +//! +//! - [`handlers`] - XDS packet processing and dispatch functions +//! - [`types`] - XDS-specific types +//! - [`constants`] - XDS enums and constants +//! +//! For detailed function-level mappings, see the [`handlers`] module documentation +//! For type definitions, see the [`types`] module + +pub mod constants; +pub mod handlers; +pub mod types; + +use crate::bindings::*; +use crate::ctorust::FromCType; +use crate::libccxr_exports::time::apply_timing_info; +use crate::libccxr_exports::time::generate_timing_context; +use crate::xds::constants::TS_START_OF_XDS; +use crate::xds::handlers::do_end_of_xds; +use crate::xds::types::{copy_xds_context_from_rust_to_c, CcxDecodersXdsContext}; +use std::os::raw::c_int; +use std::sync::atomic::Ordering; + +/// FFI wrapper for `do_end_of_xds`. +/// +/// Finalizes XDS packet processing, validates checksum, and dispatches to appropriate handler. +/// +/// # Safety +/// - `sub` must be a valid, non-null pointer to a `cc_subtitle` struct. +/// - `ctx` must be a valid, non-null pointer to a `ccx_decoders_xds_context` struct. +/// - The caller must ensure both pointers remain valid for the duration of the call. +#[no_mangle] +pub unsafe extern "C" fn ccxr_do_end_of_xds( + sub: *mut cc_subtitle, + ctx: *mut ccx_decoders_xds_context, + expected_checksum: u8, +) { + // Null pointer checks + if sub.is_null() || ctx.is_null() { + return; + } + + // trying to sync cb_field2 etc globals from C to GLOBAL_TIMING_INFO. hope this works + apply_timing_info(); + + // Convert C context to Rust + let mut rust_ctx = match CcxDecodersXdsContext::from_ctype(*ctx) { + Some(c) => c, + None => return, + }; + + // populate timing from the C ctx's timing ptr + let c_timing_ptr = (*ctx).timing; + let mut timing_ctx = if !c_timing_ptr.is_null() { + Some(generate_timing_context(c_timing_ptr)) + } else { + None + }; + rust_ctx.timing = timing_ctx.as_mut(); + + // Call the Rust implementation + do_end_of_xds(&mut *sub, &mut rust_ctx, expected_checksum); + + // Write changes back to C context + copy_xds_context_from_rust_to_c(ctx, &rust_ctx); +} + +/// FFI wrapper for `process_xds_bytes`. +/// +/// Processes a pair of XDS data bytes, managing buffer allocation and packet state. +/// +/// # Safety +/// - `ctx` must be a valid, non-null pointer to a `ccx_decoders_xds_context` struct. +/// - The caller must ensure the pointer remains valid for the duration of the call. +#[no_mangle] +pub unsafe extern "C" fn ccxr_process_xds_bytes( + ctx: *mut ccx_decoders_xds_context, + hi: u8, + lo: c_int, +) { + if ctx.is_null() { + return; + } + + // Convert C context to Rust + let mut rust_ctx = match CcxDecodersXdsContext::from_ctype(*ctx) { + Some(c) => c, + None => return, + }; + + // Process in Rust + rust_ctx.process_xds_bytes(hi, lo as u8); + + // Write changes back to C + copy_xds_context_from_rust_to_c(ctx, &rust_ctx); +} + +/// setter function for TS_START_OF_XDS from C code +#[no_mangle] +pub extern "C" fn ccxr_set_ts_start_of_xds(value: i64) { + TS_START_OF_XDS.store(value, Ordering::SeqCst); +} diff --git a/src/rust/src/xds/types.rs b/src/rust/src/xds/types.rs new file mode 100644 index 000000000..1ee561707 --- /dev/null +++ b/src/rust/src/xds/types.rs @@ -0,0 +1,707 @@ +//! XDS (Extended Data Services) types and structures for decoding extended data. +//! +//! This module provides types for handling XDS packets which carry metadata about programs, +//! channels, and other broadcast information embedded in the vertical blanking interval. +//! +//! # Key Types +//! +//! - [`XdsBuffer`] - Buffer for accumulating XDS packet bytes +//! - [`CcxDecodersXdsContext`] - Main context structure for XDS decoding state +//! +//! # Conversion Guide +//! +//! | C (ccx_decoders_xds.c/.h) | Rust (types.rs) | +//! |------------------------------------|-----------------------------------------------| +//! | `struct xds_buffer` | [`XdsBuffer`] | +//! | `ccx_decoders_xds_context` | [`CcxDecodersXdsContext`] | +//! | `clear_xds_buffer` | [`CcxDecodersXdsContext::clear_xds_buffer`] | +//! | `how_many_used` | [`CcxDecodersXdsContext::how_many_used`] | +//! | `process_xds_bytes` | [`CcxDecodersXdsContext::process_xds_bytes`] | +//! | C struct -> Rust | [`CcxDecodersXdsContext::from_ctype`] | +//! | Rust -> C struct | [`copy_xds_context_from_rust_to_c`] | + +use crate::bindings::*; +use crate::common::CType; +use crate::ctorust::FromCType; +use crate::libccxr_exports::time::write_back_to_common_timing_ctx; +pub use crate::xds::constants::*; +use lib_ccxr::time::TimingContext; + +#[repr(C)] +#[derive(Copy, Clone)] +pub struct XdsBuffer { + pub in_use: u32, + pub xds_class: Option, + pub xds_type: Option, + pub bytes: [u8; NUM_BYTES_PER_PACKET], // Class + type (repeated for convenience) + data + zero + pub used_bytes: u8, +} + +impl Default for XdsBuffer { + fn default() -> Self { + XdsBuffer { + in_use: 0, + xds_class: None, + xds_type: None, + bytes: [0; NUM_BYTES_PER_PACKET], + used_bytes: 0, + } + } +} + +impl FromCType for XdsBuffer { + unsafe fn from_ctype(c_value: xds_buffer) -> Option { + let xds_class = if c_value.xds_class == -1 { + None + } else { + XdsClass::from_c_int(c_value.xds_class) + }; + + let xds_type = if c_value.xds_type == -1 { + None + } else { + XdsType::from_c_int(xds_class, c_value.xds_type) + }; + + Some(XdsBuffer { + in_use: c_value.in_use, + xds_class, + xds_type, + bytes: c_value.bytes, + used_bytes: c_value.used_bytes, + }) + } +} + +impl CType for XdsBuffer { + unsafe fn to_ctype(&self) -> xds_buffer { + xds_buffer { + in_use: self.in_use, + xds_class: self.xds_class.map(|c| c.to_c_int()).unwrap_or(-1), + xds_type: self.xds_type.map(|t| t.to_c_int()).unwrap_or(-1), + bytes: self.bytes, + used_bytes: self.used_bytes, + } + } +} + +#[repr(C)] +pub struct CcxDecodersXdsContext<'a> { + // Program Identification Number (Start Time) for current program + pub current_xds_min: i32, + pub current_xds_hour: i32, + pub current_xds_date: i32, + pub current_xds_month: i32, + pub current_program_type_reported: i32, // No. + pub xds_start_time_shown: i32, + pub xds_program_length_shown: i32, + pub xds_program_description: [[i8; 33]; 8], + pub current_xds_network_name: [i8; 33], + pub current_xds_program_name: [i8; 33], + pub current_xds_call_letters: [i8; 7], + pub current_xds_program_type: [i8; 33], + + pub xds_buffers: [XdsBuffer; NUM_XDS_BUFFERS], + pub cur_xds_buffer_idx: i32, + pub cur_xds_packet_class: i32, + pub cur_xds_payload: *mut u8, + pub cur_xds_payload_length: i32, + pub cur_xds_packet_type: i32, + pub timing: Option<&'a mut TimingContext>, + pub current_ar_start: u32, + pub current_ar_end: u32, + pub xds_write_to_file: bool, +} + +impl Default for CcxDecodersXdsContext<'_> { + fn default() -> Self { + CcxDecodersXdsContext { + current_xds_min: 0, + current_xds_hour: 0, + current_xds_date: 0, + current_xds_month: 0, + current_program_type_reported: 0, + xds_start_time_shown: 0, + xds_program_length_shown: 0, + xds_program_description: [[0; 33]; 8], + current_xds_network_name: [0; 33], + current_xds_program_name: [0; 33], + current_xds_call_letters: [0; 7], + current_xds_program_type: [0; 33], + xds_buffers: [XdsBuffer::default(); NUM_XDS_BUFFERS], + cur_xds_buffer_idx: 0, + cur_xds_packet_class: 0, + cur_xds_payload: std::ptr::null_mut(), + cur_xds_payload_length: 0, + cur_xds_packet_type: 0, + timing: None, + current_ar_start: 0, + current_ar_end: 0, + xds_write_to_file: false, + } + } +} + +impl FromCType for CcxDecodersXdsContext<'_> { + unsafe fn from_ctype(c_value: ccx_decoders_xds_context) -> Option { + let mut xds_buffers = [XdsBuffer::default(); NUM_XDS_BUFFERS]; + + // Convert each xds_buffer from C to Rust + for (i, c_buffer) in c_value.xds_buffers.iter().enumerate() { + if let Some(rust_buffer) = XdsBuffer::from_ctype(*c_buffer) { + xds_buffers[i] = rust_buffer; + } + } + + Some(CcxDecodersXdsContext { + current_xds_min: c_value.current_xds_min, + current_xds_hour: c_value.current_xds_hour, + current_xds_date: c_value.current_xds_date, + current_xds_month: c_value.current_xds_month, + current_program_type_reported: c_value.current_program_type_reported, + xds_start_time_shown: c_value.xds_start_time_shown, + xds_program_length_shown: c_value.xds_program_length_shown, + xds_program_description: c_value.xds_program_description, + current_xds_network_name: c_value.current_xds_network_name, + current_xds_program_name: c_value.current_xds_program_name, + current_xds_call_letters: c_value.current_xds_call_letters, + current_xds_program_type: c_value.current_xds_program_type, + xds_buffers, + cur_xds_buffer_idx: c_value.cur_xds_buffer_idx, + cur_xds_packet_class: c_value.cur_xds_packet_class, + cur_xds_payload: c_value.cur_xds_payload, + cur_xds_payload_length: c_value.cur_xds_payload_length, + cur_xds_packet_type: c_value.cur_xds_packet_type, + timing: None, // Cannot directly convert raw pointer to reference - needs to be handled separately + current_ar_start: c_value.current_ar_start, + current_ar_end: c_value.current_ar_end, + xds_write_to_file: c_value.xds_write_to_file != 0, + }) + } +} + +/// # Safety +/// +/// - `bitstream_ptr` must be non-null and point to uniquely writable memory for a +/// `ccx_decoders_xds_context` for the duration of the call. +/// - `rust_ctx` must be valid and C-layout compatible for all fields copied. +/// - If `rust_ctx.cur_xds_payload` is non-null it must be valid for +/// `rust_ctx.cur_xds_payload_length` bytes. +/// - If `rust_ctx.timing` is `Some`, the C-side `timing` pointer in `bitstream_ptr` +/// must be a valid destination for `write_back_to_common_timing_ctx`. +/// - Violating these invariants is undefined behaviour; call only from `unsafe`. +pub unsafe fn copy_xds_context_from_rust_to_c( + bitstream_ptr: *mut ccx_decoders_xds_context, + rust_ctx: &CcxDecodersXdsContext<'_>, +) { + if bitstream_ptr.is_null() { + return; + } + let original_timing = (*bitstream_ptr).timing; + let output = ccx_decoders_xds_context { + current_xds_min: rust_ctx.current_xds_min, + current_xds_hour: rust_ctx.current_xds_hour, + current_xds_date: rust_ctx.current_xds_date, + current_xds_month: rust_ctx.current_xds_month, + current_program_type_reported: rust_ctx.current_program_type_reported, + xds_start_time_shown: rust_ctx.xds_start_time_shown, + xds_program_length_shown: rust_ctx.xds_program_length_shown, + xds_program_description: rust_ctx.xds_program_description, + current_xds_network_name: rust_ctx.current_xds_network_name, + current_xds_program_name: rust_ctx.current_xds_program_name, + current_xds_call_letters: rust_ctx.current_xds_call_letters, + current_xds_program_type: rust_ctx.current_xds_program_type, + xds_buffers: rust_ctx.xds_buffers.map(|buf| buf.to_ctype()), + cur_xds_buffer_idx: rust_ctx.cur_xds_buffer_idx, + cur_xds_packet_class: rust_ctx.cur_xds_packet_class, + cur_xds_payload: rust_ctx.cur_xds_payload, + cur_xds_payload_length: rust_ctx.cur_xds_payload_length, + cur_xds_packet_type: rust_ctx.cur_xds_packet_type, + timing: original_timing, + current_ar_start: rust_ctx.current_ar_start, + current_ar_end: rust_ctx.current_ar_end, + xds_write_to_file: if rust_ctx.xds_write_to_file { 1 } else { 0 }, + }; + std::ptr::write(bitstream_ptr, output); + if let Some(ref timing_ctx) = rust_ctx.timing { + write_back_to_common_timing_ctx((*bitstream_ptr).timing, timing_ctx); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + // --- XdsClass --- + + #[test] + fn test_xds_class_from_c_int_valid() { + assert_eq!(XdsClass::from_c_int(0), Some(XdsClass::Current)); + assert_eq!(XdsClass::from_c_int(1), Some(XdsClass::Future)); + assert_eq!(XdsClass::from_c_int(2), Some(XdsClass::Channel)); + assert_eq!(XdsClass::from_c_int(3), Some(XdsClass::Misc)); + assert_eq!(XdsClass::from_c_int(4), Some(XdsClass::Public)); + assert_eq!(XdsClass::from_c_int(5), Some(XdsClass::Reserved)); + assert_eq!(XdsClass::from_c_int(6), Some(XdsClass::Private)); + assert_eq!(XdsClass::from_c_int(7), Some(XdsClass::End)); + assert_eq!(XdsClass::from_c_int(0x40), Some(XdsClass::OutOfBand)); + } + + #[test] + fn test_xds_class_from_c_int_invalid() { + assert_eq!(XdsClass::from_c_int(-1), None); + assert_eq!(XdsClass::from_c_int(8), None); + assert_eq!(XdsClass::from_c_int(100), None); + } + + #[test] + fn test_xds_class_roundtrip() { + let classes = [ + XdsClass::Current, + XdsClass::Future, + XdsClass::Channel, + XdsClass::Misc, + XdsClass::Public, + XdsClass::Reserved, + XdsClass::Private, + XdsClass::End, + XdsClass::OutOfBand, + ]; + for class in &classes { + let c_int = class.to_c_int(); + let roundtripped = XdsClass::from_c_int(c_int).unwrap(); + assert_eq!(*class, roundtripped); + } + } + + // --- XdsType --- + + #[test] + fn test_xds_type_current_future_from_c_int() { + let class = Some(XdsClass::Current); + assert_eq!( + XdsType::from_c_int(class, 1), + Some(XdsType::CurrentFuture(XdsCurrentFutureType::PinStartTime)) + ); + assert_eq!( + XdsType::from_c_int(class, 2), + Some(XdsType::CurrentFuture( + XdsCurrentFutureType::LengthAndCurrentTime + )) + ); + assert_eq!( + XdsType::from_c_int(class, 3), + Some(XdsType::CurrentFuture(XdsCurrentFutureType::ProgramName)) + ); + assert_eq!( + XdsType::from_c_int(class, 4), + Some(XdsType::CurrentFuture(XdsCurrentFutureType::ProgramType)) + ); + assert_eq!( + XdsType::from_c_int(class, 5), + Some(XdsType::CurrentFuture( + XdsCurrentFutureType::ContentAdvisory + )) + ); + assert_eq!( + XdsType::from_c_int(class, 6), + Some(XdsType::CurrentFuture(XdsCurrentFutureType::AudioServices)) + ); + assert_eq!( + XdsType::from_c_int(class, 8), + Some(XdsType::CurrentFuture(XdsCurrentFutureType::Cgms)) + ); + assert_eq!( + XdsType::from_c_int(class, 9), + Some(XdsType::CurrentFuture( + XdsCurrentFutureType::AspectRatioInfo + )) + ); + for i in 0x10..=0x17 { + assert!(XdsType::from_c_int(class, i).is_some()); + } + assert_eq!(XdsType::from_c_int(class, 7), None); + assert_eq!(XdsType::from_c_int(class, 0x18), None); + } + + #[test] + fn test_xds_type_future_class_same_as_current() { + for type_val in [ + 1, 2, 3, 4, 5, 6, 8, 9, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, + ] { + assert_eq!( + XdsType::from_c_int(Some(XdsClass::Current), type_val), + XdsType::from_c_int(Some(XdsClass::Future), type_val) + ); + } + } + + #[test] + fn test_xds_type_channel_from_c_int() { + let class = Some(XdsClass::Channel); + assert_eq!( + XdsType::from_c_int(class, 1), + Some(XdsType::Channel(XdsChannelType::NetworkName)) + ); + assert_eq!( + XdsType::from_c_int(class, 2), + Some(XdsType::Channel(XdsChannelType::CallLettersAndChannel)) + ); + assert_eq!( + XdsType::from_c_int(class, 4), + Some(XdsType::Channel(XdsChannelType::Tsid)) + ); + assert_eq!(XdsType::from_c_int(class, 3), None); + assert_eq!(XdsType::from_c_int(class, 5), None); + } + + #[test] + fn test_xds_type_misc_from_c_int() { + let class = Some(XdsClass::Misc); + assert_eq!( + XdsType::from_c_int(class, 1), + Some(XdsType::Misc(XdsMiscType::TimeOfDay)) + ); + assert_eq!( + XdsType::from_c_int(class, 4), + Some(XdsType::Misc(XdsMiscType::LocalTimeZone)) + ); + assert_eq!( + XdsType::from_c_int(class, 0x40), + Some(XdsType::Misc(XdsMiscType::OutOfBandChannelNumber)) + ); + assert_eq!(XdsType::from_c_int(class, 2), None); + } + + #[test] + fn test_xds_type_unsupported_classes() { + for class in [ + XdsClass::Public, + XdsClass::Reserved, + XdsClass::Private, + XdsClass::End, + XdsClass::OutOfBand, + ] { + assert_eq!(XdsType::from_c_int(Some(class), 1), None); + } + } + + #[test] + fn test_xds_type_none_class() { + assert_eq!(XdsType::from_c_int(None, 1), None); + } + + #[test] + fn test_xds_type_roundtrip_current_future() { + let class = Some(XdsClass::Current); + for type_val in [ + 1, 2, 3, 4, 5, 6, 8, 9, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, + ] { + let xds_type = XdsType::from_c_int(class, type_val).unwrap(); + assert_eq!(xds_type.to_c_int(), type_val); + } + } + + #[test] + fn test_xds_type_roundtrip_channel() { + let class = Some(XdsClass::Channel); + for type_val in [1, 2, 4] { + let xds_type = XdsType::from_c_int(class, type_val).unwrap(); + assert_eq!(xds_type.to_c_int(), type_val); + } + } + + #[test] + fn test_xds_type_roundtrip_misc() { + let class = Some(XdsClass::Misc); + for type_val in [1, 4, 0x40] { + let xds_type = XdsType::from_c_int(class, type_val).unwrap(); + assert_eq!(xds_type.to_c_int(), type_val); + } + } + + // --- XdsBuffer --- + + fn make_c_buf(in_use: u32, xds_class: i32, xds_type: i32, used_bytes: u8) -> xds_buffer { + let mut bytes = [0u8; NUM_BYTES_PER_PACKET]; + bytes[0] = 0x01; + bytes[1] = 0x03; + xds_buffer { + in_use, + xds_class, + xds_type, + bytes, + used_bytes, + } + } + + #[test] + fn test_xds_buffer_from_ctype_valid_current_program_name() { + let c_buf = make_c_buf(1, 0, 3, 4); + let rust_buf = unsafe { XdsBuffer::from_ctype(c_buf) }.unwrap(); + assert_eq!(rust_buf.in_use, 1); + assert_eq!(rust_buf.xds_class, Some(XdsClass::Current)); + assert_eq!( + rust_buf.xds_type, + Some(XdsType::CurrentFuture(XdsCurrentFutureType::ProgramName)) + ); + assert_eq!(rust_buf.used_bytes, 4); + assert_eq!(rust_buf.bytes[0], 0x01); + assert_eq!(rust_buf.bytes[1], 0x03); + } + + #[test] + fn test_xds_buffer_from_ctype_class_minus1_gives_none() { + let c_buf = make_c_buf(0, -1, -1, 0); + let rust_buf = unsafe { XdsBuffer::from_ctype(c_buf) }.unwrap(); + assert_eq!(rust_buf.xds_class, None); + assert_eq!(rust_buf.xds_type, None); + } + + #[test] + fn test_xds_buffer_from_ctype_type_minus1_gives_none() { + let c_buf = make_c_buf(1, 0, -1, 2); + let rust_buf = unsafe { XdsBuffer::from_ctype(c_buf) }.unwrap(); + assert_eq!(rust_buf.xds_class, Some(XdsClass::Current)); + assert_eq!(rust_buf.xds_type, None); + } + + #[test] + fn test_xds_buffer_from_ctype_invalid_class_gives_none() { + let c_buf = make_c_buf(1, 99, 1, 2); + let rust_buf = unsafe { XdsBuffer::from_ctype(c_buf) }.unwrap(); + assert_eq!(rust_buf.xds_class, None); + assert_eq!(rust_buf.xds_type, None); + } + + #[test] + fn test_xds_buffer_from_ctype_invalid_type_for_class_gives_none() { + let c_buf = make_c_buf(1, 2, 99, 2); + let rust_buf = unsafe { XdsBuffer::from_ctype(c_buf) }.unwrap(); + assert_eq!(rust_buf.xds_class, Some(XdsClass::Channel)); + assert_eq!(rust_buf.xds_type, None); + } + + #[test] + fn test_xds_buffer_to_ctype_valid() { + let c_buf = make_c_buf(1, 0, 3, 4); + let rust_buf = unsafe { XdsBuffer::from_ctype(c_buf) }.unwrap(); + let back = unsafe { rust_buf.to_ctype() }; + assert_eq!(back.in_use, 1); + assert_eq!(back.xds_class, 0); + assert_eq!(back.xds_type, 3); + assert_eq!(back.used_bytes, 4); + assert_eq!(back.bytes[0], 0x01); + assert_eq!(back.bytes[1], 0x03); + } + + #[test] + fn test_xds_buffer_to_ctype_none_becomes_minus1() { + let c_buf = make_c_buf(0, -1, -1, 0); + let rust_buf = unsafe { XdsBuffer::from_ctype(c_buf) }.unwrap(); + let back = unsafe { rust_buf.to_ctype() }; + assert_eq!(back.xds_class, -1); + assert_eq!(back.xds_type, -1); + } + + #[test] + fn test_xds_buffer_roundtrip_all_classes() { + let cases: &[(i32, i32)] = &[ + (0, 1), + (0, 2), + (0, 3), + (0, 4), + (0, 5), + (0, 6), + (0, 8), + (0, 9), + (0, 0x10), + (0, 0x17), + (1, 3), // future + programName + (2, 1), + (2, 2), + (2, 4), // channel + (3, 1), + (3, 4), + (3, 0x40), // mics + ]; + for &(class, typ) in cases { + let c_buf = make_c_buf(1, class, typ, 2); + let rust_buf = unsafe { XdsBuffer::from_ctype(c_buf) }.unwrap(); + let back = unsafe { rust_buf.to_ctype() }; + assert_eq!( + back.xds_class, class, + "class mismatch for ({}, {})", + class, typ + ); + assert_eq!(back.xds_type, typ, "type mismatch for ({}, {})", class, typ); + } + } + + #[test] + fn test_xds_buffer_to_ctype_unsupported_class_type_is_none() { + for class in [4i32, 5, 6, 7, 0x40] { + let c_buf = make_c_buf(1, class, 1, 2); + let rust_buf = unsafe { XdsBuffer::from_ctype(c_buf) }.unwrap(); + assert!( + rust_buf.xds_class.is_some(), + "expected class Some for {}", + class + ); + assert_eq!( + rust_buf.xds_type, None, + "expected type None for class {}", + class + ); + let back = unsafe { rust_buf.to_ctype() }; + assert_eq!(back.xds_type, -1, "expected -1 in C for class {}", class); + } + } + + #[test] + fn test_xds_buffer_bytes_preserved_through_roundtrip() { + let mut c_buf = make_c_buf(1, 0, 3, 10); + for (i, b) in c_buf.bytes.iter_mut().enumerate() { + *b = (i as u8).wrapping_mul(7); + } + let rust_buf = unsafe { XdsBuffer::from_ctype(c_buf) }.unwrap(); + let back = unsafe { rust_buf.to_ctype() }; + assert_eq!(c_buf.bytes, back.bytes); + } + + // --- CcxDecodersXdsContext --- + + fn make_c_xds_ctx_zeroed() -> ccx_decoders_xds_context { + unsafe { std::mem::zeroed() } + } + + #[test] + fn test_xds_context_from_ctype_scalars() { + let mut c_ctx = make_c_xds_ctx_zeroed(); + c_ctx.current_xds_min = 42; + c_ctx.current_xds_hour = 13; + c_ctx.current_xds_date = 15; + c_ctx.current_xds_month = 7; + c_ctx.current_program_type_reported = 3; + c_ctx.xds_start_time_shown = 1; + c_ctx.xds_program_length_shown = 2; + c_ctx.cur_xds_buffer_idx = 5; + c_ctx.cur_xds_packet_class = 0; + c_ctx.cur_xds_payload_length = 10; + c_ctx.cur_xds_packet_type = 3; + c_ctx.current_ar_start = 100; + c_ctx.current_ar_end = 200; + + let rust_ctx = unsafe { CcxDecodersXdsContext::from_ctype(c_ctx) }.unwrap(); + assert_eq!(rust_ctx.current_xds_min, 42); + assert_eq!(rust_ctx.current_xds_hour, 13); + assert_eq!(rust_ctx.current_xds_date, 15); + assert_eq!(rust_ctx.current_xds_month, 7); + assert_eq!(rust_ctx.current_program_type_reported, 3); + assert_eq!(rust_ctx.xds_start_time_shown, 1); + assert_eq!(rust_ctx.xds_program_length_shown, 2); + assert_eq!(rust_ctx.cur_xds_buffer_idx, 5); + assert_eq!(rust_ctx.cur_xds_packet_class, 0); + assert_eq!(rust_ctx.cur_xds_payload_length, 10); + assert_eq!(rust_ctx.cur_xds_packet_type, 3); + assert_eq!(rust_ctx.current_ar_start, 100); + assert_eq!(rust_ctx.current_ar_end, 200); + } + + #[test] + fn test_xds_context_from_ctype_write_to_file() { + let mut c_ctx = make_c_xds_ctx_zeroed(); + + c_ctx.xds_write_to_file = 0; + let rust_ctx = unsafe { CcxDecodersXdsContext::from_ctype(c_ctx) }.unwrap(); + assert!(!rust_ctx.xds_write_to_file); + + c_ctx.xds_write_to_file = 1; + let rust_ctx = unsafe { CcxDecodersXdsContext::from_ctype(c_ctx) }.unwrap(); + assert!(rust_ctx.xds_write_to_file); + + c_ctx.xds_write_to_file = 42; + let rust_ctx = unsafe { CcxDecodersXdsContext::from_ctype(c_ctx) }.unwrap(); + assert!(rust_ctx.xds_write_to_file); + } + + #[test] + fn test_xds_context_from_ctype_timing_always_none() { + let c_ctx = make_c_xds_ctx_zeroed(); // timing pointer is null + let rust_ctx = unsafe { CcxDecodersXdsContext::from_ctype(c_ctx) }.unwrap(); + assert!(rust_ctx.timing.is_none()); + } + + #[test] + fn test_xds_context_from_ctype_buffers_converted() { + let mut c_ctx = make_c_xds_ctx_zeroed(); + c_ctx.xds_buffers[0].in_use = 1; + c_ctx.xds_buffers[0].xds_class = 0; // curr + c_ctx.xds_buffers[0].xds_type = 3; // programName + c_ctx.xds_buffers[0].used_bytes = 5; + + let rust_ctx = unsafe { CcxDecodersXdsContext::from_ctype(c_ctx) }.unwrap(); + assert_eq!(rust_ctx.xds_buffers[0].in_use, 1); + assert_eq!(rust_ctx.xds_buffers[0].xds_class, Some(XdsClass::Current)); + assert_eq!( + rust_ctx.xds_buffers[0].xds_type, + Some(XdsType::CurrentFuture(XdsCurrentFutureType::ProgramName)) + ); + assert_eq!(rust_ctx.xds_buffers[0].used_bytes, 5); + } + + #[test] + fn test_copy_xds_context_scalars() { + let mut rust_ctx = CcxDecodersXdsContext::default(); + rust_ctx.current_xds_min = 30; + rust_ctx.current_xds_hour = 9; + rust_ctx.current_xds_date = 25; + rust_ctx.current_xds_month = 12; + rust_ctx.cur_xds_buffer_idx = 3; + rust_ctx.current_ar_start = 50; + rust_ctx.current_ar_end = 150; + + let mut c_ctx = make_c_xds_ctx_zeroed(); + unsafe { copy_xds_context_from_rust_to_c(&mut c_ctx as *mut _, &rust_ctx) }; + + assert_eq!(c_ctx.current_xds_min, 30); + assert_eq!(c_ctx.current_xds_hour, 9); + assert_eq!(c_ctx.current_xds_date, 25); + assert_eq!(c_ctx.current_xds_month, 12); + assert_eq!(c_ctx.cur_xds_buffer_idx, 3); + assert_eq!(c_ctx.current_ar_start, 50); + assert_eq!(c_ctx.current_ar_end, 150); + } + + #[test] + fn test_copy_xds_context_write_to_file() { + let mut c_ctx = make_c_xds_ctx_zeroed(); + + let mut rust_ctx = CcxDecodersXdsContext::default(); + rust_ctx.xds_write_to_file = false; + unsafe { copy_xds_context_from_rust_to_c(&mut c_ctx as *mut _, &rust_ctx) }; + assert_eq!(c_ctx.xds_write_to_file, 0); + + rust_ctx.xds_write_to_file = true; + unsafe { copy_xds_context_from_rust_to_c(&mut c_ctx as *mut _, &rust_ctx) }; + assert_eq!(c_ctx.xds_write_to_file, 1); + } + + #[test] + fn test_copy_xds_context_timing_preserved() { + let mut c_ctx = make_c_xds_ctx_zeroed(); + let sentinel: usize = 0xDEAD_BEEF; + c_ctx.timing = sentinel as *mut _; + + let rust_ctx = CcxDecodersXdsContext::default(); // timing = None + unsafe { copy_xds_context_from_rust_to_c(&mut c_ctx as *mut _, &rust_ctx) }; + + assert_eq!(c_ctx.timing as usize, sentinel); + } + + #[test] + fn test_copy_xds_context_null_ptr_noop() { + let rust_ctx = CcxDecodersXdsContext::default(); + unsafe { copy_xds_context_from_rust_to_c(std::ptr::null_mut(), &rust_ctx) }; + } +} diff --git a/src/rust/wrapper.h b/src/rust/wrapper.h index 25b90c297..51ab5e80e 100644 --- a/src/rust/wrapper.h +++ b/src/rust/wrapper.h @@ -14,3 +14,4 @@ #include "../lib_ccx/ccx_gxf.h" #include "../lib_ccx/ccx_demuxer_mxf.h" #include "../lib_ccx/cc_bitstream.h" +#include "../lib_ccx/ccx_decoders_xds.h" \ No newline at end of file