use crate::limits::ParseLimits;
use crate::tags::{Metadata, Tags};
use crate::{AudexError, Result, VERSION_STRING};
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use std::collections::HashMap;
use std::io::{Cursor, Read, Write};
use std::ops::Index;
use std::slice::SliceIndex;
#[cfg(feature = "async")]
use tokio::io::{AsyncRead, AsyncReadExt, AsyncSeekExt, AsyncWrite, AsyncWriteExt};
use crate::flac::Picture;
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum VorbisError {
UnsetFrameError,
EncodingError(String),
InvalidKey(String),
}
impl std::fmt::Display for VorbisError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
VorbisError::UnsetFrameError => write!(f, "framing bit was not set"),
VorbisError::EncodingError(msg) => write!(f, "encoding error: {}", msg),
VorbisError::InvalidKey(key) => write!(f, "invalid vorbis key: {}", key),
}
}
}
impl std::error::Error for VorbisError {}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
#[non_exhaustive]
pub enum ErrorMode {
#[default]
Strict,
Replace,
Ignore,
}
impl std::str::FromStr for ErrorMode {
type Err = AudexError;
fn from_str(s: &str) -> Result<Self> {
match s {
"strict" => Ok(ErrorMode::Strict),
"replace" => Ok(ErrorMode::Replace),
"ignore" => Ok(ErrorMode::Ignore),
_ => Err(AudexError::InvalidData(format!(
"invalid error mode: {}",
s
))),
}
}
}
pub fn is_valid_key(key: &str) -> bool {
if key.is_empty() {
return false;
}
for c in key.chars() {
let code = c as u32;
if !(0x20..=0x7D).contains(&code) || c == '=' {
return false;
}
}
true
}
#[derive(Debug, Clone, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct VComment {
pub vendor: String,
pub data: Vec<(String, String)>,
tags: HashMap<String, Vec<String>>,
framing: bool,
}
impl VComment {
pub fn new() -> Self {
Self {
vendor: format!("Audex {}", VERSION_STRING),
data: Vec::new(),
tags: HashMap::new(),
framing: true,
}
}
pub fn with_vendor(vendor: String) -> Self {
Self {
vendor,
data: Vec::new(),
tags: HashMap::new(),
framing: true,
}
}
pub fn load<R: Read>(
&mut self,
reader: &mut R,
errors: ErrorMode,
framing: bool,
) -> Result<()> {
self.framing = framing;
self.data.clear();
self.tags.clear();
trace_event!("loading Vorbis Comment data");
let limits = ParseLimits::default();
let vendor_length = reader
.read_u32::<LittleEndian>()
.map_err(|e| AudexError::InvalidData(format!("failed to read vendor length: {}", e)))?;
const MAX_VENDOR_LENGTH: u32 = 1_000_000;
if vendor_length > MAX_VENDOR_LENGTH {
return Err(AudexError::InvalidData(format!(
"Vorbis vendor length {} exceeds maximum {}",
vendor_length, MAX_VENDOR_LENGTH
)));
}
let mut cumulative_bytes: u64 = 4;
cumulative_bytes = cumulative_bytes.saturating_add(vendor_length as u64);
limits.check_tag_size(cumulative_bytes, "Vorbis comment data")?;
let mut vendor_bytes = vec![0u8; vendor_length as usize];
reader
.read_exact(&mut vendor_bytes)
.map_err(|e| AudexError::InvalidData(format!("failed to read vendor string: {}", e)))?;
self.vendor = Self::decode_string(vendor_bytes, errors)?;
trace_event!(vendor = %self.vendor, "Vorbis Comment vendor string");
let comment_count = reader
.read_u32::<LittleEndian>()
.map_err(|e| AudexError::InvalidData(format!("failed to read comment count: {}", e)))?;
trace_event!(comment_count = comment_count, "Vorbis Comment entry count");
const MAX_COMMENT_COUNT: u32 = 10_000;
if comment_count > MAX_COMMENT_COUNT {
return Err(AudexError::InvalidData(format!(
"Vorbis comment count {} exceeds maximum {}",
comment_count, MAX_COMMENT_COUNT
)));
}
let mut unknown_counter: u32 = 0;
cumulative_bytes = cumulative_bytes.saturating_add(4);
limits.check_tag_size(cumulative_bytes, "Vorbis comment data")?;
for _ in 0..comment_count {
let comment_length = reader.read_u32::<LittleEndian>().map_err(|e| {
AudexError::InvalidData(format!("failed to read comment length: {}", e))
})?;
const MAX_COMMENT_LENGTH: u32 = 10_000_000;
if comment_length > MAX_COMMENT_LENGTH {
return Err(AudexError::InvalidData(format!(
"Vorbis comment length {} exceeds maximum {}",
comment_length, MAX_COMMENT_LENGTH
)));
}
cumulative_bytes = cumulative_bytes
.saturating_add(4)
.saturating_add(comment_length as u64);
limits.check_tag_size(cumulative_bytes, "Vorbis comment data")?;
let mut comment_bytes = vec![0u8; comment_length as usize];
reader.read_exact(&mut comment_bytes).map_err(|e| {
AudexError::InvalidData(format!("failed to read comment data: {}", e))
})?;
let comment_string = Self::decode_string(comment_bytes, errors)?;
if let Some((key, value)) = comment_string.split_once('=') {
let normalized_key = key.to_lowercase();
if is_valid_key(&normalized_key) {
trace_event!(key = %normalized_key, "Vorbis Comment entry parsed");
let value_string = value.to_string();
self.data.push((key.to_string(), value_string.clone()));
self.tags
.entry(normalized_key)
.or_default()
.push(value_string);
} else if errors == ErrorMode::Strict {
return Err(AudexError::InvalidData(format!(
"invalid vorbis key: {}",
key
)));
} else if errors == ErrorMode::Replace {
let sanitize = |s: &str| -> String {
s.chars()
.map(|c| {
let code = c as u32;
if (0x20..=0x7D).contains(&code) && c != '=' {
c
} else {
'?'
}
})
.collect()
};
let sanitized_key = sanitize(key);
let sanitized_normalized = sanitize(&normalized_key);
let value_string = value.to_string();
self.data.push((sanitized_key, value_string.clone()));
self.tags
.entry(sanitized_normalized)
.or_default()
.push(value_string);
}
} else if errors == ErrorMode::Strict {
return Err(AudexError::InvalidData(format!(
"comment missing '=': {}",
comment_string
)));
} else if errors == ErrorMode::Replace {
let key = format!("unknown{}", unknown_counter);
unknown_counter += 1;
self.data.push((key.clone(), comment_string.to_string()));
self.tags
.entry(key)
.or_default()
.push(comment_string.to_string());
}
}
if framing {
let mut framing_byte = [0u8];
match reader.read_exact(&mut framing_byte) {
Ok(_) => {
if framing_byte[0] & 1 == 0 {
return Err(AudexError::FormatError(Box::new(
VorbisError::UnsetFrameError,
)));
}
}
Err(_) if errors != ErrorMode::Strict => {
}
Err(_) => {
return Err(AudexError::FormatError(Box::new(
VorbisError::UnsetFrameError,
)));
}
}
}
Ok(())
}
pub fn write<W: Write>(&self, writer: &mut W, framing: Option<bool>) -> Result<()> {
debug_event!("saving Vorbis Comments");
self.validate()?;
let use_framing = framing.unwrap_or(self.framing);
trace_event!(
comment_count = self.data.len(),
framing = use_framing,
"writing Vorbis Comment fields"
);
let vendor_bytes = self.vendor.as_bytes();
let vendor_len = u32::try_from(vendor_bytes.len()).map_err(|_| {
AudexError::InvalidData(
"vendor string too large for Vorbis comment format (exceeds u32 max)".into(),
)
})?;
writer.write_u32::<LittleEndian>(vendor_len).map_err(|e| {
AudexError::InvalidData(format!("failed to write vendor length: {}", e))
})?;
writer.write_all(vendor_bytes).map_err(|e| {
AudexError::InvalidData(format!("failed to write vendor string: {}", e))
})?;
let comment_count = u32::try_from(self.data.len()).map_err(|_| {
AudexError::InvalidData(
"too many comments for Vorbis comment format (exceeds u32 max)".into(),
)
})?;
writer
.write_u32::<LittleEndian>(comment_count)
.map_err(|e| {
AudexError::InvalidData(format!("failed to write comment count: {}", e))
})?;
for (key, value) in &self.data {
let comment = format!("{}={}", key, value);
let comment_bytes = comment.as_bytes();
let comment_len = u32::try_from(comment_bytes.len()).map_err(|_| {
AudexError::InvalidData(
"comment too large for Vorbis comment format (exceeds u32 max)".into(),
)
})?;
writer.write_u32::<LittleEndian>(comment_len).map_err(|e| {
AudexError::InvalidData(format!("failed to write comment length: {}", e))
})?;
writer.write_all(comment_bytes).map_err(|e| {
AudexError::InvalidData(format!("failed to write comment data: {}", e))
})?;
}
if use_framing {
writer.write_u8(1).map_err(|e| {
AudexError::InvalidData(format!("failed to write framing bit: {}", e))
})?;
}
Ok(())
}
pub fn validate(&self) -> Result<()> {
for (key, _) in &self.data {
if !is_valid_key(key) {
return Err(AudexError::FormatError(Box::new(VorbisError::InvalidKey(
key.clone(),
))));
}
}
Ok(())
}
pub fn clear(&mut self) {
self.data.clear();
self.tags.clear();
}
pub fn len(&self) -> usize {
self.data.len()
}
pub fn is_empty(&self) -> bool {
self.data.is_empty()
}
pub fn push(&mut self, key: String, value: String) -> Result<()> {
let normalized_key = key.to_lowercase();
if !is_valid_key(&normalized_key) {
return Err(crate::AudexError::InvalidData(format!(
"Invalid Vorbis comment key: {:?}",
key
)));
}
self.data.push((key, value.clone()));
self.tags.entry(normalized_key).or_default().push(value);
Ok(())
}
pub fn insert(&mut self, index: usize, key: String, value: String) -> Result<()> {
let normalized_key = key.to_lowercase();
if !is_valid_key(&normalized_key) {
return Err(crate::AudexError::InvalidData(format!(
"Invalid Vorbis comment key: {:?}",
key
)));
}
self.data.insert(index, (key, value.clone()));
self.rebuild_tags_map();
Ok(())
}
pub fn remove(&mut self, index: usize) -> Option<(String, String)> {
if index < self.data.len() {
let result = Some(self.data.remove(index));
self.rebuild_tags_map();
result
} else {
None
}
}
pub fn iter(&self) -> std::slice::Iter<'_, (String, String)> {
self.data.iter()
}
pub fn modify<F>(&mut self, f: F)
where
F: FnOnce(&mut Vec<(String, String)>),
{
f(&mut self.data);
self.rebuild_tags_map();
}
fn rebuild_tags_map(&mut self) {
self.tags.clear();
for (key, value) in &self.data {
self.tags
.entry(key.to_lowercase())
.or_default()
.push(value.clone());
}
}
fn decode_string(bytes: Vec<u8>, errors: ErrorMode) -> Result<String> {
match errors {
ErrorMode::Strict => String::from_utf8(bytes).map_err(|e| {
AudexError::FormatError(Box::new(VorbisError::EncodingError(e.to_string())))
}),
ErrorMode::Replace => Ok(String::from_utf8_lossy(&bytes).into_owned()),
ErrorMode::Ignore => {
let lossy = String::from_utf8_lossy(&bytes);
Ok(lossy.replace('\u{FFFD}', ""))
}
}
}
}
impl<I: SliceIndex<[(String, String)]>> Index<I> for VComment {
type Output = I::Output;
fn index(&self, index: I) -> &Self::Output {
&self.data[index]
}
}
impl IntoIterator for VComment {
type Item = (String, String);
type IntoIter = std::vec::IntoIter<(String, String)>;
fn into_iter(self) -> Self::IntoIter {
self.data.into_iter()
}
}
impl<'a> IntoIterator for &'a VComment {
type Item = &'a (String, String);
type IntoIter = std::slice::Iter<'a, (String, String)>;
fn into_iter(self) -> Self::IntoIter {
self.data.iter()
}
}
impl Tags for VComment {
fn get(&self, key: &str) -> Option<&[String]> {
let normalized_key = key.to_lowercase();
self.tags.get(&normalized_key).map(|v| v.as_slice())
}
fn set(&mut self, key: &str, values: Vec<String>) {
let normalized_key = key.to_lowercase();
if !is_valid_key(&normalized_key) {
warn_event!(key = %key, "ignored invalid Vorbis comment key in Tags::set");
return;
}
self.data
.retain(|(k, _)| k.to_lowercase() != normalized_key);
if values.is_empty() {
self.tags.remove(&normalized_key);
} else {
for value in &values {
self.data.push((key.to_string(), value.clone()));
}
self.tags.insert(normalized_key, values);
}
}
fn remove(&mut self, key: &str) {
let normalized_key = key.to_lowercase();
self.data
.retain(|(k, _)| k.to_lowercase() != normalized_key);
self.tags.remove(&normalized_key);
}
fn keys(&self) -> Vec<String> {
let mut keys: Vec<String> = self.tags.keys().cloned().collect();
keys.sort();
keys
}
fn pprint(&self) -> String {
let mut result = String::new();
for (key, value) in &self.data {
result.push_str(&format!("{}={}\n", key, value));
}
result
}
}
impl Metadata for VComment {
type Error = AudexError;
fn new() -> Self {
VComment::new()
}
fn load_from_fileobj(filething: &mut crate::util::AnyFileThing) -> Result<Self> {
let mut comment = VComment::new();
comment.load(filething, ErrorMode::Strict, true)?;
Ok(comment)
}
fn save_to_fileobj(&self, filething: &mut crate::util::AnyFileThing) -> Result<()> {
self.write(filething, None)
}
fn delete_from_fileobj(_filething: &mut crate::util::AnyFileThing) -> Result<()> {
Err(AudexError::NotImplementedMethod(
"delete_from_fileobj not implemented for VComment".to_string(),
))
}
}
#[derive(Debug, Clone, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct VCommentDict {
inner: VComment,
}
impl VCommentDict {
pub fn new() -> Self {
Self {
inner: VComment::new(),
}
}
pub fn with_framing(framing: bool) -> Self {
let mut inner = VComment::new();
inner.framing = framing;
Self { inner }
}
pub fn with_vendor(vendor: String) -> Self {
Self {
inner: VComment::with_vendor(vendor),
}
}
pub fn load<R: Read>(
&mut self,
reader: &mut R,
errors: ErrorMode,
framing: bool,
) -> Result<()> {
self.inner.load(reader, errors, framing)
}
pub fn write<W: Write>(&self, writer: &mut W, framing: Option<bool>) -> Result<()> {
self.inner.write(writer, framing)
}
pub fn vendor(&self) -> &str {
&self.inner.vendor
}
pub fn set_vendor(&mut self, vendor: String) {
self.inner.vendor = vendor;
}
pub fn get_values(&self, key: &str) -> Vec<String> {
let normalized_key = key.to_lowercase();
self.inner
.data
.iter()
.filter_map(|(k, v)| {
if k.to_lowercase() == normalized_key {
Some(v.clone())
} else {
None
}
})
.collect()
}
pub fn set(&mut self, key: &str, values: Vec<String>) {
self.inner.set(key, values);
}
pub fn get_first(&self, key: &str) -> Option<String> {
self.get_values(key).into_iter().next()
}
pub fn set_single(&mut self, key: &str, value: String) {
self.inner.set(key, vec![value]);
}
pub fn contains_key(&self, key: &str) -> bool {
let normalized_key = key.to_lowercase();
self.inner
.data
.iter()
.any(|(k, _)| k.to_lowercase() == normalized_key)
}
pub fn remove_key(&mut self, key: &str) {
let normalized_key = key.to_lowercase();
self.inner
.data
.retain(|(k, _)| k.to_lowercase() != normalized_key);
self.inner.tags.remove(&normalized_key);
}
pub fn keys(&self) -> Vec<String> {
self.inner.keys()
}
pub fn as_dict(&self) -> HashMap<String, Vec<String>> {
let mut dict = HashMap::new();
for key in self.keys() {
dict.insert(key.clone(), self.get_values(&key));
}
dict
}
pub fn clear(&mut self) {
self.inner.clear();
}
pub fn inner(&self) -> &VComment {
&self.inner
}
pub fn inner_mut(&mut self) -> &mut VComment {
&mut self.inner
}
pub fn add_picture(&mut self, picture: Picture) -> Result<()> {
let picture_data = picture.to_bytes()?;
let encoded = BASE64.encode(&picture_data);
let mut pictures = self.get_values("metadata_block_picture");
pictures.push(encoded);
self.set("metadata_block_picture", pictures);
Ok(())
}
pub fn get_pictures(&self) -> Vec<Picture> {
let mut result = Vec::new();
for b64_data in self.get_values("metadata_block_picture") {
let data = match BASE64.decode(b64_data.as_bytes()) {
Ok(d) => d,
Err(_) => continue, };
match Picture::from_bytes(&data) {
Ok(picture) => result.push(picture),
Err(_) => continue, }
}
result
}
pub fn clear_pictures(&mut self) {
self.remove_key("metadata_block_picture");
}
}
impl Tags for VCommentDict {
fn get(&self, key: &str) -> Option<&[String]> {
self.inner.get(key)
}
fn set(&mut self, key: &str, values: Vec<String>) {
self.inner.set(key, values);
}
fn remove(&mut self, key: &str) {
self.remove_key(key);
}
fn keys(&self) -> Vec<String> {
self.inner.keys()
}
fn pprint(&self) -> String {
self.inner.pprint()
}
}
impl Metadata for VCommentDict {
type Error = AudexError;
fn new() -> Self {
VCommentDict::new()
}
fn load_from_fileobj(filething: &mut crate::util::AnyFileThing) -> Result<Self> {
let mut dict = VCommentDict::new();
dict.load(filething, ErrorMode::Strict, true)?;
Ok(dict)
}
fn save_to_fileobj(&self, filething: &mut crate::util::AnyFileThing) -> Result<()> {
self.write(filething, None)
}
fn delete_from_fileobj(_filething: &mut crate::util::AnyFileThing) -> Result<()> {
Err(AudexError::NotImplementedMethod(
"delete_from_fileobj not implemented for VCommentDict".to_string(),
))
}
}
impl VComment {
pub fn from_bytes(data: &[u8]) -> Result<Self> {
let mut cursor = Cursor::new(data);
let mut comment = VComment::new();
comment.load(&mut cursor, ErrorMode::Strict, true)?;
Ok(comment)
}
pub fn from_bytes_with_options(data: &[u8], errors: ErrorMode, framing: bool) -> Result<Self> {
let mut cursor = Cursor::new(data);
let mut comment = VComment::new();
comment.load(&mut cursor, errors, framing)?;
Ok(comment)
}
pub fn to_bytes(&self) -> Result<Vec<u8>> {
let mut buffer = Vec::new();
self.write(&mut buffer, None)?;
Ok(buffer)
}
pub fn to_bytes_with_framing(&self, framing: bool) -> Result<Vec<u8>> {
let mut buffer = Vec::new();
self.write(&mut buffer, Some(framing))?;
Ok(buffer)
}
}
impl VCommentDict {
pub fn from_bytes(data: &[u8]) -> Result<Self> {
let mut cursor = Cursor::new(data);
let mut dict = VCommentDict::new();
dict.load(&mut cursor, ErrorMode::Strict, true)?;
Ok(dict)
}
pub fn from_bytes_with_options(data: &[u8], errors: ErrorMode, framing: bool) -> Result<Self> {
let mut cursor = Cursor::new(data);
let mut dict = VCommentDict::new();
dict.load(&mut cursor, errors, framing)?;
Ok(dict)
}
pub fn to_bytes(&self) -> Result<Vec<u8>> {
let mut buffer = Vec::new();
self.write(&mut buffer, None)?;
Ok(buffer)
}
pub fn get_replaygain(&self) -> crate::replaygain::ReplayGainInfo {
use crate::replaygain;
let mut map = std::collections::HashMap::new();
for key in self.keys() {
if let Some(values) = self.get(&key) {
map.insert(key, values.to_vec());
}
}
replaygain::from_vorbis_comments(&map)
}
pub fn set_replaygain(
&mut self,
info: &crate::replaygain::ReplayGainInfo,
) -> crate::Result<()> {
use crate::replaygain;
let mut map = std::collections::HashMap::new();
for key in self.keys() {
if let Some(values) = self.get(&key) {
map.insert(key, values.to_vec());
}
}
replaygain::to_vorbis_comments(info, &mut map)?;
for (key, values) in map {
self.set(&key, values);
}
Ok(())
}
pub fn clear_replaygain(&mut self) {
use crate::replaygain::vorbis_keys;
self.remove(vorbis_keys::TRACK_GAIN);
self.remove(vorbis_keys::TRACK_PEAK);
self.remove(vorbis_keys::ALBUM_GAIN);
self.remove(vorbis_keys::ALBUM_PEAK);
self.remove(vorbis_keys::REFERENCE_LOUDNESS);
}
}
#[cfg(feature = "async")]
impl VComment {
pub async fn load_async<R: AsyncRead + Unpin>(
&mut self,
reader: &mut R,
errors: ErrorMode,
framing: bool,
) -> Result<()> {
self.framing = framing;
self.data.clear();
self.tags.clear();
let limits = ParseLimits::default();
let mut len_buf = [0u8; 4];
reader
.read_exact(&mut len_buf)
.await
.map_err(|e| AudexError::InvalidData(format!("failed to read vendor length: {}", e)))?;
let vendor_length = u32::from_le_bytes(len_buf);
const MAX_VENDOR_LENGTH: u32 = 1_000_000;
if vendor_length > MAX_VENDOR_LENGTH {
return Err(AudexError::InvalidData(format!(
"Vorbis vendor length {} exceeds maximum {}",
vendor_length, MAX_VENDOR_LENGTH
)));
}
let mut cumulative_bytes: u64 = 4;
cumulative_bytes = cumulative_bytes.saturating_add(vendor_length as u64);
limits.check_tag_size(cumulative_bytes, "Vorbis comment data")?;
let mut vendor_bytes = vec![0u8; vendor_length as usize];
reader
.read_exact(&mut vendor_bytes)
.await
.map_err(|e| AudexError::InvalidData(format!("failed to read vendor string: {}", e)))?;
self.vendor = Self::decode_string(vendor_bytes, errors)?;
reader
.read_exact(&mut len_buf)
.await
.map_err(|e| AudexError::InvalidData(format!("failed to read comment count: {}", e)))?;
let comment_count = u32::from_le_bytes(len_buf);
const MAX_COMMENT_COUNT: u32 = 10_000;
if comment_count > MAX_COMMENT_COUNT {
return Err(AudexError::InvalidData(format!(
"Vorbis comment count {} exceeds maximum {}",
comment_count, MAX_COMMENT_COUNT
)));
}
let mut unknown_counter: u32 = 0;
cumulative_bytes = cumulative_bytes.saturating_add(4);
limits.check_tag_size(cumulative_bytes, "Vorbis comment data")?;
for _ in 0..comment_count {
reader.read_exact(&mut len_buf).await.map_err(|e| {
AudexError::InvalidData(format!("failed to read comment length: {}", e))
})?;
let comment_length = u32::from_le_bytes(len_buf);
const MAX_COMMENT_LENGTH: u32 = 10_000_000;
if comment_length > MAX_COMMENT_LENGTH {
return Err(AudexError::InvalidData(format!(
"Vorbis comment length {} exceeds maximum {}",
comment_length, MAX_COMMENT_LENGTH
)));
}
cumulative_bytes = cumulative_bytes
.saturating_add(4)
.saturating_add(comment_length as u64);
limits.check_tag_size(cumulative_bytes, "Vorbis comment data")?;
let mut comment_bytes = vec![0u8; comment_length as usize];
reader.read_exact(&mut comment_bytes).await.map_err(|e| {
AudexError::InvalidData(format!("failed to read comment data: {}", e))
})?;
let comment_string = Self::decode_string(comment_bytes, errors)?;
if let Some((key, value)) = comment_string.split_once('=') {
let normalized_key = key.to_lowercase();
if is_valid_key(&normalized_key) {
let value_string = value.to_string();
self.data.push((key.to_string(), value_string.clone()));
self.tags
.entry(normalized_key)
.or_default()
.push(value_string);
} else if errors == ErrorMode::Strict {
return Err(AudexError::InvalidData(format!(
"invalid vorbis key: {}",
key
)));
} else if errors == ErrorMode::Replace {
let sanitize = |s: &str| -> String {
s.chars()
.map(|c| {
let code = c as u32;
if (0x20..=0x7D).contains(&code) && c != '=' {
c
} else {
'?'
}
})
.collect()
};
let sanitized_key = sanitize(&normalized_key);
let value_string = value.to_string();
self.data
.push((sanitized_key.clone(), value_string.clone()));
self.tags
.entry(sanitized_key)
.or_default()
.push(value_string);
}
} else if errors == ErrorMode::Strict {
return Err(AudexError::InvalidData(format!(
"comment missing '=': {}",
comment_string
)));
} else if errors == ErrorMode::Replace {
let key = format!("unknown{}", unknown_counter);
unknown_counter += 1;
self.data.push((key.clone(), comment_string.to_string()));
self.tags
.entry(key)
.or_default()
.push(comment_string.to_string());
}
}
if framing {
let mut framing_byte = [0u8; 1];
match reader.read_exact(&mut framing_byte).await {
Ok(_) => {
if framing_byte[0] & 1 == 0 {
return Err(AudexError::FormatError(Box::new(
VorbisError::UnsetFrameError,
)));
}
}
Err(_) if errors != ErrorMode::Strict => {
}
Err(_) => {
return Err(AudexError::FormatError(Box::new(
VorbisError::UnsetFrameError,
)));
}
}
}
Ok(())
}
pub async fn write_async<W: AsyncWrite + Unpin>(
&self,
writer: &mut W,
framing: Option<bool>,
) -> Result<()> {
self.validate()?;
let use_framing = framing.unwrap_or(self.framing);
let vendor_bytes = self.vendor.as_bytes();
writer
.write_all(&(vendor_bytes.len() as u32).to_le_bytes())
.await
.map_err(|e| {
AudexError::InvalidData(format!("failed to write vendor length: {}", e))
})?;
writer.write_all(vendor_bytes).await.map_err(|e| {
AudexError::InvalidData(format!("failed to write vendor string: {}", e))
})?;
writer
.write_all(&(self.data.len() as u32).to_le_bytes())
.await
.map_err(|e| {
AudexError::InvalidData(format!("failed to write comment count: {}", e))
})?;
for (key, value) in &self.data {
let comment = format!("{}={}", key, value);
let comment_bytes = comment.as_bytes();
writer
.write_all(&(comment_bytes.len() as u32).to_le_bytes())
.await
.map_err(|e| {
AudexError::InvalidData(format!("failed to write comment length: {}", e))
})?;
writer.write_all(comment_bytes).await.map_err(|e| {
AudexError::InvalidData(format!("failed to write comment data: {}", e))
})?;
}
if use_framing {
writer.write_all(&[1u8]).await.map_err(|e| {
AudexError::InvalidData(format!("failed to write framing bit: {}", e))
})?;
}
Ok(())
}
}
#[cfg(feature = "async")]
impl VCommentDict {
pub async fn load_async<R: AsyncRead + Unpin>(
&mut self,
reader: &mut R,
errors: ErrorMode,
framing: bool,
) -> Result<()> {
self.inner.load_async(reader, errors, framing).await
}
pub async fn write_async<W: AsyncWrite + Unpin>(
&self,
writer: &mut W,
framing: Option<bool>,
) -> Result<()> {
self.inner.write_async(writer, framing).await
}
pub async fn from_bytes_async(data: &[u8]) -> Result<Self> {
let mut reader = data;
let mut dict = VCommentDict::new();
dict.load_async(&mut reader, ErrorMode::Strict, true)
.await?;
Ok(dict)
}
pub async fn from_bytes_with_options_async(
data: &[u8],
errors: ErrorMode,
framing: bool,
) -> Result<Self> {
let mut reader = data;
let mut dict = VCommentDict::new();
dict.load_async(&mut reader, errors, framing).await?;
Ok(dict)
}
}
#[cfg(feature = "async")]
#[derive(Debug, Clone)]
pub struct OggVCommentDictAsync {
inner: VCommentDict,
}
#[cfg(feature = "async")]
impl Default for OggVCommentDictAsync {
fn default() -> Self {
Self::new()
}
}
#[cfg(feature = "async")]
impl OggVCommentDictAsync {
pub fn new() -> Self {
Self {
inner: VCommentDict::new(),
}
}
pub async fn from_fileobj_async<R: AsyncRead + tokio::io::AsyncSeek + Unpin>(
fileobj: &mut R,
serial: u32,
) -> Result<Self> {
use crate::ogg::OggPage;
let mut pages = Vec::new();
let mut complete = false;
const MAX_PAGE_SEARCH: usize = 1024;
let mut pages_read = 0usize;
while !complete {
let page = OggPage::from_reader_async(fileobj).await?;
pages_read += 1;
if pages_read > MAX_PAGE_SEARCH {
return Err(AudexError::InvalidData(
"Too many OGG pages while searching for comment packet".to_string(),
));
}
if page.serial == serial {
pages.push(page.clone());
complete = page.is_complete() || page.packets.len() > 1;
}
}
let packets = OggPage::to_packets(&pages, false)?;
if packets.is_empty() || packets[0].len() < 7 {
return Err(AudexError::InvalidData(
"Invalid Vorbis comment packet".to_string(),
));
}
let data = &packets[0][7..];
let inner = VCommentDict::from_bytes_with_options(data, ErrorMode::Replace, true)?;
Ok(Self { inner })
}
pub async fn inject_async(
&self,
fileobj: &mut tokio::fs::File,
padding_func: Option<fn(&crate::tags::PaddingInfo) -> i64>,
) -> Result<()> {
use crate::ogg::OggPage;
fileobj.seek(std::io::SeekFrom::Start(0)).await?;
const MAX_PAGE_SEARCH: usize = 1024;
let mut pages_read: usize = 0;
let mut page = OggPage::from_reader_async(fileobj).await?;
pages_read += 1;
while !page
.packets
.first()
.is_some_and(|p| p.starts_with(b"\x03vorbis"))
{
pages_read += 1;
if pages_read > MAX_PAGE_SEARCH {
return Err(AudexError::InvalidData(
"Vorbis comment header not found within page limit".to_string(),
));
}
page = OggPage::from_reader_async(fileobj).await?;
}
let mut old_pages = vec![page];
while {
let last = old_pages.last().ok_or_else(|| {
AudexError::InvalidData("No Ogg pages collected for comment packet".to_string())
})?;
!(last.is_complete() || last.packets.len() > 1)
} {
pages_read += 1;
if pages_read > MAX_PAGE_SEARCH {
return Err(AudexError::InvalidData(
"Too many pages while collecting comment packet".to_string(),
));
}
let page = OggPage::from_reader_async(fileobj).await?;
if page.serial == old_pages[0].serial {
old_pages.push(page);
}
}
let packets = OggPage::to_packets(&old_pages, false)?;
if packets.is_empty() {
return Err(AudexError::InvalidData("No packets found".to_string()));
}
let content_size = {
let old_pos = fileobj.stream_position().await?;
let file_size = fileobj.seek(std::io::SeekFrom::End(0)).await?;
fileobj.seek(std::io::SeekFrom::Start(old_pos)).await?;
file_size as i64 - packets[0].len() as i64
};
let vcomment_data = {
let mut data = b"\x03vorbis".to_vec();
let mut vcomment_bytes = Vec::new();
let mut comment_to_write = self.inner.clone();
if !comment_to_write.keys().is_empty() {
comment_to_write.set_vendor(format!("Audex {}", VERSION_STRING));
}
comment_to_write.write(&mut vcomment_bytes, Some(true))?;
data.extend_from_slice(&vcomment_bytes);
data
};
let padding_left = packets[0].len() as i64 - vcomment_data.len() as i64;
let info = crate::tags::PaddingInfo::new(padding_left, content_size);
let new_padding = info.get_padding_with(padding_func);
let mut new_packets = packets;
new_packets[0] = vcomment_data;
if new_padding > 0 {
new_packets[0].extend_from_slice(&vec![0u8; usize::try_from(new_padding).unwrap_or(0)]);
}
let new_pages = OggPage::from_packets_try_preserve(new_packets.clone(), &old_pages);
let final_pages = if new_pages.is_empty() {
if old_pages.is_empty() {
return Err(AudexError::InvalidData(
"No Ogg pages found for Vorbis comment stream".to_string(),
));
}
let first_sequence = old_pages[0].sequence;
let original_granule = old_pages
.last()
.expect("old_pages confirmed non-empty")
.position as u64;
OggPage::from_packets_with_options(
new_packets,
first_sequence,
4096,
2048,
original_granule,
)?
} else {
new_pages
};
if final_pages.is_empty() {
return Err(AudexError::InvalidData(
"Failed to create new OGG pages".to_string(),
));
}
OggPage::replace_async(fileobj, &old_pages, final_pages).await?;
Ok(())
}
}
#[cfg(feature = "async")]
impl std::ops::Deref for OggVCommentDictAsync {
type Target = VCommentDict;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
#[cfg(feature = "async")]
impl std::ops::DerefMut for OggVCommentDictAsync {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.inner
}
}
#[cfg(feature = "async")]
impl Tags for OggVCommentDictAsync {
fn get(&self, key: &str) -> Option<&[String]> {
self.inner.get(key)
}
fn set(&mut self, key: &str, values: Vec<String>) {
self.inner.set(key, values)
}
fn remove(&mut self, key: &str) {
self.inner.remove(key)
}
fn keys(&self) -> Vec<String> {
self.inner.keys()
}
fn pprint(&self) -> String {
self.inner.pprint()
}
}