use crate::{AudexError, Result};
use std::fmt;
#[derive(Debug, Clone)]
pub struct ID3Error {
pub message: String,
}
impl fmt::Display for ID3Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.message)
}
}
impl std::error::Error for ID3Error {}
impl From<ID3Error> for AudexError {
fn from(err: ID3Error) -> Self {
AudexError::InvalidData(err.message)
}
}
#[derive(Debug, Clone)]
pub struct ID3NoHeaderError {
pub message: String,
}
impl fmt::Display for ID3NoHeaderError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.message)
}
}
impl std::error::Error for ID3NoHeaderError {}
impl From<ID3NoHeaderError> for AudexError {
fn from(err: ID3NoHeaderError) -> Self {
AudexError::InvalidData(err.message)
}
}
#[derive(Debug, Clone)]
pub struct ID3UnsupportedVersionError {
pub message: String,
}
impl fmt::Display for ID3UnsupportedVersionError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.message)
}
}
impl std::error::Error for ID3UnsupportedVersionError {}
impl From<ID3UnsupportedVersionError> for AudexError {
fn from(err: ID3UnsupportedVersionError) -> Self {
AudexError::UnsupportedFormat(err.message)
}
}
#[derive(Debug, Clone)]
pub struct ID3EncryptionUnsupportedError {
pub message: String,
}
impl fmt::Display for ID3EncryptionUnsupportedError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.message)
}
}
impl std::error::Error for ID3EncryptionUnsupportedError {}
impl From<ID3EncryptionUnsupportedError> for AudexError {
fn from(err: ID3EncryptionUnsupportedError) -> Self {
AudexError::UnsupportedFormat(err.message)
}
}
#[derive(Debug, Clone)]
pub struct ID3JunkFrameError {
pub message: String,
}
impl fmt::Display for ID3JunkFrameError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.message)
}
}
impl std::error::Error for ID3JunkFrameError {}
impl From<ID3JunkFrameError> for AudexError {
fn from(err: ID3JunkFrameError) -> Self {
AudexError::InvalidData(err.message)
}
}
#[derive(Debug, Clone)]
pub struct ID3BadUnsynchData {
pub message: String,
}
impl fmt::Display for ID3BadUnsynchData {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.message)
}
}
impl std::error::Error for ID3BadUnsynchData {}
#[derive(Debug, Clone)]
pub struct ID3BadCompressedData {
pub message: String,
}
impl fmt::Display for ID3BadCompressedData {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.message)
}
}
impl std::error::Error for ID3BadCompressedData {}
#[derive(Debug, Clone)]
pub struct ID3TagError {
pub message: String,
}
impl fmt::Display for ID3TagError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.message)
}
}
impl std::error::Error for ID3TagError {}
#[derive(Debug, Clone)]
pub struct ID3Warning {
pub message: String,
}
impl fmt::Display for ID3Warning {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.message)
}
}
impl std::error::Error for ID3Warning {}
pub fn is_valid_frame_id(frame_id: &str) -> bool {
if frame_id.len() != 3 && frame_id.len() != 4 {
return false;
}
let is_alnum = frame_id
.chars()
.all(|c| c.is_alphanumeric() && c.is_ascii());
let has_cased = frame_id.chars().any(|c| c.is_alphabetic());
let all_cased_upper = frame_id
.chars()
.filter(|c| c.is_alphabetic())
.all(|c| c.is_uppercase());
is_alnum && has_cased && all_cased_upper
}
#[derive(Debug, Clone)]
pub struct ID3SaveConfig {
pub v2_version: u8,
pub v23_separator: Option<String>,
}
impl ID3SaveConfig {
pub fn new(v2_version: Option<u8>, v23_separator: Option<String>) -> Result<Self> {
let version = v2_version.unwrap_or(4);
if version != 3 && version != 4 {
return Err(AudexError::InvalidData(format!(
"v2_version must be 3 or 4, got {}",
version
)));
}
Ok(ID3SaveConfig {
v2_version: version,
v23_separator,
})
}
}
pub struct Unsynch;
impl Unsynch {
pub fn decode(value: &[u8]) -> Result<Vec<u8>> {
let fragments: Vec<&[u8]> = value.split(|&b| b == 0xFF).collect();
let has_trailing_ff = fragments.len() > 1 && fragments.last() == Some(&[].as_ref());
let mut result = Vec::new();
result.extend_from_slice(fragments[0]);
let fragment_count = if has_trailing_ff {
fragments.len() - 1
} else {
fragments.len()
};
for fragment in fragments[1..fragment_count].iter() {
result.push(0xFF);
if fragment.is_empty() {
continue;
}
if fragment[0] >= 0xE0 {
result.extend_from_slice(fragment);
} else if fragment[0] == 0x00 {
if fragment.len() > 1 {
result.extend_from_slice(&fragment[1..]);
}
} else {
result.extend_from_slice(fragment);
}
}
if has_trailing_ff {
result.push(0xFF);
}
Ok(result)
}
pub fn needs_encode(value: &[u8]) -> bool {
for i in 0..value.len() {
if value[i] == 0xFF {
if i + 1 >= value.len() {
return true;
}
if value[i + 1] >= 0xE0 || value[i + 1] == 0x00 {
return true;
}
}
}
false
}
pub fn encode(value: &[u8]) -> Vec<u8> {
let fragments: Vec<&[u8]> = value.split(|&b| b == 0xFF).collect();
let mut result = Vec::new();
result.extend_from_slice(fragments[0]);
for fragment in fragments.iter().skip(1) {
result.push(0xFF);
if fragment.is_empty() || fragment[0] >= 0xE0 || fragment[0] == 0x00 {
result.push(0x00); }
result.extend_from_slice(fragment);
}
result
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct BitPaddedInt {
value: u32,
pub bits: u8,
pub bigendian: bool,
}
impl BitPaddedInt {
pub fn new(
value: BitPaddedIntInput,
bits: Option<u8>,
bigendian: Option<bool>,
) -> Result<Self> {
let bits = bits.unwrap_or(7);
let bigendian = bigendian.unwrap_or(true);
let mask = (1u32 << bits) - 1;
let mut numeric_value = 0u32;
let mut shift = 0u32;
match value {
BitPaddedIntInput::Int(val) => {
if val < 0 {
return Err(AudexError::InvalidData(
"BitPaddedInt value cannot be negative".to_string(),
));
}
let mut val = val as u32;
while val > 0 {
if shift >= 32 {
return Err(AudexError::InvalidData(
"BitPaddedInt overflow: value exceeds u32 capacity".to_string(),
));
}
let masked = val & mask;
let shifted = masked.checked_shl(shift).ok_or_else(|| {
AudexError::InvalidData(
"BitPaddedInt overflow: shift exceeds u32 range".to_string(),
)
})?;
numeric_value = numeric_value.checked_add(shifted).ok_or_else(|| {
AudexError::InvalidData(
"BitPaddedInt overflow: accumulated value exceeds u32".to_string(),
)
})?;
val >>= 8;
shift += bits as u32;
}
}
BitPaddedIntInput::Bytes(bytes) => {
let byte_iter: Box<dyn Iterator<Item = u8>> = if bigendian {
Box::new(bytes.into_iter().rev())
} else {
Box::new(bytes.into_iter())
};
for byte in byte_iter {
let masked = byte as u32 & mask;
if masked == 0 {
shift += bits as u32;
continue;
}
if shift >= 32 {
return Err(AudexError::InvalidData(
"BitPaddedInt overflow: value exceeds u32 capacity".to_string(),
));
}
let shifted = masked.checked_shl(shift).ok_or_else(|| {
AudexError::InvalidData(
"BitPaddedInt overflow: shift exceeds u32 range".to_string(),
)
})?;
if shifted >> shift != masked {
return Err(AudexError::InvalidData(
"BitPaddedInt overflow: value exceeds u32 capacity".to_string(),
));
}
numeric_value = numeric_value.checked_add(shifted).ok_or_else(|| {
AudexError::InvalidData(
"BitPaddedInt overflow: accumulated value exceeds u32".to_string(),
)
})?;
shift += bits as u32;
}
}
}
Ok(BitPaddedInt {
value: numeric_value,
bits,
bigendian,
})
}
pub fn from_bytes(bytes: &[u8], bits: u8, bigendian: bool) -> Result<Self> {
Self::new(
BitPaddedIntInput::Bytes(bytes.to_vec()),
Some(bits),
Some(bigendian),
)
}
pub fn value(&self) -> u32 {
self.value
}
pub fn as_str(&self, width: Option<i32>, minwidth: Option<u32>) -> Result<Vec<u8>> {
Self::to_str(
self.value,
Some(self.bits),
Some(self.bigendian),
width,
minwidth,
)
}
pub fn to_str(
value: u32,
bits: Option<u8>,
bigendian: Option<bool>,
width: Option<i32>,
minwidth: Option<u32>,
) -> Result<Vec<u8>> {
let bits = bits.unwrap_or(7);
let bigendian = bigendian.unwrap_or(true);
let width = width.unwrap_or(4);
let minwidth = minwidth.unwrap_or(4);
if width < -1 {
return Err(AudexError::InvalidData(format!(
"Invalid negative width: {}",
width
)));
}
const MAX_BPI_WIDTH: i32 = 16;
if width > MAX_BPI_WIDTH {
return Err(AudexError::InvalidData(format!(
"BitPaddedInt width {} exceeds maximum of {}",
width, MAX_BPI_WIDTH
)));
}
let mask = (1u32 << bits) - 1;
let mut value = value;
let mut bytes = if width != -1 {
let width = width as usize;
let mut bytes = vec![0u8; width];
let mut index = 0;
while value > 0 {
if index >= width {
return Err(AudexError::InvalidData(format!(
"Value too wide (>{} bytes)",
width
)));
}
bytes[index] = (value & mask) as u8;
value >>= bits;
index += 1;
}
bytes
} else {
let mut bytes = Vec::new();
while value > 0 {
bytes.push((value & mask) as u8);
value >>= bits;
}
while bytes.len() < minwidth as usize {
bytes.push(0x00);
}
bytes
};
if bigendian {
bytes.reverse();
}
Ok(bytes)
}
pub fn has_valid_padding(value: BitPaddedIntInput, bits: Option<u8>) -> bool {
let bits = bits.unwrap_or(7);
if bits > 8 {
return false;
}
let mask = (((1u32 << (8 - bits)) - 1) << bits) as u8;
match value {
BitPaddedIntInput::Int(val) => {
if val < 0 {
return false;
}
let mut val = val as u32;
while val > 0 {
if (val as u8) & mask != 0 {
return false;
}
val >>= 8;
}
}
BitPaddedIntInput::Bytes(bytes) => {
for byte in bytes {
if byte & mask != 0 {
return false;
}
}
}
}
true
}
}
#[derive(Debug, Clone)]
pub enum BitPaddedIntInput {
Int(i32),
Bytes(Vec<u8>),
}
impl From<i32> for BitPaddedIntInput {
fn from(val: i32) -> Self {
BitPaddedIntInput::Int(val)
}
}
impl From<u32> for BitPaddedIntInput {
fn from(val: u32) -> Self {
BitPaddedIntInput::Bytes(val.to_be_bytes().to_vec())
}
}
impl From<Vec<u8>> for BitPaddedIntInput {
fn from(val: Vec<u8>) -> Self {
BitPaddedIntInput::Bytes(val)
}
}
impl From<&[u8]> for BitPaddedIntInput {
fn from(val: &[u8]) -> Self {
BitPaddedIntInput::Bytes(val.to_vec())
}
}
impl fmt::Display for BitPaddedInt {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.value)
}
}
impl From<BitPaddedInt> for u32 {
fn from(bpi: BitPaddedInt) -> Self {
bpi.value
}
}
pub fn remove_unsynchronization(data: &[u8]) -> Result<Vec<u8>> {
Unsynch::decode(data)
}
pub fn add_unsynchronization(data: &[u8]) -> Vec<u8> {
Unsynch::encode(data)
}
pub fn decode_synchsafe_int(bytes: &[u8]) -> u32 {
let mut result = 0u32;
for &byte in bytes.iter().take(4) {
result = (result << 7) | (byte & 0x7F) as u32;
}
result
}
pub fn decode_synchsafe_int_checked(bytes: &[u8]) -> Result<u32> {
for (i, &byte) in bytes.iter().take(4).enumerate() {
if byte & 0x80 != 0 {
return Err(AudexError::InvalidData(format!(
"synchsafe integer byte {} has high bit set: 0x{:02X}",
i, byte
)));
}
}
Ok(decode_synchsafe_int(bytes))
}
pub fn encode_synchsafe_int(value: u32) -> Result<[u8; 4]> {
if value > 0x0FFF_FFFF {
return Err(AudexError::InvalidData(format!(
"synchsafe encoding only supports values up to 2^28 - 1, got {}",
value
)));
}
Ok([
((value >> 21) & 0x7F) as u8,
((value >> 14) & 0x7F) as u8,
((value >> 7) & 0x7F) as u8,
(value & 0x7F) as u8,
])
}
pub fn is_valid_frame_id_versioned(frame_id: &str, version: (u8, u8)) -> bool {
match version {
(2, 2) => {
frame_id.len() == 3 && is_valid_frame_id(frame_id)
}
(2, 3) | (2, 4) => {
frame_id.len() == 4 && is_valid_frame_id(frame_id)
}
_ => false,
}
}
pub fn upgrade_frame_id(frame_id: &str) -> Option<String> {
match frame_id {
"TT2" => Some("TIT2".to_string()), "TP1" => Some("TPE1".to_string()), "TAL" => Some("TALB".to_string()), "TYE" => Some("TYER".to_string()), "TCO" => Some("TCON".to_string()), "TRK" => Some("TRCK".to_string()), "COM" => Some("COMM".to_string()), "PIC" => Some("APIC".to_string()), "TXT" => Some("TEXT".to_string()), _ => None,
}
}
pub fn downgrade_frame_id(frame_id: &str) -> Option<String> {
match frame_id {
"TIT2" => Some("TT2".to_string()),
"TPE1" => Some("TP1".to_string()),
"TALB" => Some("TAL".to_string()),
"TYER" | "TDRC" => Some("TYE".to_string()),
"TCON" => Some("TCO".to_string()),
"TRCK" => Some("TRK".to_string()),
"COMM" => Some("COM".to_string()),
"APIC" => Some("PIC".to_string()),
"TEXT" => Some("TXT".to_string()), "TXXX" => Some("TXX".to_string()), _ => None,
}
}
pub fn calculate_tag_size(frame_data_size: usize, padding: usize) -> usize {
10 + frame_data_size + padding }
pub fn find_sync_pattern(data: &[u8]) -> Option<usize> {
for i in 0..data.len().saturating_sub(1) {
let pattern = (data[i] as u16) << 8 | data[i + 1] as u16;
if pattern & 0xFFE0 == 0xFFE0 {
return Some(i);
}
}
None
}
pub fn validate_id3_header(data: &[u8]) -> Result<()> {
if data.len() < 10 {
return Err(AudexError::InvalidData("ID3 header too short".to_string()));
}
if &data[0..3] != b"ID3" {
return Err(AudexError::InvalidData("Invalid ID3 signature".to_string()));
}
let major_version = data[3];
let _revision = data[4];
if !(2..=4).contains(&major_version) {
return Err(AudexError::InvalidData(format!(
"Unsupported ID3 version: 2.{}",
major_version
)));
}
for &byte in &data[6..10] {
if byte & 0x80 != 0 {
return Err(AudexError::InvalidData(
"Invalid synchsafe integer in header".to_string(),
));
}
}
Ok(())
}
pub fn min_version_for_frame(frame_id: &str) -> Option<(u8, u8)> {
match frame_id {
"TDRC" | "TDRL" | "TDTG" | "TIPL" | "TMCL" | "TSOA" | "TSOP" | "TSOT" => Some((2, 4)),
"TYER" | "TDAT" | "TIME" | "TORY" | "TRDA" | "TSIZ" => Some((2, 3)),
_ if frame_id.len() == 4 => Some((2, 3)),
_ if frame_id.len() == 3 => Some((2, 2)),
_ => None,
}
}
pub struct JunkFrameRecovery;
impl JunkFrameRecovery {
pub fn recover_from_junk(data: &[u8], offset: usize, version: (u8, u8)) -> Option<usize> {
let frame_id_len = match version {
(2, 2) => 3,
(2, 3) | (2, 4) => 4,
_ => return None,
};
const MAX_SCAN_DISTANCE: usize = 65536;
let scan_end = data
.len()
.saturating_sub(10)
.min(offset.saturating_add(MAX_SCAN_DISTANCE));
for i in offset..scan_end {
if i + 10 > data.len() {
break;
}
let potential_id = match std::str::from_utf8(&data[i..i + frame_id_len]) {
Ok(id) => id,
Err(_) => continue,
};
if !is_valid_frame_id(potential_id) {
continue;
}
let size_start = i + frame_id_len;
if size_start + 4 > data.len() {
continue;
}
let frame_size = match version {
(2, 4) => match decode_synchsafe_int_checked(&data[size_start..size_start + 4]) {
Ok(v) => v,
Err(_) => continue, },
_ => u32::from_be_bytes([
data[size_start],
data[size_start + 1],
data[size_start + 2],
data[size_start + 3],
]),
};
let max_frame = crate::limits::ParseLimits::default().max_tag_size;
if frame_size > 0
&& (frame_size as u64) < max_frame
&& i.checked_add(10)
.and_then(|v| v.checked_add(frame_size as usize))
.is_some_and(|end| end <= data.len())
{
return Some(i);
}
}
None
}
pub fn reconstruct_frame(data: &[u8], frame_id: &str, expected_size: u32) -> Result<Vec<u8>> {
if frame_id.starts_with('T') && frame_id != "TXXX" {
return Self::reconstruct_text_frame(data, expected_size);
}
if frame_id == "COMM" {
return Self::reconstruct_comment_frame(data, expected_size);
}
if frame_id.starts_with('W') {
return Self::reconstruct_url_frame(data, expected_size);
}
if (data.len() as u64) <= (expected_size as u64) + 10 {
Ok(data.to_vec())
} else {
Err(AudexError::InvalidData(format!(
"Cannot reconstruct frame {}",
frame_id
)))
}
}
fn reconstruct_text_frame(data: &[u8], expected_size: u32) -> Result<Vec<u8>> {
if data.is_empty() {
return Ok(vec![0x00]); }
let encoding = if !data.is_empty() && data[0] <= 3 {
data[0]
} else {
0 };
let text_start = if encoding <= 3 && data.len() > 1 {
1
} else {
0
};
let text_data = &data[text_start..];
let is_valid_text = match encoding {
0 => text_data.iter().all(|&b| b >= 0x20 || b == 0x00), 1 | 2 => text_data.len() % 2 == 0, 3 => std::str::from_utf8(text_data).is_ok(), _ => false,
};
if is_valid_text && (data.len() as u64) <= (expected_size as u64) + 5 {
Ok(data.to_vec())
} else {
Ok(vec![0x00]) }
}
fn reconstruct_comment_frame(data: &[u8], expected_size: u32) -> Result<Vec<u8>> {
if data.len() < 4 {
return Ok(vec![
0x00, b'e', b'n', b'g', 0x00, 0x00, ]);
}
let encoding = if data[0] <= 3 { data[0] } else { 0x00 };
let lang_start = 1;
let lang_bytes = if data.len() >= 4 {
&data[lang_start..lang_start + 3]
} else {
b"eng"
};
let lang_valid = lang_bytes.iter().all(|&b| b.is_ascii_alphabetic());
if lang_valid && (data.len() as u64) <= (expected_size as u64) + 10 {
Ok(data.to_vec())
} else {
Ok(vec![
encoding, b'e', b'n', b'g', 0x00, 0x00, ])
}
}
fn reconstruct_url_frame(data: &[u8], expected_size: u32) -> Result<Vec<u8>> {
let url_str = String::from_utf8_lossy(data);
let looks_like_url = url_str.starts_with("http://")
|| url_str.starts_with("https://")
|| url_str.starts_with("ftp://")
|| url_str.contains("://");
if looks_like_url && (data.len() as u64) <= (expected_size as u64) + 10 {
Ok(data.to_vec())
} else {
Ok(Vec::new())
}
}
pub fn is_frame_corrupted(data: &[u8], frame_id: &str, _flags: u16) -> bool {
if data.is_empty() && !Self::is_optional_frame(frame_id) {
return true;
}
if frame_id.starts_with('T') && frame_id != "TXXX" && !data.is_empty() && data[0] > 3 {
return true;
}
if frame_id == "COMM" && data.len() < 5 {
return true;
}
const BINARY_FRAMES: &[&str] = &[
"APIC", "GEOB", "PRIV", "MCDI", "AENC", "COMR", "ENCR", "GRID", "LINK", "OWNE", "RBUF",
"RVRB", "SYLT", "SYTC", "ETCO", "MLLT", "SEEK", "SIGN", "ASPI",
];
if !BINARY_FRAMES.contains(&frame_id) {
let null_count = data.iter().filter(|&&b| b == 0).count();
if data.len() > 10 && null_count > data.len() * 2 / 3 {
return true;
}
}
if frame_id.starts_with('T') && data.len() > 1 {
let text_data = &data[1..]; let non_printable = text_data.iter().filter(|&&b| b < 0x20 && b != 0x00).count();
if non_printable > text_data.len() / 4 {
return true;
}
}
false
}
fn is_optional_frame(frame_id: &str) -> bool {
matches!(
frame_id,
"TPOS" | "TPUB" | "TOFN" | "TOLY" | "TOPE" | "TORY" | "TOWN" | "TRSN" | "TRSO" |
"TBPM" | "TKEY" | "TLAN" | "TLEN" | "TMED" | "TMOO" | "TOAL" |
"TXXX" | "WXXX" |
"COMM" | "USLT" |
"APIC" | "GEOB" |
"RVAD" | "EQUA" | "IPLS" | "TDAT" | "TIME" | "TRDA" | "TSIZ" | "TYER"
)
}
}
pub fn default_text_encoding(version: (u8, u8)) -> u8 {
match version {
(2, 2) | (2, 3) => 0, (2, 4) => 3, _ => 0,
}
}