Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ members = [
"lofty",
"lofty_attr",
"ogg_pager",
"aud_io",
"fuzz",
"examples/custom_resolver",
]
Expand All @@ -14,13 +15,16 @@ edition = "2024"
rust-version = "1.85"
repository = "https://github.com/Serial-ATA/lofty-rs"
license = "MIT OR Apache-2.0"
authors = ["Serial <69764315+Serial-ATA@users.noreply.github.com>"]

[workspace.dependencies]
lofty = { version = "0.22.4", path = "lofty" }
lofty_attr = { version = "0.11.1", path = "lofty_attr" }
ogg_pager = { version = "0.7.0", path = "ogg_pager" }
aud_io = { version = "0.1.0", path = "aud_io" }

byteorder = "1.5.0"
log = "0.4.27"

[workspace.lints.rust]
missing_docs = "deny"
Expand Down
23 changes: 23 additions & 0 deletions aud_io/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
[package]
name = "aud_io"
version = "0.1.0"
description = "" # TODO
keywords = ["audio", "mp4"]
categories = ["multimedia", "multimedia::audio", "parser-implementations"]
readme = "" # TODO
include = ["src", "../LICENSE-APACHE", "../LICENSE-MIT"]
authors.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true

[dependencies]
byteorder.workspace = true
log.workspace = true

[dev-dependencies]
tempfile = "3.15.0"
test-log = "0.2.16"

#[lints]
#workspace = true
10 changes: 5 additions & 5 deletions lofty/src/util/alloc.rs → aud_io/src/alloc.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
use crate::error::Result;
use crate::macros::err;

use crate::err;
use crate::config::global_options;

