1use anyhow::{Context, Result, anyhow, bail};
9use std::fs::File;
10use std::io::{BufReader, Read, Seek, SeekFrom};
11use std::path::Path;
12
13pub const OMV_THEORA_TYPE_RGB: u32 = 0;
14pub const OMV_THEORA_TYPE_RGBA: u32 = 1;
15pub const OMV_THEORA_TYPE_YUV: u32 = 2;
16
17const OMV_HEADER_MIN_SIZE: usize = 0x58;
18const OMV_THEORA_PAGE_SIZE: usize = 0x1C;
19const OMV_THEORA_PACKET_SIZE: usize = 0x20;
20const OGG_SCAN_CHUNK: usize = 64 * 1024;
21
22#[derive(Debug, Clone)]
23pub struct OmvHeader {
24 pub header_size: u32,
25 pub version: u32,
26 pub theora_type: u32,
27 pub display_width: u32,
28 pub display_height: u32,
29 pub frame_time_us: u32,
30 pub max_data_size: u32,
31 pub page_count_hint: u32,
32 pub packet_count_hint: u32,
33}
34
35#[derive(Debug, Clone, Copy)]
36pub struct OmvTheoraPage {
37 pub own_page_no: i32,
38 pub is_eos: bool,
39 pub is_key_page: bool,
40 pub page_size: i32,
41 pub seek_offset: i32,
42 pub seek_page_no: i32,
43 pub packet_count: i32,
44 pub top_packet_no: i32,
45}
46
47#[derive(Debug, Clone, Copy)]
48pub struct OmvTheoraPacket {
49 pub own_packet_no: i32,
50 pub own_page_no: i32,
51 pub own_packet_no_in_page: i32,
52 pub is_key_frame: bool,
53 pub key_frame_packet_no: i32,
54 pub key_frame_page_no: i32,
55 pub frame_time_start: i32,
56 pub frame_time_end: i32,
57}
58
59#[derive(Debug, Clone)]
60pub struct OmvFile {
61 pub header: OmvHeader,
62 pub pages: Vec<OmvTheoraPage>,
63 pub packets: Vec<OmvTheoraPacket>,
64 pub seek_top: u64,
66 pub ogg_data_offset: u64,
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70pub struct OmvSeekPoint {
71 pub file_offset: u64,
74 pub key_page_file_offset: u64,
77 pub seek_page_no: usize,
78 pub first_packet_no: usize,
83 pub key_frame_page_no: usize,
84 pub key_frame_packet_no: usize,
85 pub target_packet_no: usize,
86}
87
88impl OmvFile {
89 pub fn read_header(path: impl AsRef<Path>) -> Result<OmvHeader> {
93 let path = path.as_ref();
94 let mut file = File::open(path).with_context(|| format!("open OMV: {}", path.display()))?;
95 let mut header_prefix = [0u8; OMV_HEADER_MIN_SIZE];
96 file.read_exact(&mut header_prefix)
97 .with_context(|| format!("read OMV header: {}", path.display()))?;
98 read_header(&header_prefix)
99 }
100
101 pub fn open(path: impl AsRef<Path>) -> Result<Self> {
102 let path = path.as_ref();
103 let mut file = File::open(path).with_context(|| format!("open OMV: {}", path.display()))?;
104 let mut header_prefix = [0u8; OMV_HEADER_MIN_SIZE];
105 file.read_exact(&mut header_prefix)
106 .with_context(|| format!("read OMV header: {}", path.display()))?;
107 let header = read_header(&header_prefix)?;
108 let header_size = usize::try_from(header.header_size)
109 .map_err(|_| anyhow!("OMV header size does not fit usize"))?;
110 if header_size < OMV_HEADER_MIN_SIZE {
111 bail!("invalid OMV header size: {:#x}", header.header_size);
112 }
113
114 file.seek(SeekFrom::Start(header.header_size as u64))
115 .with_context(|| format!("seek OMV tables: {}", path.display()))?;
116
117 let page_count = usize::try_from(header.page_count_hint)
118 .map_err(|_| anyhow!("OMV page count does not fit usize"))?;
119 let packet_count = usize::try_from(header.packet_count_hint)
120 .map_err(|_| anyhow!("OMV packet count does not fit usize"))?;
121 let pages_bytes = page_count
122 .checked_mul(OMV_THEORA_PAGE_SIZE)
123 .ok_or_else(|| anyhow!("OMV page table size overflow"))?;
124 let packets_bytes = packet_count
125 .checked_mul(OMV_THEORA_PACKET_SIZE)
126 .ok_or_else(|| anyhow!("OMV packet table size overflow"))?;
127 let seek_top = (header_size as u64)
128 .checked_add(pages_bytes as u64)
129 .and_then(|offset| offset.checked_add(packets_bytes as u64))
130 .ok_or_else(|| anyhow!("OMV table end offset overflow"))?;
131 let file_len = file
132 .metadata()
133 .with_context(|| format!("stat OMV: {}", path.display()))?
134 .len();
135 if seek_top > file_len {
136 bail!(
137 "OMV seek tables exceed file size: table_end={} file_size={}",
138 seek_top,
139 file_len
140 );
141 }
142
143 let mut page_table = vec![0u8; pages_bytes];
144 if !page_table.is_empty() {
145 file.read_exact(&mut page_table)
146 .with_context(|| format!("read OMV page table: {}", path.display()))?;
147 }
148 let mut packet_table = vec![0u8; packets_bytes];
149 if !packet_table.is_empty() {
150 file.read_exact(&mut packet_table)
151 .with_context(|| format!("read OMV packet table: {}", path.display()))?;
152 }
153
154 let pages = parse_pages(&page_table, page_count)?;
155 let packets = parse_packets(&packet_table, packet_count)?;
156
157 let candidate = pages
158 .first()
159 .and_then(|page| u64::try_from(page.seek_offset).ok())
160 .map(|offset| seek_top.saturating_add(offset));
161 let ogg_data_offset = match candidate {
162 Some(offset) if file_has_ogg_magic(&mut file, offset)? => offset,
163 _ => find_ogg_offset_in_file(&mut file, seek_top)?,
164 };
165
166 Ok(Self {
167 header,
168 pages,
169 packets,
170 seek_top,
171 ogg_data_offset,
172 })
173 }
174
175 pub fn seek_point_for_frame(&self, frame_no: usize) -> Result<OmvSeekPoint> {
182 let packet = self
183 .packets
184 .get(frame_no)
185 .ok_or_else(|| anyhow!("OMV frame index out of range: {frame_no}"))?;
186 if packet.own_packet_no != frame_no as i32 {
187 bail!(
188 "OMV packet table row {} identifies itself as {}",
189 frame_no,
190 packet.own_packet_no
191 );
192 }
193 let key_frame_packet_no = usize::try_from(packet.key_frame_packet_no)
194 .map_err(|_| anyhow!("OMV key-frame packet is negative for frame {frame_no}"))?;
195 if key_frame_packet_no > frame_no || key_frame_packet_no >= self.packets.len() {
196 bail!(
197 "invalid OMV key-frame packet {} for frame {}",
198 key_frame_packet_no,
199 frame_no
200 );
201 }
202 let key_packet = &self.packets[key_frame_packet_no];
203 if !key_packet.is_key_frame {
204 bail!(
205 "OMV indexed key packet {} is not marked as a key frame",
206 key_frame_packet_no
207 );
208 }
209 let key_frame_page_no = usize::try_from(packet.key_frame_page_no)
210 .map_err(|_| anyhow!("OMV key-frame page is negative for frame {frame_no}"))?;
211 if key_packet.own_page_no != packet.key_frame_page_no {
212 bail!(
213 "OMV key packet {} belongs to page {}, not indexed page {}",
214 key_frame_packet_no,
215 key_packet.own_page_no,
216 packet.key_frame_page_no
217 );
218 }
219 let key_page = self.pages.get(key_frame_page_no).ok_or_else(|| {
220 anyhow!(
221 "OMV key-frame page {} is out of range for frame {}",
222 key_frame_page_no,
223 frame_no
224 )
225 })?;
226 if key_page.own_page_no != packet.key_frame_page_no {
227 bail!(
228 "OMV page table row {} identifies itself as {}",
229 key_frame_page_no,
230 key_page.own_page_no
231 );
232 }
233 let seek_page_no = usize::try_from(key_page.seek_page_no).map_err(|_| {
234 anyhow!(
235 "OMV seek page is negative for key-frame page {}",
236 key_frame_page_no
237 )
238 })?;
239 if seek_page_no > key_frame_page_no {
240 bail!(
241 "OMV seek page {} is after key-frame page {}",
242 seek_page_no,
243 key_frame_page_no
244 );
245 }
246 let seek_page = self.pages.get(seek_page_no).ok_or_else(|| {
247 anyhow!(
248 "OMV seek page {} is out of range for frame {}",
249 seek_page_no,
250 frame_no
251 )
252 })?;
253 if seek_page.own_page_no != seek_page_no as i32 {
254 bail!(
255 "OMV page table row {} identifies itself as {}",
256 seek_page_no,
257 seek_page.own_page_no
258 );
259 }
260 let first_packet_no = usize::try_from(key_page.top_packet_no).map_err(|_| {
270 anyhow!(
271 "OMV first packet is negative for key-frame page {}",
272 key_frame_page_no
273 )
274 })?;
275 if first_packet_no > key_frame_packet_no {
276 bail!(
277 "OMV key-frame page {} starts at packet {}, after key packet {}",
278 key_frame_page_no,
279 first_packet_no,
280 key_frame_packet_no
281 );
282 }
283 let seek_offset = u64::try_from(seek_page.seek_offset)
284 .map_err(|_| anyhow!("OMV seek offset is negative for page {}", seek_page_no))?;
285 let file_offset = self
286 .seek_top
287 .checked_add(seek_offset)
288 .ok_or_else(|| anyhow!("OMV seek offset overflow for page {seek_page_no}"))?;
289 let key_page_offset = u64::try_from(key_page.seek_offset)
290 .map_err(|_| anyhow!("OMV seek offset is negative for page {key_frame_page_no}"))?;
291 let key_page_file_offset = self
292 .seek_top
293 .checked_add(key_page_offset)
294 .ok_or_else(|| anyhow!("OMV key-page seek offset overflow"))?;
295 if key_page_file_offset < file_offset {
296 bail!("OMV key-frame page precedes its seek page in the file");
297 }
298
299 Ok(OmvSeekPoint {
300 file_offset,
301 key_page_file_offset,
302 seek_page_no,
303 first_packet_no,
304 key_frame_page_no,
305 key_frame_packet_no,
306 target_packet_no: frame_no,
307 })
308 }
309
310 pub fn open_embedded_ogg_reader(&self, path: impl AsRef<Path>) -> Result<BufReader<File>> {
312 let path = path.as_ref();
313 let mut file = File::open(path).with_context(|| format!("open OMV: {}", path.display()))?;
314 file.seek(SeekFrom::Start(self.ogg_data_offset))
315 .with_context(|| format!("seek embedded Ogg: {}", path.display()))?;
316 Ok(BufReader::new(file))
317 }
318
319 pub fn read_embedded_ogg(path: impl AsRef<Path>) -> Result<Vec<u8>> {
323 let path = path.as_ref();
324 let omv = Self::open(path)?;
325 let mut reader = omv.open_embedded_ogg_reader(path)?;
326 let mut out = Vec::new();
327 reader
328 .read_to_end(&mut out)
329 .with_context(|| format!("read embedded Ogg: {}", path.display()))?;
330 Ok(out)
331 }
332}
333
334fn read_header(buf: &[u8]) -> Result<OmvHeader> {
335 if buf.len() < OMV_HEADER_MIN_SIZE {
336 bail!("OMV header too small");
337 }
338
339 let header_size = read_u32(buf, 0x00)?;
340 let version = read_u32(buf, 0x04)?;
341 let theora_type = read_u32(buf, 0x28)?;
342 let display_width = read_u32(buf, 0x2c)?;
343 let display_height = read_u32(buf, 0x30)?;
344 let frame_time_us = read_u32(buf, 0x3c)?;
345 let max_data_size = read_u32(buf, 0x40)?;
348 let page_count_hint = read_u32(buf, 0x4c)?;
349 let packet_count_hint = read_u32(buf, 0x50)?;
350
351 if header_size < OMV_HEADER_MIN_SIZE as u32 {
352 bail!("invalid OMV header size: {header_size:#x}");
353 }
354 if theora_type > OMV_THEORA_TYPE_YUV {
355 bail!("invalid OMV theora type: {theora_type}");
356 }
357 if display_width == 0 || display_height == 0 {
358 bail!(
359 "invalid OMV display size: {}x{}",
360 display_width,
361 display_height
362 );
363 }
364
365 Ok(OmvHeader {
366 header_size,
367 version,
368 theora_type,
369 display_width,
370 display_height,
371 frame_time_us,
372 max_data_size,
373 page_count_hint,
374 packet_count_hint,
375 })
376}
377
378fn parse_pages(buf: &[u8], count: usize) -> Result<Vec<OmvTheoraPage>> {
379 let mut out = Vec::with_capacity(count);
380 for i in 0..count {
381 let base = i * OMV_THEORA_PAGE_SIZE;
382 let row = buf
383 .get(base..base + OMV_THEORA_PAGE_SIZE)
384 .ok_or_else(|| anyhow!("truncated OMV page table at row {i}"))?;
385 out.push(OmvTheoraPage {
386 own_page_no: read_i32(row, 0x00)?,
387 is_eos: row[0x04] != 0,
388 is_key_page: row[0x05] != 0,
389 page_size: read_i32(row, 0x08)?,
390 seek_offset: read_i32(row, 0x0c)?,
391 seek_page_no: read_i32(row, 0x10)?,
392 packet_count: read_i32(row, 0x14)?,
393 top_packet_no: read_i32(row, 0x18)?,
394 });
395 }
396 Ok(out)
397}
398
399fn parse_packets(buf: &[u8], count: usize) -> Result<Vec<OmvTheoraPacket>> {
400 let mut out = Vec::with_capacity(count);
401 for i in 0..count {
402 let base = i * OMV_THEORA_PACKET_SIZE;
403 let row = buf
404 .get(base..base + OMV_THEORA_PACKET_SIZE)
405 .ok_or_else(|| anyhow!("truncated OMV packet table at row {i}"))?;
406 out.push(OmvTheoraPacket {
407 own_packet_no: read_i32(row, 0x00)?,
408 own_page_no: read_i32(row, 0x04)?,
409 own_packet_no_in_page: read_i32(row, 0x08)?,
410 is_key_frame: row[0x0c] != 0,
411 key_frame_packet_no: read_i32(row, 0x10)?,
412 key_frame_page_no: read_i32(row, 0x14)?,
413 frame_time_start: read_i32(row, 0x18)?,
414 frame_time_end: read_i32(row, 0x1c)?,
415 });
416 }
417 Ok(out)
418}
419
420fn read_u32(buf: &[u8], offset: usize) -> Result<u32> {
421 let bytes: [u8; 4] = buf
422 .get(offset..offset + 4)
423 .ok_or_else(|| anyhow!("OMV field at {offset:#x} is truncated"))?
424 .try_into()
425 .map_err(|_| anyhow!("OMV field at {offset:#x} has invalid width"))?;
426 Ok(u32::from_le_bytes(bytes))
427}
428
429fn read_i32(buf: &[u8], offset: usize) -> Result<i32> {
430 Ok(read_u32(buf, offset)? as i32)
431}
432
433fn file_has_ogg_magic(file: &mut File, offset: u64) -> Result<bool> {
434 file.seek(SeekFrom::Start(offset))?;
435 let mut magic = [0u8; 4];
436 match file.read_exact(&mut magic) {
437 Ok(()) => Ok(&magic == b"OggS"),
438 Err(err) if err.kind() == std::io::ErrorKind::UnexpectedEof => Ok(false),
439 Err(err) => Err(err.into()),
440 }
441}
442
443fn find_ogg_offset_in_file(file: &mut File, start: u64) -> Result<u64> {
444 file.seek(SeekFrom::Start(start))?;
445 let mut absolute = start;
446 let mut carry = Vec::<u8>::new();
447 let mut chunk = vec![0u8; OGG_SCAN_CHUNK];
448
449 loop {
450 let n = file.read(&mut chunk)?;
451 if n == 0 {
452 break;
453 }
454 let mut window = Vec::with_capacity(carry.len() + n);
455 window.extend_from_slice(&carry);
456 window.extend_from_slice(&chunk[..n]);
457 if let Some(pos) = window.windows(4).position(|bytes| bytes == b"OggS") {
458 return Ok(absolute
459 .saturating_sub(carry.len() as u64)
460 .saturating_add(pos as u64));
461 }
462 carry.clear();
463 let keep = window.len().min(3);
464 carry.extend_from_slice(&window[window.len() - keep..]);
465 absolute = absolute.saturating_add(n as u64);
466 }
467
468 bail!("OggS not found in OMV payload")
469}
470
471#[cfg(test)]
472mod seek_index_tests {
473 use super::{OMV_THEORA_TYPE_RGB, OmvFile, OmvHeader, OmvTheoraPacket, OmvTheoraPage};
474
475 #[test]
476 fn seek_plan_uses_back_page_and_packet_table() {
477 let pages = vec![
478 OmvTheoraPage {
479 own_page_no: 0,
480 is_eos: false,
481 is_key_page: true,
482 page_size: 100,
483 seek_offset: 0,
484 seek_page_no: 0,
485 packet_count: 4,
486 top_packet_no: 0,
487 },
488 OmvTheoraPage {
489 own_page_no: 1,
490 is_eos: false,
491 is_key_page: false,
492 page_size: 100,
493 seek_offset: 100,
494 seek_page_no: 0,
495 packet_count: 0,
496 top_packet_no: -1,
500 },
501 OmvTheoraPage {
502 own_page_no: 2,
503 is_eos: false,
504 is_key_page: true,
505 page_size: 100,
506 seek_offset: 200,
507 seek_page_no: 1,
508 packet_count: 2,
509 top_packet_no: 4,
510 },
511 ];
512 let packets = (0..6)
513 .map(|packet_no| OmvTheoraPacket {
514 own_packet_no: packet_no,
515 own_page_no: packet_no / 2,
516 own_packet_no_in_page: packet_no % 2,
517 is_key_frame: packet_no == 0 || packet_no == 4,
518 key_frame_packet_no: if packet_no < 4 { 0 } else { 4 },
519 key_frame_page_no: if packet_no < 4 { 0 } else { 2 },
520 frame_time_start: packet_no * 33,
521 frame_time_end: (packet_no + 1) * 33,
522 })
523 .collect();
524 let omv = OmvFile {
525 header: OmvHeader {
526 header_size: 0xb4,
527 version: 0x0001_0001,
528 theora_type: OMV_THEORA_TYPE_RGB,
529 display_width: 640,
530 display_height: 480,
531 frame_time_us: 33_333,
532 max_data_size: 0,
533 page_count_hint: 3,
534 packet_count_hint: 6,
535 },
536 pages,
537 packets,
538 seek_top: 1_000,
539 ogg_data_offset: 1_000,
540 };
541
542 let point = omv.seek_point_for_frame(5).expect("seek point");
543 assert_eq!(point.seek_page_no, 1);
544 assert_eq!(point.first_packet_no, 4);
545 assert_eq!(point.key_frame_page_no, 2);
546 assert_eq!(point.key_frame_packet_no, 4);
547 assert_eq!(point.target_packet_no, 5);
548 assert_eq!(point.file_offset, 1_100);
549 assert_eq!(point.key_page_file_offset, 1_200);
550 }
551}