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