/// Provides the `fallible_repeat` method on `Vec`
Expand Down Expand Up @@ -49,7 +48,8 @@ impl<T> VecFallibleRepeat<T> for Vec<T> {
/// Creates a `Vec` of the specified length, containing copies of `element`.
///
/// This should be used through [`try_vec!`](crate::macros::try_vec)
pub(crate) fn fallible_vec_from_element<T>(element: T, expected_size: usize) -> Result<Vec<T>>
#[doc(hidden)]
pub fn fallible_vec_from_element<T>(element: T, expected_size: usize) -> Result<Vec<T>>
where
T: Clone,
{
Expand All @@ -59,7 +59,7 @@ where
/// Provides the `try_with_capacity` method on `Vec`
///
/// This can be used directly.
pub(crate) trait VecFallibleCapacity<T>: Sized {
pub trait VecFallibleCapacity<T>: Sized {
/// Same as `Vec::with_capacity`, but takes `GlobalOptions::allocation_limit` into account.
///
/// Named `try_with_capacity_stable` to avoid conflicts with the nightly `Vec::try_with_capacity`.
Expand All @@ -81,7 +81,7 @@ impl<T> VecFallibleCapacity<T> for Vec<T> {

#[cfg(test)]
mod tests {
use crate::util::alloc::fallible_vec_from_element;
use super::fallible_vec_from_element;

#[test_log::test]
fn vec_fallible_repeat() {
Expand Down
100 changes: 100 additions & 0 deletions aud_io/src/config/global.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
use std::cell::UnsafeCell;

thread_local! {
static GLOBAL_OPTIONS: UnsafeCell<GlobalOptions> = UnsafeCell::new(GlobalOptions::default());
}

pub(crate) unsafe fn global_options() -> &'static GlobalOptions {
GLOBAL_OPTIONS.with(|global_options| unsafe { &*global_options.get() })
}

/// Options that control all interactions with Lofty for the current thread
///
/// # Examples
///
/// ```rust
/// use aud_io::config::{GlobalOptions, apply_global_options};
///
/// // I have a custom resolver that I need checked
/// let global_options = GlobalOptions::new().use_custom_resolvers(true);
/// apply_global_options(global_options);
/// ```
#[derive(Copy, Clone, Debug, Ord, PartialOrd, Eq, PartialEq)]
#[non_exhaustive]
pub struct GlobalOptions {
pub(crate) allocation_limit: usize,
}

impl GlobalOptions {
/// Default allocation limit for any single allocation
pub const DEFAULT_ALLOCATION_LIMIT: usize = 16 * 1024 * 1024;

/// Creates a new `GlobalOptions`, alias for `Default` implementation
///
/// See also: [`GlobalOptions::default`]
///
/// # Examples
///
/// ```rust
/// use aud_io::config::GlobalOptions;
///
/// let global_options = GlobalOptions::new();
/// ```
#[must_use]
pub const fn new() -> Self {
Self {
allocation_limit: Self::DEFAULT_ALLOCATION_LIMIT,
}
}

/// The maximum number of bytes to allocate for any single tag item
///
/// This is a safety measure to prevent allocating too much memory for a single tag item. If a tag item
/// exceeds this limit, the allocator will return [`AudioError::TooMuchData`].
///
/// # Examples
///
/// ```rust
/// use aud_io::config::{GlobalOptions, apply_global_options};
///
/// // I have files with gigantic images, I'll double the allocation limit!
/// let global_options = GlobalOptions::new().allocation_limit(32 * 1024 * 1024);
/// apply_global_options(global_options);
/// ```
pub fn allocation_limit(&mut self, allocation_limit: usize) -> Self {
self.allocation_limit = allocation_limit;
*self
}
}

impl Default for GlobalOptions {
/// The default implementation for `GlobalOptions`
///
/// The defaults are as follows:
///
/// ```rust,ignore
/// GlobalOptions {
/// allocation_limit: Self::DEFAULT_ALLOCATION_LIMIT,
/// }
/// ```
fn default() -> Self {
Self::new()
}
}

/// Applies the given `GlobalOptions` to the current thread
///
/// # Examples
///
/// ```rust
/// use aud_io::config::{GlobalOptions, apply_global_options};
///
/// // I have a custom resolver that I need checked
/// let global_options = GlobalOptions::new().use_custom_resolvers(true);
/// apply_global_options(global_options);
/// ```
pub fn apply_global_options(options: GlobalOptions) {
GLOBAL_OPTIONS.with(|global_options| unsafe {
*global_options.get() = options;
});
}
4 changes: 4 additions & 0 deletions aud_io/src/config/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
mod global;
pub use global::*;
mod parse;
pub use parse::*;
52 changes: 52 additions & 0 deletions aud_io/src/config/parse.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/// The parsing strictness mode
///
/// This can be set with [`Probe::options`](crate::probe::Probe).
///
/// # Examples
///
/// ```rust,no_run
/// use lofty::config::{ParseOptions, ParsingMode};
/// use lofty::probe::Probe;
///
/// # fn main() -> lofty::error::Result<()> {
/// // We only want to read spec-compliant inputs
/// let parsing_options = ParseOptions::new().parsing_mode(ParsingMode::Strict);
/// let tagged_file = Probe::open("foo.mp3")?.options(parsing_options).read()?;
/// # Ok(()) }
/// ```
#[derive(Copy, Clone, Debug, Ord, PartialOrd, Eq, PartialEq, Default)]
#[non_exhaustive]
pub enum ParsingMode {
/// Will eagerly error on invalid input
///
/// This mode will eagerly error on any non spec-compliant input.
///
/// ## Examples of behavior
///
/// * Unable to decode text - The parser will error and the entire input is discarded
/// * Unable to determine the sample rate - The parser will error and the entire input is discarded
Strict,
/// Default mode, less eager to error on recoverably malformed input
///
/// This mode will attempt to fill in any holes where possible in otherwise valid, spec-compliant input.
///
/// NOTE: A readable input does *not* necessarily make it writeable.
///
/// ## Examples of behavior
///
/// * Unable to decode text - If valid otherwise, the field will be replaced by an empty string and the parser moves on
/// * Unable to determine the sample rate - The sample rate will be 0
#[default]
BestAttempt,
/// Least eager to error, may produce invalid/partial output
///
/// This mode will discard any invalid fields, and ignore the majority of non-fatal errors.
///
/// If the input is malformed, the resulting tags may be incomplete, and the properties zeroed.
///
/// ## Examples of behavior
///
/// * Unable to decode text - The entire item is discarded and the parser moves on
/// * Unable to determine the sample rate - The sample rate will be 0
Relaxed,
}
101 changes: 101 additions & 0 deletions aud_io/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
use std::collections::TryReserveError;
use std::fmt::Display;

pub type Result<T> = std::result::Result<T, AudioError>;

#[derive(Debug)]
pub enum AudioError {
// File data related errors
/// Attempting to read/write an abnormally large amount of data
TooMuchData,
/// Expected the data to be a different size than provided
///
/// This occurs when the size of an item is written as one value, but that size is either too
/// big or small to be valid within the bounds of that item.
// TODO: Should probably have context
SizeMismatch,

/// Errors that arise while decoding text
TextDecode(&'static str),
/// Arises when an atom contains invalid data
BadAtom(&'static str),

// Conversions for external errors
/// Unable to convert bytes to a String
StringFromUtf8(std::string::FromUtf8Error),
/// Unable to convert bytes to a str
StrFromUtf8(std::str::Utf8Error),
/// Represents all cases of [`std::io::Error`].
Io(std::io::Error),
/// Represents all cases of [`std::fmt::Error`].
Fmt(std::fmt::Error),
/// Failure to allocate enough memory
Alloc(TryReserveError),
/// This should **never** be encountered
Infallible(std::convert::Infallible),
}

impl Display for AudioError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
// Conversions
AudioError::StringFromUtf8(err) => write!(f, "{err}"),
AudioError::StrFromUtf8(err) => write!(f, "{err}"),
AudioError::Io(err) => write!(f, "{err}"),
AudioError::Fmt(err) => write!(f, "{err}"),
AudioError::Alloc(err) => write!(f, "{err}"),
AudioError::Infallible(_) => write!(f, "A expected condition was not upheld"),

AudioError::TextDecode(message) => write!(f, "Text decoding: {message}"),
AudioError::BadAtom(message) => write!(f, "MP4 Atom: {message}"),

// Files
AudioError::TooMuchData => write!(
f,
"Attempted to read/write an abnormally large amount of data"
),
AudioError::SizeMismatch => write!(
f,
"Encountered an invalid item size, either too big or too small to be valid"
),
}
}
}

impl core::error::Error for AudioError {}

impl From<std::io::Error> for AudioError {
fn from(input: std::io::Error) -> Self {
AudioError::Io(input)
}
}

impl From<std::fmt::Error> for AudioError {
fn from(input: std::fmt::Error) -> Self {
AudioError::Fmt(input)
}
}

impl From<std::string::FromUtf8Error> for AudioError {
fn from(input: std::string::FromUtf8Error) -> Self {
AudioError::StringFromUtf8(input)
}
}

impl From<std::str::Utf8Error> for AudioError {
fn from(input: std::str::Utf8Error) -> Self {
AudioError::StrFromUtf8(input)
}
}

impl From<TryReserveError> for AudioError {
fn from(input: TryReserveError) -> Self {
AudioError::Alloc(input)
}
}

impl From<std::convert::Infallible> for AudioError {
fn from(input: std::convert::Infallible) -> Self {
AudioError::Infallible(input)
}
}
Loading
Loading