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        #[allow(clippy::naive_bytecount)]
157        s.as_bytes().iter().filter(|&&b| b == byte).count()
158    } else {
159        s.chars().filter(|&c| c == target).count()
160    }
161}
162
163/// Find the position of the nth occurrence of a character
164///
165/// This function finds the byte position of the nth occurrence of a character
166/// in a string, useful for caching split positions.
167///
168/// # Arguments
169///
170/// * `s` - The string to search
171/// * `target` - The character to find
172/// * `n` - Which occurrence to find (0-based)
173///
174/// # Returns
175///
176/// The byte position of the nth occurrence, or `None` if not found
177#[cfg(test)]
178#[must_use]
179pub fn find_nth_char(s: &str, target: char, n: usize) -> Option<usize> {
180    let mut count = 0;
181    for (pos, c) in s.char_indices() {
182        if c == target {
183            if count == n {
184                return Some(pos);
185            }
186            count += 1;
187        }
188    }
189    None
190}
191
192// ============================================================================
193// NORMALIZATION UTILITIES
194// ============================================================================
195
196/// Trim whitespace and normalize case efficiently
197///
198/// This function combines trimming and case normalization in a single pass
199/// when possible.
200///
201/// # Arguments
202///
203/// * `s` - The string to normalize
204/// * `to_lowercase` - Whether to convert to lowercase
205///
206/// # Returns
207///
208/// A normalized string, borrowing when no changes are needed
209#[must_use]
210pub fn normalize_string(s: &str, to_lowercase: bool) -> Cow<'_, str> {
211    let trimmed = s.trim();
212    let needs_trim = trimmed.len() != s.len();
213    let needs_lowercase = to_lowercase && trimmed.chars().any(|c| c.is_ascii_uppercase());
214
215    match (needs_trim, needs_lowercase) {
216        (false, false) => Cow::Borrowed(s),
217        (true, false) => Cow::Borrowed(trimmed),
218        (_, true) => Cow::Owned(trimmed.to_ascii_lowercase()),
219    }
220}
221
222/// Replace characters efficiently with a mapping function
223///
224/// This function applies character replacements without unnecessary allocations
225/// when no replacements are needed. Uses a single-pass algorithm that borrows
226/// when no changes are needed and only allocates on first replacement found.
227///
228/// # Arguments
229///
230/// * `s` - The input string
231/// * `replacer` - Function that maps characters to their replacements
232///
233/// # Returns
234///
235/// A string with replacements applied, borrowing when no changes are needed
236pub fn replace_chars<F>(s: &str, replacer: F) -> Cow<'_, str>
237where
238    F: Fn(char) -> Option<char>,
239{
240    // Single-pass: only allocate when we find the first replacement
241    let mut chars = s.char_indices();
242    while let Some((i, c)) = chars.next() {
243        if let Some(replacement) = replacer(c) {
244            // Found first replacement — allocate and copy prefix, then continue
245            let mut result = String::with_capacity(s.len());
246            result.push_str(&s[..i]);
247            result.push(replacement);
248            for (_, c) in chars {
249                if let Some(r) = replacer(c) {
250                    result.push(r);
251                } else {
252                    result.push(c);
253                }
254            }
255            return Cow::Owned(result);
256        }
257    }
258    Cow::Borrowed(s)
259}
260
261// ============================================================================
262// VALIDATION UTILITIES
263// ============================================================================
264
265/// Fast character class checking using lookup tables
266///
267/// This module provides optimized character validation functions using
268/// precomputed lookup tables for common character classes.
269#[allow(clippy::cast_possible_truncation)]
270pub mod char_validation {
271    /// Lookup table for ASCII alphanumeric characters
272    const ASCII_ALPHANUMERIC: [bool; 128] = {
273        let mut table = [false; 128];
274        let mut i = 0;
275        while i < 128 {
276            table[i] = matches!(i as u8, b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z');
277            i += 1;
278        }
279        table
280    };
281
282    const KEY_CHARS: [bool; 128] = {
283        let mut table = [false; 128];
284        let mut i = 0;
285        while i < 128 {
286            table[i] =
287                matches!(i as u8,  b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z' | b'_' | b'-' | b'.');
288            i += 1;
289        }
290        table
291    };
292
293    /// Fast check if a character is ASCII alphanumeric
294    #[inline]
295    #[must_use]
296    pub fn is_ascii_alphanumeric_fast(c: char) -> bool {
297        if c.is_ascii() {
298            ASCII_ALPHANUMERIC[c as u8 as usize]
299        } else {
300            false
301        }
302    }
303
304    /// Fast check if a character is allowed in keys
305    #[inline]
306    #[must_use]
307    pub fn is_key_char_fast(c: char) -> bool {
308        if c.is_ascii() {
309            KEY_CHARS[c as u8 as usize]
310        } else {
311            false
312        }
313    }
314
315    /// Check if a character is a common separator
316    #[inline]
317    #[must_use]
318    pub fn is_separator(c: char) -> bool {
319        matches!(c, '_' | '-' | '.' | '/' | ':' | '|')
320    }
321
322    /// Check if a character is whitespace (space, tab, newline, etc.)
323    #[inline]
324    #[must_use]
325    pub fn is_whitespace_fast(c: char) -> bool {
326        matches!(c, ' ' | '\t' | '\n' | '\r' | '\x0B' | '\x0C')
327    }
328}
329
330// ============================================================================
331// FEATURE DETECTION
332// ============================================================================
333
334/// Returns the name of the active hash algorithm
335///
336/// The algorithm is selected at compile time based on feature flags:
337/// - `fast` — `GxHash` (requires AES-NI), falls back to `AHash`
338/// - `secure` — `AHash` (`DoS`-resistant)
339/// - `crypto` — Blake3 (cryptographic)
340/// - default — `DefaultHasher` (std) or FNV-1a (`no_std`)
341///
342/// # Examples
343///
344/// ```rust
345/// let algo = domain_key::hash_algorithm();
346/// println!("Using hash algorithm: {algo}");
347/// ```
348#[must_use]
349pub const fn hash_algorithm() -> &'static str {
350    #[cfg(feature = "fast")]
351    {
352        #[cfg(any(
353            all(target_arch = "x86_64", target_feature = "aes"),
354            all(target_arch = "aarch64", target_feature = "aes")
355        ))]
356        return "GxHash";
357
358        #[cfg(not(any(
359            all(target_arch = "x86_64", target_feature = "aes"),
360            all(target_arch = "aarch64", target_feature = "aes")
361        )))]
362        return "AHash (GxHash fallback)";
363    }
364
365    #[cfg(all(feature = "secure", not(feature = "fast")))]
366    return "AHash";
367
368    #[cfg(all(feature = "crypto", not(any(feature = "fast", feature = "secure"))))]
369    return "Blake3";
370
371    #[cfg(not(any(feature = "fast", feature = "secure", feature = "crypto")))]
372    {
373        #[cfg(feature = "std")]
374        return "DefaultHasher";
375
376        #[cfg(not(feature = "std"))]
377        return "FNV-1a";
378    }
379}
380
381// ============================================================================
382// TESTS
383// ============================================================================
384
385#[cfg(test)]
386mod tests {
387    use super::*;
388
389    use crate::ValidationResult;
390    #[cfg(not(feature = "std"))]
391    use alloc::vec;
392    #[cfg(not(feature = "std"))]
393    use alloc::vec::Vec;
394
395    #[test]
396    fn test_add_prefix_suffix() {
397        let result = add_prefix_optimized("test", "prefix_");
398        assert_eq!(result, "prefix_test");
399
400        let result = add_suffix_optimized("test", "_suffix");
401        assert_eq!(result, "test_suffix");
402    }
403
404    #[test]
405    fn test_join_optimized() {
406        let parts = vec!["a", "b", "c"];
407        let result = join_optimized(&parts, "_");
408        assert_eq!(result, "a_b_c");
409
410        let empty: Vec<&str> = vec![];
411        let result = join_optimized(&empty, "_");
412        assert_eq!(result, "");
413
414        let single = vec!["alone"];
415        let result = join_optimized(&single, "_");
416        assert_eq!(result, "alone");
417    }
418
419    #[test]
420    fn test_char_validation() {
421        use char_validation::*;
422
423        assert!(is_ascii_alphanumeric_fast('a'));
424        assert!(is_ascii_alphanumeric_fast('Z'));
425        assert!(is_ascii_alphanumeric_fast('5'));
426        assert!(!is_ascii_alphanumeric_fast('_'));
427        assert!(!is_ascii_alphanumeric_fast('ñ'));
428
429        assert!(is_key_char_fast('a'));
430        assert!(is_key_char_fast('_'));
431        assert!(is_key_char_fast('-'));
432        assert!(is_key_char_fast('.'));
433        assert!(!is_key_char_fast(' '));
434
435        assert!(is_separator('_'));
436        assert!(is_separator('/'));
437        assert!(!is_separator('a'));
438
439        assert!(is_whitespace_fast(' '));
440        assert!(is_whitespace_fast('\t'));
441        assert!(!is_whitespace_fast('a'));
442    }
443
444    #[test]
445    fn test_string_utilities() {
446        assert_eq!(count_char("hello_world_test", '_'), 2);
447        assert_eq!(count_char("no_underscores", '_'), 1);
448
449        assert_eq!(find_nth_char("a_b_c_d", '_', 0), Some(1));
450        assert_eq!(find_nth_char("a_b_c_d", '_', 1), Some(3));
451        assert_eq!(find_nth_char("a_b_c_d", '_', 2), Some(5));
452        assert_eq!(find_nth_char("a_b_c_d", '_', 3), None);
453    }
454
455    #[test]
456    fn test_normalize_string() {
457        let result = normalize_string("  Hello  ", true);
458        assert_eq!(result, "hello");
459
460        let result = normalize_string("hello", true);
461        assert_eq!(result, "hello");
462
463        let result = normalize_string("  hello  ", false);
464        assert_eq!(result, "hello");
465
466        let result = normalize_string("hello", false);
467        assert!(matches!(result, Cow::Borrowed("hello")));
468    }
469
470    #[test]
471    fn test_float_comparison() {
472        const EPSILON: f64 = 1e-10;
473        let result = ValidationResult {
474            total_processed: 2,
475            valid: vec!["key1".to_string(), "key2".to_string()],
476            errors: vec![],
477        };
478
479        // Use approximate comparison for floats
480
481        assert!((result.success_rate() - 100.0).abs() < EPSILON);
482    }
483
484    #[test]
485    fn test_replace_chars() {
486        let result = replace_chars("hello-world", |c| if c == '-' { Some('_') } else { None });
487        assert_eq!(result, "hello_world");
488
489        let result = replace_chars("hello_world", |c| if c == '-' { Some('_') } else { None });
490        assert!(matches!(result, Cow::Borrowed("hello_world")));
491    }
492
493    #[test]
494    fn test_replace_chars_fixed() {
495        let result = replace_chars("hello-world", |c| if c == '-' { Some('_') } else { None });
496        assert_eq!(result, "hello_world");
497
498        let result = replace_chars("hello_world", |c| if c == '-' { Some('_') } else { None });
499        assert!(matches!(result, Cow::Borrowed("hello_world")));
500
501        // Test with multiple replacements
502        let result = replace_chars("a-b-c", |c| if c == '-' { Some('_') } else { None });
503        assert_eq!(result, "a_b_c");
504
505        // Test with no replacements needed
506        let result = replace_chars("hello", |c| if c == 'x' { Some('y') } else { None });
507        assert!(matches!(result, Cow::Borrowed(_)));
508
509        // Test empty string
510        let result = replace_chars("", |c| if c == 'x' { Some('y') } else { None });
511        assert_eq!(result, "");
512    }
513}