Skip to main content

libbitsub_core/vobsub/
sub_parser.rs

1//! VobSub SUB file parser.
2//!
3//! The SUB file contains MPEG-2 Private Stream 1 packets with DVD subtitle data.
4
5use memchr::memchr;
6use std::ops::Range;
7
8use super::{MAX_VOBSUB_IMAGE_PIXELS, VobSubPalette};
9
10#[derive(Debug, Clone)]
11pub enum SubtitlePacketData {
12    SharedRange { start: usize, end: usize },
13    Owned(Vec<u8>),
14}
15
16/// Parsed subtitle packet from the SUB file.
17#[derive(Debug, Clone)]
18pub struct SubtitlePacket {
19    /// Timestamp in milliseconds (from PTS)
20    pub timestamp_ms: u32,
21    /// Duration in milliseconds
22    pub duration_ms: u32,
23    /// X position
24    pub x: u16,
25    /// Y position
26    pub y: u16,
27    /// Width
28    pub width: u16,
29    /// Height
30    pub height: u16,
31    /// 4 color indices into the 16-color palette
32    pub color_indices: [u8; 4],
33    /// 4 alpha values (0-15, where 0 is transparent, 15 is opaque)
34    pub alpha_values: [u8; 4],
35    /// Underlying subtitle packet payload.
36    pub(crate) packet_data: SubtitlePacketData,
37    /// RLE-encoded pixel data range for the even field / top field.
38    pub(crate) even_field_range: Range<usize>,
39    /// RLE-encoded pixel data range for the odd field / bottom field.
40    pub(crate) odd_field_range: Range<usize>,
41}
42
43impl SubtitlePacket {
44    fn packet_slice<'a>(&'a self, sub_data: &'a [u8]) -> &'a [u8] {
45        match &self.packet_data {
46            SubtitlePacketData::SharedRange { start, end } => &sub_data[*start..*end],
47            SubtitlePacketData::Owned(data) => data,
48        }
49    }
50
51    pub fn even_field_data<'a>(&'a self, sub_data: &'a [u8]) -> &'a [u8] {
52        let packet = self.packet_slice(sub_data);
53        &packet[self.even_field_range.clone()]
54    }
55
56    pub fn odd_field_data<'a>(&'a self, sub_data: &'a [u8]) -> &'a [u8] {
57        let packet = self.packet_slice(sub_data);
58        &packet[self.odd_field_range.clone()]
59    }
60}
61
62/// Parse a subtitle packet from the SUB file at the given position.
63pub fn parse_subtitle_packet(
64    data: &[u8],
65    start_offset: usize,
66    _palette: &VobSubPalette,
67) -> Option<(SubtitlePacket, usize)> {
68    let mut offset = start_offset;
69    let data_len = data.len();
70
71    // Safety: limit how far we scan for a single packet (256KB should be more than enough)
72    let max_scan = (start_offset + 262144).min(data_len);
73
74    let mut pts: u32 = 0;
75    let mut data_chunks: Vec<(usize, usize)> = Vec::new();
76    let mut expected_size: usize = 0;
77    let mut collected_size: usize = 0;
78
79    // Look for MPEG-2 PS headers and collect all packets
80    while offset < max_scan.saturating_sub(4) {
81        // Check for start code prefix (00 00 01)
82        if let Some(pos) = memchr(0x00, &data[offset..max_scan.saturating_sub(3)]) {
83            let candidate = offset + pos;
84
85            if data[candidate + 1] != 0x00 || data[candidate + 2] != 0x01 {
86                offset = candidate + 1;
87                continue;
88            }
89            offset = candidate;
90        } else {
91            break;
92        }
93
94        let stream_id = data[offset + 3];
95
96        // Pack header (0xBA)
97        if stream_id == 0xBA {
98            offset += 4;
99
100            // Check MPEG-1 or MPEG-2 pack header
101            if offset < data_len && (data[offset] & 0xC0) == 0x40 {
102                // MPEG-2: pack header + stuffing
103                offset += 9;
104                if offset < data_len {
105                    let stuffing = (data[offset] & 0x07) as usize;
106                    offset += 1 + stuffing;
107                }
108            } else {
109                // MPEG-1: 8 bytes
110                offset += 8;
111            }
112            continue;
113        }
114
115        // Private Stream 1 (0xBD) - contains subtitle data
116        if stream_id == 0xBD {
117            offset += 4;
118
119            if offset + 2 > data_len {
120                break;
121            }
122
123            let pes_length = ((data[offset] as usize) << 8) | (data[offset + 1] as usize);
124            offset += 2;
125
126            let packet_end = match offset.checked_add(pes_length) {
127                Some(packet_end) if packet_end <= data_len => packet_end,
128                _ => break,
129            };
130
131            if offset + 3 > packet_end {
132                break;
133            }
134
135            // Parse PES header
136            let pes_flags = data[offset + 1];
137            let header_data_length = data[offset + 2] as usize;
138            offset += 3;
139
140            if offset + header_data_length > packet_end {
141                break;
142            }
143
144            // Extract PTS if present and we don't have one yet
145            if (pes_flags & 0x80) != 0 && pts == 0 && offset + 5 <= packet_end {
146                pts = extract_pts(data, offset);
147            }
148
149            offset += header_data_length;
150
151            // Skip stream ID byte
152            if offset + 1 > packet_end {
153                break;
154            }
155            offset += 1;
156
157            // Calculate payload length within this PES packet
158            let payload_length = packet_end.saturating_sub(offset);
159
160            if payload_length > 0 {
161                // First packet - read expected subtitle size
162                if expected_size == 0 && payload_length >= 2 {
163                    expected_size = ((data[offset] as usize) << 8) | (data[offset + 1] as usize);
164                }
165
166                data_chunks.push((offset, offset + payload_length));
167                collected_size += payload_length;
168                offset += payload_length;
169
170                // Check if we've collected enough data
171                if expected_size > 0 && collected_size >= expected_size {
172                    break;
173                }
174
175                continue;
176            }
177        }
178
179        // Padding stream (0xBE)
180        if stream_id == 0xBE {
181            offset += 4;
182            if offset + 2 > data_len {
183                break;
184            }
185            let length = ((data[offset] as usize) << 8) | (data[offset + 1] as usize);
186            offset += 2 + length;
187            continue;
188        }
189
190        // Other stream types
191        if stream_id >= 0xBC {
192            if !data_chunks.is_empty() {
193                break;
194            }
195            offset += 4;
196            if offset + 2 > data_len {
197                break;
198            }
199            let length = ((data[offset] as usize) << 8) | (data[offset + 1] as usize);
200            offset += 2 + length;
201            continue;
202        }
203
204        offset += 1;
205    }
206
207    // Reassemble collected data
208    if data_chunks.is_empty() {
209        return None;
210    }
211
212    if data_chunks.len() == 1 {
213        let (start, end) = data_chunks.into_iter().next().unwrap();
214        let trimmed_end = if expected_size > 0 {
215            start + expected_size.min(end - start)
216        } else {
217            end
218        };
219        let packet_source = SubtitlePacketData::SharedRange {
220            start,
221            end: trimmed_end,
222        };
223        let subtitle_data = &data[start..trimmed_end];
224        if subtitle_data.len() < 4 {
225            return None;
226        }
227
228        return parse_subtitle_data(packet_source, data, pts).map(|packet| (packet, offset));
229    } else {
230        let final_size = if expected_size > 0 {
231            expected_size.min(collected_size)
232        } else {
233            collected_size
234        };
235        let mut merged = Vec::with_capacity(final_size);
236        for (start, end) in data_chunks {
237            if merged.len() >= final_size {
238                break;
239            }
240
241            let remaining = final_size - merged.len();
242            let chunk = &data[start..end];
243            let take = remaining.min(chunk.len());
244            merged.extend_from_slice(&chunk[..take]);
245        }
246
247        if merged.len() < 4 {
248            return None;
249        }
250
251        return parse_subtitle_data(SubtitlePacketData::Owned(merged), data, pts)
252            .map(|packet| (packet, offset));
253    }
254}
255
256/// Extract PTS (Presentation Time Stamp) from PES header.
257fn extract_pts(data: &[u8], offset: usize) -> u32 {
258    if offset + 5 > data.len() {
259        return 0;
260    }
261
262    let pts32_30 = ((data[offset] >> 1) & 0x07) as u64;
263    let pts29_15 = ((data[offset + 1] as u64) << 7) | ((data[offset + 2] >> 1) as u64);
264    let pts14_0 = ((data[offset + 3] as u64) << 7) | ((data[offset + 4] >> 1) as u64);
265
266    // Combine into 33-bit value
267    let pts = (pts32_30 << 30) | (pts29_15 << 15) | pts14_0;
268
269    // Convert from 90kHz clock to milliseconds
270    (pts / 90) as u32
271}
272
273/// Parse the subtitle control and bitmap data.
274fn parse_subtitle_data(
275    packet_data: SubtitlePacketData,
276    source_data: &[u8],
277    pts: u32,
278) -> Option<SubtitlePacket> {
279    let data = match &packet_data {
280        SubtitlePacketData::SharedRange { start, end } => &source_data[*start..*end],
281        SubtitlePacketData::Owned(data) => data.as_slice(),
282    };
283
284    if data.len() < 4 {
285        return None;
286    }
287
288    let packet_start = 0;
289    let end_offset = data.len();
290
291    // First 2 bytes: total subtitle packet size
292    // let _packet_size = ((data[0] as usize) << 8) | (data[1] as usize);
293
294    // Next 2 bytes: offset to first control sequence (DCSQ offset)
295    let dcsq_offset = ((data[2] as usize) << 8) | (data[3] as usize);
296    if dcsq_offset < 4 || dcsq_offset > end_offset {
297        return None;
298    }
299
300    // Parse control sequence
301    let mut x: u16 = 0;
302    let mut y: u16 = 0;
303    let mut width: u16 = 0;
304    let mut height: u16 = 0;
305    let mut duration: u32 = 0;
306    let mut found_stop: bool = false;
307    let mut color_indices = [0u8, 1, 2, 3];
308    let mut alpha_values = [0u8, 15, 15, 15];
309    let mut top_field_offset: usize = 0;
310    let mut bottom_field_offset: usize = 0;
311
312    let mut ctrl_offset = packet_start + dcsq_offset;
313    let mut iterations = 0;
314    const MAX_ITERATIONS: usize = 1000; // Safety limit
315
316    while ctrl_offset < end_offset && iterations < MAX_ITERATIONS && !found_stop {
317        iterations += 1;
318
319        // Remember where this block started (before reading delay/next_offset)
320        let block_start = ctrl_offset;
321
322        // Each control sequence block starts with a delay value (2 bytes)
323        if ctrl_offset + 4 > end_offset {
324            break;
325        }
326
327        let delay = ((data[ctrl_offset] as u32) << 8) | (data[ctrl_offset + 1] as u32);
328        ctrl_offset += 2;
329
330        // Next 2 bytes: offset to next control block
331        let next_ctrl_offset =
332            ((data[ctrl_offset] as usize) << 8) | (data[ctrl_offset + 1] as usize);
333        ctrl_offset += 2;
334
335        // Parse commands
336        while ctrl_offset < end_offset {
337            let cmd = data[ctrl_offset];
338            ctrl_offset += 1;
339
340            match cmd {
341                0x00 => {} // Force display
342                0x01 => {} // Start display
343                0x02 => {
344                    // Stop display - delay is when to stop (duration in 1024/90000 sec units)
345                    duration = (delay * 1024) / 90;
346                    found_stop = true;
347                }
348                0x03 => {
349                    // Set palette
350                    if ctrl_offset + 2 <= end_offset {
351                        color_indices[3] = (data[ctrl_offset] >> 4) & 0x0F;
352                        color_indices[2] = data[ctrl_offset] & 0x0F;
353                        color_indices[1] = (data[ctrl_offset + 1] >> 4) & 0x0F;
354                        color_indices[0] = data[ctrl_offset + 1] & 0x0F;
355                        ctrl_offset += 2;
356                    }
357                }
358                0x04 => {
359                    // Set alpha
360                    if ctrl_offset + 2 <= end_offset {
361                        alpha_values[3] = (data[ctrl_offset] >> 4) & 0x0F;
362                        alpha_values[2] = data[ctrl_offset] & 0x0F;
363                        alpha_values[1] = (data[ctrl_offset + 1] >> 4) & 0x0F;
364                        alpha_values[0] = data[ctrl_offset + 1] & 0x0F;
365                        ctrl_offset += 2;
366                    }
367                }
368                0x05 => {
369                    // Set display area
370                    if ctrl_offset + 6 <= end_offset {
371                        let x1 = ((data[ctrl_offset] as u16) << 4)
372                            | ((data[ctrl_offset + 1] >> 4) as u16);
373                        let x2 = (((data[ctrl_offset + 1] & 0x0F) as u16) << 8)
374                            | (data[ctrl_offset + 2] as u16);
375                        let y1 = ((data[ctrl_offset + 3] as u16) << 4)
376                            | ((data[ctrl_offset + 4] >> 4) as u16);
377                        let y2 = (((data[ctrl_offset + 4] & 0x0F) as u16) << 8)
378                            | (data[ctrl_offset + 5] as u16);
379                        if x2 < x1 || y2 < y1 {
380                            return None;
381                        }
382
383                        let width_usize = (x2 - x1) as usize + 1;
384                        let height_usize = (y2 - y1) as usize + 1;
385                        if width_usize.checked_mul(height_usize)? > MAX_VOBSUB_IMAGE_PIXELS {
386                            return None;
387                        }
388
389                        x = x1;
390                        y = y1;
391                        width = width_usize as u16;
392                        height = height_usize as u16;
393                        ctrl_offset += 6;
394                    }
395                }
396                0x06 => {
397                    // Set field offsets
398                    if ctrl_offset + 4 <= end_offset {
399                        top_field_offset =
400                            ((data[ctrl_offset] as usize) << 8) | (data[ctrl_offset + 1] as usize);
401                        bottom_field_offset = ((data[ctrl_offset + 2] as usize) << 8)
402                            | (data[ctrl_offset + 3] as usize);
403                        ctrl_offset += 4;
404                    }
405                }
406                0xFF => break, // End of control sequence
407                _ => {}
408            }
409
410            if cmd == 0xFF || cmd == 0x02 {
411                break;
412            }
413        }
414
415        // Check if this is the last control block
416        // The end of the chain is indicated by next_ctrl_offset pointing to the current block or earlier
417        let next_block_abs = packet_start + next_ctrl_offset;
418
419        // Break if:
420        // 1. next_ctrl_offset points backwards into bitmap data (< dcsq_offset)
421        // 2. next_ctrl_offset points to current block or earlier (self-reference = end marker)
422        if next_ctrl_offset < dcsq_offset || next_block_abs <= block_start {
423            break;
424        }
425
426        ctrl_offset = next_block_abs;
427    }
428
429    // Calculate field data positions
430    let even_start = if top_field_offset > 0 {
431        top_field_offset
432    } else {
433        4
434    };
435    let odd_start = if bottom_field_offset > 0 {
436        bottom_field_offset
437    } else {
438        even_start
439    };
440
441    let even_field_end = odd_start;
442    let odd_field_end = packet_start + dcsq_offset;
443
444    let even_field_range = if even_start < even_field_end.min(end_offset) {
445        even_start..even_field_end.min(end_offset)
446    } else {
447        0..0
448    };
449
450    let odd_field_range = if odd_start < odd_field_end.min(end_offset) {
451        odd_start..odd_field_end.min(end_offset)
452    } else {
453        0..0
454    };
455
456    Some(SubtitlePacket {
457        timestamp_ms: pts,
458        duration_ms: if duration > 0 { duration } else { 5000 },
459        x,
460        y,
461        width,
462        height,
463        color_indices,
464        alpha_values,
465        packet_data,
466        even_field_range,
467        odd_field_range,
468    })
469}
470
471#[cfg(test)]
472mod tests {
473    use super::*;
474
475    #[test]
476    fn test_parse_subtitle_packet_rejects_short_pes_header() {
477        let data = [0x00, 0x00, 0x01, 0xBD, 0x00, 0x01, 0x00];
478
479        assert!(parse_subtitle_packet(&data, 0, &VobSubPalette::default()).is_none());
480    }
481
482    #[test]
483    fn test_parse_subtitle_packet_rejects_invalid_control_offset() {
484        let data = [0x00, 0x08, 0x00, 0x09, 0x11, 0x22, 0x33, 0x44];
485
486        assert!(parse_subtitle_data(SubtitlePacketData::Owned(data.to_vec()), &data, 0).is_none());
487    }
488
489    #[test]
490    fn test_parse_style_commands_use_dvd_nibble_order() {
491        let data = [
492            0x00, 0x10, 0x00, 0x04, // packet size, control offset
493            0x00, 0x00, 0x00, 0x04, // delay, self-referencing next offset
494            0x03, 0xfd, 0x3b, // palette
495            0x04, 0xdf, 0xd0, // alpha
496            0x01, 0xff, // start display, end
497        ];
498
499        let packet =
500            parse_subtitle_data(SubtitlePacketData::Owned(data.to_vec()), &data, 0).unwrap();
501
502        assert_eq!(packet.color_indices, [11, 3, 13, 15]);
503        assert_eq!(packet.alpha_values, [0, 13, 15, 13]);
504    }
505}