diff --git a/.github/workflows/homebrew.yml b/.github/workflows/homebrew.yml index d7379aab..7df0d807 100644 --- a/.github/workflows/homebrew.yml +++ b/.github/workflows/homebrew.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Update Homebrew formula - uses: dawidd6/action-homebrew-bump-formula@v4 + uses: dawidd6/action-homebrew-bump-formula@v7 with: token: ${{ secrets.HOMEBREW_GITHUB_API_TOKEN }} formula: ccextractor diff --git a/README.md b/README.md index 115d01be..9a18f102 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,30 @@ You can find sample files on [our website](https://ccextractor.org/public/genera - [Building on Windows using WSL](docs/build-wsl.md) +#### Linux (Autotools) build notes + +CCExtractor also supports an autotools-based build system under the `linux/` +directory. + +Important notes: +- The autotools workflow lives inside `linux/`. The `configure` script is + generated there and should be run from that directory. +- Typical build steps are: +``` +cd linux +./autogen.sh +./configure +make +``` +- Rust support is enabled automatically if `cargo` and `rustc` are available + on the system. In that case, Rust components are built and linked during + `make`. +- If you encounter unexpected build or linking issues, a clean rebuild + (`make clean` or a fresh clone) is recommended, especially when Rust is + involved. + +This build flow has been tested on Linux and WSL. + ## Compiling CCExtractor To learn more about how to compile and build CCExtractor for your platform check the [compilation guide](https://github.com/CCExtractor/ccextractor/blob/master/docs/COMPILATION.MD). diff --git a/docs/CHANGES.TXT b/docs/CHANGES.TXT index 4a24f249..e60fcf04 100644 --- a/docs/CHANGES.TXT +++ b/docs/CHANGES.TXT @@ -1,5 +1,12 @@ +0.96.6 (unreleased) +------------------- +- Fix: Prevent infinite loop on truncated MKV files +- Fix: Various memory safety and stability fixes in demuxers (MP4, PS, MKV, DVB) +- Fix: Delete empty output files instead of leaving 0-byte files (#1282) + 0.96.5 (2026-01-05) ------------------- +- New: CCExtractor is available again via Homebrew on macOS and Linux. - New: Add support for raw CDP (Caption Distribution Packet) files (#1406) - New: Add --scc-accurate-timing option for bandwidth-aware SCC output (#1120) - Fix: MXF files containing CEA-708 captions not being detected/extracted (#1647) @@ -56,6 +63,7 @@ - Extract multiple teletext pages simultaneously with separate output files - Use --tpage multiple times (e.g., --tpage 100 --tpage 200) - Output files are named with page suffix (e.g., output_p100.srt, output_p200.srt) +- Fix: SPUPNG subtitle offset calculation to center based on actual image dimensions - New: Added --list-tracks (-L) option to list all tracks in media files without processing New: Chinese, Korean, Japanese support - proper encoding and OCR. diff --git a/docs/COMPILATION.MD b/docs/COMPILATION.MD index 52f511ff..32fdccb4 100644 --- a/docs/COMPILATION.MD +++ b/docs/COMPILATION.MD @@ -1,3 +1,16 @@ +# Installation + +## Homebrew +The easiest way to install CCExtractor for Mac and Linux is through Homebrew: + +```bash +brew install ccextractor +``` +Note: If you don't have Homebrew installed, see [brew.sh](https://brew.sh/) + for installation instructions. + +--- + # Compiling CCExtractor You may compile CCExtractor across all major platforms using `CMakeLists.txt` stored under `ccextractor/src/` directory. Autoconf and custom build scripts are also available. See platform specific instructions in the below sections. diff --git a/src/lib_ccx/ccx_encoders_common.c b/src/lib_ccx/ccx_encoders_common.c index a7994dee..816f2cdd 100644 --- a/src/lib_ccx/ccx_encoders_common.c +++ b/src/lib_ccx/ccx_encoders_common.c @@ -778,6 +778,7 @@ struct encoder_ctx *init_encoder(struct encoder_cfg *opt) return NULL; } ctx->in_fileformat = opt->in_format; + ctx->is_pal = (opt->in_format == 2); /** used in case of SUB_EOD_MARKER */ ctx->prev_start = -1; diff --git a/src/lib_ccx/ccx_encoders_common.h b/src/lib_ccx/ccx_encoders_common.h index e446fba1..e1d2a886 100644 --- a/src/lib_ccx/ccx_encoders_common.h +++ b/src/lib_ccx/ccx_encoders_common.h @@ -182,10 +182,10 @@ struct encoder_ctx // OCR in SPUPNG int nospupngocr; + int is_pal; - // Teletext multi-page output (issue #665) - struct ccx_s_write *tlt_out[MAX_TLT_PAGES_EXTRACT]; // Output files per teletext page - uint16_t tlt_out_pages[MAX_TLT_PAGES_EXTRACT]; // Page numbers for each output slot + struct ccx_s_write *tlt_out[MAX_TLT_PAGES_EXTRACT]; // Output files per teletext page + uint16_t tlt_out_pages[MAX_TLT_PAGES_EXTRACT]; // Page numbers for each output slot unsigned int tlt_srt_counter[MAX_TLT_PAGES_EXTRACT]; // SRT counter per page int tlt_out_count; // Number of teletext output files }; diff --git a/src/lib_ccx/ccx_encoders_spupng.c b/src/lib_ccx/ccx_encoders_spupng.c index 09fca63f..9306f534 100644 --- a/src/lib_ccx/ccx_encoders_spupng.c +++ b/src/lib_ccx/ccx_encoders_spupng.c @@ -251,6 +251,9 @@ void set_spupng_offset(void *ctx, int x, int y) sp->xOffset = x; sp->yOffset = y; } + +// Forward declaration for calculate_spupng_offsets +static void calculate_spupng_offsets(struct spupng_t *sp, struct encoder_ctx *ctx); int save_spupng(const char *filename, uint8_t *bitmap, int w, int h, png_color *palette, png_byte *alpha, int nb_color) { @@ -384,7 +387,7 @@ int write_cc_bitmap_as_spupng(struct cc_subtitle *sub, struct encoder_ctx *conte struct cc_bitmap *rect; png_color *palette = NULL; png_byte *alpha = NULL; - int wrote_opentag = 1; + int wrote_opentag = 0; // Track if we actually wrote the tag x_pos = -1; y_pos = -1; @@ -395,13 +398,11 @@ int write_cc_bitmap_as_spupng(struct cc_subtitle *sub, struct encoder_ctx *conte return 0; inc_spupng_fileindex(sp); - write_sputag_open(sp, sub->start_time, sub->end_time - 1); if (sub->nb_data == 0 && (sub->flags & SUB_EOD_MARKER)) { context->prev_start = -1; - if (wrote_opentag) - write_sputag_close(sp); + // No subtitle data, skip writing return 0; } rect = sub->data; @@ -440,7 +441,13 @@ int write_cc_bitmap_as_spupng(struct cc_subtitle *sub, struct encoder_ctx *conte } } filename = get_spupng_filename(sp); - set_spupng_offset(sp, x_pos, y_pos); + + // Set image dimensions for offset calculation + sp->img_w = width; + sp->img_h = height; + + // Calculate centered offsets based on screen size (PAL/NTSC) + calculate_spupng_offsets(sp, context); if (sub->flags & SUB_EOD_MARKER) context->prev_start = sub->start_time; pbuf = (uint8_t *)malloc(width * height); @@ -475,6 +482,15 @@ int write_cc_bitmap_as_spupng(struct cc_subtitle *sub, struct encoder_ctx *conte /* TODO do rectangle wise, one color table should not be used for all rectangles */ mapclut_paletee(palette, alpha, (uint32_t *)rect[0].data1, rect[0].nb_colors); + + // Save PNG file first + save_spupng(filename, pbuf, width, height, palette, alpha, rect[0].nb_colors); + freep(&pbuf); + + // Write XML tag with calculated centered offsets + write_sputag_open(sp, sub->start_time, sub->end_time - 1); + wrote_opentag = 1; // Mark that we wrote the tag + #ifdef ENABLE_OCR if (!context->nospupngocr) { @@ -487,8 +503,6 @@ int write_cc_bitmap_as_spupng(struct cc_subtitle *sub, struct encoder_ctx *conte } } #endif - save_spupng(filename, pbuf, width, height, palette, alpha, rect[0].nb_colors); - freep(&pbuf); end: if (wrote_opentag) @@ -991,6 +1005,8 @@ int spupng_export_string2png(struct spupng_t *sp, char *str, FILE *output) */ // Save image + sp->img_w = canvas_width; + sp->img_h = canvas_height; write_image(buffer, output, canvas_width, canvas_height); free(tmp); free(buffer); @@ -1081,6 +1097,28 @@ int eia608_to_str(struct encoder_ctx *context, struct eia608_screen *data, char // string needs to be in UTF-8 encoding. // This function will take care of encoding. +static void calculate_spupng_offsets(struct spupng_t *sp, struct encoder_ctx *ctx) +{ + int screen_w = 720; + int screen_h; + + /* Teletext is always PAL */ + if (ctx->in_fileformat == 2 || ctx->is_pal) + { + screen_h = 576; + } + else + { + screen_h = 480; + } + + sp->xOffset = (screen_w - sp->img_w) / 2; + sp->yOffset = (screen_h - sp->img_h) / 2; + + // SPU / DVD requires even yOffset (interlacing) + if (sp->yOffset & 1) + sp->yOffset++; +} int spupng_write_string(struct spupng_t *sp, char *string, LLONG start_time, LLONG end_time, struct encoder_ctx *context) { @@ -1099,6 +1137,7 @@ int spupng_write_string(struct spupng_t *sp, char *string, LLONG start_time, LLO } // free(string_utf32); fclose(sp->fppng); + calculate_spupng_offsets(sp, context); write_sputag_open(sp, start_time, end_time); write_spucomment(sp, string); write_sputag_close(sp); diff --git a/src/lib_ccx/ccx_encoders_structs.h b/src/lib_ccx/ccx_encoders_structs.h index efecbb72..9b0cf036 100644 --- a/src/lib_ccx/ccx_encoders_structs.h +++ b/src/lib_ccx/ccx_encoders_structs.h @@ -39,6 +39,8 @@ struct spupng_t int fileIndex; int xOffset; int yOffset; + int img_w; + int img_h; }; #endif diff --git a/src/lib_ccx/dvb_subtitle_decoder.c b/src/lib_ccx/dvb_subtitle_decoder.c index 02908d39..ce4c5e2d 100644 --- a/src/lib_ccx/dvb_subtitle_decoder.c +++ b/src/lib_ccx/dvb_subtitle_decoder.c @@ -1926,7 +1926,7 @@ static int write_dvb_sub(struct lib_cc_decode *dec_ctx, struct cc_subtitle *sub) ctx->ocr_ctx = init_ocr(ctx->lang_index); ctx->ocr_initialized = 1; // Mark as initialized even if init_ocr returns NULL } - if (ctx->ocr_ctx) + if (ctx->ocr_ctx && region) { // DEBUG: Dump before OCR // dump_rect_and_log("before_ocr", rect->data0, rect->w, rect->h, rect->linesize0, 1, 0, 0); diff --git a/src/lib_ccx/file_functions.c b/src/lib_ccx/file_functions.c index 7d25e3b6..fb8653b9 100644 --- a/src/lib_ccx/file_functions.c +++ b/src/lib_ccx/file_functions.c @@ -66,6 +66,7 @@ void prepare_for_new_file(struct lib_ccx_ctx *ctx) { // Init per file variables ctx->last_reported_progress = -1; + ctx->min_global_timestamp_offset = -1; // -1 means not yet initialized ctx->stat_numuserheaders = 0; ctx->stat_dvdccheaders = 0; ctx->stat_scte20ccheaders = 0; diff --git a/src/lib_ccx/general_loop.c b/src/lib_ccx/general_loop.c index 5ee833eb..8d6fde40 100644 --- a/src/lib_ccx/general_loop.c +++ b/src/lib_ccx/general_loop.c @@ -1727,7 +1727,24 @@ int general_loop(struct lib_ccx_ctx *ctx) if (ctx->live_stream) { - int cur_sec = (int)(get_fts(dec_ctx->timing, dec_ctx->current_field) / 1000); + LLONG t = get_fts(dec_ctx->timing, dec_ctx->current_field); + if (!t && ctx->demux_ctx->global_timestamp_inited) + t = ctx->demux_ctx->global_timestamp - ctx->demux_ctx->min_global_timestamp; + // Handle multi-program TS timing + if (ctx->demux_ctx->global_timestamp_inited) + { + LLONG offset = ctx->demux_ctx->global_timestamp - ctx->demux_ctx->min_global_timestamp; + if (ctx->min_global_timestamp_offset < 0 || offset < ctx->min_global_timestamp_offset) + ctx->min_global_timestamp_offset = offset; + // Only use timestamps from the program with the lowest base + if (offset - ctx->min_global_timestamp_offset < 60000) + t = offset - ctx->min_global_timestamp_offset; + else + t = ctx->min_global_timestamp_offset > 0 ? 0 : t; + if (t < 0) + t = 0; + } + int cur_sec = (int)(t / 1000); int th = cur_sec / 10; if (ctx->last_reported_progress != th) { @@ -1745,6 +1762,28 @@ int general_loop(struct lib_ccx_ctx *ctx) LLONG t = get_fts(dec_ctx->timing, dec_ctx->current_field); if (!t && ctx->demux_ctx->global_timestamp_inited) t = ctx->demux_ctx->global_timestamp - ctx->demux_ctx->min_global_timestamp; + // For multi-program TS files, different programs can have different + // PCR bases (e.g., one at 25h, another at 23h). This causes the + // global_timestamp to jump between different bases, resulting in + // wildly different offset values. Track the minimum offset seen + // and only display times from the program with the lowest base. + if (ctx->demux_ctx->global_timestamp_inited) + { + LLONG offset = ctx->demux_ctx->global_timestamp - ctx->demux_ctx->min_global_timestamp; + // Track minimum offset (this is the PCR base of the program + // with the lowest timestamp, which represents true file time) + if (ctx->min_global_timestamp_offset < 0 || offset < ctx->min_global_timestamp_offset) + ctx->min_global_timestamp_offset = offset; + // Only use timestamps from the program with the lowest base. + // If current offset is significantly larger than minimum (by > 60s), + // it's from a program with a higher PCR base - use minimum instead. + if (offset - ctx->min_global_timestamp_offset < 60000) + t = offset - ctx->min_global_timestamp_offset; + else + t = ctx->min_global_timestamp_offset > 0 ? 0 : t; // fallback to minimum-based time + if (t < 0) + t = 0; + } int cur_sec = (int)(t / 1000); activity_progress(progress, cur_sec / 60, cur_sec % 60); ctx->last_reported_progress = progress; diff --git a/src/lib_ccx/lib_ccx.h b/src/lib_ccx/lib_ccx.h index 050d124f..958675fa 100644 --- a/src/lib_ccx/lib_ccx.h +++ b/src/lib_ccx/lib_ccx.h @@ -118,6 +118,7 @@ struct lib_ccx_ctx LLONG total_past; // Only in binary concat mode int last_reported_progress; + LLONG min_global_timestamp_offset; // Track minimum (global - min) for multi-program TS /* Stats */ int stat_numuserheaders; diff --git a/src/lib_ccx/matroska.c b/src/lib_ccx/matroska.c index ddcc38bf..7821e3e1 100644 --- a/src/lib_ccx/matroska.c +++ b/src/lib_ccx/matroska.c @@ -122,6 +122,8 @@ void parse_ebml(FILE *file) { code <<= 8; code += mkv_read_byte(file); + if (feof(file)) + break; code_len++; switch (code) @@ -186,6 +188,8 @@ void parse_segment_info(FILE *file) { code <<= 8; code += mkv_read_byte(file); + if (feof(file)) + break; code_len++; switch (code) @@ -484,6 +488,8 @@ void parse_segment_cluster_block_group(struct matroska_ctx *mkv_ctx, ULLONG clus { code <<= 8; code += mkv_read_byte(file); + if (feof(file)) + break; code_len++; switch (code) @@ -612,6 +618,8 @@ void parse_segment_cluster(struct matroska_ctx *mkv_ctx) { code <<= 8; code += mkv_read_byte(file); + if (feof(file)) + break; code_len++; switch (code) @@ -845,6 +853,8 @@ void parse_segment_track_entry(struct matroska_ctx *mkv_ctx) { code <<= 8; code += mkv_read_byte(file); + if (feof(file)) + break; code_len++; switch (code) @@ -1197,6 +1207,8 @@ void parse_segment_tracks(struct matroska_ctx *mkv_ctx) { code <<= 8; code += mkv_read_byte(file); + if (feof(file)) + break; code_len++; switch (code) @@ -1241,6 +1253,8 @@ void parse_segment(struct matroska_ctx *mkv_ctx) { code <<= 8; code += mkv_read_byte(file); + if (feof(file)) + break; code_len++; switch (code) { @@ -1915,6 +1929,9 @@ void matroska_parse(struct matroska_ctx *mkv_ctx) { code <<= 8; code += mkv_read_byte(file); + // Check for EOF after reading - feof() is only set after a failed read + if (feof(file)) + break; code_len++; switch (code) diff --git a/src/lib_ccx/output.c b/src/lib_ccx/output.c index 7cab0969..ac9be925 100644 --- a/src/lib_ccx/output.c +++ b/src/lib_ccx/output.c @@ -14,7 +14,19 @@ void dinit_write(struct ccx_s_write *wb) return; } if (wb->fh > 0) + { + // Check if the file is empty before closing + off_t file_size = lseek(wb->fh, 0, SEEK_END); close(wb->fh); + + // Delete empty output files to avoid generating useless 0-byte files + // This commonly happens with -12 option when one field has no captions + if (file_size == 0 && wb->filename != NULL) + { + unlink(wb->filename); + mprint("Deleted empty output file: %s\n", wb->filename); + } + } freep(&wb->filename); freep(&wb->original_filename); if (wb->with_semaphore && wb->semaphore_filename) diff --git a/src/rust/lib_ccxr/src/time/units.rs b/src/rust/lib_ccxr/src/time/units.rs index eb39c5d9..34ecc4b3 100644 --- a/src/rust/lib_ccxr/src/time/units.rs +++ b/src/rust/lib_ccxr/src/time/units.rs @@ -225,9 +225,6 @@ impl Timestamp { let m = millis / 60000 - 60 * h; let s = millis / 1000 - 3600 * h - 60 * m; let u = millis - 3600000 * h - 60000 * m - 1000 * s; - if h > 24 { - println!("{h}") - } Ok((h.try_into()?, m as u8, s as u8, u as u16)) } diff --git a/src/rust/src/avc/sei.rs b/src/rust/src/avc/sei.rs index 340dbb4b..3977bc01 100644 --- a/src/rust/src/avc/sei.rs +++ b/src/rust/src/avc/sei.rs @@ -226,12 +226,10 @@ pub fn user_data_registered_itu_t_t35(ctx: &mut AvcContextRust, userbuf: &[u8]) } // Save the data and process once we know the sequence number - if ((ctx.cc_count as usize + local_cc_count) * 3) + 1 > ctx.cc_databufsize { + let required_size = ((ctx.cc_count as usize + local_cc_count) * 3) + 1; + if required_size > ctx.cc_data.len() { let new_size = ((ctx.cc_count as usize + local_cc_count) * 6) + 1; - unsafe { - ctx.cc_data.set_len(new_size); - } - ctx.cc_data.reserve(new_size); + ctx.cc_data.resize(new_size, 0); ctx.cc_databufsize = new_size; } diff --git a/src/rust/src/demuxer/common_types.rs b/src/rust/src/demuxer/common_types.rs index b7dc2a21..0f7bfe20 100644 --- a/src/rust/src/demuxer/common_types.rs +++ b/src/rust/src/demuxer/common_types.rs @@ -1,8 +1,13 @@ use crate::bindings::{lib_ccx_ctx, list_head}; use lib_ccxr::common::{Codec, Decoder608Report, DecoderDtvccReport, StreamMode, StreamType}; use lib_ccxr::time::Timestamp; +use std::os::raw::c_void; use std::ptr::null_mut; +extern "C" { + fn free(ptr: *mut c_void); +} + // Size of the Startbytes Array in CcxDemuxer - const 1MB pub(crate) const ARRAY_SIZE: usize = 1024 * 1024; @@ -109,7 +114,9 @@ impl Default for PSIBuffer { fn default() -> Self { PSIBuffer { prev_ccounter: 0, - buffer: Box::into_raw(Box::new(0u8)), + // Initialize with null to avoid unnecessary heap allocations and + // signal that the buffer is currently empty. + buffer: std::ptr::null_mut(), buffer_length: 0, ccounter: 0, } @@ -274,21 +281,21 @@ impl Default for CcxDemuxer<'_> { /// null pointers which are safely ignored. impl Drop for CcxDemuxer<'_> { fn drop(&mut self) { - // Free all non-null PSIBuffer pointers (Rust-owned from Box::into_raw) + // Free all non-null PSIBuffer pointers. + // These are freed using C's free to be compatible with memory that might be allocated by C. for ptr in self.pid_buffers.drain(..) { if !ptr.is_null() { - // SAFETY: These pointers were created via Box::into_raw in copy_demuxer_from_c_to_rust unsafe { - drop(Box::from_raw(ptr)); + free(ptr as *mut c_void); } } } - // Free all non-null PMTEntry pointers (Rust-owned from Box::into_raw) + // Free all non-null PMTEntry pointers. + // These are freed using C's free to be compatible with memory that might be allocated by C. for ptr in self.pids_programs.drain(..) { if !ptr.is_null() { - // SAFETY: These pointers were created via Box::into_raw in copy_demuxer_from_c_to_rust unsafe { - drop(Box::from_raw(ptr)); + free(ptr as *mut c_void); } } } diff --git a/src/rust/src/demuxer/stream_functions.rs b/src/rust/src/demuxer/stream_functions.rs index f0957532..ed1298de 100644 --- a/src/rust/src/demuxer/stream_functions.rs +++ b/src/rust/src/demuxer/stream_functions.rs @@ -331,10 +331,15 @@ unsafe fn detect_stream_type_common(ctx: &mut CcxDemuxer, ccx_options: &mut Opti } // Now check for PS (Needs PACK header) + // The loop below checks 4 consecutive bytes (i, i+1, i+2, i+3), so we need + // to stop 3 bytes before the end to avoid out-of-bounds access. + // - If buffer < 50000: limit = buffer_size - 3 (scan entire buffer) + // - If buffer >= 50000: limit = 49997 (= 50000 - 3, cap the scan range) + // We use saturating_sub to safely handle tiny buffers (< 3 bytes). let limit = if ctx.startbytes_avail < 50000 { - ctx.startbytes_avail - 3 + ctx.startbytes_avail.saturating_sub(3) } else { - 49997 + 50000 - 3 // Don't scan huge buffers entirely; 50KB is enough } as usize; for i in 0..limit { if ctx.startbytes[i] == 0x00 @@ -427,15 +432,21 @@ pub fn is_valid_mp4_box( ) ); - // If the box type is "moov", check if it contains a valid movie header (mvhd) - if idx == 2 - && !(buffer[position + 12] == b'm' + // If the box type is "moov", it must contain "mvhd" to be valid. + // We need 16 bytes from position to check bytes 12-15 for "mvhd". + if idx == 2 { + if position + 16 > buffer.len() { + // Not enough bytes to verify mvhd - skip this box + continue; + } + if !(buffer[position + 12] == b'm' && buffer[position + 13] == b'v' && buffer[position + 14] == b'h' && buffer[position + 15] == b'd') - { - // If "moov" doesn't have "mvhd", skip it. - continue; + { + // moov without mvhd is not valid - skip it + continue; + } } // Box name matches. Do a crude validation of possible box size, diff --git a/src/rust/src/libccxr_exports/demuxer.rs b/src/rust/src/libccxr_exports/demuxer.rs index dc7ef2c5..6a875c7d 100755 --- a/src/rust/src/libccxr_exports/demuxer.rs +++ b/src/rust/src/libccxr_exports/demuxer.rs @@ -7,7 +7,6 @@ use crate::demuxer::common_types::{ }; use lib_ccxr::common::{Codec, Options, StreamMode, StreamType}; use lib_ccxr::time::Timestamp; -use std::alloc::{alloc_zeroed, Layout}; use std::ffi::CStr; use std::os::raw::{c_char, c_int, c_uchar, c_uint, c_void}; @@ -18,10 +17,12 @@ const POISON_PTR_PATTERN: usize = 0xcdcdcdcdcdcdcdcd; #[cfg(target_pointer_width = "32")] const POISON_PTR_PATTERN: usize = 0xcdcdcdcd; -// External C function declarations extern "C" { fn activity_input_file_closed(); fn close(fd: c_int) -> c_int; + fn malloc(size: usize) -> *mut c_void; + fn free(ptr: *mut c_void); + fn calloc(nmemb: usize, size: usize) -> *mut c_void; } pub fn copy_c_array_to_rust_vec( @@ -98,61 +99,89 @@ pub unsafe fn copy_demuxer_from_rust_to_c(c_demuxer: *mut ccx_demuxer, rust_demu c.global_timestamp_inited = rust_demuxer.global_timestamp_inited.millis() as c_int; // PID buffers - extra defensive version + // We iterate through all possible PIDs (up to 8191 for PSI) to ensure state synchronization. + // CRITICAL: We must free existing pointers in the C structure before overwriting them + // to prevent massive memory leaks during the demuxing process, as this function + // is called repeatedly to sync state between Rust and C. let pid_buffers_len = rust_demuxer.pid_buffers.len().min(8191); - for i in 0..pid_buffers_len { - let pid_buffer = rust_demuxer.pid_buffers[i]; - if !pid_buffer.is_null() { - // Try to safely access the pointer - match std::panic::catch_unwind(|| unsafe { &*pid_buffer }) { - Ok(rust_psi) => { - let c_psi = unsafe { rust_psi.to_ctype() }; - let c_ptr = Box::into_raw(Box::new(c_psi)); - c.PID_buffers[i] = c_ptr; - } - Err(_) => { - // Pointer was invalid, set to null - eprintln!("Warning: Invalid PID buffer pointer at index {i}"); - c.PID_buffers[i] = std::ptr::null_mut(); + for i in 0..8191 { + // Free existing pointer if any. + // SAFETY: We use C's free to be compatible with memory that might be allocated by C. + // We also check for POISON_PTR_PATTERN for safety in debug builds. + if !c.PID_buffers[i].is_null() && c.PID_buffers[i] as usize != POISON_PTR_PATTERN { + unsafe { + free(c.PID_buffers[i] as *mut c_void); + c.PID_buffers[i] = std::ptr::null_mut(); + } + } + + if i < pid_buffers_len { + let pid_buffer = rust_demuxer.pid_buffers[i]; + if !pid_buffer.is_null() { + // Try to safely access the pointer using catch_unwind to prevent + // a panic in Rust from crashing the entire C application. + // This is a defensive measure for FFI robustness. + match std::panic::catch_unwind(|| unsafe { &*pid_buffer }) { + Ok(rust_psi) => { + let c_psi = unsafe { rust_psi.to_ctype() }; + let c_ptr = + unsafe { malloc(std::mem::size_of::()) } + as *mut crate::bindings::PSI_buffer; + if !c_ptr.is_null() { + unsafe { + std::ptr::write(c_ptr, c_psi); + } + c.PID_buffers[i] = c_ptr; + } + } + Err(_) => { + // Pointer was invalid, log and skip + eprintln!("Warning: Invalid PID buffer pointer at index {i}"); + } } } - } else { - c.PID_buffers[i] = std::ptr::null_mut(); } } - // Clear remaining slots if rust array is smaller than C array - for i in pid_buffers_len..8191 { - c.PID_buffers[i] = std::ptr::null_mut(); - } - // PIDs programs - extra defensive version + // Similar to PID_buffers, we manage ownership of PMT entries. + // We check for POISON_PTR_PATTERN to avoid freeing uninitialized memory in debug builds. let pids_programs_len = rust_demuxer.pids_programs.len().min(65536); - for i in 0..pids_programs_len { - let pmt_entry = rust_demuxer.pids_programs[i]; - if !pmt_entry.is_null() { - // Try to safely access the pointer - match std::panic::catch_unwind(|| unsafe { &*pmt_entry }) { - Ok(rust_pmt) => { - let c_pmt = unsafe { rust_pmt.to_ctype() }; - let c_ptr = Box::into_raw(Box::new(c_pmt)); - c.PIDs_programs[i] = c_ptr; - } - Err(_) => { - // Pointer was invalid, set to null - eprintln!("Warning: Invalid PMT entry pointer at index {i}"); - c.PIDs_programs[i] = std::ptr::null_mut(); + for i in 0..65536 { + // Free existing pointer if any and it's not a poison pattern. + // SAFETY: We use C's free to be compatible with memory that might be allocated by C. + if !c.PIDs_programs[i].is_null() && c.PIDs_programs[i] as usize != POISON_PTR_PATTERN { + unsafe { + free(c.PIDs_programs[i] as *mut c_void); + c.PIDs_programs[i] = std::ptr::null_mut(); + } + } + + if i < pids_programs_len { + let pmt_entry = rust_demuxer.pids_programs[i]; + if !pmt_entry.is_null() { + // Safely convert and move ownership to C + match std::panic::catch_unwind(|| unsafe { &*pmt_entry }) { + Ok(rust_pmt) => { + let c_pmt = unsafe { rust_pmt.to_ctype() }; + let c_ptr = + unsafe { malloc(std::mem::size_of::()) } + as *mut crate::bindings::PMT_entry; + if !c_ptr.is_null() { + unsafe { + std::ptr::write(c_ptr, c_pmt); + } + c.PIDs_programs[i] = c_ptr; + } + } + Err(_) => { + eprintln!("Warning: Invalid PMT entry pointer at index {i}"); + } } } - } else { - c.PIDs_programs[i] = std::ptr::null_mut(); } } - // Clear remaining slots if rust array is smaller than C array - for i in pids_programs_len..65536 { - c.PIDs_programs[i] = std::ptr::null_mut(); - } - // PIDs seen array for (i, &val) in rust_demuxer.pids_seen.iter().take(65536).enumerate() { c.PIDs_seen[i] = val as c_int; @@ -265,7 +294,15 @@ pub unsafe fn copy_demuxer_from_c_to_rust(ccx: *const ccx_demuxer) -> CcxDemuxer if buffer_ptr.is_null() { None } else { - Some(Box::into_raw(Box::new(PSIBuffer::from_ctype(*buffer_ptr)?))) + let rust_item = PSIBuffer::from_ctype(*buffer_ptr)?; + let rust_ptr = + unsafe { malloc(std::mem::size_of::()) } as *mut PSIBuffer; + if !rust_ptr.is_null() { + unsafe { + std::ptr::write(rust_ptr, rust_item); + } + } + Some(rust_ptr) } }) .collect::>(); @@ -276,7 +313,14 @@ pub unsafe fn copy_demuxer_from_c_to_rust(ccx: *const ccx_demuxer) -> CcxDemuxer if buffer_ptr.is_null() || buffer_ptr as usize == POISON_PTR_PATTERN { None } else { - Some(Box::into_raw(Box::new(PMTEntry::from_ctype(*buffer_ptr)?))) + let rust_item = PMTEntry::from_ctype(*buffer_ptr)?; + let rust_ptr = unsafe { malloc(std::mem::size_of::()) } as *mut PMTEntry; + if !rust_ptr.is_null() { + unsafe { + std::ptr::write(rust_ptr, rust_item); + } + } + Some(rust_ptr) } }) .collect::>(); @@ -367,8 +411,7 @@ pub unsafe fn copy_demuxer_from_c_to_rust(ccx: *const ccx_demuxer) -> CcxDemuxer /// /// This function is unsafe because we are calling a C struct and using alloc_zeroed to initialize it. pub unsafe fn alloc_new_demuxer() -> *mut ccx_demuxer { - let layout = Layout::new::(); - let ptr = alloc_zeroed(layout) as *mut ccx_demuxer; + let ptr = calloc(1, std::mem::size_of::()) as *mut ccx_demuxer; if ptr.is_null() { panic!("Failed to allocate memory for ccx_demuxer");