i24/
lib.rs

1#![cfg_attr(not(feature = "std"), no_std)]
2// Correctness and logic
3#![warn(clippy::unit_cmp)] // Detects comparing unit types
4#![warn(clippy::match_same_arms)] // Duplicate match arms
5
6// Performance-focused
7#![warn(clippy::inefficient_to_string)] // `format!("{}", x)` vs `x.to_string()`
8#![warn(clippy::map_clone)] // Cloning inside `map()` unnecessarily
9#![warn(clippy::unnecessary_to_owned)] // Detects redundant `.to_owned()` or `.clone()`
10#![warn(clippy::large_stack_arrays)] // Helps avoid stack overflows
11#![warn(clippy::box_collection)] // Warns on boxed `Vec`, `String`, etc.
12#![warn(clippy::vec_box)] // Avoids using `Vec<Box<T>>` when unnecessary
13#![warn(clippy::needless_collect)] // Avoids `.collect().iter()` chains
14
15// Style and idiomatic Rust
16#![warn(clippy::redundant_clone)] // Detects unnecessary `.clone()`
17#![warn(clippy::identity_op)] // e.g., `x + 0`, `x * 1`
18#![warn(clippy::needless_return)] // Avoids `return` at the end of functions
19#![warn(clippy::let_unit_value)] // Avoids binding `()` to variables
20#![warn(clippy::manual_map)] // Use `.map()` instead of manual `match`
21#![warn(clippy::unwrap_used)] // Avoids using `unwrap()`
22#![warn(clippy::panic)] // Avoids using `panic!` in production code
23
24// Maintainability
25#![warn(missing_docs)] // Warns on missing documentation. Everything should be documented!
26#![warn(clippy::missing_panics_doc)] // Docs for functions that might panic
27#![warn(clippy::missing_safety_doc)] // Docs for `unsafe` functions
28#![warn(clippy::missing_const_for_fn)] // Suggests making eligible functions `const`
29
30//! # i24: Integer Types for Rust (i24, u24)
31//!
32//! The `i24` crate provides specialized integer types for Rust: **i24** (24-bit signed) and **u24** (24-bit unsigned).
33//! These types fill precision gaps in Rust's integer types
34//! and are particularly useful in audio processing, embedded systems, network protocols, and other scenarios where
35//! specific bit-width precision is required.
36//!
37//! ## Features
38//!
39//! ### Integer Types
40//! - **i24**: 24-bit signed integer (range: -8,388,608 to 8,388,607)
41//! - **u24**: 24-bit unsigned integer (range: 0 to 16,777,215)
42//!
43//! ### Core Functionality
44//! - Seamless conversion to/from standard Rust integer types
45//! - Complete arithmetic operations with overflow checking
46//! - Bitwise operations
47//! - Conversions from various byte representations (little-endian, big-endian, native)
48//! - Wire/packed format support for binary protocols
49//! - Compile-time macros: `i24!()` and `u24!()` for checked construction
50//! - Implements standard traits: `Debug`, `Display`, `PartialEq`, `Eq`, `PartialOrd`, `Ord`, `Hash`
51//!
52//! This crate came about as a part of the [Wavers](https://crates.io/crates/wavers) project, which is a Wav file reader and writer for Rust.
53//! All integer types have Python bindings available via the `pyo3` feature.
54//!
55//! ## Usage
56//!
57//! Add this to your `Cargo.toml`:
58//!
59//! ```toml
60//! [dependencies]
61//! i24 = "2.2.0"
62//! ```
63//!
64//! ### Basic Usage
65//!
66//! ```rust
67//! use i24::{i24, u24};
68//!
69//! // Using macros for compile-time checked construction
70//! let signed_24 = i24!(1000);
71//! let unsigned_24 = u24!(2000);
72//!
73//! // Arithmetic operations
74//! let sum_24 = signed_24 + i24!(500);
75//! assert_eq!(sum_24.to_i32(), 1500);
76//!
77//! // Conversions
78//! let as_i32: i32 = signed_24.into();
79//! let as_u32: u32 = unsigned_24.into();
80//! ```
81//!
82//! The `i24!()` and `u24!()` macros allow you to create values at compile time, ensuring they're within the valid range.
83//!
84//! ### Binary Data and Wire Formats
85//!
86//! For working with binary data, all types support reading/writing from byte arrays:
87//!
88//! ```rust
89//! use i24::{I24, U24};
90//!
91//! // Reading from bytes (little-endian, big-endian, native)
92//! let bytes_le = [0x00, 0x01, 0x02]; // 3-byte representation
93//! let value = I24::from_le_bytes(bytes_le);
94//!
95//! // Writing to bytes
96//! let bytes: [u8; 3] = value.to_be_bytes();
97//!
98//! // Bulk operations for slices
99//! # #[cfg(feature = "alloc")]
100//! # {
101//! let raw_data: &[u8] = &[0x00, 0x01, 0x02, 0x00, 0x01, 0xFF];
102//! let values: Vec<I24> = I24::read_i24s_be(raw_data).expect("valid buffer");
103//! let encoded: Vec<u8> = I24::write_i24s_be(&values);
104//! # }
105//! ```
106//!
107//! ## Safety and Limitations
108//!
109//! All integer types strive to behave similarly to Rust's built-in integer types, with some important considerations:
110//!
111//! ### Value Ranges
112//! - **i24**: [-8,388,608, 8,388,607]
113//! - **u24**: [0, 16,777,215]  
114
115//! ### Overflow Behavior
116//! - Arithmetic operations match the behavior of their closest standard Rust integer type
117//! - Bitwise operations are performed on the actual bit-width representation
118//! - Always use checked arithmetic operations when dealing with untrusted input
119//!
120//! ### Memory Safety
121//! All types align with `bytemuck` safety requirements (`NoUninit`, `Zeroable`, `AnyBitPattern`), ensuring safe byte-to-value conversions.
122//! The bulk I/O operations use `bytemuck::cast_slice` internally for efficient, safe conversions.
123//!
124//! ## Feature Flags
125//! - **pyo3**: Enables Python bindings for all integer types (i24, u24)
126//! - **serde**: Enables `Serialize` and `Deserialize` traits for all integer types
127//! - **alloc**: Enables bulk I/O operations and `PackedStruct` functionality
128//! - **ndarray**: Enables `ScalarOperand` trait for use with ndarray operations
129//!
130//! ## Contributing
131//!
132//! Contributions are welcome! Please feel free to submit a Pull Request. This really needs more testing and verification.
133//!
134//! ## License
135//!
136//! This project is licensed under MIT - see the [LICENSE](https://github.com/jmg049/i24/blob/main/LICENSE) file for details.
137//!
138//! ## Benchmarks
139//! See the [benchmark report](https://github.com/jmg049/i24/i24_benches/benchmark_analysis/benchmark_report.md).
140//!
141
142pub mod types;
143pub use types::{I24, I24Bytes, U24, U24Bytes};
144
145pub(crate) mod repr;
146
147#[doc(hidden)]
148pub mod __macros__ {
149    pub use bytemuck::Zeroable;
150}
151
152pub(crate) type TryFromIntError = <i8 as TryFrom<i64>>::Error;
153
154#[allow(clippy::unwrap_used)] // Intentional use of unwrap
155pub(crate) fn out_of_range() -> TryFromIntError {
156    i8::try_from(i64::MIN).unwrap_err()
157}
158
159/// creates an `i24` from a constant expression
160/// will give a compile error if the expression overflows an i24
161#[macro_export]
162macro_rules! i24 {
163    (0) => {
164        <I24 as $crate::__macros__::Zeroable>::zeroed()
165    };
166    ($e: expr) => {
167        const {
168            match $crate::I24::try_from_i32($e) {
169                Some(x) => x,
170                None => panic!(concat!(
171                    "out of range value ",
172                    stringify!($e),
173                    " used as an i24 constant"
174                )),
175            }
176        }
177    };
178}
179
180/// creates an `u24` from a constant expression
181/// will give a compile error if the expression overflows an u24
182#[macro_export]
183macro_rules! u24 {
184    (0) => {
185        <U24 as $crate::__macros__::Zeroable>::zeroed()
186    };
187    ($e: expr) => {
188        const {
189            match $crate::U24::try_from_u32($e) {
190                Some(x) => x,
191                None => panic!(concat!(
192                    "out of range value ",
193                    stringify!($e),
194                    " used as an u24 constant"
195                )),
196            }
197        }
198    };
199}
200
201/// Helper utilities for working with packed structures containing `I24` values.
202///
203/// This module provides utilities to work around the limitation where Rust doesn't
204/// allow nested `#[repr(packed)]` attributes. Since `I24` uses internal packing,
205/// you cannot directly use `#[repr(packed, C)]` on structures containing `I24`.
206///
207/// These utilities provide safe alternatives for serializing and deserializing
208/// mixed structures containing `I24` and native types.
209#[cfg(feature = "alloc")]
210extern crate alloc;
211
212#[cfg(feature = "alloc")]
213use alloc::vec::Vec;
214use bytemuck::{Pod, try_cast_slice};
215
216/// A trait for types that can be read from and written to packed byte representations.
217///
218/// This trait provides methods for handling structures that contain `I24` values
219/// mixed with native types, working around the nested packing limitation.
220pub trait PackedStruct: Sized {
221    /// The size in bytes of the packed representation.
222    const PACKED_SIZE: usize;
223
224    /// Reads a single instance from packed bytes.
225    ///
226    /// # Arguments
227    ///
228    /// * `bytes` - A byte slice containing the packed representation.
229    ///   Must be at least `PACKED_SIZE` bytes long.
230    ///
231    /// # Returns
232    ///
233    /// The deserialized structure, or `None` if the input is too short.
234    fn from_packed_bytes(bytes: &[u8]) -> Option<Self>;
235
236    /// Writes the structure to a packed byte representation.
237    ///
238    /// # Returns
239    ///
240    /// A vector containing the packed bytes.
241    #[cfg(feature = "alloc")]
242    fn to_packed_bytes(&self) -> Vec<u8>;
243
244    /// Reads multiple instances from a packed byte slice.
245    ///
246    /// # Arguments
247    ///
248    /// * `bytes` - A byte slice containing multiple packed structures.
249    ///   Length must be a multiple of `PACKED_SIZE`.
250    ///
251    /// # Returns
252    ///
253    /// A vector of deserialized structures, or `None` if the input length
254    /// is not a multiple of `PACKED_SIZE`.
255    #[cfg(feature = "alloc")]
256    fn from_packed_slice(bytes: &[u8]) -> Option<Vec<Self>> {
257        if !bytes.len().is_multiple_of(Self::PACKED_SIZE) {
258            return None;
259        }
260
261        let mut result = Vec::with_capacity(bytes.len() / Self::PACKED_SIZE);
262        for chunk in bytes.chunks_exact(Self::PACKED_SIZE) {
263            result.push(Self::from_packed_bytes(chunk)?);
264        }
265        Some(result)
266    }
267
268    /// Writes multiple structures to a packed byte representation.
269    ///
270    /// # Arguments
271    ///
272    /// * `structs` - A slice of structures to serialize.
273    ///
274    /// # Returns
275    ///
276    /// A vector containing all packed bytes concatenated.
277    #[cfg(feature = "alloc")]
278    fn to_packed_slice(structs: &[Self]) -> Vec<u8> {
279        structs.iter().flat_map(|s| s.to_packed_bytes()).collect()
280    }
281}
282
283/// Helper function to safely cast native types in mixed structures.
284///
285/// When you have a portion of your structure that contains only native types
286/// (no I24), you can use this function to safely cast that portion using bytemuck.
287///
288/// # Arguments
289///
290/// * `bytes` - Byte slice containing the native types.
291///
292/// # Returns
293///
294/// A slice of the target type, or an error if the cast fails.
295pub fn cast_native_slice<T: Pod>(bytes: &[u8]) -> Result<&[T], bytemuck::PodCastError> {
296    try_cast_slice(bytes)
297}
298
299#[cfg(feature = "pyo3")]
300pub use crate::types::{PyI24, PyU24};
301
302#[cfg(feature = "pyo3")]
303use pyo3::prelude::*;
304
305#[cfg(feature = "pyo3")]
306#[pymodule(name = "i24")]
307fn pyi24(m: &Bound<'_, PyModule>) -> PyResult<()> {
308    m.add_class::<PyI24>()?;
309    m.add_class::<PyU24>()?;
310    Ok(())
311}