* fix: MKV subtitle track .(null) extension for KATE and unknown codec IDs
The matroska_track_text_subtitle_id_extensions array had 7 entries for
an 8-value enum, leaving MATROSKA_TRACK_SUBTITLE_CODEC_ID_KATE (index 7)
out of bounds. On most platforms this read NULL, which then caused
strlen(NULL) UB and snprintf to emit .(null) in the output filename.
Two fixes:
- Add "kate" at index 7 in the extensions array so KATE tracks
produce correct .kate output filenames
- Add a NULL guard in generate_filename_from_track() so any future
unknown codec ID safely falls back to .bin instead of crashing or
producing .(null)
Fixes#972
* fix: MKV subtitle track .(null) extension for KATE and unknown codec IDs
The matroska_track_text_subtitle_id_extensions array had 7 entries for
an 8-value enum, leaving MATROSKA_TRACK_SUBTITLE_CODEC_ID_KATE (index 7)
out of bounds. On most platforms this read NULL, which then caused
strlen(NULL) UB and snprintf to emit .(null) in the output filename.
Two fixes:
- Add "kate" at index 7 in the extensions array so KATE tracks
produce correct .kate output filenames
- Add a NULL guard in generate_filename_from_track() so any future
unknown codec ID safely falls back to .bin instead of crashing or
producing .(null)
Fixes#972
* fix: MKV subtitle track .(null) extension for KATE and unknown codec IDs
The matroska_track_text_subtitle_id_extensions array had 7 entries for
an 8-value enum, leaving MATROSKA_TRACK_SUBTITLE_CODEC_ID_KATE (index 7)
out of bounds. On most platforms this read NULL, which then caused
strlen(NULL) UB and snprintf to emit .(null) in the output filename.
Two fixes:
- Add "kate" at index 7 in the extensions array so KATE tracks
produce correct .kate output filenames
- Add a NULL guard in generate_filename_from_track() so any future
unknown codec ID safely falls back to .bin instead of crashing or
producing .(null)
Fixes#972
---------
Co-authored-by: Dhanush Varma <your@email.com>
cc_count * 3 used i32 arithmetic with no upper-bound check. For cc_count
> i32::MAX / 3 (~715 million), debug builds panic on overflow detection
and release builds silently wrap around to a negative range, discarding
all CC data for the frame. Both are triggerable from malformed media files.
Fix:
1. Cast cc_count to usize immediately after the existing <= 0 guard,
before any arithmetic — eliminates the overflow entirely
2. Add MAX_CC_COUNT = 31 upper-bound guard — CEA-708/ATSC A/53 encodes
cc_count in a 5-bit bitstream field (0x1F mask in avc_functions.c:514),
making 31 the spec-defined per-frame maximum; this value is also
independently documented in es/userdata.rs ("Maximum cc_count is 31").
Returns -1 with a warn!() log for out-of-range values, consistent with
existing error-handling style in the function.
3. Remove the now-redundant `x as usize` cast in the map closure since
the range is already usize..usize
Fixes#2234
show_timestamp.to_srt_time().expect() and hide_timestamp.to_srt_time().expect()
in TeletextContext::process_page() panicked for any negative Timestamp value.
Negative timestamps are common in broadcast captures with wrap-around or
uninitialized PTS — crashing after potentially processing an entire file.
to_srt_time() → as_hms_millis() → i64::try_into::<u64>() returns
OutOfRangeError for negative values; .expect() made this fatal.
Fix: process_page() already returns Option<Subtitle>, so replace both
.expect() calls with .ok()? — silently skipping the subtitle when the
timestamp is out of range, matching the function's existing None-on-empty
contract.
Fixes#2233
The Rust FFI function copy_from_rust() computed num_input_files by filtering
empty strings from the inputfiles Vec, but passed an unfiltered clone to
string_to_c_chars() to build the C inputfile[] array. This mismatch made the
C array length and num_input_files disagree: switch_to_next_file() could index
inputfile[current_file] where current_file < num_input_files but >= array size,
reading one slot past the end of the allocated array — confirmed by
AddressSanitizer (heap-buffer-overflow at file_functions.c:183).
The same count/size mismatch also caused free_rust_c_string_array() to
reconstruct the Vec with an incorrect capacity, producing heap corruption on
every clean shutdown.
Fix: filter empty strings into a single Vec<String> first, then derive both
num_input_files (filtered.len()) and the C array (string_to_c_chars(filtered))
from that same source, eliminating the mismatch entirely.
Fixes#2182
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Two related strdup bugs across multiple lib_ccx files:
1. strdup(variable) return not checked for NULL — use after potential
NULL dereference causes undefined behavior / segfault on OOM.
Fixed by adding NULL check + fatal(EXIT_NOT_ENOUGH_MEMORY, ...).
2. strdup("literal") in get_buffer_type_str returned directly as
function result — unchecked and leaks memory on every call since
the function has no callers that free it. Fixed by removing strdup
and returning string literals directly; return type changed from
char * to const char * (no callers exist, no header declaration).
Files changed:
src/lib_ccx/ccx_common_common.c
src/lib_ccx/ccx_encoders_common.c
src/lib_ccx/ccx_encoders_helpers.c
src/lib_ccx/configuration.c
src/lib_ccx/hardsubx.c
src/lib_ccx/hardsubx_decoder.c
src/lib_ccx/ocr.c
src/lib_ccx/output.c
src/lib_ccx/ts_functions.c
Fixes#2194
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Two bugs: (1) general_loop.c dereferences NULL dec_ctx in the
live_stream progress code when no data was processed. (2) Rust
switch_to_next_file calls demux_ctx.open() for stdin, triggering
null pointer dereference via ptr::null().
Fix: add NULL check for dec_ctx in general_loop.c, and return
early from switch_to_next_file for stdin/network/tcp sources
instead of calling demux_ctx.open().
Co-authored-by: Dhanush Varma <your@email.com>
* fix(general_loop): Add NULL checks for fopen() and alloc_demuxer_data() in process_hex()
* docs: update changelog for process_hex NULL checks fix
* fix: use EXIT_READ_ERROR for fopen failure and remove CHANGES.TXT entry
Replace all 6 hardcoded 1000/29.97 frame delay calculations in
dtvcc_write_scc() with 1000/current_fps so that CEA-708 SCC output
uses the actual stream framerate instead of assuming NTSC 29.97.
Fixes#2172
detect_stream_type() reads up to 1MB (STARTBYTESLENGTH) via
buffered_read_opt() for format detection. For input files smaller
than 1MB, the read hits EOF and—because binary_concat defaults to
enabled—buffered_read_opt() calls switch_to_next_file(). This
increments current_file past the valid range and closes the file
descriptor, leaving format-specific handlers (matroska_loop, MP4,
etc.) to crash when they access inputfile[current_file].
Fix: temporarily disable binary_concat around detect_stream_type()
so that hitting EOF during detection never triggers file switching.
Fixes the root cause of the crash reported in PR #2206 (which
proposed a band-aid of using current_file-1).
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add $(ExtraDefines) to PreprocessorDefinitions in all 4 configurations
of the vcxproj. This allows passing /p:ExtraDefines=DISABLE_RUST from
the MSBuild command line to use C code paths for switchable modules.
The Windows CI now produces two Release artifacts per architecture:
- "CCExtractor Windows x64 Release build" — min Rust (DISABLE_RUST)
- "CCExtractor Windows x64 Release build (with migrations)" — max Rust
The migrations build uses /t:Rebuild to do a clean rebuild without
DISABLE_RUST after the min-rust build completes.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: flush pending EIT sections in EPG_free() before freeing buffers
* ci: add dual build artifacts to compare C vs Rust code paths
Add -min-rust flag to linux/build that passes -DDISABLE_RUST to gcc,
causing switchable modules (DTVCC, demuxer, AVC, networking, hex utils)
to use their C implementations instead of Rust. The Rust library still
compiles since many modules are Rust-only.
The Linux CI now produces two artifacts:
- "CCExtractor Linux build" — min Rust (C paths where available)
- "CCExtractor Linux build (with migrations)" — max Rust
Both should produce identical output on the sample platform. If they
diverge, it means a Rust port introduced a behavioral difference.
The sample platform will need a corresponding update to recognize and
test the new "with migrations" artifact.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Varadraj75 <agrawalvaradraj2007@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix ownership of default Matroska track language
* matroska: restore lang_ietf preference in filename gen and lang matching
Restore the working IETF language preference that was accidentally
removed in the previous commit:
- generate_filename_from_track(): use lang_ietf ? lang_ietf : lang
for buffer sizing and both snprintf calls
- save_vobsub_track(): same lang_tag pattern for base filename
- matroska_save_all(): check lang_ietf first (BCP-47), fall back to
lang (ISO-639-2) when --mkvlang filter is active; remove now-unused
char *match variable
The strdup NULL check and broken switch-case removal from the prior
commit are unchanged.
* matroska: fix clang-format style
* matroska: fix filename underscores, LLD macro, braces, param name
pkg_check_modules provides library names without paths.
Without link_directories, the linker cannot find tesseract
and leptonica on systems where they are not in default
search paths (e.g. Homebrew on macOS arm64).
Co-authored-by: Dhanush Varma <your@email.com>
* feat(ssa): add guarded ASS \pos positioning for CEA-608 captions
* fix(ssa): correct ASS positioning anchor, validate row adjacency, and clean up variable placement
* fix(ssa): adjust top margin to prevent clipping of top-positioned CEA-608 captions
* ssa: map CEA-608 row+col to ASS coords using FFmpeg safe-area formula and fix \an2→\an7 anchor
create_file() returns the result of fopen() which can be NULL if the
file cannot be opened. matroska_loop() never checked this, passing
the NULL pointer into matroska_parse() where it is immediately used
in feof(), causing a crash.
Add a NULL check that calls fatal(EXIT_READ_ERROR, ...) on failure,
consistent with other file-open error handling in the codebase.
* fix: memory leaks and invalid CSS in WebVTT encoder
- Remove 6 unnecessary strdup() calls on string literals in
write_cc_buffer_as_webvtt() — literals are passed directly to
write_wrapped() which takes void*, no heap allocation needed.
This runs in a per-character inner loop and leaked on every
styled subtitle in a broadcast.
- Fix invalid CSS: rgba(0, 256, 0, 0.5) -> rgba(0, 255, 0, 0.5)
CSS color channels are 0-255; 256 is out of range.
- Fix missing free(unescaped) on write-error path in
write_stringz_as_webvtt() — matched the existing pattern on
the adjacent error path which correctly freed both el and unescaped.
Fixes#2154
* fix: move WebVTT changelog entry to unreleased 0.96.7 section
* Feat(rust): Implement WebVTT-specific timestamp format and layout anchor
* style: apply rustfmt to g608.rs
* fix(rust): use WebVTT-spec dot separator for milliseconds in timestamp line
Follow-up to #2137:
- Add NULL check on private_data in tlt_print_seen_pages_json
- Remove duplicate get_sib_stream_by_type call in print_file_report_json
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Verify that CEA-708 service decoders are not allocated at startup
and are only created on first use when data arrives for that service.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
MKV files with MPEG-2 video (common in DVD sources) were silently skipped.
Add V_MPEG2 track detection and processing using the existing process_m2v()
infrastructure, matching how mp4.c handles MPEG-2 streams.
Fixes#2149