From b63a29cd2ee24c1d7f576f6b8b3dfb7b58b1a5af Mon Sep 17 00:00:00 2001 From: Hridesh MG Date: Sat, 19 Jul 2025 16:57:26 +0530 Subject: [PATCH 1/7] fix: elementary stream regressions --- docs/CHANGES.TXT | 1 + src/ccextractor.c | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/CHANGES.TXT b/docs/CHANGES.TXT index e9dfe614c..5042253ba 100644 --- a/docs/CHANGES.TXT +++ b/docs/CHANGES.TXT @@ -1,5 +1,6 @@ 1.0 (to be released) ----------------- +- Fix: Elementary stream regressions - Fix: Segmentation faults on XDS files - Fix: Clippy Errors Based on Rust 1.88 - IMPROVEMENT: Refactor and optimize Dockerfile diff --git a/src/ccextractor.c b/src/ccextractor.c index a8676405c..62bd604ae 100644 --- a/src/ccextractor.c +++ b/src/ccextractor.c @@ -183,11 +183,11 @@ int api_start(struct ccx_s_options api_options) ----------------------------------------------------------------- */ switch (stream_mode) { + // Note: This case is meant to fall through case CCX_SM_ELEMENTARY_OR_NOT_FOUND: if (!api_options.use_gop_as_pts) // If !0 then the user selected something api_options.use_gop_as_pts = 1; // Force GOP timing for ES ccx_common_timing_settings.is_elementary_stream = 1; - break; case CCX_SM_TRANSPORT: case CCX_SM_PROGRAM: case CCX_SM_ASF: From b801c4ac4e22544ead74c5a8d16935b3cbcfe97e Mon Sep 17 00:00:00 2001 From: Deepnarayan Sett Date: Sun, 27 Jul 2025 20:21:26 +0530 Subject: [PATCH 2/7] TS Module: Added epg_tables library --- src/rust/Cargo.lock | 202 +++ src/rust/Cargo.toml | 1 + src/rust/build.rs | 5 + src/rust/src/transportstream/epg_event.rs | 771 +++++++++ src/rust/src/transportstream/epg_tables.rs | 1776 ++++++++++++++++++-- src/rust/src/transportstream/mod.rs | 1 + src/rust/src/transportstream/tables.rs | 1 + 7 files changed, 2640 insertions(+), 117 deletions(-) create mode 100644 src/rust/src/transportstream/epg_event.rs diff --git a/src/rust/Cargo.lock b/src/rust/Cargo.lock index 72d9fb2ab..56cde17e8 100644 --- a/src/rust/Cargo.lock +++ b/src/rust/Cargo.lock @@ -11,6 +11,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstream" version = "0.6.15" @@ -143,6 +158,12 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + [[package]] name = "byteorder" version = "1.5.0" @@ -155,6 +176,15 @@ version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3" +[[package]] +name = "cc" +version = "1.2.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "deec109607ca693028562ed836a5f1c4b8bd77755c4e132fc5ce11b0b6211ae7" +dependencies = [ + "shlex", +] + [[package]] name = "ccx_rust" version = "0.1.0" @@ -162,6 +192,7 @@ dependencies = [ "bindgen 0.64.0", "byteorder", "cfg-if", + "chrono", "clap", "encoding_rs", "env_logger", @@ -198,6 +229,20 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + [[package]] name = "clang-sys" version = "1.8.1" @@ -261,6 +306,12 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "crc32fast" version = "1.4.2" @@ -514,6 +565,30 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +[[package]] +name = "iana-time-zone" +version = "0.1.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "iconv" version = "0.1.1" @@ -694,6 +769,16 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -1518,6 +1603,64 @@ dependencies = [ "wit-bindgen-rt", ] +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn 2.0.100", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + [[package]] name = "which" version = "4.4.2" @@ -1561,6 +1704,65 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.100", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.52.0" diff --git a/src/rust/Cargo.toml b/src/rust/Cargo.toml index b1803edff..53b115f2d 100644 --- a/src/rust/Cargo.toml +++ b/src/rust/Cargo.toml @@ -33,6 +33,7 @@ memoffset = "0.9.1" byteorder = "1.5.0" serial_test = "3.2.0" encoding_rs = "0.8.35" +chrono = "0.4.41" [build-dependencies] bindgen = "0.64.0" diff --git a/src/rust/build.rs b/src/rust/build.rs index 7390e8c81..25319dbbd 100644 --- a/src/rust/build.rs +++ b/src/rust/build.rs @@ -17,6 +17,11 @@ fn main() { "start_tcp_srv", // shall be removed after NET "net_tcp_read", // shall be removed after NET "net_udp_read", // shall be removed after NET + "net_send_epg", // shall be removed after NET + "fopen", + "fclose", + "fprintf", + "fwrite", #[cfg(windows)] "_open_osfhandle", #[cfg(windows)] diff --git a/src/rust/src/transportstream/epg_event.rs b/src/rust/src/transportstream/epg_event.rs new file mode 100644 index 000000000..da171fdcc --- /dev/null +++ b/src/rust/src/transportstream/epg_event.rs @@ -0,0 +1,771 @@ +use crate::bindings::{EPG_event, EPG_rating}; +use crate::common::CType; +use crate::ctorust::FromCType; +use std::ffi::{CStr, CString}; +use std::os::raw::{c_char, c_longlong}; +use std::ptr; + +// Rustified version of the EPG_event struct +#[derive(Debug, Clone, PartialEq, Default)] +pub struct EPGEventRust { + pub id: u32, + pub start_time_string: String, + pub end_time_string: String, + pub running_status: u8, + pub free_ca_mode: u8, + pub iso_639_language_code: String, + pub event_name: Option, + pub text: Option, + pub extended_iso_639_language_code: String, + pub extended_text: Option, + pub has_simple: bool, + pub ratings: Vec, + pub categories: Vec, + pub service_id: u16, + pub count: i64, + pub live_output: bool, +} + +impl EPGEventRust { + pub(crate) fn default() -> EPGEventRust { + EPGEventRust { + id: 0, + start_time_string: String::new(), + end_time_string: String::new(), + running_status: 0, + free_ca_mode: 0, + iso_639_language_code: String::new(), + event_name: None, + text: None, + extended_iso_639_language_code: String::new(), + extended_text: None, + has_simple: false, + ratings: Vec::new(), + categories: Vec::new(), + service_id: 0, + count: 0, + live_output: false, + } + } +} + +#[derive(Debug, Clone, PartialEq)] +pub struct EPGRatingRust { + pub country_code: String, + pub age: u8, +} + +// Helper function to convert C char array to String +unsafe fn c_char_array_to_string(arr: &[c_char; N]) -> String { + let cstr = CStr::from_ptr(arr.as_ptr()); + cstr.to_string_lossy().into_owned() +} + +// Helper function to convert String to C char array +fn string_to_c_char_array(s: &str) -> [c_char; N] { + let mut arr = [0; N]; + let bytes = s.as_bytes(); + let len = std::cmp::min(bytes.len(), N - 1); // Leave space for null terminator + + for i in 0..len { + arr[i] = bytes[i] as c_char; + } + + // arr[len] is already 0 (null terminator) + arr +} + +// Helper function to convert C string pointer to Option +unsafe fn c_string_ptr_to_option_string(ptr: *mut c_char) -> Option { + if ptr.is_null() { + None + } else { + let cstr = CStr::from_ptr(ptr); + Some(cstr.to_string_lossy().into_owned()) + } +} + +// Helper function to convert Option to C string pointer +fn option_string_to_c_string_ptr(opt_str: &Option) -> *mut c_char { + match opt_str { + Some(s) => { + if let Ok(cstring) = CString::new(s.as_str()) { + cstring.into_raw() + } else { + ptr::null_mut() + } + } + None => ptr::null_mut(), + } +} + +impl FromCType for EPGRatingRust { + unsafe fn from_ctype(rating: EPG_rating) -> Option { + Some(EPGRatingRust { + country_code: c_char_array_to_string(&rating.country_code), + age: rating.age, + }) + } +} + +impl CType for EPGRatingRust { + unsafe fn to_ctype(&self) -> EPG_rating { + EPG_rating { + country_code: string_to_c_char_array(&self.country_code), + age: self.age, + } + } +} + +impl FromCType for EPGEventRust { + unsafe fn from_ctype(event: EPG_event) -> Option { + // Convert ratings array + let ratings = if event.ratings.is_null() || event.num_ratings == 0 { + Vec::new() + } else { + let ratings_slice = + std::slice::from_raw_parts(event.ratings, event.num_ratings as usize); + ratings_slice + .iter() + .filter_map(|r| EPGRatingRust::from_ctype(*r)) + .collect() + }; + + // Convert categories array + let categories = if event.categories.is_null() || event.num_categories == 0 { + Vec::new() + } else { + let categories_slice = + std::slice::from_raw_parts(event.categories, event.num_categories as usize); + categories_slice.to_vec() + }; + + Some(EPGEventRust { + id: event.id, + start_time_string: c_char_array_to_string(&event.start_time_string), + end_time_string: c_char_array_to_string(&event.end_time_string), + running_status: event.running_status, + free_ca_mode: event.free_ca_mode, + iso_639_language_code: c_char_array_to_string(&event.ISO_639_language_code), + event_name: c_string_ptr_to_option_string(event.event_name), + text: c_string_ptr_to_option_string(event.text), + extended_iso_639_language_code: c_char_array_to_string( + &event.extended_ISO_639_language_code, + ), + extended_text: c_string_ptr_to_option_string(event.extended_text), + has_simple: event.has_simple != 0, + ratings, + categories, + service_id: event.service_id, + count: event.count, + live_output: event.live_output != 0, + }) + } +} + +impl CType for EPGEventRust { + unsafe fn to_ctype(&self) -> EPG_event { + // Convert ratings to C array + let (ratings_ptr, num_ratings) = if self.ratings.is_empty() { + (ptr::null_mut(), 0) + } else { + let ratings_vec: Vec = self.ratings.iter().map(|r| r.to_ctype()).collect(); + let ratings_boxed = ratings_vec.into_boxed_slice(); + let ptr = Box::into_raw(ratings_boxed) as *mut EPG_rating; + (ptr, self.ratings.len() as u32) + }; + + // Convert categories to C array + let (categories_ptr, num_categories) = if self.categories.is_empty() { + (ptr::null_mut(), 0) + } else { + let categories_boxed = self.categories.clone().into_boxed_slice(); + let ptr = Box::into_raw(categories_boxed) as *mut u8; + (ptr, self.categories.len() as u32) + }; + + EPG_event { + id: self.id, + start_time_string: string_to_c_char_array(&self.start_time_string), + end_time_string: string_to_c_char_array(&self.end_time_string), + running_status: self.running_status, + free_ca_mode: self.free_ca_mode, + ISO_639_language_code: string_to_c_char_array(&self.iso_639_language_code), + event_name: option_string_to_c_string_ptr(&self.event_name), + text: option_string_to_c_string_ptr(&self.text), + extended_ISO_639_language_code: string_to_c_char_array( + &self.extended_iso_639_language_code, + ), + extended_text: option_string_to_c_string_ptr(&self.extended_text), + has_simple: if self.has_simple { 1 } else { 0 }, + ratings: ratings_ptr, + num_ratings, + categories: categories_ptr, + num_categories, + service_id: self.service_id, + count: self.count as c_longlong, + live_output: if self.live_output { 1 } else { 0 }, + } + } +} + +// Cleanup function to properly free allocated memory +impl EPGEventRust { + /// # Safety + /// This function must be called with a valid `EPG_event` pointer that was created by the `to_ctype` method. + pub unsafe fn cleanup_c_event(event: EPG_event) { + // Free string pointers + if !event.event_name.is_null() { + let _ = CString::from_raw(event.event_name); + } + if !event.text.is_null() { + let _ = CString::from_raw(event.text); + } + if !event.extended_text.is_null() { + let _ = CString::from_raw(event.extended_text); + } + + if !event.ratings.is_null() && event.num_ratings > 0 { + let ratings_slice = Box::from_raw(std::slice::from_raw_parts_mut( + event.ratings, + event.num_ratings as usize, + )); + drop(ratings_slice); + } + + if !event.categories.is_null() && event.num_categories > 0 { + let categories_slice = Box::from_raw(std::slice::from_raw_parts_mut( + event.categories, + event.num_categories as usize, + )); + drop(categories_slice); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::ffi::CString; + + fn create_test_rust_event() -> EPGEventRust { + EPGEventRust { + id: 12345, + start_time_string: "20231201120000 +0000".to_string(), + end_time_string: "20231201130000 +0000".to_string(), + running_status: 4, + free_ca_mode: 0, + iso_639_language_code: "eng".to_string(), + event_name: Some("Test Event Name".to_string()), + text: Some("Test event description".to_string()), + extended_iso_639_language_code: "eng".to_string(), + extended_text: Some("Extended description".to_string()), + has_simple: true, + ratings: vec![ + EPGRatingRust { + country_code: "US".to_string(), + age: 18, + }, + EPGRatingRust { + country_code: "GB".to_string(), + age: 12, + }, + ], + categories: vec![1, 2, 3, 4], + service_id: 256, + count: 42, + live_output: false, + } + } + + unsafe fn create_test_c_event() -> EPG_event { + let event_name = CString::new("Test Event Name").unwrap().into_raw(); + let text = CString::new("Test event description").unwrap().into_raw(); + let extended_text = CString::new("Extended description").unwrap().into_raw(); + + let ratings = vec![ + EPG_rating { + country_code: string_to_c_char_array("US"), + age: 18, + }, + EPG_rating { + country_code: string_to_c_char_array("GB"), + age: 12, + }, + ]; + let ratings_boxed = ratings.into_boxed_slice(); + let ratings_ptr = Box::into_raw(ratings_boxed) as *mut EPG_rating; + + let categories = vec![1u8, 2, 3, 4]; + let categories_boxed = categories.into_boxed_slice(); + let categories_ptr = Box::into_raw(categories_boxed) as *mut u8; + + EPG_event { + id: 12345, + start_time_string: string_to_c_char_array("20231201120000 +0000"), + end_time_string: string_to_c_char_array("20231201130000 +0000"), + running_status: 4, + free_ca_mode: 0, + ISO_639_language_code: string_to_c_char_array("eng"), + event_name, + text, + extended_ISO_639_language_code: string_to_c_char_array("eng"), + extended_text, + has_simple: 1, + ratings: ratings_ptr, + num_ratings: 2, + categories: categories_ptr, + num_categories: 4, + service_id: 256, + count: 42, + live_output: 0, + } + } + + #[test] + fn test_rust_to_c_conversion() { + let rust_event = create_test_rust_event(); + + unsafe { + let c_event = rust_event.to_ctype(); + + // Test basic fields + assert_eq!(c_event.id, 12345); + assert_eq!(c_event.running_status, 4); + assert_eq!(c_event.free_ca_mode, 0); + assert_eq!(c_event.service_id, 256); + assert_eq!(c_event.count, 42); + assert_eq!(c_event.has_simple, 1); + assert_eq!(c_event.live_output, 0); + + // Test string arrays + let start_time = c_char_array_to_string(&c_event.start_time_string); + assert_eq!(start_time, "20231201120000 +0000"); + + let end_time = c_char_array_to_string(&c_event.end_time_string); + assert_eq!(end_time, "20231201130000 +0000"); + + let lang_code = c_char_array_to_string(&c_event.ISO_639_language_code); + assert_eq!(lang_code, "eng"); + + let ext_lang_code = c_char_array_to_string(&c_event.extended_ISO_639_language_code); + assert_eq!(ext_lang_code, "eng"); + + // Test string pointers + assert!(!c_event.event_name.is_null()); + let event_name = CStr::from_ptr(c_event.event_name).to_string_lossy(); + assert_eq!(event_name, "Test Event Name"); + + assert!(!c_event.text.is_null()); + let text = CStr::from_ptr(c_event.text).to_string_lossy(); + assert_eq!(text, "Test event description"); + + assert!(!c_event.extended_text.is_null()); + let extended_text = CStr::from_ptr(c_event.extended_text).to_string_lossy(); + assert_eq!(extended_text, "Extended description"); + + // Test ratings array + assert_eq!(c_event.num_ratings, 2); + assert!(!c_event.ratings.is_null()); + let ratings_slice = std::slice::from_raw_parts(c_event.ratings, 2); + assert_eq!(ratings_slice[0].age, 18); + assert_eq!(c_char_array_to_string(&ratings_slice[0].country_code), "US"); + assert_eq!(ratings_slice[1].age, 12); + assert_eq!(c_char_array_to_string(&ratings_slice[1].country_code), "GB"); + + // Test categories array + assert_eq!(c_event.num_categories, 4); + assert!(!c_event.categories.is_null()); + let categories_slice = std::slice::from_raw_parts(c_event.categories, 4); + assert_eq!(categories_slice, &[1, 2, 3, 4]); + + // Cleanup + EPGEventRust::cleanup_c_event(c_event); + } + } + + #[test] + fn test_c_to_rust_conversion() { + unsafe { + let c_event = create_test_c_event(); + let rust_event = EPGEventRust::from_ctype(c_event).expect("Conversion should succeed"); + + // Test basic fields + assert_eq!(rust_event.id, 12345); + assert_eq!(rust_event.running_status, 4); + assert_eq!(rust_event.free_ca_mode, 0); + assert_eq!(rust_event.service_id, 256); + assert_eq!(rust_event.count, 42); + assert!(rust_event.has_simple); + assert!(!rust_event.live_output); + + // Test strings + assert_eq!(rust_event.start_time_string, "20231201120000 +0000"); + assert_eq!(rust_event.end_time_string, "20231201130000 +0000"); + assert_eq!(rust_event.iso_639_language_code, "eng"); + assert_eq!(rust_event.extended_iso_639_language_code, "eng"); + + // Test optional strings + assert_eq!(rust_event.event_name, Some("Test Event Name".to_string())); + assert_eq!(rust_event.text, Some("Test event description".to_string())); + assert_eq!( + rust_event.extended_text, + Some("Extended description".to_string()) + ); + + // Test ratings + assert_eq!(rust_event.ratings.len(), 2); + assert_eq!(rust_event.ratings[0].age, 18); + assert_eq!(rust_event.ratings[0].country_code, "US"); + assert_eq!(rust_event.ratings[1].age, 12); + assert_eq!(rust_event.ratings[1].country_code, "GB"); + + // Test categories + assert_eq!(rust_event.categories, vec![1, 2, 3, 4]); + + // Cleanup + EPGEventRust::cleanup_c_event(c_event); + } + } + + #[test] + fn test_round_trip_conversion() { + let original_rust = create_test_rust_event(); + + unsafe { + // Rust -> C -> Rust + let c_event = original_rust.to_ctype(); + let converted_rust = + EPGEventRust::from_ctype(c_event).expect("Conversion should succeed"); + + // Compare all fields + assert_eq!(original_rust.id, converted_rust.id); + assert_eq!( + original_rust.start_time_string, + converted_rust.start_time_string + ); + assert_eq!( + original_rust.end_time_string, + converted_rust.end_time_string + ); + assert_eq!(original_rust.running_status, converted_rust.running_status); + assert_eq!(original_rust.free_ca_mode, converted_rust.free_ca_mode); + assert_eq!( + original_rust.iso_639_language_code, + converted_rust.iso_639_language_code + ); + assert_eq!(original_rust.event_name, converted_rust.event_name); + assert_eq!(original_rust.text, converted_rust.text); + assert_eq!( + original_rust.extended_iso_639_language_code, + converted_rust.extended_iso_639_language_code + ); + assert_eq!(original_rust.extended_text, converted_rust.extended_text); + assert_eq!(original_rust.has_simple, converted_rust.has_simple); + assert_eq!(original_rust.ratings, converted_rust.ratings); + assert_eq!(original_rust.categories, converted_rust.categories); + assert_eq!(original_rust.service_id, converted_rust.service_id); + assert_eq!(original_rust.count, converted_rust.count); + assert_eq!(original_rust.live_output, converted_rust.live_output); + + // Should be completely equal + assert_eq!(original_rust, converted_rust); + + // Cleanup + EPGEventRust::cleanup_c_event(c_event); + } + } + + #[test] + fn test_null_pointers() { + let rust_event = EPGEventRust { + id: 1, + start_time_string: "20231201120000 +0000".to_string(), + end_time_string: "20231201130000 +0000".to_string(), + running_status: 0, + free_ca_mode: 0, + iso_639_language_code: "eng".to_string(), + event_name: None, + text: None, + extended_iso_639_language_code: "".to_string(), + extended_text: None, + has_simple: false, + ratings: Vec::new(), + categories: Vec::new(), + service_id: 0, + count: 0, + live_output: false, + }; + + unsafe { + let c_event = rust_event.to_ctype(); + + // Test null pointers + assert!(c_event.event_name.is_null()); + assert!(c_event.text.is_null()); + assert!(c_event.extended_text.is_null()); + assert!(c_event.ratings.is_null()); + assert!(c_event.categories.is_null()); + assert_eq!(c_event.num_ratings, 0); + assert_eq!(c_event.num_categories, 0); + + // Test conversion back + let converted_rust = + EPGEventRust::from_ctype(c_event).expect("Conversion should succeed"); + assert_eq!(converted_rust.event_name, None); + assert_eq!(converted_rust.text, None); + assert_eq!(converted_rust.extended_text, None); + assert!(converted_rust.ratings.is_empty()); + assert!(converted_rust.categories.is_empty()); + + // Cleanup (should be safe with null pointers) + EPGEventRust::cleanup_c_event(c_event); + } + } + + #[test] + fn test_empty_strings() { + let rust_event = EPGEventRust { + id: 1, + start_time_string: "".to_string(), + end_time_string: "".to_string(), + running_status: 0, + free_ca_mode: 0, + iso_639_language_code: "".to_string(), + event_name: Some("".to_string()), + text: Some("".to_string()), + extended_iso_639_language_code: "".to_string(), + extended_text: Some("".to_string()), + has_simple: false, + ratings: Vec::new(), + categories: Vec::new(), + service_id: 0, + count: 0, + live_output: false, + }; + + unsafe { + let c_event = rust_event.to_ctype(); + let converted_rust = + EPGEventRust::from_ctype(c_event).expect("Conversion should succeed"); + + // Empty strings should be preserved + assert_eq!(converted_rust.start_time_string, ""); + assert_eq!(converted_rust.end_time_string, ""); + assert_eq!(converted_rust.iso_639_language_code, ""); + assert_eq!(converted_rust.event_name, Some("".to_string())); + assert_eq!(converted_rust.text, Some("".to_string())); + assert_eq!(converted_rust.extended_text, Some("".to_string())); + + EPGEventRust::cleanup_c_event(c_event); + } + } + + #[test] + fn test_long_strings() { + let long_string = "a".repeat(100); + let rust_event = EPGEventRust { + id: 1, + start_time_string: long_string.clone(), + end_time_string: long_string.clone(), + running_status: 0, + free_ca_mode: 0, + iso_639_language_code: long_string.clone(), + event_name: Some(long_string.clone()), + text: Some(long_string.clone()), + extended_iso_639_language_code: long_string.clone(), + extended_text: Some(long_string.clone()), + has_simple: false, + ratings: Vec::new(), + categories: Vec::new(), + service_id: 0, + count: 0, + live_output: false, + }; + + unsafe { + let c_event = rust_event.to_ctype(); + let converted_rust = + EPGEventRust::from_ctype(c_event).expect("Conversion should succeed"); + + // Fixed-size arrays should be truncated to fit + assert!(converted_rust.start_time_string.len() <= 20); // Array size - 1 for null terminator + assert!(converted_rust.end_time_string.len() <= 20); + assert!(converted_rust.iso_639_language_code.len() <= 3); + assert!(converted_rust.extended_iso_639_language_code.len() <= 3); + + // Dynamically allocated strings should preserve full length + assert_eq!(converted_rust.event_name, Some(long_string.clone())); + assert_eq!(converted_rust.text, Some(long_string.clone())); + assert_eq!(converted_rust.extended_text, Some(long_string.clone())); + + EPGEventRust::cleanup_c_event(c_event); + } + } + + #[test] + fn test_boolean_conversions() { + let test_cases = vec![(true, false), (false, true), (true, true), (false, false)]; + + for (has_simple, live_output) in test_cases { + let rust_event = EPGEventRust { + id: 1, + start_time_string: "test".to_string(), + end_time_string: "test".to_string(), + running_status: 0, + free_ca_mode: 0, + iso_639_language_code: "eng".to_string(), + event_name: None, + text: None, + extended_iso_639_language_code: "eng".to_string(), + extended_text: None, + has_simple, + ratings: Vec::new(), + categories: Vec::new(), + service_id: 0, + count: 0, + live_output, + }; + + unsafe { + let c_event = rust_event.to_ctype(); + assert_eq!(c_event.has_simple, if has_simple { 1 } else { 0 }); + assert_eq!(c_event.live_output, if live_output { 1 } else { 0 }); + + let converted_rust = + EPGEventRust::from_ctype(c_event).expect("Conversion should succeed"); + assert_eq!(converted_rust.has_simple, has_simple); + assert_eq!(converted_rust.live_output, live_output); + + EPGEventRust::cleanup_c_event(c_event); + } + } + } + #[test] + fn test_epg_rating_conversion() { + // Test individual rating conversion + let rust_rating = EPGRatingRust { + country_code: "US".to_string(), + age: 18, + }; + + unsafe { + let c_rating = rust_rating.to_ctype(); + assert_eq!(c_rating.age, 18); + assert_eq!(c_char_array_to_string(&c_rating.country_code), "US"); + + let converted_rating = + EPGRatingRust::from_ctype(c_rating).expect("Rating conversion should succeed"); + assert_eq!(converted_rating.country_code, "US"); + assert_eq!(converted_rating.age, 18); + assert_eq!(converted_rating, rust_rating); + } + } + + #[test] + fn test_epg_rating_edge_cases() { + // Test empty country code + let rust_rating = EPGRatingRust { + country_code: "".to_string(), + age: 0, + }; + + unsafe { + let c_rating = rust_rating.to_ctype(); + let converted_rating = + EPGRatingRust::from_ctype(c_rating).expect("Rating conversion should succeed"); + assert_eq!(converted_rating.country_code, ""); + assert_eq!(converted_rating.age, 0); + } + + // Test long country code (should be truncated) + let rust_rating = EPGRatingRust { + country_code: "TOOLONG".to_string(), + age: 21, + }; + + unsafe { + let c_rating = rust_rating.to_ctype(); + let converted_rating = + EPGRatingRust::from_ctype(c_rating).expect("Rating conversion should succeed"); + // Should be truncated to fit in 4 chars (3 + null terminator) + assert!(converted_rating.country_code.len() <= 3); + assert_eq!(converted_rating.age, 21); + } + + // Test maximum age value + let rust_rating = EPGRatingRust { + country_code: "MAX".to_string(), + age: 255, + }; + + unsafe { + let c_rating = rust_rating.to_ctype(); + let converted_rating = + EPGRatingRust::from_ctype(c_rating).expect("Rating conversion should succeed"); + assert_eq!(converted_rating.country_code, "MAX"); + assert_eq!(converted_rating.age, 255); + } + } + + #[test] + fn test_multiple_ratings_conversion() { + let ratings = vec![ + EPGRatingRust { + country_code: "US".to_string(), + age: 13, + }, + EPGRatingRust { + country_code: "GB".to_string(), + age: 15, + }, + EPGRatingRust { + country_code: "DE".to_string(), + age: 16, + }, + EPGRatingRust { + country_code: "FR".to_string(), + age: 12, + }, + ]; + + let rust_event = EPGEventRust { + id: 1, + start_time_string: "test".to_string(), + end_time_string: "test".to_string(), + running_status: 0, + free_ca_mode: 0, + iso_639_language_code: "eng".to_string(), + event_name: None, + text: None, + extended_iso_639_language_code: "eng".to_string(), + extended_text: None, + has_simple: false, + ratings: ratings.clone(), + categories: Vec::new(), + service_id: 0, + count: 0, + live_output: false, + }; + + unsafe { + let c_event = rust_event.to_ctype(); + assert_eq!(c_event.num_ratings, 4); + + let ratings_slice = std::slice::from_raw_parts(c_event.ratings, 4); + for (i, rating) in ratings_slice.iter().enumerate() { + assert_eq!(rating.age, ratings[i].age); + assert_eq!( + c_char_array_to_string(&rating.country_code), + ratings[i].country_code + ); + } + + let converted_rust = + EPGEventRust::from_ctype(c_event).expect("Conversion should succeed"); + assert_eq!(converted_rust.ratings, ratings); + + EPGEventRust::cleanup_c_event(c_event); + } + } +} diff --git a/src/rust/src/transportstream/epg_tables.rs b/src/rust/src/transportstream/epg_tables.rs index 1760a1c69..c4ed97021 100644 --- a/src/rust/src/transportstream/epg_tables.rs +++ b/src/rust/src/transportstream/epg_tables.rs @@ -1,161 +1,1703 @@ -use iconv::{decode, IconvError}; +use crate::bindings::{fclose, fopen, fprintf, fwrite, lib_ccx_ctx, net_send_epg, FILE}; +use crate::common::CType; +use crate::ctorust::FromCType; +use crate::transportstream::epg_event::{EPGEventRust, EPGRatingRust}; +use crate::transportstream::tables::TS_PMT_MAP_SIZE; +use chrono::{Datelike, NaiveDate, NaiveDateTime, NaiveTime, Timelike}; +use encoding_rs::*; +use lib_ccxr::common::Options; +use lib_ccxr::debug; use lib_ccxr::info; -use std::fmt; +use lib_ccxr::util::log::DebugMessageFlag; +use std::alloc::{alloc, dealloc, realloc, Layout}; +use std::ffi::{CStr, CString}; +use std::os::raw::{c_char, c_ulong, c_void}; +use std::ptr; /// Specific errors for EPG DVB string decoding. #[derive(Debug)] -pub enum EpgDecodeError { - /// Input too short to contain a dynamic code‐page header. - InvalidHeader, - /// The requested encoding is known invalid (e.g. ISO8859-12). - UnsupportedEncoding(&'static str), - /// Low‐level iconv error. - Iconv(IconvError), -} +pub struct EpgDecodeError; + +/// Decode an EPG DVB‐encoded byte slice into UTF‑8. +pub fn epg_dvb_decode_string(input: &[u8]) -> Result { + // Handle empty input - return empty string like C version + if input.is_empty() { + return Ok(String::new()); + } + + let first = input[0]; -impl fmt::Display for EpgDecodeError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - EpgDecodeError::InvalidHeader => write!(f, "Invalid encoding header"), - EpgDecodeError::UnsupportedEncoding(enc) => { - write!(f, "Encoding not supported: {enc}") + // If first byte >= 0x20, skip encoding conversion (like C version) + if first >= 0x20 { + // Use windows-1252 instead of iso-8859-9 as fallback since encoding_rs + // treats iso-8859-1 as windows-1252, and iso-8859-9 may not be available + if let Some(enc) = Encoding::for_label(b"windows-1252") { + let (cow, _, had_errors) = enc.decode(input); + if !had_errors { + return Ok(cow.into_owned()); } - EpgDecodeError::Iconv(e) => write!(f, "iconv error: {e}"), } + // If encoding fails, fall back to ASCII filtering like C version + let filtered: Vec = input.iter().filter(|&&b| b <= 127).cloned().collect(); + return Ok(String::from_utf8_lossy(&filtered).into_owned()); + } + + // Determine how many bytes to skip and what remains to decode + let (_, to_decode) = if first == 0x10 { + if input.len() < 3 { + return Err(EpgDecodeError); + } + (3, &input[3..]) + } else { + if input.is_empty() { + return Err(EpgDecodeError); + } + (1, &input[1..]) + }; + + // Map encoding byte to encoding label + let encoding_label = match first { + 0x01 => "iso-8859-5", // ISO8859-5 (Cyrillic) + 0x02 => "iso-8859-6", // ISO8859-6 (Arabic) + 0x03 => "iso-8859-7", // ISO8859-7 (Greek) + 0x04 => "iso-8859-8", // ISO8859-8 (Hebrew) + 0x05 => "windows-1254", // ISO8859-9 -> windows-1254 (Turkish) + 0x06 => "iso-8859-10", // ISO8859-10 (Nordic) + 0x07 => "iso-8859-11", // ISO8859-11 (Thai) + 0x08 => "windows-1252", // ISO8859-12 doesn't exist, fallback + 0x09 => "iso-8859-13", // ISO8859-13 (Baltic) + 0x0a => "iso-8859-14", // ISO8859-14 (Celtic) + 0x0b => "iso-8859-15", // ISO8859-15 (Latin-9 with Euro) + 0x10 => { + // Dynamic ISO encoding based on next two bytes + let cpn = ((input[1] as u16) << 8) | (input[2] as u16); + // Handle common cases that might not be supported + match cpn { + 1 => "windows-1252", // ISO-8859-1 -> windows-1252 + 9 => "windows-1254", // ISO-8859-9 -> windows-1254 + _ => { + // Try to construct the label, but have fallback ready + let label = format!("iso-8859-{cpn}"); + if Encoding::for_label(label.as_bytes()).is_some() { + // This is a bit of a hack - we'll handle this case separately below + return decode_with_dynamic_label(to_decode, &label); + } else { + "windows-1252" // fallback + } + } + } + } + 0x11 => "utf-8", // UTF-8 + 0x12 => "euc-kr", // KS_C_5601-1987 -> EUC-KR + 0x13 => "gb2312", // GB2312 + 0x14 => "big5", // BIG-5 + 0x15 => "utf-8", // UTF-8 + _ => "windows-1252", // Default fallback like C version uses ISO8859-9 + }; + + // Try to decode with the determined encoding + if let Some(enc) = Encoding::for_label(encoding_label.as_bytes()) { + let (cow, _, had_errors) = enc.decode(to_decode); + if !had_errors { + return Ok(cow.into_owned()); + } + // If decoding had errors but we got some result, still use it + return Ok(cow.into_owned()); + } + + // Fallback: ASCII filtering like the C version does + let filtered: Vec = to_decode.iter().filter(|&&b| b <= 127).cloned().collect(); + Ok(String::from_utf8_lossy(&filtered).into_owned()) +} +// Helper function for dynamic ISO encoding labels +fn decode_with_dynamic_label(data: &[u8], label: &str) -> Result { + if let Some(enc) = Encoding::for_label(label.as_bytes()) { + let (cow, _, _) = enc.decode(data); + Ok(cow.into_owned()) + } else { + // Fallback to ASCII filtering + let filtered: Vec = data.iter().filter(|&&b| b <= 127).cloned().collect(); + Ok(String::from_utf8_lossy(&filtered).into_owned()) } } +pub fn epg_fprintxml(f: &mut FILE, string: &str) { + let bytes = string.as_bytes(); + let mut start_idx = 0; + + for (i, &byte) in bytes.iter().enumerate() { + let replacement: &[u8] = match byte { + b'<' => b"<", + b'>' => b">", + b'"' => b""", + b'&' => b"&", + b'\'' => b"'", + _ => continue, + }; + + // Write the segment before the special character using fwrite (raw bytes) + if i > start_idx { + write_raw_bytes(f, &bytes[start_idx..i]); + } + + // Write the XML entity using fwrite (raw bytes) + write_raw_bytes(f, replacement); + + // Update start position for next segment + start_idx = i + 1; + } -impl From for EpgDecodeError { - fn from(err: IconvError) -> Self { - EpgDecodeError::Iconv(err) + // Write any remaining segment using fwrite (raw bytes) + if start_idx < bytes.len() { + write_raw_bytes(f, &bytes[start_idx..]); } } -pub fn epg_dvb_decode_string(input: &[u8]) -> Result { - // Empty input => empty string. - if input.is_empty() { - return Ok(String::new()); +// Helper function to write raw bytes using fwrite (like the C version) +fn write_raw_bytes(f: &mut FILE, bytes: &[u8]) { + if bytes.is_empty() { + return; } - let mut data = input; + unsafe { + fwrite( + bytes.as_ptr() as *const c_void, + 1, + bytes.len() as c_ulong, + f as *mut FILE, + ); + } +} + +// Fills given string with given (event.*_time_string) ATSC time converted to XMLTV style time string +pub fn epg_atsc_calc_time(output: &mut String, time: u32) { + // Start from January 6, 1980 + let base_date = NaiveDate::from_ymd_opt(1980, 1, 6).unwrap(); + let base_time = NaiveTime::from_hms_opt(0, 0, 0).unwrap(); + let base_datetime = NaiveDateTime::new(base_date, base_time); + + // Add the time in seconds + let result_datetime = base_datetime + chrono::Duration::seconds(time as i64); + + *output = format!( + "{:04}{:02}{:02}{:02}{:02}{:02} +0000", + result_datetime.year(), + result_datetime.month(), + result_datetime.day(), + result_datetime.hour(), + result_datetime.minute(), + result_datetime.second() + ); +} - // If first byte ≥ 0x20, assume it's already ASCII/UTF‑8. - if data[0] >= 0x20 { - let filtered: Vec = data.iter().cloned().filter(|&b| b <= 127).collect(); - return Ok(String::from_utf8_lossy(&filtered).to_string()); +// Fills event.start_time_string in XMLTV format with passed DVB time +pub fn epg_dvb_calc_start_time(event: &mut EPGEventRust, time: u64) { + let mjd = time >> 24; + event.start_time_string.clear(); + + if mjd > 0 { + let y = ((mjd as f64 - 15078.2) / 365.25) as i32; + let m = ((mjd as f64 - 14956.1 - (y as f64 * 365.25)) / 30.6001) as i32; + let d = mjd as i32 - 14956 - (y as f64 * 365.25) as i32 - (m as f64 * 30.6001) as i32; + let k = if m == 14 || m == 15 { 1 } else { 0 }; + let year = y + k + 1900; + let month = m - 1 - k * 12; + + event.start_time_string = + format!("{:02}{:02}{:02}{:06}+0000", year, month, d, time & 0xffffff); } +} + +// Fills event.end_time_string in XMLTV with passed DVB time + duration +pub fn epg_dvb_calc_end_time(event: &mut EPGEventRust, time: u64, duration: u32) { + let mjd = time >> 24; + event.end_time_string.clear(); + + if mjd > 0 { + let y = ((mjd as f64 - 15078.2) / 365.25) as i32; + let m = ((mjd as f64 - 14956.1 - (y as f64 * 365.25)) / 30.6001) as i32; + let d = mjd as i32 - 14956 - (y as f64 * 365.25) as i32 - (m as f64 * 30.6001) as i32; + let k = if m == 14 || m == 15 { 1 } else { 0 }; + let year = y + k + 1900; + let month = m - 1 - k * 12; + + let mut tm_year = year - 1900; + let mut tm_mon = month - 1; + let mut tm_mday = d; - // Determine encoding tag. - let encoding: &str = match data[0] { - 0x01 => { - data = &data[1..]; - "ISO8859-5" + let mut tm_sec = ((time & 0x0f) + + (10 * ((time & 0xf0) >> 4)) + + ((duration & 0x0f) as u64) + + (10 * (((duration & 0xf0) >> 4) as u64))) as i32; + let mut tm_min = (((time & 0x0f00) >> 8) + + (10 * (((time & 0xf000) >> 4) >> 8)) + + (((duration & 0x0f00) >> 8) as u64) + + (10 * ((((duration & 0xf000) >> 4) >> 8) as u64))) as i32; + let mut tm_hour = (((time & 0x0f0000) >> 16) + + (10 * (((time & 0xf00000) >> 4) >> 16)) + + (((duration & 0x0f0000) >> 16) as u64) + + (10 * ((((duration & 0xf00000) >> 4) >> 16) as u64))) + as i32; + + // Simulate mktime() normalization + if tm_sec >= 60 { + tm_min += tm_sec / 60; + tm_sec %= 60; } - 0x02 => { - data = &data[1..]; - "ISO8859-6" + if tm_min >= 60 { + tm_hour += tm_min / 60; + tm_min %= 60; } - 0x03 => { - data = &data[1..]; - "ISO8859-7" + if tm_hour >= 24 { + tm_mday += tm_hour / 24; + tm_hour %= 24; } - 0x04 => { - data = &data[1..]; - "ISO8859-8" + + // Handle month/year overflow + while tm_mday > days_in_month(tm_year + 1900, tm_mon + 1) { + tm_mday -= days_in_month(tm_year + 1900, tm_mon + 1); + tm_mon += 1; + if tm_mon >= 12 { + tm_mon = 0; + tm_year += 1; + } } - 0x05 => { - data = &data[1..]; - "ISO8859-9" + + event.end_time_string = format!( + "{:04}{:02}{:02}{:02}{:02}{:02} +0000", + tm_year + 1900, + tm_mon + 1, + tm_mday, + tm_hour, + tm_min, + tm_sec + ); + } +} + +// Helper function to get days in month +fn days_in_month(year: i32, month: i32) -> i32 { + match month { + 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31, + 4 | 6 | 9 | 11 => 30, + 2 => { + if (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0) { + 29 + } else { + 28 + } } - 0x06 => { - data = &data[1..]; - "ISO8859-10" + _ => 30, // fallback + } +} +// Returns english string description of the passed DVB category ID +pub fn epg_dvb_content_type_to_string(cat: u8) -> &'static str { + match cat { + 0x00 => "reserved", + 0x10 => "movie/drama (general)", + 0x11 => "detective/thriller", + 0x12 => "adventure/western/war", + 0x13 => "science fiction/fantasy/horror", + 0x14 => "comedy", + 0x15 => "soap/melodram/folkloric", + 0x16 => "romance", + 0x17 => "serious/classical/religious/historical movie/drama", + 0x18 => "adult movie/drama", + 0x1E => "reserved", + 0x1F => "user defined", + 0x20 => "news/current affairs (general)", + 0x21 => "news/weather report", + 0x22 => "news magazine", + 0x23 => "documentary", + 0x24 => "discussion/interview/debate", + 0x2E => "reserved", + 0x2F => "user defined", + 0x30 => "show/game show (general)", + 0x31 => "game show/quiz/contest", + 0x32 => "variety show", + 0x33 => "talk show", + 0x3E => "reserved", + 0x3F => "user defined", + 0x40 => "sports (general)", + 0x41 => "special events", + 0x42 => "sports magazine", + 0x43 => "football/soccer", + 0x44 => "tennis/squash", + 0x45 => "team sports", + 0x46 => "athletics", + 0x47 => "motor sport", + 0x48 => "water sport", + 0x49 => "winter sport", + 0x4A => "equestrian", + 0x4B => "martial sports", + 0x4E => "reserved", + 0x4F => "user defined", + 0x50 => "childrens's/youth program (general)", + 0x51 => "pre-school children's program", + 0x52 => "entertainment (6-14 year old)", + 0x53 => "entertainment (10-16 year old)", + 0x54 => "information/education/school program", + 0x55 => "cartoon/puppets", + 0x5E => "reserved", + 0x5F => "user defined", + 0x60 => "music/ballet/dance (general)", + 0x61 => "rock/pop", + 0x62 => "serious music/classic music", + 0x63 => "folk/traditional music", + 0x64 => "jazz", + 0x65 => "musical/opera", + 0x66 => "ballet", + 0x6E => "reserved", + 0x6F => "user defined", + 0x70 => "arts/culture (without music, general)", + 0x71 => "performing arts", + 0x72 => "fine arts", + 0x73 => "religion", + 0x74 => "popular culture/traditional arts", + 0x75 => "literature", + 0x76 => "film/cinema", + 0x77 => "experimental film/video", + 0x78 => "broadcasting/press", + 0x79 => "new media", + 0x7A => "arts/culture magazine", + 0x7B => "fashion", + 0x7E => "reserved", + 0x7F => "user defined", + 0x80 => "social/political issues/economics (general)", + 0x81 => "magazines/reports/documentary", + 0x82 => "economics/social advisory", + 0x83 => "remarkable people", + 0x8E => "reserved", + 0x8F => "user defined", + 0x90 => "education/science/factual topics (general)", + 0x91 => "nature/animals/environment", + 0x92 => "technology/natural science", + 0x93 => "medicine/physiology/psychology", + 0x94 => "foreign countries/expeditions", + 0x95 => "social/spiritual science", + 0x96 => "further education", + 0x97 => "languages", + 0x9E => "reserved", + 0x9F => "user defined", + 0xA0 => "leisure hobbies (general)", + 0xA1 => "tourism/travel", + 0xA2 => "handicraft", + 0xA3 => "motoring", + 0xA4 => "fitness & health", + 0xA5 => "cooking", + 0xA6 => "advertisement/shopping", + 0xA7 => "gardening", + 0xAE => "reserved", + 0xAF => "user defined", + 0xB0 => "original language", + 0xB1 => "black & white", + 0xB2 => "unpublished", + 0xB3 => "live broadcast", + 0xBE => "reserved", + 0xBF => "user defined", + 0xEF => "reserved", + 0xFF => "user defined", + _ => "undefined content", + } +} +fn write_fprintf(f: &mut FILE, s: &str) { + let format_str = "%s\0"; + let format_cstr = format_str.as_ptr() as *const c_char; + let c_string = CString::new(s).unwrap(); + + unsafe { + fprintf(f as *mut FILE, format_cstr, c_string.as_ptr()); + } +} + +fn write_fprintf_with_int(f: &mut FILE, format: &str, value: u32) { + let formatted = format.replace("{}", &value.to_string()); + write_fprintf(f, &formatted); +} + +fn write_fprintf_with_str(f: &mut FILE, format: &str, value: &str) { + let formatted = format.replace("{}", value); + write_fprintf(f, &formatted); +} + +// Prints given event to already opened XMLTV file +pub fn epg_print_event(event: &EPGEventRust, channel: u32, f: &mut FILE) { + // Write program opening tag with attributes + write_fprintf(f, " \n", channel); + + // Write simple event information if available + if event.has_simple { + if let Some(ref event_name) = event.event_name { + write_fprintf_with_str(f, " ", &event.iso_639_language_code); + epg_fprintxml(f, event_name); + write_fprintf(f, "\n"); } - 0x07 => { - data = &data[1..]; - "ISO8859-11" + + if let Some(ref text) = event.text { + write_fprintf_with_str( + f, + " ", + &event.iso_639_language_code, + ); + epg_fprintxml(f, text); + write_fprintf(f, "\n"); } - 0x08 => { - data = &data[1..]; - "ISO8859-12" + } + + // Write extended description if available + if let Some(ref extended_text) = event.extended_text { + write_fprintf_with_str( + f, + " ", + &event.extended_iso_639_language_code, + ); + //mprint("Extented Text = %s\n",event->extended_text); + info!("Extented Text = {}\n", extended_text); + epg_fprintxml(f, extended_text); + write_fprintf(f, "\n"); + } + for rating in &event.ratings { + if rating.age > 0 && rating.age < 0x10 { + let rating_str = format!( + " {}\n", + rating.country_code, + rating.age + 3 + ); + write_fprintf(f, &rating_str); } - 0x09 => { - data = &data[1..]; - "ISO8859-13" + } + + // Write categories + for &category in &event.categories { + write_fprintf(f, " "); + let category_str = epg_dvb_content_type_to_string(category); + epg_fprintxml(f, category_str); + write_fprintf(f, "\n"); + } + + // Write metadata ID and closing tag + write_fprintf_with_int(f, " {}\n", event.id); + write_fprintf(f, " \n"); +} + +// Network output function +/// # Safety +/// This function is unsafe because it dereferences raw pointers and performs network operations. +pub unsafe fn epg_output_net(ctx: &mut lib_ccx_ctx, ccx_options: &mut Options) { + // Check if demux context exists + if ctx.demux_ctx.is_null() { + return; + } + + let demux_ctx = unsafe { &*ctx.demux_ctx }; + + if demux_ctx.nb_program == 0 { + return; + } + + // Find the program matching ts_forced_program + let mut program_index = None; + for (i, pinfo) in demux_ctx.pinfo.iter().enumerate() { + if pinfo.program_number == ccx_options.demux_cfg.ts_forced_program.unwrap_or(i32::MIN) { + program_index = Some(i); + break; } - 0x0a => { - data = &data[1..]; - "ISO8859-14" + } + + let program_index = match program_index { + Some(idx) => idx, + None => return, + }; + + // Process events for the found program + if !ctx.eit_programs.is_null() { + let eit_program = unsafe { &mut *ctx.eit_programs.add(program_index) }; + + for j in 0..eit_program.array_len { + let event = &mut eit_program.epg_events[j as usize]; + + if event.live_output != 0 { + continue; + } + + event.live_output = 1; + + let category = if event.num_categories > 0 { + let category_val = unsafe { *event.categories.offset(0) }; + epg_dvb_content_type_to_string(category_val) + } else { + "undefined content" + }; + + // Convert C strings to Rust strings for the network call + let start_time = unsafe { + CStr::from_ptr(event.start_time_string.as_ptr()) + .to_str() + .unwrap_or("") + }; + let end_time = unsafe { + CStr::from_ptr(event.end_time_string.as_ptr()) + .to_str() + .unwrap_or("") + }; + let event_name = if !event.event_name.is_null() { + unsafe { CStr::from_ptr(event.event_name).to_str().unwrap_or("") } + } else { + "" + }; + let extended_text = if !event.extended_text.is_null() { + unsafe { CStr::from_ptr(event.extended_text).to_str().unwrap_or("") } + } else { + "" + }; + let language_code = unsafe { + CStr::from_ptr(event.ISO_639_language_code.as_ptr()) + .to_str() + .unwrap_or("") + }; + + // Call the network send function with C strings + let start_cstr = CString::new(start_time).unwrap(); + let end_cstr = CString::new(end_time).unwrap(); + let name_cstr = CString::new(event_name).unwrap(); + let text_cstr = CString::new(extended_text).unwrap(); + let lang_cstr = CString::new(language_code).unwrap(); + let cat_cstr = CString::new(category).unwrap(); + + unsafe { + net_send_epg( + start_cstr.as_ptr(), + end_cstr.as_ptr(), + name_cstr.as_ptr(), + text_cstr.as_ptr(), + lang_cstr.as_ptr(), + cat_cstr.as_ptr(), + ); + } } - 0x0b => { - data = &data[1..]; - "ISO8859-15" + } +} +/// Creates fills and closes a new XMLTV file for live mode output. +/// File should include only events not previously output. +/// # Safety +/// This function is unsafe because it dereferences raw pointers and performs file operations. +pub unsafe fn epg_output_live(ctx_ref: &mut lib_ccx_ctx) { + let mut has_new_events = false; + + // Check if demux context exists + if ctx_ref.demux_ctx.is_null() { + return; + } + + let demux_ctx = &*ctx_ref.demux_ctx; + + // Check if there are any events not previously output + if !ctx_ref.eit_programs.is_null() { + for i in 0..demux_ctx.nb_program { + let eit_program = &*ctx_ref.eit_programs.offset(i as isize); + for j in 0..eit_program.array_len { + let epg_event = &eit_program.epg_events[j as usize]; + if epg_event.live_output == 0 { + has_new_events = true; + break; + } + } + if has_new_events { + break; + } } - 0x10 => { - // Dynamic code‑page: need at least three bytes for CPN. - if data.len() < 3 { - return Err(EpgDecodeError::InvalidHeader); - } - let cpn = u16::from_be_bytes([data[1], data[2]]); - data = &data[3..]; - // Build the label, then try to decode below. - // We’ll store it in a String and validate. - let label = format!("ISO8859-{cpn}"); - // Reject known invalid. - if label == "ISO8859-12" { - return Err(EpgDecodeError::UnsupportedEncoding("ISO8859-12")); - } - // Use a local String and return from the match arm. - // The decode call below will need to be adjusted to accept String as encoding. - return match decode(data, &label) { - Ok(s) => Ok(s), - Err(e) => { - info!( - "Warning: EPG_DVB_decode_string: iconv({}) failed: {}\n", - label, e + } + + if !has_new_events { + return; + } + + // Create filename + let basefilename = if !ctx_ref.basefilename.is_null() { + CStr::from_ptr(ctx_ref.basefilename) + .to_str() + .unwrap_or("output") + } else { + "output" + }; + + let filename = format!("{}_{}.xml.part", basefilename, ctx_ref.epg_last_live_output); + let filename_cstring = + CString::new(filename.clone()).unwrap_or_else(|_| CString::new("").unwrap()); + + // Open file for writing + let mode = CString::new("w").unwrap(); + let file = fopen(filename_cstring.as_ptr(), mode.as_ptr()); + if file.is_null() { + return; + } + + let f = &mut *(file as *mut FILE); + + // Write XML header + write_fprintf(f, "\n"); + write_fprintf(f, "\n\n"); + write_fprintf(f, "\n"); + + // Write channel information + for i in 0..demux_ctx.nb_program { + let pinfo = &demux_ctx.pinfo[i as usize]; + write_fprintf_with_int(f, " ", pinfo.program_number as u32); + write_fprintf(f, "\n"); + write_fprintf_with_int( + f, + " {}", + pinfo.program_number as u32, + ); + write_fprintf(f, "\n"); + write_fprintf(f, " \n"); + } + + // Write events that haven't been output yet + for i in 0..demux_ctx.nb_program { + let eit_program = &mut *ctx_ref.eit_programs.offset(i as isize); + let pinfo = &demux_ctx.pinfo[i as usize]; + + for j in 0..eit_program.array_len { + let epg_event = &mut eit_program.epg_events[j as usize]; + if epg_event.live_output == 0 { + epg_event.live_output = 1; + epg_print_event( + &EPGEventRust::from_ctype(*epg_event).unwrap_or(EPGEventRust::default()), + pinfo.program_number as u32, + f, + ); + } + } + } + + write_fprintf(f, ""); + fclose(file); + + // Rename file (remove .part extension) - using system rename + let final_filename = filename.strip_suffix(".part").unwrap_or(&filename); + + // Use system call for rename since we can't use libc + std::fs::rename(&filename, final_filename).unwrap_or(()); +} +/// Creates fills and closes a new XMLTV file for full output mode. +/// File should include all events in memory. +/// # Safety +/// This function is unsafe because it dereferences raw pointers and performs file operations. +pub unsafe fn epg_output(ctx_ref: &mut lib_ccx_ctx, ccx_options: &Options) { + // Check if demux context exists + if ctx_ref.demux_ctx.is_null() { + return; + } + + let demux_ctx = &*ctx_ref.demux_ctx; + + // Create filename + let basefilename = if !ctx_ref.basefilename.is_null() { + CStr::from_ptr(ctx_ref.basefilename) + .to_str() + .unwrap_or("output") + } else { + "output" + }; + + let filename = format!("{basefilename}_epg.xml"); + let filename_cstring = + CString::new(filename.clone()).unwrap_or_else(|_| CString::new("").unwrap()); + + // Open file for writing + let mode = CString::new("w").unwrap(); + let file = fopen(filename_cstring.as_ptr(), mode.as_ptr()); + if file.is_null() { + info!("Unable to open {}\n", filename); + return; + } + + let f = &mut *(file as *mut FILE); + + // Write XML header + write_fprintf( + f, + "\n\n\n\n", + ); + + // Write channel information + for i in 0..demux_ctx.nb_program { + let pinfo = &demux_ctx.pinfo[i as usize]; + write_fprintf_with_int(f, " \n", pinfo.program_number as u32); + write_fprintf(f, " "); + + // Check if name is available and not empty + let name_str = if !pinfo.name.is_empty() { + let name_cstr = CStr::from_ptr(pinfo.name.as_ptr()); + let name = name_cstr.to_str().unwrap_or(""); + if !name.is_empty() { + Some(name) + } else { + None + } + } else { + None + }; + + if let Some(name) = name_str { + epg_fprintxml(f, name); + } else { + write_fprintf_with_int(f, "{}\n", pinfo.program_number as u32); + } + + write_fprintf(f, "\n"); + write_fprintf(f, " \n"); + } + + // Check xmltvonlycurrent option and write events accordingly + if !ccx_options.xmltvonlycurrent { + // Print all events + if !ctx_ref.eit_programs.is_null() { + for i in 0..demux_ctx.nb_program { + let eit_program = &*ctx_ref.eit_programs.offset(i as isize); + let pinfo = &demux_ctx.pinfo[i as usize]; + + for j in 0..eit_program.array_len { + let epg_event = &eit_program.epg_events[j as usize]; + epg_print_event( + &EPGEventRust::from_ctype(*epg_event).unwrap_or(EPGEventRust::default()), + pinfo.program_number as u32, + f, ); - let filtered: Vec = data.iter().cloned().filter(|&b| b <= 127).collect(); - Ok(String::from_utf8_lossy(&filtered).to_string()) } + } + + // Stream has no PMT, fall back to unordered events + if demux_ctx.nb_program == 0 { + let fallback_program = &*ctx_ref.eit_programs.add(TS_PMT_MAP_SIZE); + for j in 0..fallback_program.array_len { + let epg_event = &fallback_program.epg_events[j as usize]; + epg_print_event( + &EPGEventRust::from_ctype(*epg_event).unwrap_or(EPGEventRust::default()), + epg_event.service_id as u32, + f, + ); + } + } + } + } else { + // Print current events only + if !ctx_ref.eit_programs.is_null() && !ctx_ref.eit_current_events.is_null() { + for i in 0..demux_ctx.nb_program { + let eit_program = &*ctx_ref.eit_programs.offset(i as isize); + let pinfo = &demux_ctx.pinfo[i as usize]; + let ce = *ctx_ref.eit_current_events.offset(i as isize); + + for j in 0..eit_program.array_len { + let epg_event = &eit_program.epg_events[j as usize]; + if ce as u32 == epg_event.id { + epg_print_event( + &EPGEventRust::from_ctype(*epg_event) + .unwrap_or(EPGEventRust::default()), + pinfo.program_number as u32, + f, + ); + } + } + } + } + } + + write_fprintf(f, ""); + fclose(file); +} + +/// Compare 2 events. Return false if they are different. +pub fn epg_event_cmp(e1: &EPGEventRust, e2: &EPGEventRust) -> bool { + if e1.id != e2.id + || e1.start_time_string != e2.start_time_string + || e1.end_time_string != e2.end_time_string + { + return false; + } + // Could add full checking of strings here if desired. + true +} + +/// Add given event to array of events. +/// Return false if nothing changed, true if this is a new or updated event. +/// # Safety +/// This function is unsafe because it dereferences raw pointers and performs memory operations. +pub unsafe fn epg_add_event(ctx_ref: &mut lib_ccx_ctx, pmt_map: i32, event: &EPGEventRust) -> bool { + // Get the EIT program array from the C struct + let eit_programs_ptr = ctx_ref.eit_programs; + if eit_programs_ptr.is_null() { + return false; + } + + // Validate pmt_map index - you need to know the actual size of the eit_programs array + // This is a guess - you should replace with the actual maximum size + if !(0..1000).contains(&pmt_map) { + // Replace 1000 with actual max size + return false; + } + + // Access the specific EIT program at pmt_map index + let eit_program_ptr = eit_programs_ptr.offset(pmt_map as isize); + + // Search for existing event with same ID + for j in 0..(*eit_program_ptr).array_len { + let existing_event = &mut (*eit_program_ptr).epg_events[j as usize]; + + if existing_event.id == event.id { + // Convert existing C event to Rust for comparison + if let Some(existing_rust_event) = EPGEventRust::from_ctype(*existing_event) { + // Compare events using the existing comparison function + if epg_event_cmp(event, &existing_rust_event) { + // Events are identical, nothing to do + return false; + } else { + // Event with this ID exists but has changed - update it + // First, preserve the count from the existing event + let mut updated_event = event.clone(); + updated_event.count = existing_rust_event.count; + + // Free the existing event (equivalent to EPG_free_event) + EPGEventRust::cleanup_c_event(*existing_event); + + // Convert the updated Rust event back to C and copy it + let new_c_event = updated_event.to_ctype(); + *existing_event = new_c_event; + + return true; + } + } + } + } + + // ID not found in array - add new event + // Create new event with count = 0 + let mut new_event = event.clone(); + new_event.count = 0; + + // Convert to C type and add to array + let c_event = new_event.to_ctype(); + let array_len = (*eit_program_ptr).array_len as usize; + + // Check bounds to avoid buffer overflow + if array_len >= 10080 { + return false; + } + + (*eit_program_ptr).epg_events[array_len] = c_event; + + // Increment array length + (*eit_program_ptr).array_len += 1; + + true +} + +/// EN 300 468 V1.3.1 (1998-02) +/// 6.2.4 Content descriptor +pub fn epg_decode_content_descriptor( + offset: &[u8], + descriptor_length: u32, + event: &mut EPGEventRust, +) { + let num_items = descriptor_length as usize / 2; + + if num_items == 0 { + debug!( + msg_type = DebugMessageFlag::GENERIC_NOTICE; + "Warning: Invalid EIT content_descriptor length detected." + ); + return; + } + + // Clear existing categories and reserve space + event.categories.clear(); + event.categories.reserve(num_items); + + // Extract categories from the descriptor + for i in 0..num_items { + if i * 2 < offset.len() { + event.categories.push(offset[i * 2]); + } + } +} + +/// EN 300 468 V1.3.1 (1998-02) +/// 6.2.20 Parental rating description +pub fn epg_decode_parental_rating_descriptor( + offset: &[u8], + descriptor_length: u32, + event: &mut EPGEventRust, +) { + let num_items = descriptor_length as usize / 4; + + if num_items == 0 { + debug!( + msg_type = DebugMessageFlag::GENERIC_NOTICE; + "Warning: Invalid EIT parental_rating length detected." + ); + return; + } + + // Clear existing ratings and reserve space + event.ratings.clear(); + event.ratings.reserve(num_items); + + // Extract ratings from the descriptor + for i in 0..num_items { + let base_idx = i * 4; + + // Ensure we have enough bytes for this rating entry + if base_idx + 3 < offset.len() { + // Extract country code (3 bytes + null terminator equivalent) + let country_code = String::from_utf8_lossy(&offset[base_idx..base_idx + 3]).to_string(); + + // Extract and validate age rating + let age = if offset[base_idx + 3] == 0x00 || offset[base_idx + 3] >= 0x10 { + 0 + } else { + offset[base_idx + 3] }; + + // Create and add the rating + let rating = EPGRatingRust { country_code, age }; + + event.ratings.push(rating); } - 0x11 => { - data = &data[1..]; - "ISO-10646/UTF8" + } +} +/// EN 300 468 V1.3.1 (1998-02) +/// 6.2.27 Short event descriptor +pub fn epg_decode_short_event_descriptor( + offset: &[u8], + descriptor_length: u32, + event: &mut EPGEventRust, +) { + if offset.len() < 4 { + debug!( + msg_type = DebugMessageFlag::GENERIC_NOTICE; + "Warning: Invalid short_event_descriptor size detected." + ); + return; + } + + event.has_simple = true; + event.iso_639_language_code = format!( + "{}{}{}", + offset[0] as char, offset[1] as char, offset[2] as char + ); + + let event_name_length = offset[3] as usize; + if event_name_length + 4 > descriptor_length as usize { + debug!( + msg_type = DebugMessageFlag::GENERIC_NOTICE; + "Warning: Invalid short_event_descriptor size detected." + ); + return; + } + + if offset.len() < 4 + event_name_length { + debug!( + msg_type = DebugMessageFlag::GENERIC_NOTICE; + "Warning: Invalid short_event_descriptor size detected." + ); + return; + } + + // Decode event name + match epg_dvb_decode_string(&offset[4..4 + event_name_length]) { + Ok(decoded_name) => event.event_name = Some(decoded_name), + Err(_) => { + debug!( + msg_type = DebugMessageFlag::GENERIC_NOTICE; + "Warning: Failed to decode event name." + ); } - 0x12 => { - data = &data[1..]; - "KS_C_5601-1987" + } + + if offset.len() < 5 + event_name_length { + debug!( + msg_type = DebugMessageFlag::GENERIC_NOTICE; + "Warning: Invalid short_event_descriptor size detected." + ); + return; + } + + let text_length = offset[4 + event_name_length] as usize; + if text_length + event_name_length + 5 > descriptor_length as usize { + debug!( + msg_type = DebugMessageFlag::GENERIC_NOTICE; + "Warning: Invalid short_event_descriptor size detected." + ); + return; + } + + if offset.len() < 5 + event_name_length + text_length { + debug!( + msg_type = DebugMessageFlag::GENERIC_NOTICE; + "Warning: Invalid short_event_descriptor size detected." + ); + return; + } + + // Decode text + match epg_dvb_decode_string(&offset[5 + event_name_length..5 + event_name_length + text_length]) + { + Ok(decoded_text) => event.text = Some(decoded_text), + Err(_) => { + debug!( + msg_type = DebugMessageFlag::GENERIC_NOTICE; + "Warning: Failed to decode event text." + ); } - 0x13 => { - data = &data[1..]; - "GB2312" + } +} + +/// EN 300 468 V1.3.1 (1998-02) +/// 6.2.9 Extended event descriptor +pub fn epg_decode_extended_event_descriptor( + offset: &[u8], + descriptor_length: u32, + event: &mut EPGEventRust, +) { + if offset.len() < 5 { + debug!( + msg_type = DebugMessageFlag::GENERIC_NOTICE; + "Warning: Invalid extended_event_descriptor size detected." + ); + return; + } + + let descriptor_number = (offset[0] >> 4) as usize; + let last_descriptor_number = (offset[0] & 0x0f) as usize; + + event.extended_iso_639_language_code = format!( + "{}{}{}", + offset[1] as char, offset[2] as char, offset[3] as char + ); + + let length_of_items = offset[4] as usize; + if length_of_items > descriptor_length as usize - 5 { + debug!( + msg_type = DebugMessageFlag::GENERIC_NOTICE; + "Warning: Invalid extended_event_descriptor size detected." + ); + return; + } + + if offset.len() < length_of_items + 6 { + debug!( + msg_type = DebugMessageFlag::GENERIC_NOTICE; + "Warning: Invalid extended_event_descriptor size detected." + ); + return; + } + + let text_offset = length_of_items + 5; + let mut text_length = offset[text_offset] as usize; + + if text_length > descriptor_length as usize - 5 - length_of_items - 1 { + debug!( + msg_type = DebugMessageFlag::GENERIC_NOTICE; + "Warning: Invalid extended_event_text_length size detected." + ); + return; + } + + let mut text_start_offset = text_offset + 1; + #[allow(unused_variables)] // TODO check this + let mut oldlen = 0; + + // Handle continuation descriptors + if descriptor_number > 0 { + if offset.len() > text_start_offset && offset[text_start_offset] < 0x20 { + text_start_offset += 1; + text_length -= 1; } - 0x14 => { - data = &data[1..]; - "BIG-5" + + // Get existing extended text length + #[allow(unused_assignments)] // TODO check this + if let Some(ref existing_text) = event.extended_text { + oldlen = existing_text.len(); } - 0x15 => { - data = &data[1..]; - "UTF-8" + } + + if offset.len() < text_start_offset + text_length { + debug!( + msg_type = DebugMessageFlag::GENERIC_NOTICE; + "Warning: Invalid extended_event_descriptor size detected." + ); + return; + } + + // Extract new text segment + let text_segment = &offset[text_start_offset..text_start_offset + text_length]; + + // Append to existing extended text or create new + let combined_text = if descriptor_number > 0 { + if let Some(ref existing_text) = event.extended_text { + let mut combined = existing_text.as_bytes().to_vec(); + combined.extend_from_slice(text_segment); + combined + } else { + text_segment.to_vec() } - other => { - info!("Warning: Reserved encoding detected: {:02x}\n", other); - data = &data[1..]; - "ISO8859-9" + } else { + text_segment.to_vec() + }; + + // If this is the last descriptor, decode the complete text + if descriptor_number == last_descriptor_number { + match epg_dvb_decode_string(&combined_text) { + Ok(decoded_text) => event.extended_text = Some(decoded_text), + Err(_) => { + debug!( + msg_type = DebugMessageFlag::GENERIC_NOTICE; + "Warning: Failed to decode extended event text." + ); + // Fallback to raw text as UTF-8 lossy + event.extended_text = Some(String::from_utf8_lossy(&combined_text).to_string()); + } } + } else { + // Store raw bytes as string for continuation + event.extended_text = Some(String::from_utf8_lossy(&combined_text).to_string()); + } +} +/// Decode an ATSC multiple_string +/// Extremely basic implementation +/// Only handles single segment, single language ANSI string! +pub fn epg_atsc_decode_multiple_string(offset: &[u8], length: u32, event: &mut EPGEventRust) { + if length == 0 { + return; + } + + let mut pos = 0usize; + let length = length as usize; + + macro_rules! check_offset { + ($val:expr) => { + if pos + $val >= length || pos + $val >= offset.len() { + return; + } + }; + } + + check_offset!(1); + let number_strings = offset[pos]; + pos += 1; + + for _i in 0..number_strings { + check_offset!(4); + let number_segments = offset[pos + 3]; + let iso_639_language_code = [ + offset[pos] as char, + offset[pos + 1] as char, + offset[pos + 2] as char, + ]; + pos += 4; + + for j in 0..number_segments { + check_offset!(3); + let compression_type = offset[pos]; + let mode = offset[pos + 1]; + let number_bytes = offset[pos + 2] as usize; + pos += 3; + + if mode == 0 && compression_type == 0 && j == 0 { + check_offset!(number_bytes); + event.has_simple = true; + event.iso_639_language_code = format!( + "{}{}{}", + iso_639_language_code[0], iso_639_language_code[1], iso_639_language_code[2] + ); + + let text_slice = &offset[pos..pos + number_bytes]; + let text = String::from_utf8_lossy(text_slice).to_string(); + + event.event_name = Some(text.clone()); + event.text = Some(text); + } else { + // Warning: Unsupported ATSC multiple_string encoding detected! + info!("Warning: Unsupported ATSC multiple_string encoding detected!"); + } + pos += number_bytes; + } + } +} + +/// Decode ATSC EIT table +/// # Safety +/// This function is unsafe because it dereferences raw pointers and performs memory operations. +pub unsafe fn epg_atsc_decode_eit( + ctx_ref: &mut lib_ccx_ctx, + payload_start: *const u8, + size: u32, + ccx_options: &mut Options, +) { + if payload_start.is_null() || size < 11 { + return; + } + let payload_slice = std::slice::from_raw_parts(payload_start, size as usize); + + let source_id = ((payload_slice[3] as u16) << 8) | payload_slice[4] as u16; + + let mut event = EPGEventRust { + has_simple: false, + extended_text: None, + ratings: Vec::new(), + categories: Vec::new(), + live_output: false, + ..Default::default() }; - // Try the high‑level decode. On any error, fall back to ASCII stripping. - match decode(data, encoding) { - Ok(s) => Ok(s), - Err(e) => { - info!( - "Warning: EPG_DVB_decode_string: iconv({}) failed: {}\n", - encoding, e - ); - let filtered: Vec = data.iter().cloned().filter(|&b| b <= 127).collect(); - Ok(String::from_utf8_lossy(&filtered).to_string()) + let mut pmt_map = -1i32; + + if !ctx_ref.demux_ctx.is_null() { + let demux_ctx = &*ctx_ref.demux_ctx; + for i in 0..demux_ctx.nb_program { + let pinfo = &demux_ctx.pinfo[i as usize]; + // This is a placeholder for the mapping logic + if pinfo.program_number == source_id as i32 { + pmt_map = i; + break; + } + } + } + + // Don't know how to store EPG until we know the programs. Ignore it. + if pmt_map == -1 { + pmt_map = TS_PMT_MAP_SIZE as i32; + } + + let num_events_in_section = payload_slice[9]; + let mut pos = 10usize; + let mut has_new = false; + + macro_rules! check_offset { + ($val:expr) => { + if pos + $val >= size as usize { + return; + } + }; + } + + for _j in 0..num_events_in_section { + if pos >= size as usize { + break; + } + + check_offset!(10); + + let event_id = ((payload_slice[pos] & 0x3F) as u16) << 8 | payload_slice[pos + 1] as u16; + let full_id = ((source_id as u32) << 16) | event_id as u32; + event.id = full_id; + event.service_id = source_id; + + let start_time = ((payload_slice[pos + 2] as u32) << 24) + | ((payload_slice[pos + 3] as u32) << 16) + | ((payload_slice[pos + 4] as u32) << 8) + | (payload_slice[pos + 5] as u32); + + epg_atsc_calc_time(&mut event.start_time_string, start_time); + + let length_in_seconds = (((payload_slice[pos + 6] & 0x0F) as u32) << 16) + | ((payload_slice[pos + 7] as u32) << 8) + | (payload_slice[pos + 8] as u32); + + epg_atsc_calc_time(&mut event.end_time_string, start_time + length_in_seconds); + + let title_length = payload_slice[pos + 9] as u32; + + check_offset!(11 + title_length as usize); + + epg_atsc_decode_multiple_string(&payload_slice[pos + 10..], title_length, &mut event); + + let descriptors_loop_length = + (((payload_slice[pos + 10 + title_length as usize] & 0x0f) as u16) << 8) + | payload_slice[pos + 10 + title_length as usize + 1] as u16; + + has_new |= epg_add_event(ctx_ref, pmt_map, &event); + pos += 12 + descriptors_loop_length as usize + title_length as usize; + } + + if (ccx_options.xmltv == 1 || ccx_options.xmltv == 3) + && ccx_options.xmltvoutputinterval.millis() == 0 + && has_new + { + epg_output(ctx_ref, ccx_options); + } +} +/// Decode ATSC VCT table +/// # Safety +/// This function is unsafe because it dereferences raw pointers and performs memory operations. +pub unsafe fn epg_atsc_decode_vct(ctx: &mut lib_ccx_ctx, payload_start: *const u8, size: u32) { + if payload_start.is_null() || size <= 10 { + return; + } + + let payload_slice = std::slice::from_raw_parts(payload_start, size as usize); + let num_channels_in_section = payload_slice[9]; + let mut offset_pos = 10usize; + + for _i in 0..num_channels_in_section { + if offset_pos + 31 >= size as usize { + break; + } + + let program_number = + ((payload_slice[offset_pos + 24] as u16) << 8) | payload_slice[offset_pos + 25] as u16; + let source_id = + ((payload_slice[offset_pos + 28] as u16) << 8) | payload_slice[offset_pos + 29] as u16; + let descriptors_loop_length = (((payload_slice[offset_pos + 30] & 0x03) as u16) << 8) + | payload_slice[offset_pos + 31] as u16; + + // Copy short_name (7 * 2 = 14 bytes) + let mut short_name = [0u8; 14]; + short_name.copy_from_slice(&payload_slice[offset_pos..offset_pos + 14]); + + offset_pos += 32 + descriptors_loop_length as usize; + + // Store in ATSC source program map + if !ctx.ATSC_source_pg_map.is_null() { + *ctx.ATSC_source_pg_map.offset(source_id as isize) = program_number as i16; + } + } +} + +/// Decode DVB EIT table +/// # Safety +/// This function is unsafe because it dereferences raw pointers, performs memory operations and calls unsafe functions like epg_add_event. +pub unsafe fn epg_dvb_decode_eit( + ctx_ref: &mut lib_ccx_ctx, + payload_start: *const u8, + size: u32, + ccx_options: &mut Options, +) { + if payload_start.is_null() || size < 13 { + return; + } + + let payload_slice = std::slice::from_raw_parts(payload_start, size as usize); + + let table_id = payload_slice[0]; + let section_length = (((payload_slice[1] & 0x0F) as u16) << 8) | payload_slice[2] as u16; + let service_id = ((payload_slice[3] as u16) << 8) | payload_slice[4] as u16; + let section_number = payload_slice[6]; + let events_length = section_length - 11; + + let mut pmt_map = -1i32; + let mut has_new = false; + + // Find PMT map + if !ctx_ref.demux_ctx.is_null() { + let demux_ctx = &*ctx_ref.demux_ctx; + for i in 0..demux_ctx.nb_program { + let pinfo = &demux_ctx.pinfo[i as usize]; + if pinfo.program_number == service_id as i32 { + pmt_map = i; + break; + } + } + } + + // For any service we don't have a PMT for (yet), store it in the special last array pos + if pmt_map == -1 { + pmt_map = TS_PMT_MAP_SIZE as i32; + } + + if events_length as u32 > size - 14 { + info!("Warning: Invalid EIT packet size detected."); + return; + } + + let mut offset_pos = 14usize; + let mut remaining = events_length as usize; + + while remaining > 4 && offset_pos + 12 <= size as usize { + let mut event = EPGEventRust { + id: ((payload_slice[offset_pos] as u32) << 8) | payload_slice[offset_pos + 1] as u32, + has_simple: false, + extended_text: None, + ratings: Vec::new(), + categories: Vec::new(), + live_output: false, + service_id, + ..Default::default() + }; + + // Extract start time (40 bits) + let start_time = ((payload_slice[offset_pos + 2] as u64) << 32) + | ((payload_slice[offset_pos + 3] as u64) << 24) + | ((payload_slice[offset_pos + 4] as u64) << 16) + | ((payload_slice[offset_pos + 5] as u64) << 8) + | (payload_slice[offset_pos + 6] as u64); + + epg_dvb_calc_start_time(&mut event, start_time); + + // Extract duration (24 bits) + let duration = ((payload_slice[offset_pos + 7] as u32) << 16) + | ((payload_slice[offset_pos + 8] as u32) << 8) + | (payload_slice[offset_pos + 9] as u32); + + epg_dvb_calc_end_time(&mut event, start_time, duration); + + event.running_status = (payload_slice[offset_pos + 10] & 0xE0) >> 5; + event.free_ca_mode = (payload_slice[offset_pos + 10] & 0x10) >> 4; + + // Extract descriptors loop length (12 bits) + let descriptors_loop_length = (((payload_slice[offset_pos + 10] & 0x0f) as u16) << 8) + | payload_slice[offset_pos + 11] as u16; + + if descriptors_loop_length as usize > remaining - 12 { + info!("Warning: Invalid EIT descriptors_loop_length detected."); + return; + } + + let mut desc_pos = offset_pos + 12; + let desc_end = desc_pos + descriptors_loop_length as usize; + + // Process descriptors + while desc_pos < desc_end && desc_pos + 1 < size as usize { + if desc_pos + payload_slice[desc_pos + 1] as usize + 2 > size as usize { + info!("Warning: Invalid EIT descriptor_loop_length detected."); + return; + } + + let descriptor_tag = payload_slice[desc_pos]; + let descriptor_length = payload_slice[desc_pos + 1]; + + if descriptor_length + 2 == 0 { + info!("Warning: Invalid EIT descriptor_length detected."); + return; + } + + match descriptor_tag { + 0x4d => { + // Short event descriptor + epg_decode_short_event_descriptor( + &payload_slice[desc_pos + 2..], + descriptor_length as u32, + &mut event, + ); + } + 0x4e => { + // Extended event descriptor + epg_decode_extended_event_descriptor( + &payload_slice[desc_pos + 2..], + descriptor_length as u32, + &mut event, + ); + } + 0x54 => { + // Content descriptor + epg_decode_content_descriptor( + &payload_slice[desc_pos + 2..], + descriptor_length as u32, + &mut event, + ); + } + 0x55 => { + // Parental rating descriptor + epg_decode_parental_rating_descriptor( + &payload_slice[desc_pos + 2..], + descriptor_length as u32, + &mut event, + ); + } + _ => {} + } + + desc_pos += descriptor_length as usize + 2; } + + remaining -= descriptors_loop_length as usize + 12; + offset_pos += descriptors_loop_length as usize + 12; + + has_new |= epg_add_event(ctx_ref, pmt_map, &event); + + if has_new + && section_number == 0 + && table_id == 0x4e + && !ctx_ref.eit_current_events.is_null() + { + *ctx_ref.eit_current_events.offset(pmt_map as isize) = event.id as i32; + } + } + + // Output EPG if conditions are met + if has_new + && (ccx_options.xmltv == 1 || ccx_options.xmltv == 3) + && ccx_options.xmltvoutputinterval.millis() == 0 + { + epg_output(ctx_ref, ccx_options); + } +} +/// Handle outputting to XML files +/// # Safety +/// This function is unsafe because it dereferences raw pointers and calls unsafe functions like epg_output_net. +pub unsafe fn epg_handle_output(ctx_ref: &mut lib_ccx_ctx, ccx_options: &mut Options) { + if ctx_ref.demux_ctx.is_null() { + return; + } + + let demux_ctx = &*ctx_ref.demux_ctx; + let cur_sec = ((demux_ctx.global_timestamp - demux_ctx.min_global_timestamp) / 1000) as i32; + + // Full output + if (ccx_options.xmltv == 1 || ccx_options.xmltv == 3) + && ccx_options.xmltvoutputinterval.millis() != 0 + && cur_sec > ctx_ref.epg_last_output + ccx_options.xmltvliveinterval.millis() as i32 + { + ctx_ref.epg_last_output = cur_sec; + epg_output(ctx_ref, ccx_options); + } + + // Live output + if (ccx_options.xmltv == 2 || ccx_options.xmltv == 3 || ccx_options.send_to_srv) + && cur_sec > ctx_ref.epg_last_live_output + ccx_options.xmltvliveinterval.millis() as i32 + { + ctx_ref.epg_last_live_output = cur_sec; + if ccx_options.send_to_srv { + epg_output_net(ctx_ref, ccx_options); + } else { + epg_output_live(ctx_ref); + } + } +} + +/// Determine table type and call the correct function to handle it +/// # Safety +/// This function is unsafe because it dereferences raw pointers and performs memory operations. +pub unsafe fn epg_parse_table( + ctx: &mut lib_ccx_ctx, + b: *const u8, + size: u32, + ccx_options: &mut Options, +) { + if b.is_null() || size < 2 { + return; + } + + let buffer_slice = std::slice::from_raw_parts(b, size as usize); + let pointer_field = buffer_slice[0]; + + // XXX hack, should accumulate data + if pointer_field as u32 + 2 > size { + return; + } + + let payload_start_offset = (pointer_field + 1) as usize; + if payload_start_offset >= size as usize { + return; + } + + let payload_start = b.add(payload_start_offset); + let remaining_size = size - payload_start_offset as u32; + + if remaining_size == 0 { + return; + } + + let table_id = buffer_slice[payload_start_offset]; + + match table_id { + 0x0cb => { + epg_atsc_decode_eit(ctx, payload_start, remaining_size, ccx_options); + } + 0xc8 => { + epg_atsc_decode_vct(ctx, payload_start, remaining_size); + } + _ => { + if (0x4e..=0x6f).contains(&table_id) { + epg_dvb_decode_eit(ctx, payload_start, remaining_size, ccx_options); + } + } + } + + epg_handle_output(ctx, ccx_options); +} +/// Reconstructs DVB EIT and ATSC tables +/// # Safety +/// This function is unsafe because it dereferences raw pointers and performs memory operations. +pub unsafe fn parse_epg_packet(ctx: &mut lib_ccx_ctx, tspacket: &[u8; 188], options: &mut Options) { + let mut payload_start = tspacket.as_ptr().offset(4); + let mut payload_length = 188 - 4; + let payload_start_indicator = (tspacket[1] & 0x40) >> 6; + // let transport_priority = (tspacket[1] & 0x20) >> 5; + let pid = (((tspacket[1] & 0x1F) as u16) << 8 | tspacket[2] as u16) & 0x1FFF; + // let transport_scrambling_control = (tspacket[3] & 0xC0) >> 6; + let adaptation_field_control = (tspacket[3] & 0x30) >> 4; + let ccounter = tspacket[3] & 0xF; + let adaptation_field_length; + let mut buffer_map = 0xfff; + if adaptation_field_control & 2 != 0 { + adaptation_field_length = tspacket[4]; + payload_start = payload_start.offset(adaptation_field_length as isize + 1); + payload_length = (tspacket.as_ptr().offset(188) as usize - payload_start as usize) as u32; + } + + if (pid != 0x12 && pid != 0x1ffb && pid < 0x1000) || pid == 0x1fff { + return; + } + + if pid != 0x12 { + buffer_map = (pid - 0x1000) as usize; + } + + // Get reference to the EPG buffer + let epg_buffer = &mut *ctx.epg_buffers.add(buffer_map); + + if payload_start_indicator != 0 { + if epg_buffer.ccounter > 0 { + epg_buffer.ccounter = 0; + epg_parse_table(ctx, epg_buffer.buffer, epg_buffer.buffer_length, options); + } + + epg_buffer.prev_ccounter = ccounter as u32; + + if !epg_buffer.buffer.is_null() { + // Free using Rust's allocator + let layout = Layout::from_size_align_unchecked(epg_buffer.buffer_length as usize, 1); + dealloc(epg_buffer.buffer, layout); + } + // else { + // // must be first EIT packet + // } + + // Allocate using Rust's allocator + let layout = Layout::from_size_align_unchecked(payload_length as usize, 1); + epg_buffer.buffer = alloc(layout); + ptr::copy_nonoverlapping(payload_start, epg_buffer.buffer, payload_length as usize); + epg_buffer.buffer_length = payload_length; + epg_buffer.ccounter += 1; + } else if ccounter as u32 == epg_buffer.prev_ccounter + 1 + || (epg_buffer.prev_ccounter == 0x0f && ccounter == 0) + { + epg_buffer.prev_ccounter = ccounter as u32; + + // Reallocate using Rust's allocator + let old_layout = Layout::from_size_align_unchecked(epg_buffer.buffer_length as usize, 1); + let new_size = epg_buffer.buffer_length + payload_length; + epg_buffer.buffer = realloc(epg_buffer.buffer, old_layout, new_size as usize); + + ptr::copy_nonoverlapping( + payload_start, + epg_buffer.buffer.offset(epg_buffer.buffer_length as isize), + payload_length as usize, + ); + epg_buffer.ccounter += 1; + epg_buffer.buffer_length += payload_length; + } else { + debug!( + msg_type = DebugMessageFlag::GENERIC_NOTICE; + "\rWarning: Out of order EPG packets detected.\n\0" + ); } } diff --git a/src/rust/src/transportstream/mod.rs b/src/rust/src/transportstream/mod.rs index bab7c4af6..764be20e9 100644 --- a/src/rust/src/transportstream/mod.rs +++ b/src/rust/src/transportstream/mod.rs @@ -1,2 +1,3 @@ +pub mod epg_event; pub mod epg_tables; pub mod tables; diff --git a/src/rust/src/transportstream/tables.rs b/src/rust/src/transportstream/tables.rs index eed9c4527..33b8fd3e4 100644 --- a/src/rust/src/transportstream/tables.rs +++ b/src/rust/src/transportstream/tables.rs @@ -9,6 +9,7 @@ use crate::transportstream::epg_tables::epg_dvb_decode_string; use lib_ccxr::activity::ActivityExt; use lib_ccxr::common::{MpegDescriptor, Options, StreamType}; use lib_ccxr::util::log::{debug, info, DebugMessageFlag}; +pub const TS_PMT_MAP_SIZE: usize = 8192; pub fn get_printable_stream_type(stream_type: StreamType) -> StreamType { match stream_type { From d6d5c00a005e2ba8c872040450e8cc4ed5beb476 Mon Sep 17 00:00:00 2001 From: Deepnarayan Sett Date: Mon, 28 Jul 2025 21:37:31 +0530 Subject: [PATCH 3/7] TS Module: Failing CI for windows --- src/rust/src/lib.rs | 4 ++++ src/rust/src/transportstream/epg_tables.rs | 7 ++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/rust/src/lib.rs b/src/rust/src/lib.rs index 9df635a6a..527350490 100644 --- a/src/rust/src/lib.rs +++ b/src/rust/src/lib.rs @@ -270,6 +270,10 @@ extern "C" { #[allow(clashing_extern_declarations)] pub fn ccx_gxf_probe(buf: *const c_uchar, len: c_int) -> c_int; pub fn ccx_gxf_init(arg: *mut ccx_demuxer) -> *mut ccx_gxf; + pub fn fprintf(__stream: *mut FILE, __format: *const c_char, ...) -> ::std::os::raw::c_int; + pub fn fwrite(__ptr: *const c_void, __size: c_ulong, __n: c_ulong, __s: *mut FILE) -> c_ulong; + pub fn fclose(__stream: *mut FILE) -> c_int; + pub fn fopen(__filename: *const c_char, __modes: *const c_char) -> *mut FILE; } /// # Safety diff --git a/src/rust/src/transportstream/epg_tables.rs b/src/rust/src/transportstream/epg_tables.rs index c4ed97021..b8337a78f 100644 --- a/src/rust/src/transportstream/epg_tables.rs +++ b/src/rust/src/transportstream/epg_tables.rs @@ -1,8 +1,9 @@ -use crate::bindings::{fclose, fopen, fprintf, fwrite, lib_ccx_ctx, net_send_epg, FILE}; +use crate::bindings::{lib_ccx_ctx, net_send_epg, FILE}; use crate::common::CType; use crate::ctorust::FromCType; use crate::transportstream::epg_event::{EPGEventRust, EPGRatingRust}; use crate::transportstream::tables::TS_PMT_MAP_SIZE; +use crate::{fclose, fopen, fprintf, fwrite}; use chrono::{Datelike, NaiveDate, NaiveDateTime, NaiveTime, Timelike}; use encoding_rs::*; use lib_ccxr::common::Options; @@ -11,7 +12,7 @@ use lib_ccxr::info; use lib_ccxr::util::log::DebugMessageFlag; use std::alloc::{alloc, dealloc, realloc, Layout}; use std::ffi::{CStr, CString}; -use std::os::raw::{c_char, c_ulong, c_void}; +use std::os::raw::{c_char, c_void}; use std::ptr; /// Specific errors for EPG DVB string decoding. @@ -162,7 +163,7 @@ fn write_raw_bytes(f: &mut FILE, bytes: &[u8]) { fwrite( bytes.as_ptr() as *const c_void, 1, - bytes.len() as c_ulong, + bytes.len() as _, f as *mut FILE, ); } From 96e5868e67b728643c0bb0312b97232462dfe007 Mon Sep 17 00:00:00 2001 From: Deepnarayan Sett Date: Tue, 29 Jul 2025 13:20:02 +0530 Subject: [PATCH 4/7] TS Module: Mac CI tests failing --- mac/Makefile.am | 1 + 1 file changed, 1 insertion(+) diff --git a/mac/Makefile.am b/mac/Makefile.am index 9870c07bf..eefded659 100644 --- a/mac/Makefile.am +++ b/mac/Makefile.am @@ -308,6 +308,7 @@ endif if WITH_RUST ccextractor_LDADD += ./rust/@RUST_TARGET_SUBDIR@/libccx_rust.a +ccextractor_LDADD += -framework CoreFoundation else ccextractor_CFLAGS += -DDISABLE_RUST ccextractor_CPPFLAGS += -DDISABLE_RUST From baa2cb4e5170aa9497a304f4810ab6831749d739 Mon Sep 17 00:00:00 2001 From: Deepnarayan Sett Date: Tue, 29 Jul 2025 13:22:15 +0530 Subject: [PATCH 5/7] TS Module: Mac CI tests failing --- mac/Makefile.am | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mac/Makefile.am b/mac/Makefile.am index eefded659..797257d37 100644 --- a/mac/Makefile.am +++ b/mac/Makefile.am @@ -245,6 +245,7 @@ endif ccextractor_CFLAGS = -std=gnu99 -Wno-write-strings -Wno-pointer-sign -D_FILE_OFFSET_BITS=64 -DVERSION_FILE_PRESENT -DFT2_BUILD_LIBRARY -DGPAC_DISABLE_VTT -DGPAC_DISABLE_OD_DUMP -DGPAC_DISABLE_REMOTERY -DNO_GZIP ccextractor_LDFLAGS = $(shell pkg-config --libs gpac) +ccextractor_LDFLAGS += -framework CoreFoundation GPAC_CPPFLAGS = $(shell pkg-config --cflags gpac) ccextractor_CPPFLAGS =-I../src/lib_ccx/ -I../src/thirdparty/libpng/ -I../src/thirdparty/zlib/ -I../src/lib_ccx/zvbi/ -I../src/thirdparty/lib_hash/ -I../src/thirdparty/protobuf-c/ -I../src/thirdparty -I../src/ -I../src/thirdparty/freetype/include/ @@ -308,7 +309,6 @@ endif if WITH_RUST ccextractor_LDADD += ./rust/@RUST_TARGET_SUBDIR@/libccx_rust.a -ccextractor_LDADD += -framework CoreFoundation else ccextractor_CFLAGS += -DDISABLE_RUST ccextractor_CPPFLAGS += -DDISABLE_RUST From 3a945bfc61f0c93d4ac02b97f32fd72477f7c101 Mon Sep 17 00:00:00 2001 From: Deepnarayan Sett Date: Tue, 29 Jul 2025 23:59:35 +0530 Subject: [PATCH 6/7] TS Module: Seg Fault in Windows fixed --- src/rust/src/transportstream/epg_tables.rs | 26 ++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/src/rust/src/transportstream/epg_tables.rs b/src/rust/src/transportstream/epg_tables.rs index b8337a78f..3d7f1970e 100644 --- a/src/rust/src/transportstream/epg_tables.rs +++ b/src/rust/src/transportstream/epg_tables.rs @@ -1668,13 +1668,20 @@ pub unsafe fn parse_epg_packet(ctx: &mut lib_ccx_ctx, tspacket: &[u8; 188], opti let layout = Layout::from_size_align_unchecked(epg_buffer.buffer_length as usize, 1); dealloc(epg_buffer.buffer, layout); } - // else { - // // must be first EIT packet - // } // Allocate using Rust's allocator let layout = Layout::from_size_align_unchecked(payload_length as usize, 1); epg_buffer.buffer = alloc(layout); + + // CHECK FOR ALLOCATION FAILURE + if epg_buffer.buffer.is_null() { + debug!( + msg_type = DebugMessageFlag::GENERIC_NOTICE; + "\rError: Failed to allocate memory for EPG buffer.\n\0" + ); + return; + } + ptr::copy_nonoverlapping(payload_start, epg_buffer.buffer, payload_length as usize); epg_buffer.buffer_length = payload_length; epg_buffer.ccounter += 1; @@ -1686,7 +1693,18 @@ pub unsafe fn parse_epg_packet(ctx: &mut lib_ccx_ctx, tspacket: &[u8; 188], opti // Reallocate using Rust's allocator let old_layout = Layout::from_size_align_unchecked(epg_buffer.buffer_length as usize, 1); let new_size = epg_buffer.buffer_length + payload_length; - epg_buffer.buffer = realloc(epg_buffer.buffer, old_layout, new_size as usize); + let new_buffer = realloc(epg_buffer.buffer, old_layout, new_size as usize); + + // CHECK FOR REALLOCATION FAILURE + if new_buffer.is_null() { + debug!( + msg_type = DebugMessageFlag::GENERIC_NOTICE; + "\rError: Failed to reallocate memory for EPG buffer.\n\0" + ); + return; + } + + epg_buffer.buffer = new_buffer; ptr::copy_nonoverlapping( payload_start, From 7043f1292ed21fbce307365767b6e995dd9ec97b Mon Sep 17 00:00:00 2001 From: Deepnarayan Sett Date: Wed, 30 Jul 2025 00:49:02 +0530 Subject: [PATCH 7/7] TS Module: Removed Minor problem in epg_tables --- src/rust/src/transportstream/epg_tables.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/rust/src/transportstream/epg_tables.rs b/src/rust/src/transportstream/epg_tables.rs index 3d7f1970e..8713e237c 100644 --- a/src/rust/src/transportstream/epg_tables.rs +++ b/src/rust/src/transportstream/epg_tables.rs @@ -458,8 +458,6 @@ pub fn epg_print_event(event: &EPGEventRust, channel: u32, f: &mut FILE) { " ", &event.extended_iso_639_language_code, ); - //mprint("Extented Text = %s\n",event->extended_text); - info!("Extented Text = {}\n", extended_text); epg_fprintxml(f, extended_text); write_fprintf(f, "\n"); }