Skip to main content

libbitsub_core/vobsub/
mks_parser.rs

1//! Matroska subtitle extraction for embedded VobSub tracks.
2
3use miniz_oxide::inflate::{TINFLStatus, decompress_to_vec_zlib_with_limit};
4use std::fmt::Write;
5use std::ops::Range;
6
7const EBML_ID_SEGMENT: u32 = 0x1853_8067;
8const EBML_ID_SEGMENT_INFO: u32 = 0x1549_A966;
9const EBML_ID_TRACKS: u32 = 0x1654_AE6B;
10const EBML_ID_TRACK_ENTRY: u32 = 0xAE;
11const EBML_ID_TRACK_NUMBER: u32 = 0xD7;
12const EBML_ID_TRACK_TYPE: u32 = 0x83;
13const EBML_ID_CODEC_ID: u32 = 0x86;
14const EBML_ID_CODEC_PRIVATE: u32 = 0x63A2;
15const EBML_ID_LANGUAGE: u32 = 0x22B5_9C;
16const EBML_ID_LANGUAGE_IETF: u32 = 0x22B5_9D;
17const EBML_ID_NAME: u32 = 0x536E;
18const EBML_ID_CONTENT_ENCODINGS: u32 = 0x6D80;
19const EBML_ID_CONTENT_ENCODING: u32 = 0x6240;
20const EBML_ID_CONTENT_COMPRESSION: u32 = 0x5034;
21const EBML_ID_CONTENT_COMP_ALGO: u32 = 0x4254;
22const EBML_ID_CONTENT_COMP_SETTINGS: u32 = 0x4255;
23const EBML_ID_TIMECODE_SCALE: u32 = 0x002A_D7B1;
24const EBML_ID_CLUSTER: u32 = 0x1F43_B675;
25const EBML_ID_CLUSTER_TIMESTAMP: u32 = 0xE7;
26const EBML_ID_BLOCK_GROUP: u32 = 0xA0;
27const EBML_ID_BLOCK: u32 = 0xA1;
28const EBML_ID_SIMPLE_BLOCK: u32 = 0xA3;
29
30const MATROSKA_SUBTITLE_TRACK_TYPE: u64 = 0x11;
31const MAX_CODEC_PRIVATE_SIZE: usize = 1 << 16;
32const MAX_BLOCK_PAYLOAD_SIZE: usize = 1 << 20;
33const MAX_TRACK_FRAMES: usize = 65_536;
34const MAX_EXTRACTED_SUB_SIZE: usize = 128 << 20;
35const MAX_CONTENT_COMP_SETTINGS_SIZE: usize = 1 << 12;
36const MPEG_PACK_HEADER: [u8; 14] = [
37    0x00, 0x00, 0x01, 0xBA, 0x44, 0x00, 0x04, 0x00, 0x04, 0x01, 0x00, 0x00, 0x03, 0xF8,
38];
39
40#[derive(Debug, Clone)]
41pub struct ExtractedVobSub {
42    pub idx_content: String,
43    pub sub_data: Vec<u8>,
44    pub language: Option<String>,
45    pub track_id: Option<String>,
46}
47
48#[derive(Debug, Clone, Default)]
49struct ParsedTrack {
50    track_num: u64,
51    codec_id: String,
52    language: Option<String>,
53    name: Option<String>,
54    codec_private: Option<Range<usize>>,
55    compression: TrackCompression,
56}
57
58#[derive(Debug, Clone, Default)]
59enum TrackCompression {
60    #[default]
61    None,
62    Zlib,
63    HeaderStrip(Range<usize>),
64}
65
66#[derive(Debug, Clone)]
67enum TrackPayload {
68    BorrowedRange(Range<usize>),
69    Owned(Vec<u8>),
70}
71
72impl TrackPayload {
73    fn as_slice<'a>(&'a self, data: &'a [u8]) -> &'a [u8] {
74        match self {
75            TrackPayload::BorrowedRange(range) => &data[range.clone()],
76            TrackPayload::Owned(payload) => payload,
77        }
78    }
79}
80
81#[derive(Debug, Clone)]
82struct TrackFrame {
83    timestamp_ms: u32,
84    payload: TrackPayload,
85}
86
87#[derive(Debug, Clone)]
88struct SegmentBounds {
89    data_start: usize,
90    data_end: usize,
91}
92
93pub fn extract_vobsub_from_mks(data: &[u8]) -> Result<ExtractedVobSub, String> {
94    let segment = find_segment(data)?;
95    let mut timescale_ns = 1_000_000u64;
96    let tracks = parse_segment_headers(data, &segment, &mut timescale_ns)?;
97
98    let selected_track = tracks
99        .into_iter()
100        .find(|track| track.codec_id == "S_VOBSUB")
101        .ok_or_else(|| "No S_VOBSUB track found in Matroska subtitle container".to_string())?;
102
103    let codec_private = selected_track
104        .codec_private
105        .clone()
106        .ok_or_else(|| "Selected S_VOBSUB track is missing CodecPrivate metadata".to_string())?;
107
108    if codec_private.is_empty() {
109        return Err("Selected S_VOBSUB track is missing CodecPrivate metadata".to_string());
110    }
111
112    let mut frames = parse_segment_clusters(data, &segment, &selected_track, timescale_ns)?;
113    if frames.is_empty() {
114        return Err("Selected S_VOBSUB track contained no subtitle blocks".to_string());
115    }
116
117    frames.sort_by_key(|frame| frame.timestamp_ms);
118
119    let mut sub_data = Vec::new();
120    let mut idx_content = normalize_idx_header(&data[codec_private]);
121    if idx_content.trim().is_empty() {
122        return Err(
123            "Selected S_VOBSUB track has an empty or invalid CodecPrivate header".to_string(),
124        );
125    }
126
127    for frame in &frames {
128        if sub_data.len() >= MAX_EXTRACTED_SUB_SIZE {
129            return Err("Extracted VobSub output exceeds supported size limit".to_string());
130        }
131        let file_position = sub_data.len() as u64;
132        append_ps_pes_packet(
133            &mut sub_data,
134            frame.timestamp_ms,
135            0x20,
136            frame.payload.as_slice(data),
137        )?;
138        if sub_data.len() > MAX_EXTRACTED_SUB_SIZE {
139            return Err("Extracted VobSub output exceeds supported size limit".to_string());
140        }
141        let _ = writeln!(
142            idx_content,
143            "timestamp: {}, filepos: {:08X}",
144            format_timestamp(frame.timestamp_ms),
145            file_position
146        );
147    }
148
149    Ok(ExtractedVobSub {
150        idx_content,
151        sub_data,
152        language: selected_track.language.filter(|value| !value.is_empty()),
153        track_id: Some(
154            selected_track
155                .name
156                .filter(|value| !value.is_empty())
157                .unwrap_or_else(|| selected_track.track_num.to_string()),
158        ),
159    })
160}
161
162fn find_segment(data: &[u8]) -> Result<SegmentBounds, String> {
163    let mut pos = 0usize;
164
165    while pos < data.len() {
166        let (id, id_len) =
167            read_element_id(data, pos).ok_or_else(|| "Invalid EBML element ID".to_string())?;
168        let size_pos = pos + id_len;
169        let (size, size_len) = read_size_vint(data, size_pos)
170            .ok_or_else(|| "Invalid EBML element size".to_string())?;
171        let data_start = size_pos + size_len;
172        let data_end = match size {
173            Some(size) => data_start
174                .checked_add(size as usize)
175                .filter(|end| *end <= data.len())
176                .ok_or_else(|| "Truncated Matroska element payload".to_string())?,
177            None => data.len(),
178        };
179
180        if id == EBML_ID_SEGMENT {
181            return Ok(SegmentBounds {
182                data_start,
183                data_end,
184            });
185        }
186
187        pos = data_end;
188    }
189
190    Err("Matroska Segment element not found".to_string())
191}
192
193fn parse_segment_headers(
194    data: &[u8],
195    segment: &SegmentBounds,
196    timescale_ns: &mut u64,
197) -> Result<Vec<ParsedTrack>, String> {
198    let mut tracks = Vec::new();
199    let mut pos = segment.data_start;
200
201    while pos < segment.data_end {
202        let (id, data_start, data_end) = next_element(data, pos, segment.data_end)?;
203
204        match id {
205            EBML_ID_SEGMENT_INFO => parse_segment_info(data, data_start, data_end, timescale_ns)?,
206            EBML_ID_TRACKS => parse_tracks(data, data_start, data_end, &mut tracks)?,
207            _ => {}
208        }
209
210        pos = data_end;
211    }
212
213    Ok(tracks)
214}
215
216fn parse_segment_info(
217    data: &[u8],
218    start: usize,
219    end: usize,
220    timescale_ns: &mut u64,
221) -> Result<(), String> {
222    let mut pos = start;
223
224    while pos < end {
225        let (id, data_start, data_end) = next_element(data, pos, end)?;
226        if id == EBML_ID_TIMECODE_SCALE {
227            *timescale_ns = read_uint(data, data_start, data_end)?;
228        }
229        pos = data_end;
230    }
231
232    Ok(())
233}
234
235fn parse_tracks(
236    data: &[u8],
237    start: usize,
238    end: usize,
239    tracks: &mut Vec<ParsedTrack>,
240) -> Result<(), String> {
241    let mut pos = start;
242
243    while pos < end {
244        let (id, data_start, data_end) = next_element(data, pos, end)?;
245        if id == EBML_ID_TRACK_ENTRY {
246            let track = parse_track_entry(data, data_start, data_end)?;
247            if track.codec_id == "S_VOBSUB" {
248                tracks.push(track);
249            }
250        }
251        pos = data_end;
252    }
253
254    Ok(())
255}
256
257fn parse_track_entry(data: &[u8], start: usize, end: usize) -> Result<ParsedTrack, String> {
258    let mut track = ParsedTrack::default();
259    let mut track_type = 0u64;
260    let mut pos = start;
261
262    while pos < end {
263        let (id, data_start, data_end) = next_element(data, pos, end)?;
264
265        match id {
266            EBML_ID_TRACK_NUMBER => track.track_num = read_uint(data, data_start, data_end)?,
267            EBML_ID_TRACK_TYPE => track_type = read_uint(data, data_start, data_end)?,
268            EBML_ID_CODEC_ID => track.codec_id = read_string(data, data_start, data_end),
269            EBML_ID_CODEC_PRIVATE => {
270                let size = data_end - data_start;
271                if size > MAX_CODEC_PRIVATE_SIZE {
272                    return Err("Matroska CodecPrivate exceeds supported size limit".to_string());
273                }
274                track.codec_private = Some(data_start..data_end);
275            }
276            EBML_ID_LANGUAGE => track.language = Some(read_string(data, data_start, data_end)),
277            EBML_ID_LANGUAGE_IETF => track.language = Some(read_string(data, data_start, data_end)),
278            EBML_ID_NAME => track.name = Some(read_string(data, data_start, data_end)),
279            EBML_ID_CONTENT_ENCODINGS => {
280                track.compression = parse_content_encodings(data, data_start, data_end)?;
281            }
282            _ => {}
283        }
284
285        pos = data_end;
286    }
287
288    if track_type != MATROSKA_SUBTITLE_TRACK_TYPE || track.track_num == 0 {
289        return Ok(ParsedTrack::default());
290    }
291
292    Ok(track)
293}
294
295fn parse_segment_clusters(
296    data: &[u8],
297    segment: &SegmentBounds,
298    selected_track: &ParsedTrack,
299    timescale_ns: u64,
300) -> Result<Vec<TrackFrame>, String> {
301    let mut frames = Vec::new();
302    let mut pos = segment.data_start;
303
304    while pos < segment.data_end {
305        let (id, data_start, data_end) = next_element(data, pos, segment.data_end)?;
306        if id == EBML_ID_CLUSTER {
307            parse_cluster(
308                data,
309                data_start,
310                data_end,
311                selected_track,
312                timescale_ns,
313                &mut frames,
314            )?;
315        }
316        pos = data_end;
317    }
318
319    Ok(frames)
320}
321
322fn parse_cluster(
323    data: &[u8],
324    start: usize,
325    end: usize,
326    selected_track: &ParsedTrack,
327    timescale_ns: u64,
328    frames: &mut Vec<TrackFrame>,
329) -> Result<(), String> {
330    let mut cluster_timestamp = 0i64;
331    let mut pos = start;
332
333    while pos < end {
334        let (id, data_start, data_end) = next_element(data, pos, end)?;
335
336        match id {
337            EBML_ID_CLUSTER_TIMESTAMP => {
338                cluster_timestamp = read_uint(data, data_start, data_end)? as i64;
339            }
340            EBML_ID_SIMPLE_BLOCK => {
341                if let Some(frame) = parse_block(
342                    data,
343                    data_start,
344                    data_end,
345                    selected_track,
346                    cluster_timestamp,
347                    timescale_ns,
348                )? {
349                    push_frame(frames, frame)?;
350                }
351            }
352            EBML_ID_BLOCK_GROUP => {
353                if let Some(frame) = parse_block_group(
354                    data,
355                    data_start,
356                    data_end,
357                    selected_track,
358                    cluster_timestamp,
359                    timescale_ns,
360                )? {
361                    push_frame(frames, frame)?;
362                }
363            }
364            _ => {}
365        }
366
367        pos = data_end;
368    }
369
370    Ok(())
371}
372
373fn parse_block_group(
374    data: &[u8],
375    start: usize,
376    end: usize,
377    selected_track: &ParsedTrack,
378    cluster_timestamp: i64,
379    timescale_ns: u64,
380) -> Result<Option<TrackFrame>, String> {
381    let mut pos = start;
382
383    while pos < end {
384        let (id, data_start, data_end) = next_element(data, pos, end)?;
385        if id == EBML_ID_BLOCK {
386            return parse_block(
387                data,
388                data_start,
389                data_end,
390                selected_track,
391                cluster_timestamp,
392                timescale_ns,
393            );
394        }
395        pos = data_end;
396    }
397
398    Ok(None)
399}
400
401fn parse_block(
402    source_data: &[u8],
403    start: usize,
404    end: usize,
405    selected_track: &ParsedTrack,
406    cluster_timestamp: i64,
407    timescale_ns: u64,
408) -> Result<Option<TrackFrame>, String> {
409    let data = &source_data[start..end];
410    let (track_num, track_num_len) =
411        read_vint(data, 0).ok_or_else(|| "Invalid Matroska block track number".to_string())?;
412
413    if track_num != selected_track.track_num {
414        return Ok(None);
415    }
416
417    if data.len() < track_num_len + 3 {
418        return Err("Truncated Matroska block header".to_string());
419    }
420
421    let relative_timestamp =
422        i16::from_be_bytes([data[track_num_len], data[track_num_len + 1]]) as i64;
423    let flags = data[track_num_len + 2];
424    if (flags & 0x06) != 0 {
425        return Err("Laced Matroska VobSub blocks are not supported".to_string());
426    }
427
428    let payload_start = start + track_num_len + 3;
429    let payload_end = end;
430    let payload = &data[track_num_len + 3..];
431    if payload.is_empty() {
432        return Ok(None);
433    }
434    if payload.len() > MAX_BLOCK_PAYLOAD_SIZE {
435        return Err("Matroska subtitle block exceeds supported size limit".to_string());
436    }
437
438    let absolute_ticks = cluster_timestamp.saturating_add(relative_timestamp);
439    if absolute_ticks < 0 {
440        return Err("Matroska subtitle block timestamp underflowed before zero".to_string());
441    }
442
443    let timestamp_ms = timestamp_to_ms(absolute_ticks as u64, timescale_ns)?;
444
445    Ok(Some(TrackFrame {
446        timestamp_ms,
447        payload: decode_track_payload(
448            payload,
449            payload_start..payload_end,
450            source_data,
451            &selected_track.compression,
452        )?,
453    }))
454}
455
456fn parse_content_encodings(
457    data: &[u8],
458    start: usize,
459    end: usize,
460) -> Result<TrackCompression, String> {
461    let mut pos = start;
462
463    while pos < end {
464        let (id, data_start, data_end) = next_element(data, pos, end)?;
465        if id == EBML_ID_CONTENT_ENCODING {
466            let compression = parse_content_encoding(data, data_start, data_end)?;
467            if !matches!(compression, TrackCompression::None) {
468                return Ok(compression);
469            }
470        }
471        pos = data_end;
472    }
473
474    Ok(TrackCompression::None)
475}
476
477fn parse_content_encoding(
478    data: &[u8],
479    start: usize,
480    end: usize,
481) -> Result<TrackCompression, String> {
482    let mut pos = start;
483
484    while pos < end {
485        let (id, data_start, data_end) = next_element(data, pos, end)?;
486        if id == EBML_ID_CONTENT_COMPRESSION {
487            return parse_content_compression(data, data_start, data_end);
488        }
489        pos = data_end;
490    }
491
492    Ok(TrackCompression::None)
493}
494
495fn parse_content_compression(
496    data: &[u8],
497    start: usize,
498    end: usize,
499) -> Result<TrackCompression, String> {
500    if start == end {
501        return Ok(TrackCompression::Zlib);
502    }
503
504    let mut algo = 0u64;
505    let mut settings = None;
506    let mut pos = start;
507
508    while pos < end {
509        let (id, data_start, data_end) = next_element(data, pos, end)?;
510        match id {
511            EBML_ID_CONTENT_COMP_ALGO => algo = read_uint(data, data_start, data_end)?,
512            EBML_ID_CONTENT_COMP_SETTINGS => {
513                let settings_len = data_end - data_start;
514                if settings_len > MAX_CONTENT_COMP_SETTINGS_SIZE {
515                    return Err(
516                        "Matroska content compression settings exceed supported size limit"
517                            .to_string(),
518                    );
519                }
520                settings = Some(data_start..data_end);
521            }
522            _ => {}
523        }
524        pos = data_end;
525    }
526
527    match algo {
528        0 => Ok(TrackCompression::Zlib),
529        3 => Ok(TrackCompression::HeaderStrip(settings.unwrap_or(0..0))),
530        other => Err(format!(
531            "Unsupported Matroska content compression algorithm: {other}"
532        )),
533    }
534}
535
536fn decode_track_payload(
537    payload: &[u8],
538    payload_range: Range<usize>,
539    source_data: &[u8],
540    compression: &TrackCompression,
541) -> Result<TrackPayload, String> {
542    let decoded = match compression {
543        TrackCompression::None => TrackPayload::BorrowedRange(payload_range),
544        TrackCompression::Zlib => {
545            decompress_to_vec_zlib_with_limit(payload, MAX_BLOCK_PAYLOAD_SIZE)
546                .map_err(|error| match error.status {
547                    TINFLStatus::HasMoreOutput => {
548                        "Inflated Matroska subtitle block exceeds supported size limit".to_string()
549                    }
550                    TINFLStatus::Adler32Mismatch => {
551                        "Matroska subtitle block failed checksum verification".to_string()
552                    }
553                    _ => "Failed to inflate zlib-compressed Matroska subtitle block".to_string(),
554                })
555                .map(TrackPayload::Owned)?
556        }
557        TrackCompression::HeaderStrip(prefix) => {
558            let prefix_slice = &source_data[prefix.clone()];
559            if prefix_slice.len().saturating_add(payload.len()) > MAX_BLOCK_PAYLOAD_SIZE {
560                return Err(
561                    "Header-stripped Matroska subtitle block exceeds supported size limit"
562                        .to_string(),
563                );
564            }
565            let mut out = Vec::with_capacity(prefix_slice.len() + payload.len());
566            out.extend_from_slice(prefix_slice);
567            out.extend_from_slice(payload);
568            TrackPayload::Owned(out)
569        }
570    };
571
572    validate_vobsub_payload(decoded.as_slice(source_data))?;
573    Ok(decoded)
574}
575
576fn push_frame(frames: &mut Vec<TrackFrame>, frame: TrackFrame) -> Result<(), String> {
577    if frames.len() >= MAX_TRACK_FRAMES {
578        return Err("Matroska subtitle track exceeds supported frame count".to_string());
579    }
580    frames.push(frame);
581    Ok(())
582}
583
584fn normalize_idx_header(codec_private: &[u8]) -> String {
585    let mut header = String::new();
586    let text = String::from_utf8_lossy(codec_private)
587        .replace("\r\n", "\n")
588        .replace('\r', "\n");
589
590    for line in text.lines() {
591        let trimmed = line.trim_matches(char::from(0)).trim();
592        if trimmed.is_empty() || trimmed.starts_with("timestamp:") {
593            continue;
594        }
595        header.push_str(trimmed);
596        header.push('\n');
597    }
598
599    header
600}
601
602fn append_ps_pes_packet(
603    out: &mut Vec<u8>,
604    timestamp_ms: u32,
605    sub_stream_id: u8,
606    payload: &[u8],
607) -> Result<(), String> {
608    if out.len() >= MAX_EXTRACTED_SUB_SIZE {
609        return Err("Extracted VobSub output exceeds supported size limit".to_string());
610    }
611
612    let pes_length = payload
613        .len()
614        .checked_add(9)
615        .ok_or_else(|| "VobSub PES payload length overflowed".to_string())?;
616    if pes_length > u16::MAX as usize {
617        return Err("VobSub payload exceeds maximum PES packet length".to_string());
618    }
619
620    let packet_size = MPEG_PACK_HEADER
621        .len()
622        .checked_add(6)
623        .and_then(|size| size.checked_add(pes_length))
624        .ok_or_else(|| "VobSub PES packet size overflowed".to_string())?;
625    if out.len().saturating_add(packet_size) > MAX_EXTRACTED_SUB_SIZE {
626        return Err("Extracted VobSub output exceeds supported size limit".to_string());
627    }
628
629    out.extend_from_slice(&MPEG_PACK_HEADER);
630    out.extend_from_slice(&[0x00, 0x00, 0x01, 0xBD]);
631    out.extend_from_slice(&(pes_length as u16).to_be_bytes());
632    out.extend_from_slice(&[0x80, 0x80, 0x05]);
633    out.extend_from_slice(&encode_pts(timestamp_ms as u64 * 90));
634    out.push(sub_stream_id);
635    out.extend_from_slice(payload);
636    Ok(())
637}
638
639fn encode_pts(pts: u64) -> [u8; 5] {
640    let pts = pts & 0x1FFF_FFFFF;
641    [
642        (((pts >> 30) as u8 & 0x07) << 1) | 0x21,
643        ((pts >> 22) & 0xFF) as u8,
644        ((((pts >> 15) & 0x7F) as u8) << 1) | 0x01,
645        ((pts >> 7) & 0xFF) as u8,
646        (((pts & 0x7F) as u8) << 1) | 0x01,
647    ]
648}
649
650fn timestamp_to_ms(timestamp_ticks: u64, timescale_ns: u64) -> Result<u32, String> {
651    let value = (timestamp_ticks as u128)
652        .checked_mul(timescale_ns as u128)
653        .ok_or_else(|| "Matroska subtitle timestamp overflowed".to_string())?
654        / 1_000_000u128;
655    Ok(value.min(u32::MAX as u128) as u32)
656}
657
658fn format_timestamp(timestamp_ms: u32) -> String {
659    let hours = timestamp_ms / 3_600_000;
660    let minutes = (timestamp_ms % 3_600_000) / 60_000;
661    let seconds = (timestamp_ms % 60_000) / 1_000;
662    let millis = timestamp_ms % 1_000;
663    format!("{hours:02}:{minutes:02}:{seconds:02}:{millis:03}")
664}
665
666fn validate_vobsub_payload(payload: &[u8]) -> Result<(), String> {
667    if payload.len() < 4 {
668        return Err("Matroska subtitle block is too short to contain a VobSub packet".to_string());
669    }
670
671    let packet_size = u16::from_be_bytes([payload[0], payload[1]]) as usize;
672    let dcsq_offset = u16::from_be_bytes([payload[2], payload[3]]) as usize;
673
674    if packet_size < 4 || packet_size > MAX_BLOCK_PAYLOAD_SIZE {
675        return Err("Matroska subtitle block declares an invalid VobSub packet size".to_string());
676    }
677    if payload.len() != packet_size {
678        return Err(
679            "Matroska subtitle block size does not match declared VobSub packet length".to_string(),
680        );
681    }
682    if dcsq_offset < 4 || dcsq_offset > packet_size {
683        return Err(
684            "Matroska subtitle block declares an invalid VobSub control offset".to_string(),
685        );
686    }
687
688    Ok(())
689}
690
691fn next_element(data: &[u8], pos: usize, limit: usize) -> Result<(u32, usize, usize), String> {
692    let (id, id_len) =
693        read_element_id(data, pos).ok_or_else(|| "Invalid EBML element ID".to_string())?;
694    let size_pos = pos + id_len;
695    let (size, size_len) =
696        read_size_vint(data, size_pos).ok_or_else(|| "Invalid EBML element size".to_string())?;
697    let data_start = size_pos + size_len;
698    let data_end = match size {
699        Some(size) => data_start
700            .checked_add(size as usize)
701            .filter(|end| *end <= limit)
702            .ok_or_else(|| "Truncated Matroska element payload".to_string())?,
703        None => limit,
704    };
705    Ok((id, data_start, data_end))
706}
707
708fn read_uint(data: &[u8], start: usize, end: usize) -> Result<u64, String> {
709    let size = end.saturating_sub(start);
710    if size == 0 || size > 8 {
711        return Err("Unsupported EBML integer size".to_string());
712    }
713
714    let mut value = 0u64;
715    for &byte in &data[start..end] {
716        value = (value << 8) | byte as u64;
717    }
718    Ok(value)
719}
720
721fn read_string(data: &[u8], start: usize, end: usize) -> String {
722    String::from_utf8_lossy(&data[start..end])
723        .trim_matches(char::from(0))
724        .trim()
725        .to_string()
726}
727
728fn read_element_id(data: &[u8], pos: usize) -> Option<(u32, usize)> {
729    let first = *data.get(pos)?;
730    let width = vint_width(first)?;
731    if width > 4 || pos + width > data.len() {
732        return None;
733    }
734
735    let mut value = first as u32;
736    for &byte in &data[pos + 1..pos + width] {
737        value = (value << 8) | byte as u32;
738    }
739
740    Some((value, width))
741}
742
743fn read_size_vint(data: &[u8], pos: usize) -> Option<(Option<u64>, usize)> {
744    let (value, width) = read_vint(data, pos)?;
745    let unknown_value = if width == 8 {
746        u64::MAX >> 8
747    } else {
748        (1u64 << (width * 7)) - 1
749    };
750
751    if value == unknown_value {
752        Some((None, width))
753    } else {
754        Some((Some(value), width))
755    }
756}
757
758fn read_vint(data: &[u8], pos: usize) -> Option<(u64, usize)> {
759    let first = *data.get(pos)?;
760    let width = vint_width(first)?;
761    if pos + width > data.len() {
762        return None;
763    }
764
765    let mask = if width == 8 {
766        0
767    } else {
768        (1u8 << (8 - width)) - 1
769    };
770    let mut value = (first & mask) as u64;
771    for &byte in &data[pos + 1..pos + width] {
772        value = (value << 8) | byte as u64;
773    }
774
775    Some((value, width))
776}
777
778fn vint_width(first: u8) -> Option<usize> {
779    if first & 0x80 != 0 {
780        Some(1)
781    } else if first & 0x40 != 0 {
782        Some(2)
783    } else if first & 0x20 != 0 {
784        Some(3)
785    } else if first & 0x10 != 0 {
786        Some(4)
787    } else if first & 0x08 != 0 {
788        Some(5)
789    } else if first & 0x04 != 0 {
790        Some(6)
791    } else if first & 0x02 != 0 {
792        Some(7)
793    } else if first & 0x01 != 0 {
794        Some(8)
795    } else {
796        None
797    }
798}
799
800#[cfg(test)]
801mod tests {
802    use super::*;
803    use crate::vobsub::{VobSubParser, parse_idx, parse_subtitle_packet};
804    use memchr::memchr;
805    use miniz_oxide::deflate::compress_to_vec_zlib;
806
807    #[test]
808    fn extracts_embedded_vobsub_track_from_mks() {
809        let idx_content = include_str!("../testfiles/vobsub.idx");
810        let sub_data = include_bytes!("../testfiles/vobsub.sub");
811        let idx_header = extract_idx_header(idx_content);
812        let payload = extract_first_spu_payload(sub_data);
813        let mks = build_test_mks(&idx_header, &payload, 1_000, "eng", 1);
814
815        let extracted = extract_vobsub_from_mks(&mks).expect("expected embedded VobSub track");
816        let idx = parse_idx(&extracted.idx_content);
817
818        assert_eq!(idx.timestamps.len(), 1);
819        assert_eq!(idx.timestamps[0].timestamp_ms, 1_000);
820        assert_eq!(extracted.language.as_deref(), Some("eng"));
821        assert_eq!(extracted.track_id.as_deref(), Some("1"));
822
823        let packet = parse_subtitle_packet(
824            &extracted.sub_data,
825            idx.timestamps[0].file_position as usize,
826            &idx.palette,
827        )
828        .expect("expected extracted .sub packet to parse");
829
830        assert!(packet.0.width > 0);
831        assert!(packet.0.height > 0);
832    }
833
834    #[test]
835    fn loads_mks_via_vobsub_parser() {
836        let idx_content = include_str!("../testfiles/vobsub.idx");
837        let sub_data = include_bytes!("../testfiles/vobsub.sub");
838        let idx_header = extract_idx_header(idx_content);
839        let payload = extract_first_spu_payload(sub_data);
840        let mks = build_test_mks(&idx_header, &payload, 2_500, "eng", 1);
841
842        let mut parser = VobSubParser::new();
843        parser
844            .load_from_mks(&mks)
845            .expect("expected MKS parsing to succeed");
846
847        assert_eq!(parser.count(), 1);
848        assert_eq!(parser.language(), "eng");
849        assert_eq!(parser.track_id(), "1");
850        assert!(parser.has_idx_metadata());
851        assert_eq!(parser.get_cue_start_time(0), 2500.0);
852
853        let frame = parser
854            .render_at_index(0)
855            .expect("expected decoded VobSub frame");
856        assert!(frame.width() > 0);
857        assert!(frame.height() > 0);
858    }
859
860    #[test]
861    fn parses_real_mks_fixture() {
862        let mks_data = include_bytes!("../testfiles/vobsub.mks");
863
864        let extracted =
865            extract_vobsub_from_mks(mks_data).expect("expected real MKS fixture to extract");
866        let idx = parse_idx(&extracted.idx_content);
867
868        assert!(!idx.timestamps.is_empty());
869        assert!(!extracted.sub_data.is_empty());
870        assert!(idx.metadata.width > 0);
871        assert!(idx.metadata.height > 0);
872
873        let first_packet = parse_subtitle_packet(
874            &extracted.sub_data,
875            idx.timestamps[0].file_position as usize,
876            &idx.palette,
877        )
878        .expect("expected first extracted packet from real fixture to parse");
879
880        assert!(first_packet.0.width > 0);
881        assert!(first_packet.0.height > 0);
882
883        let mut parser = VobSubParser::new();
884        parser
885            .load_from_mks(mks_data)
886            .expect("expected real MKS fixture to load through VobSubParser");
887
888        assert_eq!(parser.count(), idx.timestamps.len());
889        assert_eq!(
890            parser.get_cue_start_time(0),
891            idx.timestamps[0].timestamp_ms as f64
892        );
893
894        let frame = parser
895            .render_at_index(0)
896            .expect("expected first cue from real MKS fixture to render");
897        assert!(frame.width() > 0);
898        assert!(frame.height() > 0);
899    }
900
901    #[test]
902    fn rejects_corrupt_vobsub_payload_size_mismatch() {
903        let idx_content = include_str!("../testfiles/vobsub.idx");
904        let sub_data = include_bytes!("../testfiles/vobsub.sub");
905        let idx_header = extract_idx_header(idx_content);
906        let mut payload = extract_first_spu_payload(sub_data);
907        payload[0] = 0;
908        payload[1] = 4;
909
910        let mks = build_test_mks(&idx_header, &payload, 1_000, "eng", 1);
911        let error =
912            extract_vobsub_from_mks(&mks).expect_err("expected corrupt payload to be rejected");
913
914        assert!(error.contains("declared VobSub packet length"));
915    }
916
917    #[test]
918    fn rejects_oversized_inflated_payload() {
919        let idx_header = "size: 720x480\npalette: 000000, ffffff, 808080, 404040\n";
920        let payload = vec![0u8; MAX_BLOCK_PAYLOAD_SIZE + 1];
921        let compressed = compress_to_vec_zlib(&payload, 6);
922        let mks = build_test_mks_with_compression(
923            idx_header,
924            &compressed,
925            TrackCompression::Zlib,
926            1_000,
927            "eng",
928            1,
929        );
930
931        let error =
932            extract_vobsub_from_mks(&mks).expect_err("expected oversized inflation to be rejected");
933        assert!(error.contains("supported size limit"));
934    }
935
936    fn extract_idx_header(idx_content: &str) -> String {
937        let mut header = String::new();
938        for line in idx_content.lines() {
939            if line.trim_start().starts_with("timestamp:") {
940                break;
941            }
942            if line.trim().is_empty() {
943                continue;
944            }
945            header.push_str(line);
946            header.push('\n');
947        }
948        header
949    }
950
951    fn extract_first_spu_payload(sub_data: &[u8]) -> Vec<u8> {
952        let mut offset = 0usize;
953        let len = sub_data.len();
954        let mut chunks = Vec::new();
955        let mut expected_size = 0usize;
956        let mut collected = 0usize;
957
958        while offset < len.saturating_sub(4) {
959            let Some(pos) = memchr(0x00, &sub_data[offset..]) else {
960                break;
961            };
962            let candidate = offset + pos;
963            if candidate + 3 >= len
964                || sub_data[candidate + 1] != 0x00
965                || sub_data[candidate + 2] != 0x01
966            {
967                offset = candidate + 1;
968                continue;
969            }
970
971            let stream_id = sub_data[candidate + 3];
972            offset = candidate + 4;
973
974            if stream_id == 0xBA {
975                if offset < len && (sub_data[offset] & 0xC0) == 0x40 {
976                    offset += 9;
977                    let stuffing = sub_data[offset] as usize & 0x07;
978                    offset += 1 + stuffing;
979                } else {
980                    offset += 8;
981                }
982                continue;
983            }
984
985            if stream_id != 0xBD {
986                if offset + 2 > len {
987                    break;
988                }
989                let packet_len =
990                    u16::from_be_bytes([sub_data[offset], sub_data[offset + 1]]) as usize;
991                offset += 2 + packet_len;
992                continue;
993            }
994
995            if offset + 2 > len {
996                break;
997            }
998            let packet_len = u16::from_be_bytes([sub_data[offset], sub_data[offset + 1]]) as usize;
999            offset += 2;
1000            let packet_end = offset + packet_len;
1001
1002            let header_data_len = sub_data[offset + 2] as usize;
1003            offset += 3 + header_data_len;
1004            offset += 1;
1005
1006            let payload = &sub_data[offset..packet_end];
1007            if expected_size == 0 && payload.len() >= 2 {
1008                expected_size = u16::from_be_bytes([payload[0], payload[1]]) as usize;
1009            }
1010            collected += payload.len();
1011            chunks.extend_from_slice(payload);
1012
1013            if expected_size > 0 && collected >= expected_size {
1014                chunks.truncate(expected_size);
1015                return chunks;
1016            }
1017
1018            offset = packet_end;
1019        }
1020
1021        panic!("failed to extract raw VobSub payload from test fixture")
1022    }
1023
1024    fn build_test_mks(
1025        idx_header: &str,
1026        payload: &[u8],
1027        timestamp_ms: u64,
1028        language: &str,
1029        track_num: u64,
1030    ) -> Vec<u8> {
1031        build_test_mks_with_compression(
1032            idx_header,
1033            payload,
1034            TrackCompression::None,
1035            timestamp_ms,
1036            language,
1037            track_num,
1038        )
1039    }
1040
1041    fn build_test_mks_with_compression(
1042        idx_header: &str,
1043        payload: &[u8],
1044        compression: TrackCompression,
1045        timestamp_ms: u64,
1046        language: &str,
1047        track_num: u64,
1048    ) -> Vec<u8> {
1049        let ebml_header = element(0x1A45_DFA3, &element(0x4286, &[0x01]));
1050
1051        let info = element(
1052            EBML_ID_SEGMENT_INFO,
1053            &element(EBML_ID_TIMECODE_SCALE, &[0x0F, 0x42, 0x40]),
1054        );
1055
1056        let mut track_children = vec![
1057            element(EBML_ID_TRACK_NUMBER, &[track_num as u8]),
1058            element(EBML_ID_TRACK_TYPE, &[MATROSKA_SUBTITLE_TRACK_TYPE as u8]),
1059            element(EBML_ID_CODEC_ID, b"S_VOBSUB"),
1060            element(EBML_ID_CODEC_PRIVATE, idx_header.as_bytes()),
1061            element(EBML_ID_LANGUAGE, language.as_bytes()),
1062        ];
1063
1064        if !matches!(compression, TrackCompression::None) {
1065            let compression_payload = match compression {
1066                TrackCompression::Zlib => Vec::new(),
1067                TrackCompression::HeaderStrip(_) => panic!(
1068                    "header-strip test helper is not implemented for borrowed range compression state"
1069                ),
1070                TrackCompression::None => Vec::new(),
1071            };
1072
1073            track_children.push(element(
1074                EBML_ID_CONTENT_ENCODINGS,
1075                &element(
1076                    EBML_ID_CONTENT_ENCODING,
1077                    &element(EBML_ID_CONTENT_COMPRESSION, &compression_payload),
1078                ),
1079            ));
1080        }
1081
1082        let track_entry = element(EBML_ID_TRACK_ENTRY, &track_children.concat());
1083        let tracks = element(EBML_ID_TRACKS, &track_entry);
1084
1085        let cluster = element(
1086            EBML_ID_CLUSTER,
1087            &[
1088                element(EBML_ID_CLUSTER_TIMESTAMP, &encode_uint(timestamp_ms)),
1089                element(
1090                    EBML_ID_SIMPLE_BLOCK,
1091                    &build_simple_block(track_num, 0, payload),
1092                ),
1093            ]
1094            .concat(),
1095        );
1096
1097        let segment = element(EBML_ID_SEGMENT, &[info, tracks, cluster].concat());
1098        [ebml_header, segment].concat()
1099    }
1100
1101    fn build_simple_block(track_num: u64, relative_timestamp: i16, payload: &[u8]) -> Vec<u8> {
1102        let mut block = encode_track_number(track_num);
1103        block.extend_from_slice(&relative_timestamp.to_be_bytes());
1104        block.push(0x80);
1105        block.extend_from_slice(payload);
1106        block
1107    }
1108
1109    fn element(id: u32, payload: &[u8]) -> Vec<u8> {
1110        let mut out = encode_element_id(id);
1111        out.extend_from_slice(&encode_size(payload.len() as u64));
1112        out.extend_from_slice(payload);
1113        out
1114    }
1115
1116    fn encode_element_id(id: u32) -> Vec<u8> {
1117        if id > 0x00FF_FFFF {
1118            vec![
1119                ((id >> 24) & 0xFF) as u8,
1120                ((id >> 16) & 0xFF) as u8,
1121                ((id >> 8) & 0xFF) as u8,
1122                (id & 0xFF) as u8,
1123            ]
1124        } else if id > 0x0000_FFFF {
1125            vec![
1126                ((id >> 16) & 0xFF) as u8,
1127                ((id >> 8) & 0xFF) as u8,
1128                (id & 0xFF) as u8,
1129            ]
1130        } else if id > 0x0000_00FF {
1131            vec![((id >> 8) & 0xFF) as u8, (id & 0xFF) as u8]
1132        } else {
1133            vec![id as u8]
1134        }
1135    }
1136
1137    fn encode_size(value: u64) -> Vec<u8> {
1138        for width in 1..=8 {
1139            let max_value = if width == 8 {
1140                u64::MAX >> 8
1141            } else {
1142                (1u64 << (width * 7)) - 2
1143            };
1144            if value <= max_value {
1145                let mut bytes = vec![0u8; width];
1146                let mut temp = value;
1147                for index in (0..width).rev() {
1148                    bytes[index] = (temp & 0xFF) as u8;
1149                    temp >>= 8;
1150                }
1151                bytes[0] |= 1 << (8 - width);
1152                return bytes;
1153            }
1154        }
1155
1156        panic!("size too large for EBML vint encoding")
1157    }
1158
1159    fn encode_track_number(track_num: u64) -> Vec<u8> {
1160        if track_num == 0 || track_num >= 0x7F {
1161            panic!("test track number must fit in a one-byte block vint")
1162        }
1163        vec![0x80 | track_num as u8]
1164    }
1165
1166    fn encode_uint(value: u64) -> Vec<u8> {
1167        if value == 0 {
1168            return vec![0];
1169        }
1170
1171        let bytes = value.to_be_bytes();
1172        let first_non_zero = bytes
1173            .iter()
1174            .position(|byte| *byte != 0)
1175            .unwrap_or(bytes.len() - 1);
1176        bytes[first_non_zero..].to_vec()
1177    }
1178}