Skip to main content

fast_hex_lite/
lib.rs

1//! Fast, allocation-free hex encoding/decoding.
2//!
3//! - `no_std` by default (scalar path)
4//! - Optional `std` support
5//! - Optional `simd` accelerated decode/validate on supported targets
6//!
7//! ## API
8//!
9//! - Decode: [`decode_to_slice`], [`decode_to_array`], [`decode_in_place`]
10//! - Encode: [`encode_to_slice`]
11//!
12//! ## Examples
13//!
14//! Decode hex → bytes:
15//!
16//! ```rust
17//! use fast_hex_lite::decode_to_slice;
18//!
19//! let hex = b"deadbeef";
20//! let mut buf = [0u8; 4];
21//! let n = decode_to_slice(hex, &mut buf).unwrap();
22//! assert_eq!(&buf[..n], &[0xde, 0xad, 0xbe, 0xef]);
23//! ```
24//!
25//! Encode bytes → hex (lowercase):
26//!
27//! ```rust
28//! use fast_hex_lite::encode_to_slice;
29//!
30//! let src = [0xde, 0xad, 0xbe, 0xef];
31//! let mut out = [0u8; 8];
32//! let n = encode_to_slice(&src, &mut out, true).unwrap();
33//! assert_eq!(&out[..n], b"deadbeef");
34//! ```
35
36#![cfg_attr(not(feature = "std"), no_std)]
37#![warn(missing_docs, clippy::all, clippy::pedantic)]
38#![allow(
39    clippy::module_name_repetitions,
40    clippy::missing_errors_doc,
41    clippy::missing_panics_doc,
42    clippy::must_use_candidate
43)]
44
45mod decode;
46mod encode;
47
48#[cfg(feature = "simd")]
49mod simd;
50
51pub use decode::{decode_in_place, decode_to_array, decode_to_slice, decoded_len};
52pub use encode::{encode_to_slice, encoded_len};
53
54// `encode_to_string` requires allocation (String), so it is only available with `std`.
55#[cfg(feature = "std")]
56pub use encode::encode_to_string;
57
58/// Errors that can occur during hex encoding or decoding.
59#[derive(Debug, Clone, PartialEq, Eq)]
60pub enum Error {
61    /// The input length is odd; hex strings must have even number of bytes.
62    OddLength,
63    /// The output buffer is too small.
64    OutputTooSmall,
65    /// An invalid byte was encountered at the given position.
66    InvalidByte {
67        /// Zero-based index into the source slice.
68        index: usize,
69        /// The offending byte value.
70        byte: u8,
71    },
72}
73
74impl core::fmt::Display for Error {
75    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
76        match self {
77            Error::OddLength => f.write_str("hex string has odd length"),
78            Error::OutputTooSmall => f.write_str("output buffer is too small"),
79            Error::InvalidByte { index, byte } => {
80                let b = *byte;
81                write!(
82                    f,
83                    "invalid hex byte 0x{:02x} ('{}') at index {}",
84                    b,
85                    if b.is_ascii_graphic() { b as char } else { '?' },
86                    index
87                )
88            }
89        }
90    }
91}
92
93#[cfg(feature = "std")]
94impl std::error::Error for Error {}
95
96#[cfg(test)]
97mod tests {
98    extern crate std;
99    use super::*;
100    use std::prelude::v1::*;
101
102    // ── Error: PartialEq / Clone ───────────────────────────────────────────
103
104    #[test]
105    fn test_error_eq_odd_length() {
106        assert_eq!(Error::OddLength, Error::OddLength);
107    }
108
109    #[test]
110    fn test_error_eq_output_too_small() {
111        assert_eq!(Error::OutputTooSmall, Error::OutputTooSmall);
112    }
113
114    #[test]
115    fn test_error_eq_invalid_byte() {
116        assert_eq!(
117            Error::InvalidByte {
118                index: 3,
119                byte: b'X'
120            },
121            Error::InvalidByte {
122                index: 3,
123                byte: b'X'
124            },
125        );
126    }
127
128    #[test]
129    fn test_error_ne_different_variants() {
130        assert_ne!(Error::OddLength, Error::OutputTooSmall);
131        assert_ne!(Error::OddLength, Error::InvalidByte { index: 0, byte: 0 },);
132    }
133
134    #[test]
135    fn test_error_ne_different_index() {
136        assert_ne!(
137            Error::InvalidByte {
138                index: 0,
139                byte: b'X'
140            },
141            Error::InvalidByte {
142                index: 1,
143                byte: b'X'
144            },
145        );
146    }
147
148    #[test]
149    fn test_error_ne_different_byte() {
150        assert_ne!(
151            Error::InvalidByte {
152                index: 0,
153                byte: b'X'
154            },
155            Error::InvalidByte {
156                index: 0,
157                byte: b'Y'
158            },
159        );
160    }
161
162    #[test]
163    fn test_error_clone() {
164        let e = Error::InvalidByte {
165            index: 7,
166            byte: 0xAB,
167        };
168        assert_eq!(e.clone(), e);
169
170        assert_eq!(Error::OddLength.clone(), Error::OddLength);
171        assert_eq!(Error::OutputTooSmall.clone(), Error::OutputTooSmall);
172    }
173
174    // ── Error: Display ─────────────────────────────────────────────────────
175
176    #[test]
177    fn test_display_odd_length() {
178        let s = std::format!("{}", Error::OddLength);
179        assert_eq!(s, "hex string has odd length");
180    }
181
182    #[test]
183    fn test_display_output_too_small() {
184        let s = std::format!("{}", Error::OutputTooSmall);
185        assert_eq!(s, "output buffer is too small");
186    }
187
188    #[test]
189    fn test_display_invalid_byte_graphic() {
190        // 'X' is ASCII graphic → должна подставиться как символ
191        let s = std::format!(
192            "{}",
193            Error::InvalidByte {
194                index: 4,
195                byte: b'X'
196            }
197        );
198        assert!(s.contains("0x58"), "hex value missing: {s}");
199        assert!(s.contains('X'), "char missing: {s}");
200        assert!(s.contains('4'), "index missing: {s}");
201    }
202
203    #[test]
204    fn test_display_invalid_byte_non_graphic() {
205        // 0x01 — не ASCII graphic → должен подставиться '?'
206        let s = std::format!(
207            "{}",
208            Error::InvalidByte {
209                index: 0,
210                byte: 0x01
211            }
212        );
213        assert!(s.contains("0x01"), "hex value missing: {s}");
214        assert!(s.contains('?'), "fallback char missing: {s}");
215        assert!(s.contains('0'), "index missing: {s}");
216    }
217
218    #[test]
219    fn test_display_invalid_byte_null() {
220        let s = std::format!(
221            "{}",
222            Error::InvalidByte {
223                index: 0,
224                byte: 0x00
225            }
226        );
227        assert!(s.contains("0x00"));
228        assert!(s.contains('?'));
229    }
230
231    #[test]
232    fn test_display_invalid_byte_high_ascii() {
233        let s = std::format!(
234            "{}",
235            Error::InvalidByte {
236                index: 10,
237                byte: 0xFF
238            }
239        );
240        assert!(s.contains("0xff"));
241        assert!(s.contains('?'));
242        assert!(s.contains("10"));
243    }
244
245    #[test]
246    fn test_display_invalid_byte_space() {
247        // ' ' (0x20) is_ascii_graphic() == false → '?'
248        let s = std::format!(
249            "{}",
250            Error::InvalidByte {
251                index: 1,
252                byte: b' '
253            }
254        );
255        assert!(s.contains("0x20"));
256        assert!(s.contains('?'));
257    }
258
259    // ── Error: Debug ───────────────────────────────────────────────────────
260
261    #[test]
262    fn test_debug_odd_length() {
263        let s = std::format!("{:?}", Error::OddLength);
264        assert_eq!(s, "OddLength");
265    }
266
267    #[test]
268    fn test_debug_output_too_small() {
269        let s = std::format!("{:?}", Error::OutputTooSmall);
270        assert_eq!(s, "OutputTooSmall");
271    }
272
273    #[test]
274    fn test_debug_invalid_byte() {
275        let s = std::format!(
276            "{:?}",
277            Error::InvalidByte {
278                index: 2,
279                byte: 0xAB
280            }
281        );
282        assert!(s.contains("InvalidByte"));
283        assert!(s.contains("index: 2"));
284        assert!(s.contains("byte: 171")); // 0xAB == 171
285    }
286
287    // ── std::error::Error ──────────────────────────────────────────────────
288
289    #[cfg(feature = "std")]
290    #[test]
291    fn test_std_error_trait_odd_length() {
292        let e: &dyn std::error::Error = &Error::OddLength;
293        assert!(e.source().is_none());
294    }
295
296    #[cfg(feature = "std")]
297    #[test]
298    fn test_std_error_trait_invalid_byte() {
299        let e: &dyn std::error::Error = &Error::InvalidByte { index: 0, byte: 0 };
300        assert!(e.source().is_none());
301    }
302
303    #[cfg(feature = "std")]
304    #[test]
305    fn test_std_error_display_matches_fmt() {
306        let errors = [
307            Error::OddLength,
308            Error::OutputTooSmall,
309            Error::InvalidByte {
310                index: 5,
311                byte: b'Z',
312            },
313        ];
314        for e in &errors {
315            let via_display = std::format!("{e}");
316            let via_error: &dyn std::error::Error = e;
317            assert_eq!(via_display, std::format!("{via_error}"));
318        }
319    }
320
321    // ── публичный API: re-exports доступны из корня ────────────────────────
322
323    #[test]
324    fn test_public_api_decode_to_slice() {
325        let mut buf = [0u8; 4];
326        let n = decode_to_slice(b"deadbeef", &mut buf).unwrap();
327        assert_eq!(n, 4);
328        assert_eq!(&buf, &[0xde, 0xad, 0xbe, 0xef]);
329    }
330
331    #[test]
332    fn test_public_api_decode_to_array() {
333        let arr = decode_to_array::<4>(b"deadbeef").unwrap();
334        assert_eq!(arr, [0xde, 0xad, 0xbe, 0xef]);
335    }
336
337    #[test]
338    fn test_public_api_decode_in_place() {
339        let mut buf = *b"deadbeef";
340        let n = decode_in_place(&mut buf).unwrap();
341        assert_eq!(&buf[..n], &[0xde, 0xad, 0xbe, 0xef]);
342    }
343
344    #[test]
345    fn test_public_api_decoded_len() {
346        assert_eq!(decoded_len(8).unwrap(), 4);
347        assert_eq!(decoded_len(1), Err(Error::OddLength));
348    }
349
350    #[test]
351    fn test_public_api_encode_to_slice() {
352        let mut out = [0u8; 8];
353        let n = encode_to_slice(&[0xde, 0xad, 0xbe, 0xef], &mut out, true).unwrap();
354        assert_eq!(n, 8);
355        assert_eq!(&out, b"deadbeef");
356    }
357
358    #[test]
359    fn test_public_api_encoded_len() {
360        assert_eq!(encoded_len(4), 8);
361        assert_eq!(encoded_len(0), 0);
362    }
363
364    #[cfg(feature = "std")]
365    #[test]
366    fn test_public_api_encode_to_string() {
367        let s = encode_to_string(&[0xde, 0xad, 0xbe, 0xef], true);
368        assert_eq!(s, "deadbeef");
369    }
370
371    // ── сквозной round-trip через публичный API ────────────────────────────
372
373    #[test]
374    fn test_roundtrip_encode_decode() {
375        let src: std::vec::Vec<u8> = (0u8..=255).collect();
376        let mut hex = std::vec![0u8; src.len() * 2];
377        encode_to_slice(&src, &mut hex, true).unwrap();
378        let mut dst = std::vec![0u8; src.len()];
379        decode_to_slice(&hex, &mut dst).unwrap();
380        assert_eq!(dst, src);
381    }
382
383    #[test]
384    fn test_roundtrip_encode_decode_uppercase() {
385        let src: std::vec::Vec<u8> = (0u8..=255).collect();
386        let mut hex = std::vec![0u8; src.len() * 2];
387        encode_to_slice(&src, &mut hex, false).unwrap();
388        let mut dst = std::vec![0u8; src.len()];
389        decode_to_slice(&hex, &mut dst).unwrap();
390        assert_eq!(dst, src);
391    }
392}