use crate::{AudexError, FileType, ReadWriteSeek, Result, StreamInfo, tags::Tags};
use std::collections::HashMap;
use std::io::{Cursor, Read, Seek, SeekFrom, Write};
use std::path::Path;
use std::time::Duration;
const MAX_APE_ITEMS: u32 = 10_000;
#[cfg(feature = "async")]
use crate::util::{loadfile_read_async, loadfile_write_async, resize_bytes_async};
#[cfg(feature = "async")]
use tokio::fs::File as TokioFile;
#[cfg(feature = "async")]
use tokio::io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum APEValueType {
Text = 0,
Binary = 1,
External = 2,
}
const HAS_HEADER: u32 = 1 << 31; const IS_HEADER: u32 = 1 << 29;
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct APEValue {
pub value_type: APEValueType,
#[cfg_attr(
feature = "serde",
serde(with = "crate::serde_helpers::bytes_as_base64")
)]
pub data: Vec<u8>,
}
impl APEValue {
pub fn text<S: Into<String>>(text: S) -> Self {
Self {
value_type: APEValueType::Text,
data: text.into().as_bytes().to_vec(),
}
}
pub fn binary(data: Vec<u8>) -> Self {
Self {
value_type: APEValueType::Binary,
data,
}
}
pub fn external<S: Into<String>>(uri: S) -> Self {
Self {
value_type: APEValueType::External,
data: uri.into().as_bytes().to_vec(),
}
}
pub fn as_string(&self) -> Result<String> {
match self.value_type {
APEValueType::Text | APEValueType::External => String::from_utf8(self.data.clone())
.map_err(|e| AudexError::InvalidData(format!("Invalid UTF-8: {}", e))),
APEValueType::Binary => Err(AudexError::InvalidData(
"Cannot convert binary data to string".to_string(),
)),
}
}
pub fn as_text_list(&self) -> Result<Vec<String>> {
let text = self.as_string()?;
Ok(text.split('\0').map(|s| s.to_string()).collect())
}
pub fn pprint(&self) -> String {
match self.value_type {
APEValueType::Text => {
if let Ok(text_list) = self.as_text_list() {
text_list.join(" / ")
} else {
format!("[Invalid text: {} bytes]", self.data.len())
}
}
APEValueType::Binary => {
format!("[{} bytes]", self.data.len())
}
APEValueType::External => {
if let Ok(uri) = self.as_string() {
format!("[External] {}", uri)
} else {
format!("[Invalid external: {} bytes]", self.data.len())
}
}
}
}
}
#[derive(Debug, Default, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct APEv2Tags {
items: HashMap<String, APEValue>,
case_map: HashMap<String, String>,
}
impl Tags for APEv2Tags {
fn get(&self, _key: &str) -> Option<&[String]> {
None
}
fn set(&mut self, key: &str, values: Vec<String>) {
let joined = values.join("\0");
let _ = self.set_text(key, joined);
}
fn remove(&mut self, key: &str) {
APEv2Tags::remove(self, key);
}
fn keys(&self) -> Vec<String> {
APEv2Tags::keys(self)
}
fn pprint(&self) -> String {
let mut result = String::new();
let mut keys: Vec<_> = self.keys();
keys.sort();
for key in keys {
if let Some(ape_value) = self.get(&key) {
if let Ok(value_str) = ape_value.as_string() {
result.push_str(&format!("{}={}\n", key, value_str));
}
}
}
result
}
}
impl APEv2Tags {
pub fn new() -> Self {
Self {
items: HashMap::new(),
case_map: HashMap::new(),
}
}
pub fn get(&self, key: &str) -> Option<&APEValue> {
self.items.get(&key.to_lowercase())
}
pub fn set(&mut self, key: &str, value: APEValue) -> Result<()> {
if !is_valid_apev2_key(key) {
return Err(AudexError::InvalidData(format!(
"{:?} is not a valid APEv2 key",
key
)));
}
let lower_key = key.to_lowercase();
self.case_map.insert(lower_key.clone(), key.to_string());
self.items.insert(lower_key, value);
Ok(())
}
pub fn set_text(&mut self, key: &str, text: String) -> Result<()> {
self.set(key, APEValue::text(text))
}
pub fn set_text_list(&mut self, key: &str, texts: Vec<String>) -> Result<()> {
self.set(key, APEValue::text(texts.join("\0")))
}
pub fn remove(&mut self, key: &str) -> Option<APEValue> {
let lower_key = key.to_lowercase();
self.case_map.remove(&lower_key);
self.items.remove(&lower_key)
}
pub fn keys(&self) -> Vec<String> {
self.items
.keys()
.map(|k| self.case_map.get(k).unwrap_or(k).clone())
.collect()
}
pub fn values(&self) -> Vec<&APEValue> {
self.items.values().collect()
}
pub fn items(&self) -> Vec<(String, &APEValue)> {
self.items
.iter()
.map(|(k, v)| (self.case_map.get(k).unwrap_or(k).clone(), v))
.collect()
}
pub fn contains_key(&self, key: &str) -> bool {
self.items.contains_key(&key.to_lowercase())
}
pub fn len(&self) -> usize {
self.items.len()
}
pub fn is_empty(&self) -> bool {
self.items.is_empty()
}
pub fn clear(&mut self) {
self.items.clear();
self.case_map.clear();
}
pub fn pprint(&self) -> String {
let mut lines: Vec<String> = self
.items()
.iter()
.map(|(k, v)| format!("{}={}", k, v.pprint()))
.collect();
lines.sort();
lines.join("\n")
}
pub fn add_cover_art(&mut self, key: &str, image_data: Vec<u8>) -> Result<()> {
self.set(key, APEValue::binary(image_data))
}
pub fn get_cover_art(&self, key: &str) -> Option<&[u8]> {
self.get(key).and_then(|value| {
if value.value_type == APEValueType::Binary {
Some(&value.data[..])
} else {
None
}
})
}
pub fn get_all_cover_art(&self) -> Vec<(String, &[u8])> {
self.items()
.into_iter()
.filter_map(|(key, value)| {
if key.to_lowercase().starts_with("cover art")
&& value.value_type == APEValueType::Binary
{
Some((key, &value.data[..]))
} else {
None
}
})
.collect()
}
pub fn remove_cover_art(&mut self, key: &str) -> Option<APEValue> {
self.remove(key)
}
pub fn clear_all_cover_art(&mut self) {
let cover_keys: Vec<String> = self
.keys()
.into_iter()
.filter(|k| k.to_lowercase().starts_with("cover art"))
.collect();
for key in cover_keys {
self.remove(&key);
}
}
}
#[derive(Debug)]
struct APEData {
start: Option<u64>,
header: Option<u64>,
data: Option<u64>,
footer: Option<u64>,
end: Option<u64>,
metadata: Option<u64>,
version: u32,
size: u32,
items: u32,
flags: u32,
is_at_start: bool,
tag_data: Option<Vec<u8>>,
}
impl APEData {
fn new() -> Self {
Self {
start: None,
header: None,
data: None,
footer: None,
end: None,
metadata: None,
version: 0,
size: 0,
items: 0,
flags: 0,
is_at_start: false,
tag_data: None,
}
}
fn find_metadata<R: Read + Seek>(&mut self, reader: &mut R) -> Result<()> {
if reader.seek(SeekFrom::End(-32)).is_ok() {
let mut buf = [0u8; 8];
if reader.read_exact(&mut buf).is_ok() && &buf == b"APETAGEX" {
reader.seek(SeekFrom::Current(-8))?;
self.footer = Some(reader.stream_position()?);
return Ok(());
}
}
if reader.seek(SeekFrom::End(-128)).is_ok() {
let mut tag_buf = [0u8; 3];
if reader.read_exact(&mut tag_buf).is_ok() && &tag_buf == b"TAG" {
reader.seek(SeekFrom::Current(-35))?; let mut ape_buf = [0u8; 8];
if reader.read_exact(&mut ape_buf).is_ok() && &ape_buf == b"APETAGEX" {
reader.seek(SeekFrom::Current(-8))?;
self.footer = Some(reader.stream_position()?);
return Ok(());
}
reader.seek(SeekFrom::Current(15))?;
let mut lyrics_buf = [0u8; 9];
if reader.read_exact(&mut lyrics_buf).is_ok() && &lyrics_buf == b"LYRICS200" {
reader.seek(SeekFrom::Current(-15))?;
let mut size_buf = [0u8; 6];
if reader.read_exact(&mut size_buf).is_ok() {
if let Ok(size_str) = std::str::from_utf8(&size_buf) {
if let Ok(offset) = size_str.parse::<i64>() {
let current = reader.stream_position()?;
let seek_delta = match (-32i64)
.checked_sub(offset)
.and_then(|v| v.checked_sub(6))
{
Some(d) => d,
None => {
return Err(AudexError::InvalidData(
"Lyrics3v2 seek offset overflow".into(),
));
}
};
let target = match (current as i64).checked_add(seek_delta) {
Some(t) => t,
None => {
return Err(AudexError::InvalidData(
"Lyrics3v2 seek target overflow".into(),
));
}
};
if offset > 0 && target >= 0 {
reader.seek(SeekFrom::Current(seek_delta))?;
let mut ape_buf = [0u8; 8];
if reader.read_exact(&mut ape_buf).is_ok()
&& &ape_buf == b"APETAGEX"
{
reader.seek(SeekFrom::Current(-8))?;
self.footer = Some(reader.stream_position()?);
return Ok(());
}
}
}
}
}
}
}
}
reader.seek(SeekFrom::Start(0))?;
let mut buf = [0u8; 8];
if reader.read_exact(&mut buf).is_ok() && &buf == b"APETAGEX" {
self.is_at_start = true;
self.header = Some(0);
}
Ok(())
}
fn fill_missing<R: Read + Seek>(&mut self, reader: &mut R) -> Result<()> {
self.metadata = if self.header.is_some() && self.footer.is_some() {
std::cmp::max(self.header, self.footer)
} else if self.header.is_some() {
self.header
} else {
self.footer
};
let metadata_pos = self.metadata.ok_or(AudexError::APENoHeader)?;
reader.seek(SeekFrom::Start(metadata_pos + 8))?;
let mut buf = [0u8; 16];
reader.read_exact(&mut buf)?;
self.version = u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]);
self.size = u32::from_le_bytes([buf[4], buf[5], buf[6], buf[7]]);
self.items = u32::from_le_bytes([buf[8], buf[9], buf[10], buf[11]]);
self.flags = u32::from_le_bytes([buf[12], buf[13], buf[14], buf[15]]);
if self.items > MAX_APE_ITEMS {
return Err(AudexError::InvalidData(format!(
"APEv2 item count {} exceeds maximum of {}",
self.items, MAX_APE_ITEMS
)));
}
if let Some(header) = self.header {
let data = header
.checked_add(32)
.ok_or_else(|| AudexError::InvalidData("APE header offset overflow".into()))?;
let end = data.checked_add(self.size as u64).ok_or_else(|| {
AudexError::InvalidData("APEv2 tag end offset overflow".to_string())
})?;
self.data = Some(data);
self.end = Some(end);
if end >= 32 {
reader.seek(SeekFrom::Start(end - 32))?;
let mut footer_buf = [0u8; 8];
if reader.read_exact(&mut footer_buf).is_ok() && &footer_buf == b"APETAGEX" {
self.footer = Some(end - 32);
}
}
} else if let Some(footer) = self.footer {
let end = footer + 32;
let tag_size = self.size as u64;
if tag_size > end {
return Err(AudexError::InvalidData(format!(
"APE footer claims {} bytes but only {} bytes precede it",
tag_size, end
)));
}
let data = end - tag_size;
self.end = Some(end);
self.data = Some(data);
if self.flags & HAS_HEADER != 0 {
if data < 32 {
return Err(AudexError::InvalidData(format!(
"APE header flag set but data starts at offset {} (need at least 32)",
data
)));
}
self.header = Some(data - 32);
} else {
self.header = self.data;
}
} else {
return Err(AudexError::APENoHeader);
}
if self.footer.is_some() {
if self.size < 32 {
return Err(AudexError::InvalidData(format!(
"APE tag size {} is smaller than the 32-byte footer",
self.size
)));
}
self.size -= 32;
}
Ok(())
}
fn fix_brokenness<R: Read + Seek>(&mut self, reader: &mut R) -> Result<()> {
let mut start = if let Some(header) = self.header {
header
} else if let Some(data) = self.data {
data
} else {
return Ok(());
};
reader.seek(SeekFrom::Start(start))?;
const MAX_SCAN_ITERATIONS: u32 = 1000;
let mut iterations = 0u32;
while start > 0 {
if iterations >= MAX_SCAN_ITERATIONS {
break;
}
iterations += 1;
if reader.seek(SeekFrom::Current(-24)).is_err() {
break;
}
let mut buf = [0u8; 8];
if reader.read_exact(&mut buf).is_ok() && &buf == b"APETAGEX" {
reader.seek(SeekFrom::Current(-8))?;
start = reader.stream_position()?;
} else {
break;
}
}
self.start = Some(start);
Ok(())
}
fn read_tag_data<R: Read + Seek>(
&mut self,
reader: &mut R,
limits: crate::limits::ParseLimits,
) -> Result<()> {
if let Some(data_pos) = self.data {
limits.check_tag_size(self.size as u64, "APEv2")?;
let stream_end = reader.seek(SeekFrom::End(0))?;
let available = stream_end.saturating_sub(data_pos);
if (self.size as u64) > available {
return Err(AudexError::ParseError(format!(
"APE tag size ({} bytes) exceeds remaining stream data ({} bytes)",
self.size, available
)));
}
reader.seek(SeekFrom::Start(data_pos))?;
let mut tag_data = vec![0u8; self.size as usize];
reader.read_exact(&mut tag_data)?;
self.tag_data = Some(tag_data);
}
Ok(())
}
}
#[cfg(feature = "async")]
#[derive(Debug)]
struct APEDataAsync {
start: Option<u64>,
header: Option<u64>,
data: Option<u64>,
footer: Option<u64>,
end: Option<u64>,
metadata: Option<u64>,
version: u32,
size: u32,
items: u32,
flags: u32,
is_at_start: bool,
tag_data: Option<Vec<u8>>,
}
#[cfg(feature = "async")]
impl APEDataAsync {
fn new() -> Self {
Self {
start: None,
header: None,
data: None,
footer: None,
end: None,
metadata: None,
version: 0,
size: 0,
items: 0,
flags: 0,
is_at_start: false,
tag_data: None,
}
}
async fn find_metadata(&mut self, file: &mut TokioFile) -> Result<()> {
if file.seek(SeekFrom::End(-32)).await.is_ok() {
let mut buf = [0u8; 8];
if file.read_exact(&mut buf).await.is_ok() && &buf == b"APETAGEX" {
file.seek(SeekFrom::Current(-8)).await?;
self.footer = Some(file.stream_position().await?);
return Ok(());
}
}
if file.seek(SeekFrom::End(-128)).await.is_ok() {
let mut tag_buf = [0u8; 3];
if file.read_exact(&mut tag_buf).await.is_ok() && &tag_buf == b"TAG" {
file.seek(SeekFrom::Current(-35)).await?;
let mut ape_buf = [0u8; 8];
if file.read_exact(&mut ape_buf).await.is_ok() && &ape_buf == b"APETAGEX" {
file.seek(SeekFrom::Current(-8)).await?;
self.footer = Some(file.stream_position().await?);
return Ok(());
}
file.seek(SeekFrom::Current(15)).await?;
let mut lyrics_buf = [0u8; 9];
if file.read_exact(&mut lyrics_buf).await.is_ok() && &lyrics_buf == b"LYRICS200" {
file.seek(SeekFrom::Current(-15)).await?;
let mut size_buf = [0u8; 6];
if file.read_exact(&mut size_buf).await.is_ok() {
if let Ok(size_str) = std::str::from_utf8(&size_buf) {
if let Ok(offset) = size_str.parse::<i64>() {
let current = file.stream_position().await?;
let seek_delta = match (-32i64)
.checked_sub(offset)
.and_then(|v| v.checked_sub(6))
{
Some(d) => d,
None => {
return Err(AudexError::InvalidData(
"Lyrics3v2 seek offset overflow".into(),
));
}
};
let target = match (current as i64).checked_add(seek_delta) {
Some(t) => t,
None => {
return Err(AudexError::InvalidData(
"Lyrics3v2 seek target overflow".into(),
));
}
};
if offset > 0 && target >= 0 {
file.seek(SeekFrom::Current(seek_delta)).await?;
let mut ape_buf = [0u8; 8];
if file.read_exact(&mut ape_buf).await.is_ok()
&& &ape_buf == b"APETAGEX"
{
file.seek(SeekFrom::Current(-8)).await?;
self.footer = Some(file.stream_position().await?);
return Ok(());
}
}
}
}
}
}
}
}
file.seek(SeekFrom::Start(0)).await?;
let mut buf = [0u8; 8];
if file.read_exact(&mut buf).await.is_ok() && &buf == b"APETAGEX" {
self.is_at_start = true;
self.header = Some(0);
}
Ok(())
}
async fn fill_missing(&mut self, file: &mut TokioFile) -> Result<()> {
self.metadata = if self.header.is_some() && self.footer.is_some() {
std::cmp::max(self.header, self.footer)
} else if self.header.is_some() {
self.header
} else {
self.footer
};
let metadata_pos = self.metadata.ok_or(AudexError::APENoHeader)?;
file.seek(SeekFrom::Start(metadata_pos + 8)).await?;
let mut buf = [0u8; 16];
file.read_exact(&mut buf).await?;
self.version = u32::from_le_bytes([buf[0], buf[1], buf[2], buf[3]]);
self.size = u32::from_le_bytes([buf[4], buf[5], buf[6], buf[7]]);
self.items = u32::from_le_bytes([buf[8], buf[9], buf[10], buf[11]]);
self.flags = u32::from_le_bytes([buf[12], buf[13], buf[14], buf[15]]);
if self.items > MAX_APE_ITEMS {
return Err(AudexError::InvalidData(format!(
"APEv2 item count {} exceeds maximum of {}",
self.items, MAX_APE_ITEMS
)));
}
if let Some(header) = self.header {
let data = header
.checked_add(32)
.ok_or_else(|| AudexError::InvalidData("APE header offset overflow".into()))?;
let end = data.checked_add(self.size as u64).ok_or_else(|| {
AudexError::InvalidData("APEv2 tag end offset overflow".to_string())
})?;
self.data = Some(data);
self.end = Some(end);
if end >= 32 {
file.seek(SeekFrom::Start(end - 32)).await?;
let mut footer_buf = [0u8; 8];
if file.read_exact(&mut footer_buf).await.is_ok() && &footer_buf == b"APETAGEX" {
self.footer = Some(end - 32);
}
}
} else if let Some(footer) = self.footer {
let end = footer + 32;
let tag_size = self.size as u64;
if tag_size > end {
return Err(AudexError::InvalidData(format!(
"APE footer claims {} bytes but only {} bytes precede it",
tag_size, end
)));
}
let data = end - tag_size;
self.end = Some(end);
self.data = Some(data);
if self.flags & HAS_HEADER != 0 {
if data < 32 {
return Err(AudexError::InvalidData(format!(
"APE header flag set but data starts at offset {} (need at least 32)",
data
)));
}
self.header = Some(data - 32);
} else {
self.header = self.data;
}
} else {
return Err(AudexError::APENoHeader);
}
if self.footer.is_some() {
if self.size < 32 {
return Err(AudexError::InvalidData(format!(
"APE tag size {} is smaller than the 32-byte footer",
self.size
)));
}
self.size -= 32;
}
Ok(())
}
async fn fix_brokenness(&mut self, file: &mut TokioFile) -> Result<()> {
let mut start = if let Some(header) = self.header {
header
} else if let Some(data) = self.data {
data
} else {
return Ok(());
};
file.seek(SeekFrom::Start(start)).await?;
const MAX_SCAN_ITERATIONS: u32 = 1000;
let mut iterations = 0u32;
while start > 0 {
if iterations >= MAX_SCAN_ITERATIONS {
break;
}
iterations += 1;
if file.seek(SeekFrom::Current(-24)).await.is_err() {
break;
}
let mut buf = [0u8; 8];
if file.read_exact(&mut buf).await.is_ok() && &buf == b"APETAGEX" {
file.seek(SeekFrom::Current(-8)).await?;
start = file.stream_position().await?;
} else {
break;
}
}
self.start = Some(start);
Ok(())
}
async fn read_tag_data(
&mut self,
file: &mut TokioFile,
limits: crate::limits::ParseLimits,
) -> Result<()> {
if let Some(data_pos) = self.data {
limits.check_tag_size(self.size as u64, "APEv2 async")?;
let stream_end = file.seek(SeekFrom::End(0)).await?;
let available = stream_end.saturating_sub(data_pos);
if (self.size as u64) > available {
return Err(AudexError::ParseError(format!(
"APE tag size ({} bytes) exceeds remaining stream data ({} bytes)",
self.size, available
)));
}
file.seek(SeekFrom::Start(data_pos)).await?;
let mut tag_data = vec![0u8; self.size as usize];
file.read_exact(&mut tag_data).await?;
self.tag_data = Some(tag_data);
}
Ok(())
}
}
#[derive(Debug, Default)]
pub struct APEStreamInfo {
pub length: Option<Duration>,
pub bitrate: Option<u32>,
}
impl StreamInfo for APEStreamInfo {
fn length(&self) -> Option<Duration> {
self.length
}
fn bitrate(&self) -> Option<u32> {
self.bitrate
}
fn sample_rate(&self) -> Option<u32> {
None
}
fn channels(&self) -> Option<u16> {
None
}
fn bits_per_sample(&self) -> Option<u16> {
None
}
}
impl APEStreamInfo {
pub fn pprint(&self) -> String {
"Unknown format with APEv2 tag.".to_string()
}
}
#[derive(Debug)]
pub struct APEv2 {
pub tags: APEv2Tags,
pub info: APEStreamInfo,
pub filename: Option<String>,
}
impl APEv2 {
pub fn new() -> Self {
Self {
tags: APEv2Tags::new(),
info: APEStreamInfo::default(),
filename: None,
}
}
pub fn parse_tag(&mut self, tag_data: &[u8], item_count: u32) -> Result<()> {
let mut cursor = Cursor::new(tag_data);
let limits = crate::limits::ParseLimits::default();
let mut cumulative_size: u64 = 0;
for _item_index in 0..item_count {
let mut header = [0u8; 8];
if Read::read_exact(&mut cursor, &mut header).is_err() {
warn_event!(
declared = item_count,
parsed = _item_index,
"APEv2: tag header declared more items than data contains (data truncated)"
);
break;
}
let size = u32::from_le_bytes([header[0], header[1], header[2], header[3]]);
let flags = u32::from_le_bytes([header[4], header[5], header[6], header[7]]);
let kind = (flags & 6) >> 1;
let value_type = match kind {
0 => APEValueType::Text,
1 => APEValueType::Binary,
2 => APEValueType::External,
_ => {
return Err(AudexError::APEBadItem(
"value type must be 0, 1, or 2".to_string(),
));
}
};
let mut key_bytes = Vec::new();
loop {
let mut byte = [0u8; 1];
if Read::read_exact(&mut cursor, &mut byte).is_err() {
return Err(AudexError::APEBadItem("incomplete key".to_string()));
}
if byte[0] == 0 {
break;
}
key_bytes.push(byte[0]);
if key_bytes.len() > 255 {
return Err(AudexError::APEBadItem(
"key exceeds 255-byte spec limit".to_string(),
));
}
}
let key = String::from_utf8(key_bytes)
.map_err(|e| AudexError::APEBadItem(format!("invalid key encoding: {}", e)))?;
if !is_valid_apev2_key(&key) {
return Err(AudexError::APEBadItem(format!(
"{:?} is not a valid APEv2 key",
key
)));
}
if value_type == APEValueType::Binary {
limits.check_image_size(size as u64, "APEv2 binary item")?;
} else {
limits.check_tag_size(size as u64, "APEv2 item")?;
}
cumulative_size = cumulative_size.saturating_add(size as u64);
limits.check_tag_size(cumulative_size, "APEv2 cumulative items")?;
let remaining = tag_data.len() as u64 - cursor.position();
if (size as u64) > remaining {
return Err(AudexError::APEBadItem(format!(
"item claims {} bytes but only {} remain",
size, remaining
)));
}
let mut value_data = vec![0u8; size as usize];
if Read::read_exact(&mut cursor, &mut value_data).is_err() {
return Err(AudexError::APEBadItem("incomplete value data".to_string()));
}
let value = APEValue {
value_type,
data: value_data,
};
trace_event!(key = %key, size = size, "APEv2 item parsed");
self.tags.set(&key, value)?;
}
Ok(())
}
fn write_tag<W: Write>(&self, writer: &mut W) -> Result<u32> {
let mut tag_items = Vec::new();
for (key, value) in self.tags.items() {
let mut item_data = Vec::new();
let value_len = u32::try_from(value.data.len()).map_err(|_| {
AudexError::InvalidData("APEv2 item value too large (exceeds u32 max)".into())
})?;
item_data.extend_from_slice(&value_len.to_le_bytes());
let flags = (value.value_type as u32) << 1;
item_data.extend_from_slice(&flags.to_le_bytes());
item_data.extend_from_slice(key.as_bytes());
item_data.push(0);
item_data.extend_from_slice(&value.data);
tag_items.push(item_data);
}
tag_items.sort_by(|a, b| a.len().cmp(&b.len()).then_with(|| a.cmp(b)));
let num_items = u32::try_from(tag_items.len()).map_err(|_| {
AudexError::InvalidData("APEv2 item count exceeds u32 capacity".to_string())
})?;
let tag_data: Vec<u8> = tag_items.into_iter().flatten().collect();
let tag_size = u32::try_from(tag_data.len()).map_err(|_| {
AudexError::InvalidData("APEv2 tag data exceeds u32 capacity (4 GB limit)".to_string())
})?;
let size_with_footer = tag_size.checked_add(32).ok_or_else(|| {
AudexError::InvalidData(
"APEv2 tag size too large: adding footer exceeds u32 capacity".to_string(),
)
})?;
let total_size = tag_size.checked_add(64).ok_or_else(|| {
AudexError::InvalidData(
"APEv2 tag size too large: adding header and footer exceeds u32 capacity"
.to_string(),
)
})?;
writer.write_all(b"APETAGEX")?;
writer.write_all(&2000u32.to_le_bytes())?; writer.write_all(&size_with_footer.to_le_bytes())?; writer.write_all(&num_items.to_le_bytes())?; writer.write_all(&(HAS_HEADER | IS_HEADER).to_le_bytes())?; writer.write_all(&[0u8; 8])?;
writer.write_all(&tag_data)?;
writer.write_all(b"APETAGEX")?;
writer.write_all(&2000u32.to_le_bytes())?; writer.write_all(&size_with_footer.to_le_bytes())?; writer.write_all(&num_items.to_le_bytes())?; writer.write_all(&HAS_HEADER.to_le_bytes())?; writer.write_all(&[0u8; 8])?;
Ok(total_size) }
#[cfg(feature = "async")]
pub async fn load_async<P: AsRef<Path>>(path: P) -> Result<Self> {
let mut file = loadfile_read_async(&path).await?;
let mut ape = APEv2::new();
ape.filename = Some(path.as_ref().to_string_lossy().to_string());
let limits = crate::limits::ParseLimits::default();
let mut ape_data = APEDataAsync::new();
ape_data.find_metadata(&mut file).await?;
ape_data.fill_missing(&mut file).await?;
ape_data.fix_brokenness(&mut file).await?;
ape_data.read_tag_data(&mut file, limits).await?;
if let Some(tag_data) = &ape_data.tag_data {
ape.parse_tag(tag_data, ape_data.items)?;
} else {
return Err(AudexError::APENoHeader);
}
Ok(ape)
}
#[cfg(feature = "async")]
pub async fn save_async(&mut self) -> Result<()> {
let filename = self
.filename
.as_ref()
.ok_or(AudexError::InvalidData("No filename set".to_string()))?
.clone();
let mut file = loadfile_write_async(&filename).await?;
let mut ape_data = APEDataAsync::new();
ape_data.find_metadata(&mut file).await?;
if ape_data.footer.is_some() || ape_data.header.is_some() {
ape_data.fill_missing(&mut file).await?;
ape_data.fix_brokenness(&mut file).await?;
if let (Some(start), Some(end)) = (ape_data.start, ape_data.end) {
let old_tag_size = end - start;
resize_bytes_async(&mut file, old_tag_size, 0, start).await?;
}
}
file.seek(SeekFrom::End(0)).await?;
self.write_tag_async(&mut file).await?;
Ok(())
}
#[cfg(feature = "async")]
async fn write_tag_async(&self, file: &mut TokioFile) -> Result<u32> {
let mut tag_items = Vec::new();
for (key, value) in self.tags.items() {
let mut item_data = Vec::new();
let value_len = u32::try_from(value.data.len()).map_err(|_| {
AudexError::InvalidData("APEv2 item value too large (exceeds u32 max)".into())
})?;
item_data.extend_from_slice(&value_len.to_le_bytes());
let flags = (value.value_type as u32) << 1;
item_data.extend_from_slice(&flags.to_le_bytes());
item_data.extend_from_slice(key.as_bytes());
item_data.push(0);
item_data.extend_from_slice(&value.data);
tag_items.push(item_data);
}
tag_items.sort_by(|a, b| a.len().cmp(&b.len()).then_with(|| a.cmp(b)));
let num_items = u32::try_from(tag_items.len()).map_err(|_| {
AudexError::InvalidData("APEv2 item count exceeds u32 capacity".to_string())
})?;
let tag_data: Vec<u8> = tag_items.into_iter().flatten().collect();
let tag_size = u32::try_from(tag_data.len()).map_err(|_| {
AudexError::InvalidData("APEv2 tag data exceeds u32 capacity (4 GB limit)".to_string())
})?;
let size_with_footer = tag_size.checked_add(32).ok_or_else(|| {
AudexError::InvalidData(
"APEv2 tag size too large: adding footer exceeds u32 capacity".to_string(),
)
})?;
let total_size = tag_size.checked_add(64).ok_or_else(|| {
AudexError::InvalidData(
"APEv2 tag size too large: adding header and footer exceeds u32 capacity"
.to_string(),
)
})?;
file.write_all(b"APETAGEX").await?;
file.write_all(&2000u32.to_le_bytes()).await?; file.write_all(&size_with_footer.to_le_bytes()).await?; file.write_all(&num_items.to_le_bytes()).await?; file.write_all(&(HAS_HEADER | IS_HEADER).to_le_bytes())
.await?; file.write_all(&[0u8; 8]).await?;
file.write_all(&tag_data).await?;
file.write_all(b"APETAGEX").await?;
file.write_all(&2000u32.to_le_bytes()).await?; file.write_all(&size_with_footer.to_le_bytes()).await?; file.write_all(&num_items.to_le_bytes()).await?; file.write_all(&HAS_HEADER.to_le_bytes()).await?; file.write_all(&[0u8; 8]).await?;
file.flush().await?;
Ok(total_size) }
#[cfg(feature = "async")]
pub async fn clear_async(&mut self) -> Result<()> {
let filename = self
.filename
.as_ref()
.ok_or(AudexError::InvalidData("No filename set".to_string()))?
.clone();
let mut file = loadfile_write_async(&filename).await?;
let mut ape_data = APEDataAsync::new();
ape_data.find_metadata(&mut file).await?;
ape_data.fill_missing(&mut file).await?;
ape_data.fix_brokenness(&mut file).await?;
if let (Some(start), Some(end)) = (ape_data.start, ape_data.end) {
let old_tag_size = end - start;
resize_bytes_async(&mut file, old_tag_size, 0, start).await?;
}
self.tags.clear();
Ok(())
}
#[cfg(feature = "async")]
pub async fn delete_async<P: AsRef<Path>>(path: P) -> Result<()> {
let mut file = loadfile_write_async(&path).await?;
let mut ape_data = APEDataAsync::new();
ape_data.find_metadata(&mut file).await?;
ape_data.fill_missing(&mut file).await?;
ape_data.fix_brokenness(&mut file).await?;
if let (Some(start), Some(end)) = (ape_data.start, ape_data.end) {
let old_tag_size = end - start;
resize_bytes_async(&mut file, old_tag_size, 0, start).await?;
}
Ok(())
}
}
impl Default for APEv2 {
fn default() -> Self {
Self::new()
}
}
impl APEv2 {
fn write_zeros(writer: &mut dyn ReadWriteSeek, count: u64) -> Result<()> {
const CHUNK_SIZE: usize = 64 * 1024; let buf = [0u8; CHUNK_SIZE];
let mut remaining = count;
while remaining > 0 {
let n = (remaining as usize).min(CHUNK_SIZE);
writer.write_all(&buf[..n])?;
remaining -= n as u64;
}
Ok(())
}
fn save_to_writer_inner(&mut self, mut writer: &mut dyn ReadWriteSeek) -> Result<()> {
let mut ape_data = APEData::new();
ape_data.find_metadata(&mut writer)?;
if ape_data.footer.is_some() || ape_data.header.is_some() {
ape_data.fill_missing(&mut writer)?;
ape_data.fix_brokenness(&mut writer)?;
if let (Some(start), Some(end)) = (ape_data.start, ape_data.end) {
let file_size = writer.seek(SeekFrom::End(0))?;
if end >= file_size {
writer.seek(SeekFrom::Start(start))?;
self.write_tag(&mut writer)?;
let new_end = writer.stream_position()?;
if new_end < file_size {
Self::write_zeros(writer, file_size - new_end)?;
writer.seek(SeekFrom::Start(new_end))?;
}
return Ok(());
} else {
let old_tag_size = end - start;
let trailing = file_size - end;
crate::util::move_bytes(&mut writer, start, end, trailing, None)?;
let logical_end = file_size - old_tag_size;
writer.seek(SeekFrom::Start(logical_end))?;
Self::write_zeros(writer, old_tag_size)?;
writer.seek(SeekFrom::Start(logical_end))?;
self.write_tag(&mut writer)?;
return Ok(());
}
}
}
writer.seek(SeekFrom::End(0))?;
self.write_tag(&mut writer)?;
Ok(())
}
fn clear_writer_inner(&mut self, mut writer: &mut dyn ReadWriteSeek) -> Result<()> {
let mut ape_data = APEData::new();
ape_data.find_metadata(&mut writer)?;
ape_data.fill_missing(&mut writer)?;
ape_data.fix_brokenness(&mut writer)?;
if let (Some(start), Some(end)) = (ape_data.start, ape_data.end) {
let file_size = writer.seek(SeekFrom::End(0))?;
if end >= file_size {
writer.seek(SeekFrom::Start(start))?;
Self::write_zeros(writer, file_size - start)?;
writer.seek(SeekFrom::Start(start))?;
} else {
let old_tag_size = end - start;
let trailing = file_size - end;
crate::util::move_bytes(&mut writer, start, end, trailing, None)?;
let logical_end = file_size - old_tag_size;
writer.seek(SeekFrom::Start(logical_end))?;
Self::write_zeros(writer, old_tag_size)?;
writer.seek(SeekFrom::Start(logical_end))?;
}
}
self.tags.clear();
Ok(())
}
}
impl FileType for APEv2 {
type Tags = APEv2Tags;
type Info = APEStreamInfo;
fn format_id() -> &'static str {
"APEv2"
}
fn load<P: AsRef<Path>>(path: P) -> Result<Self> {
debug_event!("parsing APEv2 tags");
let mut file = std::fs::File::open(&path)?;
let mut ape = APEv2::new();
ape.filename = Some(path.as_ref().to_string_lossy().to_string());
let limits = crate::limits::ParseLimits::default();
let mut ape_data = APEData::new();
ape_data.find_metadata(&mut file)?;
ape_data.fill_missing(&mut file)?;
ape_data.fix_brokenness(&mut file)?;
ape_data.read_tag_data(&mut file, limits)?;
trace_event!(item_count = ape_data.items, "APEv2 header info");
if let Some(tag_data) = &ape_data.tag_data {
ape.parse_tag(tag_data, ape_data.items)?;
} else {
return Err(AudexError::APENoHeader);
}
Ok(ape)
}
fn load_from_reader(reader: &mut dyn crate::ReadSeek) -> Result<Self> {
debug_event!("parsing APEv2 tags from reader");
let mut ape = APEv2::new();
let limits = crate::limits::ParseLimits::default();
let mut reader = reader;
let mut ape_data = APEData::new();
ape_data.find_metadata(&mut reader)?;
ape_data.fill_missing(&mut reader)?;
ape_data.fix_brokenness(&mut reader)?;
ape_data.read_tag_data(&mut reader, limits)?;
trace_event!(item_count = ape_data.items, "APEv2 header info");
if let Some(tag_data) = &ape_data.tag_data {
ape.parse_tag(tag_data, ape_data.items)?;
} else {
return Err(AudexError::APENoHeader);
}
Ok(ape)
}
fn save(&mut self) -> Result<()> {
debug_event!("saving APEv2 tags");
let filename = self
.filename
.as_ref()
.ok_or(AudexError::InvalidData("No filename set".to_string()))?;
let mut file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(filename)?;
let mut ape_data = APEData::new();
ape_data.find_metadata(&mut file)?;
if ape_data.footer.is_some() || ape_data.header.is_some() {
ape_data.fill_missing(&mut file)?;
ape_data.fix_brokenness(&mut file)?;
if let (Some(start), Some(end)) = (ape_data.start, ape_data.end) {
let file_size = file.seek(SeekFrom::End(0))?;
if end >= file_size {
file.set_len(start)?;
} else {
let old_tag_size = end - start;
crate::util::resize_bytes(&mut file, old_tag_size, 0, start)?;
}
}
}
file.seek(SeekFrom::End(0))?;
trace_event!(item_count = self.items().len(), "writing APEv2 tag items");
self.write_tag(&mut file)?;
Ok(())
}
fn clear(&mut self) -> Result<()> {
let filename = self
.filename
.as_ref()
.ok_or(AudexError::InvalidData("No filename set".to_string()))?;
let mut file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(filename)?;
let mut ape_data = APEData::new();
ape_data.find_metadata(&mut file)?;
ape_data.fill_missing(&mut file)?;
ape_data.fix_brokenness(&mut file)?;
if let (Some(start), Some(end)) = (ape_data.start, ape_data.end) {
let file_size = file.seek(SeekFrom::End(0))?;
if end >= file_size {
file.set_len(start)?;
} else {
let old_tag_size = end - start;
crate::util::resize_bytes(&mut file, old_tag_size, 0, start)?;
}
}
self.tags.clear();
Ok(())
}
fn save_to_writer(&mut self, writer: &mut dyn ReadWriteSeek) -> Result<()> {
self.save_to_writer_inner(writer)
}
fn clear_writer(&mut self, writer: &mut dyn ReadWriteSeek) -> Result<()> {
self.clear_writer_inner(writer)
}
fn save_to_path(&mut self, path: &Path) -> Result<()> {
let mut file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(path)?;
let mut ape_data = APEData::new();
ape_data.find_metadata(&mut file)?;
if ape_data.footer.is_some() || ape_data.header.is_some() {
ape_data.fill_missing(&mut file)?;
ape_data.fix_brokenness(&mut file)?;
if let (Some(start), Some(end)) = (ape_data.start, ape_data.end) {
let file_size = file.seek(SeekFrom::End(0))?;
if end >= file_size {
file.set_len(start)?;
} else {
let old_tag_size = end - start;
crate::util::resize_bytes(&mut file, old_tag_size, 0, start)?;
}
}
}
file.seek(SeekFrom::End(0))?;
self.write_tag(&mut file)?;
Ok(())
}
fn add_tags(&mut self) -> Result<()> {
Err(AudexError::InvalidOperation(
"Tags already exist".to_string(),
))
}
fn tags(&self) -> Option<&Self::Tags> {
Some(&self.tags)
}
fn tags_mut(&mut self) -> Option<&mut Self::Tags> {
Some(&mut self.tags)
}
fn info(&self) -> &Self::Info {
&self.info
}
fn score(_filename: &str, header: &[u8]) -> i32 {
let mut score = 0;
if header.len() >= 8 && &header[0..8] == b"APETAGEX" {
score += 10;
}
score
}
fn mime_types() -> &'static [&'static str] {
&["application/x-ape", "audio/x-ape"]
}
}
pub fn is_valid_apev2_key(key: &str) -> bool {
if key.len() < 2 || key.len() > 255 {
return false;
}
for ch in key.chars() {
if (ch as u32) < 0x20 || (ch as u32) > 0x7E {
return false;
}
}
let forbidden = ["OggS", "TAG", "ID3", "MP+"];
!forbidden.iter().any(|f| f.eq_ignore_ascii_case(key))
}
pub fn clear<P: AsRef<Path>>(path: P) -> Result<()> {
match APEv2::load(&path) {
Ok(mut ape) => ape.clear(),
Err(AudexError::APENoHeader) => Ok(()), Err(e) => Err(e),
}
}
#[cfg(feature = "async")]
pub async fn clear_async<P: AsRef<Path>>(path: P) -> Result<()> {
match APEv2::load_async(&path).await {
Ok(mut ape) => ape.clear_async().await,
Err(AudexError::APENoHeader) => Ok(()), Err(e) => Err(e),
}
}
#[cfg(feature = "async")]
pub async fn open_async<P: AsRef<Path>>(path: P) -> Result<APEv2> {
APEv2::load_async(path).await
}