Skip to main content

domain_key/
utils.rs

1//! Utility functions and helper types for domain-key
2//!
3//! This module contains internal utility functions used throughout the library,
4//! including optimized string operations, caching utilities, and performance helpers.
5
6use smartstring::alias::String as SmartString;
7
8#[cfg(not(feature = "std"))]
9use alloc::borrow::Cow;
10#[cfg(not(feature = "std"))]
11use alloc::string::{String, ToString};
12#[cfg(feature = "std")]
13use std::borrow::Cow;
14
15// ============================================================================
16// STRING MANIPULATION UTILITIES
17// ============================================================================
18
19/// Add a prefix to a string with optimized allocation
20///
21/// This function efficiently adds a prefix to a string by pre-calculating
22/// the required capacity and performing a single allocation.
23///
24/// # Arguments
25///
26/// * `key` - The original string
27/// * `prefix` - The prefix to add
28///
29/// # Returns
30///
31/// A new `SmartString` with the prefix added
32#[must_use]
33pub fn add_prefix_optimized(key: &str, prefix: &str) -> SmartString {
34    let total = prefix.len() + key.len();
35    if total <= 23 {
36        // Fits inline in SmartString — no heap allocation
37        let mut result = SmartString::new();
38        result.push_str(prefix);
39        result.push_str(key);
40        result
41    } else {
42        let mut s = String::with_capacity(total);
43        s.push_str(prefix);
44        s.push_str(key);
45        SmartString::from(s)
46    }
47}
48
49/// Add a suffix to a string with optimized allocation
50///
51/// This function efficiently adds a suffix to a string by pre-calculating
52/// the required capacity and performing a single allocation.
53///
54/// # Arguments
55///
56/// * `key` - The original string
57/// * `suffix` - The suffix to add
58///
59/// # Returns
60///
61/// A new `SmartString` with the suffix added
62#[must_use]
63pub fn add_suffix_optimized(key: &str, suffix: &str) -> SmartString {
64    let total = key.len() + suffix.len();
65    if total <= 23 {
66        let mut result = SmartString::new();
67        result.push_str(key);
68        result.push_str(suffix);
69        result
70    } else {
71        let mut s = String::with_capacity(total);
72        s.push_str(key);
73        s.push_str(suffix);
74        SmartString::from(s)
75    }
76}
77
78/// Create a new split cache for consistent API
79///
80/// This function creates a split iterator that can be used consistently
81/// across different optimization levels.
82///
83/// # Arguments
84///
85/// * `s` - The string to split
86/// * `delimiter` - The character to split on
87///
88/// # Returns
89///
90/// A split iterator over the string
91#[inline]
92#[must_use]
93pub fn new_split_cache(s: &str, delimiter: char) -> core::str::Split<'_, char> {
94    s.split(delimiter)
95}
96
97/// Join string parts with a delimiter, optimizing for common cases
98///
99/// This function efficiently joins string parts using pre-calculated sizing
100/// to minimize allocations.
101///
102/// # Arguments
103///
104/// * `parts` - The string parts to join
105/// * `delimiter` - The delimiter to use between parts
106///
107/// # Returns
108///
109/// A new string with all parts joined
110#[must_use]
111pub fn join_optimized(parts: &[&str], delimiter: &str) -> String {
112    if parts.is_empty() {
113        return String::new();
114    }
115
116    if parts.len() == 1 {
117        return parts[0].to_string();
118    }
119
120    // Calculate total capacity needed
121    let total_content_len: usize = parts.iter().map(|s| s.len()).sum();
122    let delimiter_len = delimiter.len() * (parts.len().saturating_sub(1));
123    let total_capacity = total_content_len + delimiter_len;
124
125    let mut result = String::with_capacity(total_capacity);
126
127    for (i, part) in parts.iter().enumerate() {
128        if i > 0 {
129            result.push_str(delimiter);
130        }
131        result.push_str(part);
132    }
133
134    result
135}
136
137/// Count the number of occurrences of a character in a string
138///
139/// This function efficiently counts character occurrences without
140/// allocating intermediate collections. Uses byte-level iteration
141/// for ASCII characters.
142///
143/// # Arguments
144///
145/// * `s` - The string to search
146/// * `target` - The character to count
147///
148/// # Returns
149///
150/// The number of times the character appears in the string
151#[cfg(test)]
152#[must_use]
153pub fn count_char(s: &str, target: char) -> usize {
154    if target.is_ascii() {
155        let byte = target as u8;
156        #[expect(
157            clippy::naive_bytecount,
158            reason = "intentional simple byte scan — fast enough for ASCII char counting"
159        )]
160        s.as_bytes().iter().filter(|&&b| b == byte).count()
161    } else {
162        s.chars().filter(|&c| c == target).count()
163    }
164}
165
166/// Find the position of the nth occurrence of a character
167///
168/// This function finds the byte position of the nth occurrence of a character
169/// in a string, useful for caching split positions.
170///
171/// # Arguments
172///
173/// * `s` - The string to search
174/// * `target` - The character to find
175/// * `n` - Which occurrence to find (0-based)
176///
177/// # Returns
178///
179/// The byte position of the nth occurrence, or `None` if not found
180#[cfg(test)]
181#[must_use]
182pub fn find_nth_char(s: &str, target: char, n: usize) -> Option<usize> {
183    let mut count = 0;
184    for (pos, c) in s.char_indices() {
185        if c == target {
186            if count == n {
187                return Some(pos);
188            }
189            count += 1;
190        }
191    }
192    None
193}
194
195// ============================================================================
196// NORMALIZATION UTILITIES
197// ============================================================================
198
199/// Trim whitespace and normalize case efficiently
200///
201/// This function combines trimming and case normalization in a single pass
202/// when possible.
203///
204/// # Arguments
205///
206/// * `s` - The string to normalize
207/// * `to_lowercase` - Whether to convert to lowercase
208///
209/// # Returns
210///
211/// A normalized string, borrowing when no changes are needed
212#[must_use]
213pub fn normalize_string(s: &str, to_lowercase: bool) -> Cow<'_, str> {
214    let trimmed = s.trim();
215    let needs_trim = trimmed.len() != s.len();
216    let needs_lowercase = to_lowercase && trimmed.chars().any(|c| c.is_ascii_uppercase());
217
218    match (needs_trim, needs_lowercase) {
219        (false, false) => Cow::Borrowed(s),
220        (true, false) => Cow::Borrowed(trimmed),
221        (_, true) => Cow::Owned(trimmed.to_ascii_lowercase()),
222    }
223}
224
225/// Replace characters efficiently with a mapping function
226///
227/// This function applies character replacements without unnecessary allocations
228/// when no replacements are needed. Uses a single-pass algorithm that borrows
229/// when no changes are needed and only allocates on first replacement found.
230///
231/// # Arguments
232///
233/// * `s` - The input string
234/// * `replacer` - Function that maps characters to their replacements
235///
236/// # Returns
237///
238/// A string with replacements applied, borrowing when no changes are needed
239pub fn replace_chars<F>(s: &str, replacer: F) -> Cow<'_, str>
240where
241    F: Fn(char) -> Option<char>,
242{
243    // Single-pass: only allocate when we find the first replacement
244    let mut chars = s.char_indices();
245    while let Some((i, c)) = chars.next() {
246        if let Some(replacement) = replacer(c) {
247            // Found first replacement — allocate and copy prefix, then continue
248            let mut result = String::with_capacity(s.len());
249            result.push_str(&s[..i]);
250            result.push(replacement);
251            for (_, c) in chars {
252                if let Some(r) = replacer(c) {
253                    result.push(r);
254                } else {
255                    result.push(c);
256                }
257            }
258            return Cow::Owned(result);
259        }
260    }
261    Cow::Borrowed(s)
262}
263
264// ============================================================================
265// VALIDATION UTILITIES
266// ============================================================================
267
268/// Fast character class checking using lookup tables
269///
270/// This module provides optimized character validation functions using
271/// precomputed lookup tables for common character classes.
272#[expect(
273    clippy::cast_possible_truncation,
274    reason = "index is always < 128 so truncation from usize to u8 is safe"
275)]
276pub mod char_validation {
277    /// Lookup table for ASCII alphanumeric characters
278    const ASCII_ALPHANUMERIC: [bool; 128] = {
279        let mut table = [false; 128];
280        let mut i = 0;
281        while i < 128 {
282            table[i] = matches!(i as u8, b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z');
283            i += 1;
284        }
285        table
286    };
287
288    const KEY_CHARS: [bool; 128] = {
289        let mut table = [false; 128];
290        let mut i = 0;
291        while i < 128 {
292            table[i] =
293                matches!(i as u8,  b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z' | b'_' | b'-' | b'.');
294            i += 1;
295        }
296        table
297    };
298
299    /// Fast check if a character is ASCII alphanumeric
300    #[inline]
301    #[must_use]
302    pub fn is_ascii_alphanumeric_fast(c: char) -> bool {
303        if c.is_ascii() {
304            ASCII_ALPHANUMERIC[c as u8 as usize]
305        } else {
306            false
307        }
308    }
309
310    /// Fast check if a character is allowed in keys
311    #[inline]
312    #[must_use]
313    pub fn is_key_char_fast(c: char) -> bool {
314        if c.is_ascii() {
315            KEY_CHARS[c as u8 as usize]
316        } else {
317            false
318        }
319    }
320
321    /// Check if a character is a common separator
322    #[inline]
323    #[must_use]
324    pub fn is_separator(c: char) -> bool {
325        matches!(c, '_' | '-' | '.' | '/' | ':' | '|')
326    }
327
328    /// Check if a character is whitespace (space, tab, newline, etc.)
329    #[inline]
330    #[must_use]
331    pub fn is_whitespace_fast(c: char) -> bool {
332        matches!(c, ' ' | '\t' | '\n' | '\r' | '\x0B' | '\x0C')
333    }
334}
335
336// ============================================================================
337// FEATURE DETECTION
338// ============================================================================
339
340/// Returns the name of the active hash algorithm
341///
342/// The algorithm is selected at compile time based on feature flags:
343/// - `fast` — `GxHash` (requires AES-NI), falls back to `AHash`
344/// - `secure` — `AHash` (`DoS`-resistant)
345/// - `crypto` — Blake3 (cryptographic)
346/// - default — `DefaultHasher` (std) or FNV-1a (`no_std`)
347///
348/// # Examples
349///
350/// ```rust
351/// let algo = domain_key::hash_algorithm();
352/// println!("Using hash algorithm: {algo}");
353/// ```
354#[must_use]
355pub const fn hash_algorithm() -> &'static str {
356    #[cfg(feature = "fast")]
357    {
358        #[cfg(any(
359            all(target_arch = "x86_64", target_feature = "aes"),
360            all(target_arch = "aarch64", target_feature = "aes")
361        ))]
362        return "GxHash";
363
364        #[cfg(not(any(
365            all(target_arch = "x86_64", target_feature = "aes"),
366            all(target_arch = "aarch64", target_feature = "aes")
367        )))]
368        return "AHash (GxHash fallback)";
369    }
370
371    #[cfg(all(feature = "secure", not(feature = "fast")))]
372    return "AHash";
373
374    #[cfg(all(feature = "crypto", not(any(feature = "fast", feature = "secure"))))]
375    return "Blake3";
376
377    #[cfg(not(any(feature = "fast", feature = "secure", feature = "crypto")))]
378    {
379        #[cfg(feature = "std")]
380        return "DefaultHasher";
381
382        #[cfg(not(feature = "std"))]
383        return "FNV-1a";
384    }
385}
386
387// ============================================================================
388// TESTS
389// ============================================================================
390
391#[cfg(test)]
392mod tests {
393    use super::*;
394
395    use crate::ValidationResult;
396    #[cfg(not(feature = "std"))]
397    use alloc::vec;
398    #[cfg(not(feature = "std"))]
399    use alloc::vec::Vec;
400
401    #[test]
402    fn test_add_prefix_suffix() {
403        let result = add_prefix_optimized("test", "prefix_");
404        assert_eq!(result, "prefix_test");
405
406        let result = add_suffix_optimized("test", "_suffix");
407        assert_eq!(result, "test_suffix");
408    }
409
410    #[test]
411    fn test_join_optimized() {
412        let parts = vec!["a", "b", "c"];
413        let result = join_optimized(&parts, "_");
414        assert_eq!(result, "a_b_c");
415
416        let empty: Vec<&str> = vec![];
417        let result = join_optimized(&empty, "_");
418        assert_eq!(result, "");
419
420        let single = vec!["alone"];
421        let result = join_optimized(&single, "_");
422        assert_eq!(result, "alone");
423    }
424
425    #[test]
426    fn test_char_validation() {
427        use char_validation::*;
428
429        assert!(is_ascii_alphanumeric_fast('a'));
430        assert!(is_ascii_alphanumeric_fast('Z'));
431        assert!(is_ascii_alphanumeric_fast('5'));
432        assert!(!is_ascii_alphanumeric_fast('_'));
433        assert!(!is_ascii_alphanumeric_fast('ñ'));
434
435        assert!(is_key_char_fast('a'));
436        assert!(is_key_char_fast('_'));
437        assert!(is_key_char_fast('-'));
438        assert!(is_key_char_fast('.'));
439        assert!(!is_key_char_fast(' '));
440
441        assert!(is_separator('_'));
442        assert!(is_separator('/'));
443        assert!(!is_separator('a'));
444
445        assert!(is_whitespace_fast(' '));
446        assert!(is_whitespace_fast('\t'));
447        assert!(!is_whitespace_fast('a'));
448    }
449
450    #[test]
451    fn test_string_utilities() {
452        assert_eq!(count_char("hello_world_test", '_'), 2);
453        assert_eq!(count_char("no_underscores", '_'), 1);
454
455        assert_eq!(find_nth_char("a_b_c_d", '_', 0), Some(1));
456        assert_eq!(find_nth_char("a_b_c_d", '_', 1), Some(3));
457        assert_eq!(find_nth_char("a_b_c_d", '_', 2), Some(5));
458        assert_eq!(find_nth_char("a_b_c_d", '_', 3), None);
459    }
460
461    #[test]
462    fn test_normalize_string() {
463        let result = normalize_string("  Hello  ", true);
464        assert_eq!(result, "hello");
465
466        let result = normalize_string("hello", true);
467        assert_eq!(result, "hello");
468
469        let result = normalize_string("  hello  ", false);
470        assert_eq!(result, "hello");
471
472        let result = normalize_string("hello", false);
473        assert!(matches!(result, Cow::Borrowed("hello")));
474    }
475
476    #[test]
477    fn test_float_comparison() {
478        const EPSILON: f64 = 1e-10;
479        let result = ValidationResult {
480            total_processed: 2,
481            valid: vec!["key1".to_string(), "key2".to_string()],
482            errors: vec![],
483        };
484
485        // Use approximate comparison for floats
486
487        assert!((result.success_rate() - 100.0).abs() < EPSILON);
488    }
489
490    #[test]
491    fn test_replace_chars() {
492        let result = replace_chars("hello-world", |c| if c == '-' { Some('_') } else { None });
493        assert_eq!(result, "hello_world");
494
495        let result = replace_chars("hello_world", |c| if c == '-' { Some('_') } else { None });
496        assert!(matches!(result, Cow::Borrowed("hello_world")));
497    }
498
499    #[test]
500    fn test_replace_chars_fixed() {
501        let result = replace_chars("hello-world", |c| if c == '-' { Some('_') } else { None });
502        assert_eq!(result, "hello_world");
503
504        let result = replace_chars("hello_world", |c| if c == '-' { Some('_') } else { None });
505        assert!(matches!(result, Cow::Borrowed("hello_world")));
506
507        // Test with multiple replacements
508        let result = replace_chars("a-b-c", |c| if c == '-' { Some('_') } else { None });
509        assert_eq!(result, "a_b_c");
510
511        // Test with no replacements needed
512        let result = replace_chars("hello", |c| if c == 'x' { Some('y') } else { None });
513        assert!(matches!(result, Cow::Borrowed(_)));
514
515        // Test empty string
516        let result = replace_chars("", |c| if c == 'x' { Some('y') } else { None });
517        assert_eq!(result, "");
518    }
519}