1use 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}