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#[must_use]
92pub fn new_split_cache(s: &str, delimiter: char) -> core::str::Split<'_, char> {
93    s.split(delimiter)
94}
95
96/// Join string parts with a delimiter, optimizing for common cases
97///
98/// This function efficiently joins string parts using pre-calculated sizing
99/// to minimize allocations.
100///
101/// # Arguments
102///
103/// * `parts` - The string parts to join
104/// * `delimiter` - The delimiter to use between parts
105///
106/// # Returns
107///
108/// A new string with all parts joined
109#[must_use]
110pub fn join_optimized(parts: &[&str], delimiter: &str) -> String {
111    if parts.is_empty() {
112        return String::new();
113    }
114
115    if parts.len() == 1 {
116        return parts[0].to_string();
117    }
118
119    // Calculate total capacity needed
120    let total_content_len: usize = parts.iter().map(|s| s.len()).sum();
121    let delimiter_len = delimiter.len() * (parts.len().saturating_sub(1));
122    let total_capacity = total_content_len + delimiter_len;
123
124    let mut result = String::with_capacity(total_capacity);
125
126    for (i, part) in parts.iter().enumerate() {
127        if i > 0 {
128            result.push_str(delimiter);
129        }
130        result.push_str(part);
131    }
132
133    result
134}
135
136/// Efficiently check if a string contains only ASCII characters
137///
138/// This function provides a fast path for ASCII-only validation.
139///
140/// # Arguments
141///
142/// * `s` - The string to check
143///
144/// # Returns
145///
146/// `true` if the string contains only ASCII characters
147#[inline]
148#[must_use]
149pub fn is_ascii_only(s: &str) -> bool {
150    s.is_ascii()
151}
152
153/// Count the number of occurrences of a character in a string
154///
155/// This function efficiently counts character occurrences without
156/// allocating intermediate collections. Uses byte-level iteration
157/// for ASCII characters.
158///
159/// # Arguments
160///
161/// * `s` - The string to search
162/// * `target` - The character to count
163///
164/// # Returns
165///
166/// The number of times the character appears in the string
167#[must_use]
168pub fn count_char(s: &str, target: char) -> usize {
169    if target.is_ascii() {
170        let byte = target as u8;
171        #[expect(
172            clippy::naive_bytecount,
173            reason = "not worth adding bytecount dep for one use"
174        )]
175        s.as_bytes().iter().filter(|&&b| b == byte).count()
176    } else {
177        s.chars().filter(|&c| c == target).count()
178    }
179}
180
181/// Find the position of the nth occurrence of a character
182///
183/// This function finds the byte position of the nth occurrence of a character
184/// in a string, useful for caching split positions.
185///
186/// # Arguments
187///
188/// * `s` - The string to search
189/// * `target` - The character to find
190/// * `n` - Which occurrence to find (0-based)
191///
192/// # Returns
193///
194/// The byte position of the nth occurrence, or `None` if not found
195#[must_use]
196pub fn find_nth_char(s: &str, target: char, n: usize) -> Option<usize> {
197    let mut count = 0;
198    for (pos, c) in s.char_indices() {
199        if c == target {
200            if count == n {
201                return Some(pos);
202            }
203            count += 1;
204        }
205    }
206    None
207}
208
209// ============================================================================
210// NORMALIZATION UTILITIES
211// ============================================================================
212
213/// Trim whitespace and normalize case efficiently
214///
215/// This function combines trimming and case normalization in a single pass
216/// when possible.
217///
218/// # Arguments
219///
220/// * `s` - The string to normalize
221/// * `to_lowercase` - Whether to convert to lowercase
222///
223/// # Returns
224///
225/// A normalized string, borrowing when no changes are needed
226#[must_use]
227pub fn normalize_string(s: &str, to_lowercase: bool) -> Cow<'_, str> {
228    let trimmed = s.trim();
229    let needs_trim = trimmed.len() != s.len();
230    let needs_lowercase = to_lowercase && trimmed.chars().any(|c| c.is_ascii_uppercase());
231
232    match (needs_trim, needs_lowercase) {
233        (false, false) => Cow::Borrowed(s),
234        (true, false) => Cow::Owned(trimmed.to_string()),
235        (_, true) => Cow::Owned(trimmed.to_ascii_lowercase()),
236    }
237}
238
239/// Replace characters efficiently with a mapping function
240///
241/// This function applies character replacements without unnecessary allocations
242/// when no replacements are needed. Uses a single-pass algorithm that borrows
243/// when no changes are needed and only allocates on first replacement found.
244///
245/// # Arguments
246///
247/// * `s` - The input string
248/// * `replacer` - Function that maps characters to their replacements
249///
250/// # Returns
251///
252/// A string with replacements applied, borrowing when no changes are needed
253pub fn replace_chars<F>(s: &str, replacer: F) -> Cow<'_, str>
254where
255    F: Fn(char) -> Option<char>,
256{
257    // Single-pass: only allocate when we find the first replacement
258    let mut chars = s.char_indices();
259    while let Some((i, c)) = chars.next() {
260        if let Some(replacement) = replacer(c) {
261            // Found first replacement — allocate and copy prefix, then continue
262            let mut result = String::with_capacity(s.len());
263            result.push_str(&s[..i]);
264            result.push(replacement);
265            for (_, c) in chars {
266                if let Some(r) = replacer(c) {
267                    result.push(r);
268                } else {
269                    result.push(c);
270                }
271            }
272            return Cow::Owned(result);
273        }
274    }
275    Cow::Borrowed(s)
276}
277
278// ============================================================================
279// VALIDATION UTILITIES
280// ============================================================================
281
282/// Fast character class checking using lookup tables
283///
284/// This module provides optimized character validation functions using
285/// precomputed lookup tables for common character classes.
286#[expect(
287    clippy::cast_possible_truncation,
288    reason = "indices are within 0..128 ASCII range"
289)]
290pub mod char_validation {
291    /// Lookup table for ASCII alphanumeric characters
292    const ASCII_ALPHANUMERIC: [bool; 128] = {
293        let mut table = [false; 128];
294        let mut i = 0;
295        while i < 128 {
296            table[i] = matches!(i as u8, b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z');
297            i += 1;
298        }
299        table
300    };
301
302    const KEY_CHARS: [bool; 128] = {
303        let mut table = [false; 128];
304        let mut i = 0;
305        while i < 128 {
306            table[i] =
307                matches!(i as u8,  b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z' | b'_' | b'-' | b'.');
308            i += 1;
309        }
310        table
311    };
312
313    /// Fast check if a character is ASCII alphanumeric
314    #[inline]
315    #[must_use]
316    pub fn is_ascii_alphanumeric_fast(c: char) -> bool {
317        if c.is_ascii() {
318            ASCII_ALPHANUMERIC[c as u8 as usize]
319        } else {
320            false
321        }
322    }
323
324    /// Fast check if a character is allowed in keys
325    #[inline]
326    #[must_use]
327    pub fn is_key_char_fast(c: char) -> bool {
328        if c.is_ascii() {
329            KEY_CHARS[c as u8 as usize]
330        } else {
331            false
332        }
333    }
334
335    /// Check if a character is a common separator
336    #[inline]
337    #[must_use]
338    pub fn is_separator(c: char) -> bool {
339        matches!(c, '_' | '-' | '.' | '/' | ':' | '|')
340    }
341
342    /// Check if a character is whitespace (space, tab, newline, etc.)
343    #[inline]
344    #[must_use]
345    pub fn is_whitespace_fast(c: char) -> bool {
346        matches!(c, ' ' | '\t' | '\n' | '\r' | '\x0B' | '\x0C')
347    }
348}
349
350// ============================================================================
351// MEMORY UTILITIES
352// ============================================================================
353
354/// Calculate the memory usage of a string
355///
356/// This function calculates the total memory usage of a string, including
357/// heap allocation overhead.
358///
359/// # Arguments
360///
361/// * `s` - The string to measure
362///
363/// # Returns
364///
365/// The estimated memory usage in bytes
366#[must_use]
367pub fn string_memory_usage(s: &str) -> usize {
368    // Base string object size + heap allocation (if any)
369    core::mem::size_of::<String>() + s.len()
370}
371
372/// Calculate the memory usage of a `SmartString`
373///
374/// This function calculates the memory usage of a `SmartString`, accounting
375/// for inline vs heap storage.
376///
377/// # Arguments
378///
379/// * `s` - The string content to measure
380///
381/// # Returns
382///
383/// The estimated memory usage in bytes
384#[must_use]
385pub fn smart_string_memory_usage(s: &str) -> usize {
386    // SmartString uses inline storage for strings <= 23 bytes
387    if s.len() <= 23 {
388        core::mem::size_of::<SmartString>()
389    } else {
390        core::mem::size_of::<SmartString>() + s.len()
391    }
392}
393
394// ============================================================================
395// FEATURE DETECTION
396// ============================================================================
397
398/// Returns the name of the active hash algorithm
399///
400/// The algorithm is selected at compile time based on feature flags:
401/// - `fast` — `GxHash` (requires AES-NI), falls back to `AHash`
402/// - `secure` — `AHash` (`DoS`-resistant)
403/// - `crypto` — Blake3 (cryptographic)
404/// - default — `DefaultHasher` (std) or FNV-1a (`no_std`)
405///
406/// # Examples
407///
408/// ```rust
409/// let algo = domain_key::hash_algorithm();
410/// println!("Using hash algorithm: {algo}");
411/// ```
412#[must_use]
413pub const fn hash_algorithm() -> &'static str {
414    #[cfg(feature = "fast")]
415    {
416        #[cfg(any(
417            all(target_arch = "x86_64", target_feature = "aes"),
418            all(target_arch = "aarch64", target_feature = "aes")
419        ))]
420        return "GxHash";
421
422        #[cfg(not(any(
423            all(target_arch = "x86_64", target_feature = "aes"),
424            all(target_arch = "aarch64", target_feature = "aes")
425        )))]
426        return "AHash (GxHash fallback)";
427    }
428
429    #[cfg(all(feature = "secure", not(feature = "fast")))]
430    return "AHash";
431
432    #[cfg(all(feature = "crypto", not(any(feature = "fast", feature = "secure"))))]
433    return "Blake3";
434
435    #[cfg(not(any(feature = "fast", feature = "secure", feature = "crypto")))]
436    {
437        #[cfg(feature = "std")]
438        return "DefaultHasher";
439
440        #[cfg(not(feature = "std"))]
441        return "FNV-1a";
442    }
443}
444
445// ============================================================================
446// TESTS
447// ============================================================================
448
449#[cfg(test)]
450mod tests {
451    use super::*;
452
453    use crate::ValidationResult;
454    #[cfg(not(feature = "std"))]
455    use alloc::vec;
456    #[cfg(not(feature = "std"))]
457    use alloc::vec::Vec;
458
459    #[test]
460    fn test_add_prefix_suffix() {
461        let result = add_prefix_optimized("test", "prefix_");
462        assert_eq!(result, "prefix_test");
463
464        let result = add_suffix_optimized("test", "_suffix");
465        assert_eq!(result, "test_suffix");
466    }
467
468    #[test]
469    fn test_join_optimized() {
470        let parts = vec!["a", "b", "c"];
471        let result = join_optimized(&parts, "_");
472        assert_eq!(result, "a_b_c");
473
474        let empty: Vec<&str> = vec![];
475        let result = join_optimized(&empty, "_");
476        assert_eq!(result, "");
477
478        let single = vec!["alone"];
479        let result = join_optimized(&single, "_");
480        assert_eq!(result, "alone");
481    }
482
483    #[test]
484    fn test_char_validation() {
485        use char_validation::*;
486
487        assert!(is_ascii_alphanumeric_fast('a'));
488        assert!(is_ascii_alphanumeric_fast('Z'));
489        assert!(is_ascii_alphanumeric_fast('5'));
490        assert!(!is_ascii_alphanumeric_fast('_'));
491        assert!(!is_ascii_alphanumeric_fast('ñ'));
492
493        assert!(is_key_char_fast('a'));
494        assert!(is_key_char_fast('_'));
495        assert!(is_key_char_fast('-'));
496        assert!(is_key_char_fast('.'));
497        assert!(!is_key_char_fast(' '));
498
499        assert!(is_separator('_'));
500        assert!(is_separator('/'));
501        assert!(!is_separator('a'));
502
503        assert!(is_whitespace_fast(' '));
504        assert!(is_whitespace_fast('\t'));
505        assert!(!is_whitespace_fast('a'));
506    }
507
508    #[test]
509    fn test_string_utilities() {
510        assert!(is_ascii_only("hello"));
511        assert!(!is_ascii_only("héllo"));
512
513        assert_eq!(count_char("hello_world_test", '_'), 2);
514        assert_eq!(count_char("no_underscores", '_'), 1);
515
516        assert_eq!(find_nth_char("a_b_c_d", '_', 0), Some(1));
517        assert_eq!(find_nth_char("a_b_c_d", '_', 1), Some(3));
518        assert_eq!(find_nth_char("a_b_c_d", '_', 2), Some(5));
519        assert_eq!(find_nth_char("a_b_c_d", '_', 3), None);
520    }
521
522    #[test]
523    fn test_normalize_string() {
524        let result = normalize_string("  Hello  ", true);
525        assert_eq!(result, "hello");
526
527        let result = normalize_string("hello", true);
528        assert_eq!(result, "hello");
529
530        let result = normalize_string("  hello  ", false);
531        assert_eq!(result, "hello");
532
533        let result = normalize_string("hello", false);
534        assert!(matches!(result, Cow::Borrowed("hello")));
535    }
536
537    #[test]
538    fn test_memory_utilities() {
539        let s = "hello";
540        let usage = string_memory_usage(s);
541        assert!(usage >= s.len());
542    }
543
544    #[test]
545    fn test_float_comparison() {
546        const EPSILON: f64 = 1e-10;
547        let result = ValidationResult {
548            total_processed: 2,
549            valid: vec!["key1".to_string(), "key2".to_string()],
550            errors: vec![],
551        };
552
553        // Use approximate comparison for floats
554
555        assert!((result.success_rate() - 100.0).abs() < EPSILON);
556    }
557
558    #[test]
559    fn test_replace_chars() {
560        let result = replace_chars("hello-world", |c| if c == '-' { Some('_') } else { None });
561        assert_eq!(result, "hello_world");
562
563        let result = replace_chars("hello_world", |c| if c == '-' { Some('_') } else { None });
564        assert!(matches!(result, Cow::Borrowed("hello_world")));
565    }
566
567    #[test]
568    fn test_replace_chars_fixed() {
569        let result = replace_chars("hello-world", |c| if c == '-' { Some('_') } else { None });
570        assert_eq!(result, "hello_world");
571
572        let result = replace_chars("hello_world", |c| if c == '-' { Some('_') } else { None });
573        assert!(matches!(result, Cow::Borrowed("hello_world")));
574
575        // Test with multiple replacements
576        let result = replace_chars("a-b-c", |c| if c == '-' { Some('_') } else { None });
577        assert_eq!(result, "a_b_c");
578
579        // Test with no replacements needed
580        let result = replace_chars("hello", |c| if c == 'x' { Some('y') } else { None });
581        assert!(matches!(result, Cow::Borrowed(_)));
582
583        // Test empty string
584        let result = replace_chars("", |c| if c == 'x' { Some('y') } else { None });
585        assert_eq!(result, "");
586    }
587}