use bitflags::bitflags;
use nom::{
bytes::complete::{tag, take},
number::complete::{le_u32, le_u64, le_u8},
IResult, Parser,
};
use oximedia_core::{OxiError, OxiResult};
pub const OGG_MAGIC: &[u8; 4] = b"OggS";
#[allow(dead_code)]
pub const MAX_PAGE_SIZE: usize = 27 + 255 + 255 * 255;
pub const MIN_HEADER_SIZE: usize = 27;
bitflags! {
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct PageFlags: u8 {
const CONTINUATION = 0x01;
const BOS = 0x02;
const EOS = 0x04;
}
}
impl Default for PageFlags {
fn default() -> Self {
Self::empty()
}
}
#[derive(Clone, Debug)]
pub struct OggPage {
pub version: u8,
pub flags: PageFlags,
pub granule_position: u64,
pub serial_number: u32,
pub page_sequence: u32,
pub checksum: u32,
pub segments: Vec<u8>,
pub data: Vec<u8>,
}
impl OggPage {
pub fn parse(input: &[u8]) -> OxiResult<(Self, usize)> {
match parse_page(input) {
Ok((remaining, page)) => {
let consumed = input.len() - remaining.len();
Ok((page, consumed))
}
Err(_) => Err(OxiError::Parse {
offset: 0,
message: "Invalid Ogg page".into(),
}),
}
}
#[must_use]
pub fn is_bos(&self) -> bool {
self.flags.contains(PageFlags::BOS)
}
#[must_use]
pub fn is_eos(&self) -> bool {
self.flags.contains(PageFlags::EOS)
}
#[must_use]
pub fn is_continuation(&self) -> bool {
self.flags.contains(PageFlags::CONTINUATION)
}
#[must_use]
pub fn has_granule(&self) -> bool {
self.granule_position != u64::MAX
}
#[must_use]
pub fn packets(&self) -> Vec<(Vec<u8>, bool)> {
let mut packets = Vec::new();
let mut current_packet = Vec::new();
let mut offset = 0;
for &segment_size in &self.segments {
let size = segment_size as usize;
if offset + size <= self.data.len() {
current_packet.extend_from_slice(&self.data[offset..offset + size]);
offset += size;
}
if segment_size < 255 {
let complete = true;
packets.push((std::mem::take(&mut current_packet), complete));
}
}
if !current_packet.is_empty() {
packets.push((current_packet, false));
}
packets
}
#[must_use]
pub fn total_size(&self) -> usize {
MIN_HEADER_SIZE + self.segments.len() + self.data.len()
}
}
fn parse_page(input: &[u8]) -> IResult<&[u8], OggPage> {
let (input, _) = tag(&OGG_MAGIC[..])(input)?;
let (
input,
(version, flags, granule_position, serial_number, page_sequence, checksum, segment_count),
) = (le_u8, le_u8, le_u64, le_u32, le_u32, le_u32, le_u8).parse(input)?;
let (input, segment_table) = take(segment_count as usize)(input)?;
let segments: Vec<u8> = segment_table.to_vec();
let data_size: usize = segments.iter().map(|&s| usize::from(s)).sum();
let (input, data) = take(data_size)(input)?;
Ok((
input,
OggPage {
version,
flags: PageFlags::from_bits_truncate(flags),
granule_position,
serial_number,
page_sequence,
checksum,
segments,
data: data.to_vec(),
},
))
}
#[must_use]
#[allow(dead_code)]
pub fn crc32_ogg(data: &[u8]) -> u32 {
const CRC_TABLE: [u32; 256] = generate_crc_table();
let mut crc = 0u32;
for &byte in data {
crc = (crc << 8) ^ CRC_TABLE[((crc >> 24) as u8 ^ byte) as usize];
}
crc
}
#[allow(dead_code, clippy::cast_possible_truncation)]
const fn generate_crc_table() -> [u32; 256] {
let mut table = [0u32; 256];
let mut i = 0;
while i < 256 {
let mut crc = (i as u32) << 24;
let mut j = 0;
while j < 8 {
if crc & 0x8000_0000 != 0 {
crc = (crc << 1) ^ 0x04C1_1DB7;
} else {
crc <<= 1;
}
j += 1;
}
table[i] = crc;
i += 1;
}
table
}
#[must_use]
#[allow(dead_code)]
pub fn verify_page_crc(page_data: &[u8]) -> bool {
if page_data.len() < MIN_HEADER_SIZE {
return false;
}
let stored_crc =
u32::from_le_bytes([page_data[22], page_data[23], page_data[24], page_data[25]]);
let mut data_copy = page_data.to_vec();
data_copy[22] = 0;
data_copy[23] = 0;
data_copy[24] = 0;
data_copy[25] = 0;
let calculated_crc = crc32_ogg(&data_copy);
stored_crc == calculated_crc
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_page_flags() {
assert!(PageFlags::BOS.contains(PageFlags::BOS));
assert!(!PageFlags::BOS.contains(PageFlags::EOS));
let combined = PageFlags::BOS | PageFlags::EOS;
assert!(combined.contains(PageFlags::BOS));
assert!(combined.contains(PageFlags::EOS));
}
#[test]
fn test_page_flags_default() {
let flags = PageFlags::default();
assert!(flags.is_empty());
}
#[test]
fn test_crc32() {
let data = b"OggS";
let crc = crc32_ogg(data);
assert_ne!(crc, 0);
}
#[test]
fn test_crc32_empty() {
let crc = crc32_ogg(&[]);
assert_eq!(crc, 0);
}
#[test]
fn test_page_parse_minimal() {
let mut page_data = Vec::new();
page_data.extend_from_slice(OGG_MAGIC); page_data.push(0); page_data.push(0x02); page_data.extend_from_slice(&0u64.to_le_bytes()); page_data.extend_from_slice(&1u32.to_le_bytes()); page_data.extend_from_slice(&0u32.to_le_bytes()); page_data.extend_from_slice(&0u32.to_le_bytes()); page_data.push(1); page_data.push(5); page_data.extend_from_slice(b"hello");
let result = OggPage::parse(&page_data);
assert!(result.is_ok());
let (page, consumed) = result.expect("operation should succeed");
assert_eq!(consumed, page_data.len());
assert!(page.is_bos());
assert!(!page.is_eos());
assert!(!page.is_continuation());
assert_eq!(page.serial_number, 1);
assert_eq!(page.data, b"hello");
}
#[test]
fn test_page_parse_invalid() {
let data = b"NotOgg";
let result = OggPage::parse(data);
assert!(result.is_err());
}
#[test]
fn test_packets_extraction() {
let mut page = OggPage {
version: 0,
flags: PageFlags::empty(),
granule_position: 100,
serial_number: 1,
page_sequence: 0,
checksum: 0,
segments: vec![5, 10, 255, 20], data: vec![0; 5 + 10 + 255 + 20],
};
for i in 0..5 {
page.data[i] = 1;
}
for i in 5..15 {
page.data[i] = 2;
}
for i in 15..270 {
page.data[i] = 3;
}
for i in 270..290 {
page.data[i] = 4;
}
let packets = page.packets();
assert_eq!(packets.len(), 3);
assert_eq!(packets[0].0.len(), 5);
assert!(packets[0].1);
assert_eq!(packets[1].0.len(), 10);
assert!(packets[1].1);
assert_eq!(packets[2].0.len(), 255 + 20);
assert!(packets[2].1); }
#[test]
fn test_packets_incomplete() {
let page = OggPage {
version: 0,
flags: PageFlags::empty(),
granule_position: u64::MAX, serial_number: 1,
page_sequence: 0,
checksum: 0,
segments: vec![5, 255], data: vec![0; 5 + 255],
};
let packets = page.packets();
assert_eq!(packets.len(), 2);
assert_eq!(packets[0].0.len(), 5);
assert!(packets[0].1);
assert_eq!(packets[1].0.len(), 255);
assert!(!packets[1].1);
}
#[test]
fn test_has_granule() {
let page = OggPage {
version: 0,
flags: PageFlags::empty(),
granule_position: 100,
serial_number: 1,
page_sequence: 0,
checksum: 0,
segments: vec![],
data: vec![],
};
assert!(page.has_granule());
let page_no_granule = OggPage {
version: 0,
flags: PageFlags::empty(),
granule_position: u64::MAX,
serial_number: 1,
page_sequence: 0,
checksum: 0,
segments: vec![],
data: vec![],
};
assert!(!page_no_granule.has_granule());
}
#[test]
fn test_total_size() {
let page = OggPage {
version: 0,
flags: PageFlags::empty(),
granule_position: 0,
serial_number: 1,
page_sequence: 0,
checksum: 0,
segments: vec![10, 20],
data: vec![0; 30],
};
assert_eq!(page.total_size(), MIN_HEADER_SIZE + 2 + 30);
}
}