use super::Guid;
pub const ASF_HEADER_OBJECT: Guid = Guid::new(
0x75B2_2630,
0x668E,
0x11CF,
[0xA6, 0xD9, 0x00, 0xAA, 0x00, 0x62, 0xCE, 0x6C],
);
pub const ASF_STREAM_PROPERTIES_OBJECT: Guid = Guid::new(
0xB7DC_0791,
0xA9B7,
0x11CF,
[0x8E, 0xE6, 0x00, 0xC0, 0x0C, 0x20, 0x53, 0x65],
);
pub const ASF_AUDIO_MEDIA: Guid = Guid::new(
0xF869_9E40,
0x5B4D,
0x11CF,
[0xA8, 0xFD, 0x00, 0x80, 0x5F, 0x5C, 0x44, 0x2B],
);
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AsfParseError {
TruncatedHeader,
NotAnAsfFile,
InvalidHeaderObjectSize { declared: u64, buffer: usize },
InvalidSubObjectSize { declared: u64 },
SubObjectOverflowsHeader { needed: u64, remaining: u64 },
NoAudioStream,
StreamPropertiesTooShort { len: u64 },
TypeSpecificDataTooShort { len: u32 },
WaveFormatExtraOverflow { cb_size: u16, available: u32 },
}
impl core::fmt::Display for AsfParseError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
AsfParseError::TruncatedHeader => {
f.write_str("ASF input is shorter than the 30-byte Header Object preamble")
}
AsfParseError::NotAnAsfFile => {
f.write_str("ASF input does not start with the Header Object GUID")
}
AsfParseError::InvalidHeaderObjectSize { declared, buffer } => {
write!(
f,
"ASF Header Object declares size {declared} but buffer is {buffer} bytes"
)
}
AsfParseError::InvalidSubObjectSize { declared } => {
write!(f, "ASF sub-object declares size {declared} < 24 (preamble)")
}
AsfParseError::SubObjectOverflowsHeader { needed, remaining } => {
write!(
f,
"ASF sub-object needs {needed} bytes but only {remaining} remain in header"
)
}
AsfParseError::NoAudioStream => f.write_str(
"ASF Header Object contains no Stream Properties Object of type ASF_Audio_Media",
),
AsfParseError::StreamPropertiesTooShort { len } => {
write!(f, "ASF Stream Properties Object too short: {len} bytes")
}
AsfParseError::TypeSpecificDataTooShort { len } => {
write!(
f,
"Audio Type-Specific Data is {len} bytes; WAVEFORMATEX preamble needs 18"
)
}
AsfParseError::WaveFormatExtraOverflow { cb_size, available } => {
write!(
f,
"WAVEFORMATEX::cbSize={cb_size} but only {available} extradata bytes available"
)
}
}
}
}
impl std::error::Error for AsfParseError {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AmtBlueprint {
pub format_tag: u16,
pub n_channels: u16,
pub n_samples_per_sec: u32,
pub n_avg_bytes_per_sec: u32,
pub n_block_align: u16,
pub w_bits_per_sample: u16,
pub extradata: Vec<u8>,
}
impl AmtBlueprint {
pub fn wfx_total_len(&self) -> u32 {
18 + self.extradata.len() as u32
}
pub fn wma_criteria_passing(
format_tag: u16,
n_channels: u16,
n_samples_per_sec: u32,
n_avg_bytes_per_sec: u32,
n_block_align: u16,
) -> Self {
const MAGIC_CLSID: &[u8; 37] = b"1A0F78F0-EC8A-11d2-BBBE-006008320064\0";
let preamble_len = match format_tag {
0x0160 => 4,
0x0161 => 10,
_ => 10, };
let mut extradata = vec![0u8; preamble_len];
extradata.extend_from_slice(MAGIC_CLSID);
AmtBlueprint {
format_tag,
n_channels,
n_samples_per_sec,
n_avg_bytes_per_sec,
n_block_align,
w_bits_per_sample: 16,
extradata,
}
}
pub fn wma_with_ffmpeg_extradata_prefix(
format_tag: u16,
n_channels: u16,
n_samples_per_sec: u32,
n_avg_bytes_per_sec: u32,
n_block_align: u16,
) -> Self {
const MAGIC_CLSID: &[u8; 37] = b"1A0F78F0-EC8A-11d2-BBBE-006008320064\0";
let preamble: &[u8] = match format_tag {
0x0160 => &[0x00, 0x00, 0x01, 0x00],
0x0161 => &[0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00],
_ => &[0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00],
};
let mut extradata = Vec::with_capacity(preamble.len() + MAGIC_CLSID.len());
extradata.extend_from_slice(preamble);
extradata.extend_from_slice(MAGIC_CLSID);
AmtBlueprint {
format_tag,
n_channels,
n_samples_per_sec,
n_avg_bytes_per_sec,
n_block_align,
w_bits_per_sample: 16,
extradata,
}
}
}
pub fn extract_wma_amt_from_asf(bytes: &[u8]) -> Result<AmtBlueprint, AsfParseError> {
if bytes.len() < 30 {
return Err(AsfParseError::TruncatedHeader);
}
let leading_guid = read_guid(bytes, 0).ok_or(AsfParseError::TruncatedHeader)?;
if leading_guid != ASF_HEADER_OBJECT {
return Err(AsfParseError::NotAnAsfFile);
}
let header_obj_size = read_u64_le(bytes, 16).ok_or(AsfParseError::TruncatedHeader)?;
if header_obj_size < 30 || header_obj_size > bytes.len() as u64 {
return Err(AsfParseError::InvalidHeaderObjectSize {
declared: header_obj_size,
buffer: bytes.len(),
});
}
let mut cursor: u64 = 30;
let end = header_obj_size;
while cursor + 24 <= end {
let off = cursor as usize;
let sub_guid = read_guid(bytes, off).ok_or(AsfParseError::TruncatedHeader)?;
let sub_size = read_u64_le(bytes, off + 16).ok_or(AsfParseError::TruncatedHeader)?;
if sub_size < 24 {
return Err(AsfParseError::InvalidSubObjectSize { declared: sub_size });
}
if cursor + sub_size > end {
return Err(AsfParseError::SubObjectOverflowsHeader {
needed: sub_size,
remaining: end - cursor,
});
}
if sub_guid == ASF_STREAM_PROPERTIES_OBJECT {
if sub_size < 78 {
return Err(AsfParseError::StreamPropertiesTooShort { len: sub_size });
}
let stream_type = read_guid(bytes, off + 24).ok_or(AsfParseError::TruncatedHeader)?;
if stream_type == ASF_AUDIO_MEDIA {
let tsd_len = read_u32_le(bytes, off + 64).ok_or(AsfParseError::TruncatedHeader)?;
if (tsd_len as u64) + 78 > sub_size {
return Err(AsfParseError::StreamPropertiesTooShort { len: sub_size });
}
if tsd_len < 18 {
return Err(AsfParseError::TypeSpecificDataTooShort { len: tsd_len });
}
let wfx_off = off + 78;
let format_tag =
read_u16_le(bytes, wfx_off).ok_or(AsfParseError::TruncatedHeader)?;
let n_channels =
read_u16_le(bytes, wfx_off + 2).ok_or(AsfParseError::TruncatedHeader)?;
let n_samples_per_sec =
read_u32_le(bytes, wfx_off + 4).ok_or(AsfParseError::TruncatedHeader)?;
let n_avg_bytes_per_sec =
read_u32_le(bytes, wfx_off + 8).ok_or(AsfParseError::TruncatedHeader)?;
let n_block_align =
read_u16_le(bytes, wfx_off + 12).ok_or(AsfParseError::TruncatedHeader)?;
let w_bits_per_sample =
read_u16_le(bytes, wfx_off + 14).ok_or(AsfParseError::TruncatedHeader)?;
let cb_size =
read_u16_le(bytes, wfx_off + 16).ok_or(AsfParseError::TruncatedHeader)?;
let available_extra = tsd_len.saturating_sub(18);
if cb_size as u32 > available_extra {
return Err(AsfParseError::WaveFormatExtraOverflow {
cb_size,
available: available_extra,
});
}
let extra_start = wfx_off + 18;
let extra_end = extra_start + cb_size as usize;
let extradata = bytes[extra_start..extra_end].to_vec();
return Ok(AmtBlueprint {
format_tag,
n_channels,
n_samples_per_sec,
n_avg_bytes_per_sec,
n_block_align,
w_bits_per_sample,
extradata,
});
}
}
cursor += sub_size;
}
Err(AsfParseError::NoAudioStream)
}
pub fn locate_first_data_packet(bytes: &[u8]) -> Option<&[u8]> {
const ASF_DATA_OBJECT: Guid = Guid::new(
0x75B2_2636,
0x668E,
0x11CF,
[0xA6, 0xD9, 0x00, 0xAA, 0x00, 0x62, 0xCE, 0x6C],
);
let header_obj_size = read_u64_le(bytes, 16)? as usize;
if header_obj_size + 24 > bytes.len() {
return None;
}
let off = header_obj_size;
let g = read_guid(bytes, off)?;
if g != ASF_DATA_OBJECT {
return None;
}
let data_obj_size = read_u64_le(bytes, off + 16)? as usize;
if off + data_obj_size > bytes.len() {
return None;
}
let first = off + 50;
if first >= bytes.len() {
return None;
}
Some(&bytes[first..(off + data_obj_size).min(bytes.len())])
}
fn read_guid(bytes: &[u8], at: usize) -> Option<Guid> {
if at + 16 > bytes.len() {
return None;
}
Guid::read_le(&bytes[at..at + 16])
}
fn read_u16_le(bytes: &[u8], at: usize) -> Option<u16> {
if at + 2 > bytes.len() {
return None;
}
Some(u16::from_le_bytes([bytes[at], bytes[at + 1]]))
}
fn read_u32_le(bytes: &[u8], at: usize) -> Option<u32> {
if at + 4 > bytes.len() {
return None;
}
Some(u32::from_le_bytes([
bytes[at],
bytes[at + 1],
bytes[at + 2],
bytes[at + 3],
]))
}
fn read_u64_le(bytes: &[u8], at: usize) -> Option<u64> {
if at + 8 > bytes.len() {
return None;
}
Some(u64::from_le_bytes([
bytes[at],
bytes[at + 1],
bytes[at + 2],
bytes[at + 3],
bytes[at + 4],
bytes[at + 5],
bytes[at + 6],
bytes[at + 7],
]))
}
#[cfg(test)]
mod tests {
use super::*;
fn synthetic_wma1_asf() -> Vec<u8> {
let mut out = Vec::new();
out.extend_from_slice(&ASF_HEADER_OBJECT.write_le());
let size_off = out.len();
out.extend_from_slice(&0u64.to_le_bytes()); out.extend_from_slice(&1u32.to_le_bytes()); out.push(0x01); out.push(0x02); let spo_start = out.len();
out.extend_from_slice(&ASF_STREAM_PROPERTIES_OBJECT.write_le()); let spo_size_off = out.len();
out.extend_from_slice(&0u64.to_le_bytes()); out.extend_from_slice(&ASF_AUDIO_MEDIA.write_le()); out.extend_from_slice(&[0u8; 16]); out.extend_from_slice(&0u64.to_le_bytes()); out.extend_from_slice(&22u32.to_le_bytes()); out.extend_from_slice(&0u32.to_le_bytes()); out.extend_from_slice(&0u16.to_le_bytes()); out.extend_from_slice(&0u32.to_le_bytes()); out.extend_from_slice(&0x0160u16.to_le_bytes()); out.extend_from_slice(&1u16.to_le_bytes()); out.extend_from_slice(&44_100u32.to_le_bytes()); out.extend_from_slice(&4_000u32.to_le_bytes()); out.extend_from_slice(&185u16.to_le_bytes()); out.extend_from_slice(&16u16.to_le_bytes()); out.extend_from_slice(&4u16.to_le_bytes()); out.extend_from_slice(&[0x00, 0x00, 0x01, 0x00]); let spo_size = (out.len() - spo_start) as u64;
out[spo_size_off..spo_size_off + 8].copy_from_slice(&spo_size.to_le_bytes());
let total = out.len() as u64;
out[size_off..size_off + 8].copy_from_slice(&total.to_le_bytes());
out
}
#[test]
fn wma_with_ffmpeg_extradata_prefix_wma1_layout() {
let bp = AmtBlueprint::wma_with_ffmpeg_extradata_prefix(0x0160, 1, 44_100, 4_000, 185);
assert_eq!(bp.format_tag, 0x0160);
assert_eq!(bp.extradata.len(), 4 + 37);
assert_eq!(&bp.extradata[0..4], &[0x00, 0x00, 0x01, 0x00]);
assert_eq!(
&bp.extradata[4..],
b"1A0F78F0-EC8A-11d2-BBBE-006008320064\0"
);
assert!(bp.extradata.len() >= 0x29);
assert_eq!(bp.wfx_total_len(), 18 + 41);
}
#[test]
fn wma_with_ffmpeg_extradata_prefix_wma2_layout() {
let bp = AmtBlueprint::wma_with_ffmpeg_extradata_prefix(0x0161, 1, 44_100, 4_000, 185);
assert_eq!(bp.format_tag, 0x0161);
assert_eq!(bp.extradata.len(), 10 + 37);
assert_eq!(
&bp.extradata[0..10],
&[0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00]
);
assert_eq!(
&bp.extradata[10..],
b"1A0F78F0-EC8A-11d2-BBBE-006008320064\0"
);
assert!(bp.extradata.len() >= 0x2F);
assert_eq!(bp.wfx_total_len(), 18 + 47);
}
#[test]
fn extract_from_synthetic_wma1_blob() {
let blob = synthetic_wma1_asf();
let bp = extract_wma_amt_from_asf(&blob).unwrap();
assert_eq!(bp.format_tag, 0x0160);
assert_eq!(bp.n_channels, 1);
assert_eq!(bp.n_samples_per_sec, 44_100);
assert_eq!(bp.n_avg_bytes_per_sec, 4_000);
assert_eq!(bp.n_block_align, 185);
assert_eq!(bp.w_bits_per_sample, 16);
assert_eq!(bp.extradata, vec![0x00, 0x00, 0x01, 0x00]);
assert_eq!(bp.wfx_total_len(), 22);
}
#[test]
fn truncated_buffer_rejected() {
let err = extract_wma_amt_from_asf(&[0u8; 10]).unwrap_err();
assert_eq!(err, AsfParseError::TruncatedHeader);
}
#[test]
fn non_asf_file_rejected() {
let mut bad = vec![0xAAu8; 30];
bad[16..24].copy_from_slice(&30u64.to_le_bytes());
let err = extract_wma_amt_from_asf(&bad).unwrap_err();
assert_eq!(err, AsfParseError::NotAnAsfFile);
}
#[test]
fn header_object_guids_round_trip_via_braced_form() {
assert_eq!(
ASF_HEADER_OBJECT.to_braced_string(),
"{75B22630-668E-11CF-A6D9-00AA0062CE6C}"
);
assert_eq!(
ASF_STREAM_PROPERTIES_OBJECT.to_braced_string(),
"{B7DC0791-A9B7-11CF-8EE6-00C00C205365}"
);
assert_eq!(
ASF_AUDIO_MEDIA.to_braced_string(),
"{F8699E40-5B4D-11CF-A8FD-00805F5C442B}"
);
}
#[test]
fn header_with_no_audio_stream_rejected() {
let mut blob = Vec::new();
blob.extend_from_slice(&ASF_HEADER_OBJECT.write_le());
blob.extend_from_slice(&30u64.to_le_bytes());
blob.extend_from_slice(&0u32.to_le_bytes());
blob.push(0);
blob.push(0);
let err = extract_wma_amt_from_asf(&blob).unwrap_err();
assert_eq!(err, AsfParseError::NoAudioStream);
}
}