Skip to main content

protest_extras/generators/
domain.rs

1//! Domain-specific generators
2//!
3//! This module provides generators for common domain-specific data types:
4//! - Hexadecimal strings
5//! - Base64 encoded strings
6//! - File system paths
7//! - UUIDs (v4)
8//!
9//! All generators use std library only (no external dependencies).
10
11use protest::{Generator, GeneratorConfig};
12use rand::Rng;
13use std::path::{MAIN_SEPARATOR, PathBuf};
14
15// ============================================================================
16// Hex Generator
17// ============================================================================
18
19/// Generator for hexadecimal strings
20///
21/// Generates strings containing only hex characters (0-9, a-f)
22#[derive(Debug, Clone)]
23pub struct HexGenerator {
24    min_len: usize,
25    max_len: usize,
26    uppercase: bool,
27}
28
29impl HexGenerator {
30    /// Create a new hex generator with length bounds
31    pub fn new(min_len: usize, max_len: usize) -> Self {
32        Self {
33            min_len,
34            max_len,
35            uppercase: false,
36        }
37    }
38
39    /// Create a hex generator with uppercase letters (A-F instead of a-f)
40    pub fn uppercase(min_len: usize, max_len: usize) -> Self {
41        Self {
42            min_len,
43            max_len,
44            uppercase: true,
45        }
46    }
47
48    fn generate_hex_char(&self, rng: &mut dyn rand::RngCore) -> char {
49        let hex_chars = if self.uppercase {
50            b"0123456789ABCDEF"
51        } else {
52            b"0123456789abcdef"
53        };
54        hex_chars[rng.r#gen_range(0..hex_chars.len())] as char
55    }
56}
57
58impl Generator<String> for HexGenerator {
59    fn generate(&self, rng: &mut dyn rand::RngCore, _config: &GeneratorConfig) -> String {
60        let len = rng.r#gen_range(self.min_len..=self.max_len);
61        (0..len).map(|_| self.generate_hex_char(rng)).collect()
62    }
63
64    fn shrink(&self, value: &String) -> Box<dyn Iterator<Item = String>> {
65        let mut shrinks = Vec::new();
66
67        // Try empty string
68        if !value.is_empty() && self.min_len == 0 {
69            shrinks.push(String::new());
70        }
71
72        // Try all zeros
73        if *value != "0".repeat(value.len()) && value.len() >= self.min_len {
74            shrinks.push("0".repeat(value.len()));
75        }
76
77        // Try min length
78        if value.len() > self.min_len {
79            shrinks.push(value.chars().take(self.min_len).collect());
80        }
81
82        // Try removing characters
83        if value.len() > self.min_len {
84            for i in 0..value.len().min(3) {
85                let mut chars: Vec<char> = value.chars().collect();
86                chars.remove(i);
87                let shrunk: String = chars.into_iter().collect();
88                if shrunk.len() >= self.min_len {
89                    shrinks.push(shrunk);
90                }
91            }
92        }
93
94        Box::new(shrinks.into_iter())
95    }
96}
97
98// ============================================================================
99// Base64 Generator
100// ============================================================================
101
102/// Generator for Base64 encoded strings
103///
104/// Generates valid Base64 strings (standard encoding with padding)
105#[derive(Debug, Clone)]
106pub struct Base64Generator {
107    min_bytes: usize,
108    max_bytes: usize,
109}
110
111impl Base64Generator {
112    /// Create a new Base64 generator
113    ///
114    /// The min_bytes and max_bytes refer to the number of random bytes to encode,
115    /// not the length of the resulting Base64 string (which will be ~33% longer)
116    pub fn new(min_bytes: usize, max_bytes: usize) -> Self {
117        Self {
118            min_bytes,
119            max_bytes,
120        }
121    }
122
123    fn encode_base64(bytes: &[u8]) -> String {
124        const CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
125
126        let mut result = String::new();
127        let mut i = 0;
128
129        // Process 3 bytes at a time
130        while i + 2 < bytes.len() {
131            let b1 = bytes[i];
132            let b2 = bytes[i + 1];
133            let b3 = bytes[i + 2];
134
135            result.push(CHARS[(b1 >> 2) as usize] as char);
136            result.push(CHARS[(((b1 & 0x03) << 4) | (b2 >> 4)) as usize] as char);
137            result.push(CHARS[(((b2 & 0x0f) << 2) | (b3 >> 6)) as usize] as char);
138            result.push(CHARS[(b3 & 0x3f) as usize] as char);
139
140            i += 3;
141        }
142
143        // Handle remaining bytes with padding
144        if i < bytes.len() {
145            let b1 = bytes[i];
146            result.push(CHARS[(b1 >> 2) as usize] as char);
147
148            if i + 1 < bytes.len() {
149                let b2 = bytes[i + 1];
150                result.push(CHARS[(((b1 & 0x03) << 4) | (b2 >> 4)) as usize] as char);
151                result.push(CHARS[((b2 & 0x0f) << 2) as usize] as char);
152                result.push('=');
153            } else {
154                result.push(CHARS[((b1 & 0x03) << 4) as usize] as char);
155                result.push_str("==");
156            }
157        }
158
159        result
160    }
161}
162
163impl Generator<String> for Base64Generator {
164    fn generate(&self, rng: &mut dyn rand::RngCore, _config: &GeneratorConfig) -> String {
165        let num_bytes = rng.r#gen_range(self.min_bytes..=self.max_bytes);
166        let bytes: Vec<u8> = (0..num_bytes).map(|_| rng.r#gen()).collect();
167        Self::encode_base64(&bytes)
168    }
169
170    fn shrink(&self, value: &String) -> Box<dyn Iterator<Item = String>> {
171        let mut shrinks = Vec::new();
172
173        // Try empty (0 bytes)
174        if !value.is_empty() && self.min_bytes == 0 {
175            shrinks.push(String::new());
176        }
177
178        // Try min bytes
179        if !value.is_empty() && self.min_bytes > 0 {
180            let bytes: Vec<u8> = (0..self.min_bytes).map(|_| 0u8).collect();
181            shrinks.push(Self::encode_base64(&bytes));
182        }
183
184        // Try all zeros
185        // Estimate byte count from base64 length
186        let approx_bytes = (value.len() * 3) / 4;
187        if approx_bytes >= self.min_bytes {
188            let bytes = vec![0u8; approx_bytes];
189            let encoded = Self::encode_base64(&bytes);
190            if encoded != *value {
191                shrinks.push(encoded);
192            }
193        }
194
195        Box::new(shrinks.into_iter())
196    }
197}
198
199// ============================================================================
200// Path Generator
201// ============================================================================
202
203/// Generator for file system paths
204///
205/// Generates valid PathBuf instances with configurable depth and component lengths
206#[derive(Debug, Clone)]
207pub struct PathGenerator {
208    min_depth: usize,
209    max_depth: usize,
210    absolute: bool,
211}
212
213impl PathGenerator {
214    /// Create a new path generator
215    ///
216    /// depth refers to the number of path components (e.g., "a/b/c" has depth 3)
217    pub fn new(min_depth: usize, max_depth: usize) -> Self {
218        Self {
219            min_depth,
220            max_depth,
221            absolute: false,
222        }
223    }
224
225    /// Create a generator for absolute paths (starting with /)
226    pub fn absolute(min_depth: usize, max_depth: usize) -> Self {
227        Self {
228            min_depth,
229            max_depth,
230            absolute: true,
231        }
232    }
233
234    fn generate_component(&self, rng: &mut dyn rand::RngCore) -> String {
235        let len = rng.r#gen_range(1..=12);
236        let mut component = String::with_capacity(len);
237
238        // First char: letter or underscore
239        let first_chars = b"abcdefghijklmnopqrstuvwxyz_";
240        component.push(first_chars[rng.r#gen_range(0..first_chars.len())] as char);
241
242        // Remaining chars: letters, digits, underscore, hyphen, dot
243        let chars = b"abcdefghijklmnopqrstuvwxyz0123456789_-.";
244        for _ in 1..len {
245            component.push(chars[rng.r#gen_range(0..chars.len())] as char);
246        }
247
248        component
249    }
250}
251
252impl Generator<PathBuf> for PathGenerator {
253    fn generate(&self, rng: &mut dyn rand::RngCore, _config: &GeneratorConfig) -> PathBuf {
254        let depth = rng.r#gen_range(self.min_depth.max(1)..=self.max_depth.max(1));
255        let mut path = PathBuf::new();
256
257        if self.absolute {
258            // Use platform-appropriate root
259            // Unix: "/", Windows: "C:\", others: "/"
260            #[cfg(target_family = "unix")]
261            {
262                path.push(MAIN_SEPARATOR.to_string());
263            }
264            #[cfg(target_family = "windows")]
265            {
266                path.push(format!("C:{}", MAIN_SEPARATOR));
267            }
268            #[cfg(not(any(target_family = "unix", target_family = "windows")))]
269            {
270                path.push(MAIN_SEPARATOR.to_string());
271            }
272        }
273
274        for _ in 0..depth {
275            path.push(self.generate_component(rng));
276        }
277
278        path
279    }
280
281    fn shrink(&self, value: &PathBuf) -> Box<dyn Iterator<Item = PathBuf>> {
282        let mut shrinks = Vec::new();
283
284        let components: Vec<_> = value.components().collect();
285        let depth = components.len();
286
287        // Try min depth
288        if depth > self.min_depth {
289            let mut path = PathBuf::new();
290            if self.absolute {
291                path.push("/");
292            }
293            for component in components.iter().take(self.min_depth.max(1)) {
294                path.push(component);
295            }
296            shrinks.push(path);
297        }
298
299        // Try removing one component at a time
300        if depth > self.min_depth {
301            for i in (if self.absolute { 1 } else { 0 })..components.len().min(4) {
302                let mut path = PathBuf::new();
303                if self.absolute {
304                    path.push("/");
305                }
306                for (j, component) in components.iter().enumerate() {
307                    if j != i {
308                        path.push(component);
309                    }
310                }
311                if path.components().count() >= self.min_depth {
312                    shrinks.push(path);
313                }
314            }
315        }
316
317        Box::new(shrinks.into_iter())
318    }
319}
320
321// ============================================================================
322// UUID v4 Generator
323// ============================================================================
324
325/// Generator for UUID v4 (random UUIDs)
326///
327/// Generates valid UUID v4 strings in the standard format:
328/// xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
329///
330/// Where x is any hex digit and y is one of 8, 9, a, or b.
331/// Implementation uses std library only (no uuid crate dependency).
332#[derive(Debug, Clone, Copy, Default)]
333pub struct UuidV4Generator;
334
335impl UuidV4Generator {
336    /// Create a new UUID v4 generator
337    pub fn new() -> Self {
338        Self
339    }
340
341    fn format_uuid(bytes: [u8; 16]) -> String {
342        format!(
343            "{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
344            bytes[0],
345            bytes[1],
346            bytes[2],
347            bytes[3],
348            bytes[4],
349            bytes[5],
350            bytes[6],
351            bytes[7],
352            bytes[8],
353            bytes[9],
354            bytes[10],
355            bytes[11],
356            bytes[12],
357            bytes[13],
358            bytes[14],
359            bytes[15]
360        )
361    }
362}
363
364impl Generator<String> for UuidV4Generator {
365    fn generate(&self, rng: &mut dyn rand::RngCore, _config: &GeneratorConfig) -> String {
366        let mut bytes = [0u8; 16];
367        rng.fill_bytes(&mut bytes);
368
369        // Set version to 4 (bits 12-15 of time_hi_and_version)
370        bytes[6] = (bytes[6] & 0x0f) | 0x40;
371
372        // Set variant to RFC4122 (bits 6-7 of clock_seq_hi_and_reserved)
373        bytes[8] = (bytes[8] & 0x3f) | 0x80;
374
375        Self::format_uuid(bytes)
376    }
377
378    fn shrink(&self, _value: &String) -> Box<dyn Iterator<Item = String>> {
379        // UUIDs don't shrink meaningfully - they're random identifiers
380        // Return a few fixed UUIDs as minimal examples
381        let shrinks = vec![
382            "00000000-0000-4000-8000-000000000000".to_string(),
383            "00000000-0000-4000-8000-000000000001".to_string(),
384        ];
385        Box::new(shrinks.into_iter())
386    }
387}
388
389#[cfg(test)]
390mod tests {
391    use super::*;
392    use rand::thread_rng;
393
394    #[test]
395    fn test_hex_generator() {
396        let generator = HexGenerator::new(8, 16);
397        let mut rng = thread_rng();
398        let config = GeneratorConfig::default();
399
400        for _ in 0..10 {
401            let hex = generator.generate(&mut rng, &config);
402            assert!(hex.len() >= 8 && hex.len() <= 16);
403            assert!(hex.chars().all(|c| c.is_ascii_hexdigit()));
404            assert!(hex.chars().all(|c| !c.is_uppercase()));
405        }
406    }
407
408    #[test]
409    fn test_hex_generator_uppercase() {
410        let generator = HexGenerator::uppercase(8, 16);
411        let mut rng = thread_rng();
412        let config = GeneratorConfig::default();
413
414        for _ in 0..10 {
415            let hex = generator.generate(&mut rng, &config);
416            assert!(hex.chars().all(|c| c.is_ascii_hexdigit()));
417            // Check that if there are letters, they're uppercase
418            for c in hex.chars() {
419                if c.is_alphabetic() {
420                    assert!(c.is_uppercase());
421                }
422            }
423        }
424    }
425
426    #[test]
427    fn test_base64_generator() {
428        let generator = Base64Generator::new(6, 32);
429        let mut rng = thread_rng();
430        let config = GeneratorConfig::default();
431
432        for _ in 0..10 {
433            let b64 = generator.generate(&mut rng, &config);
434            assert!(!b64.is_empty());
435
436            // Check valid Base64 characters
437            for c in b64.chars() {
438                assert!(
439                    c.is_alphanumeric() || c == '+' || c == '/' || c == '=',
440                    "Invalid Base64 char: {}",
441                    c
442                );
443            }
444
445            // Check padding is only at the end
446            if b64.contains('=') {
447                assert!(b64.ends_with('=') || b64.ends_with("=="));
448            }
449        }
450    }
451
452    #[test]
453    fn test_base64_encode() {
454        // Test known Base64 encodings
455        assert_eq!(Base64Generator::encode_base64(b"hello"), "aGVsbG8=");
456        assert_eq!(
457            Base64Generator::encode_base64(b"hello world"),
458            "aGVsbG8gd29ybGQ="
459        );
460        assert_eq!(Base64Generator::encode_base64(b"a"), "YQ==");
461        assert_eq!(Base64Generator::encode_base64(b"ab"), "YWI=");
462        assert_eq!(Base64Generator::encode_base64(b"abc"), "YWJj");
463    }
464
465    #[test]
466    fn test_path_generator() {
467        let generator = PathGenerator::new(1, 4);
468        let mut rng = thread_rng();
469        let config = GeneratorConfig::default();
470
471        for _ in 0..10 {
472            let path = generator.generate(&mut rng, &config);
473            let components: Vec<_> = path.components().collect();
474            assert!(!components.is_empty() && components.len() <= 4);
475
476            // Check each component is valid
477            for component in path.iter() {
478                let s = component.to_string_lossy();
479                assert!(!s.is_empty());
480                assert!(!s.contains('/'));
481            }
482        }
483    }
484
485    #[test]
486    fn test_path_generator_absolute() {
487        let generator = PathGenerator::absolute(2, 4);
488        let mut rng = thread_rng();
489        let config = GeneratorConfig::default();
490
491        for _ in 0..10 {
492            let path = generator.generate(&mut rng, &config);
493            assert!(path.is_absolute());
494        }
495    }
496
497    #[test]
498    fn test_uuid_v4_generator() {
499        let generator = UuidV4Generator::new();
500        let mut rng = thread_rng();
501        let config = GeneratorConfig::default();
502
503        for _ in 0..10 {
504            let uuid = generator.generate(&mut rng, &config);
505
506            // Check format: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
507            let parts: Vec<&str> = uuid.split('-').collect();
508            assert_eq!(parts.len(), 5);
509            assert_eq!(parts[0].len(), 8);
510            assert_eq!(parts[1].len(), 4);
511            assert_eq!(parts[2].len(), 4);
512            assert_eq!(parts[3].len(), 4);
513            assert_eq!(parts[4].len(), 12);
514
515            // Check version is 4
516            assert_eq!(parts[2].chars().next().unwrap(), '4');
517
518            // Check variant is 8, 9, a, or b
519            let variant = parts[3].chars().next().unwrap();
520            assert!(
521                variant == '8' || variant == '9' || variant == 'a' || variant == 'b',
522                "Invalid variant: {}",
523                variant
524            );
525
526            // Check all characters are hex
527            for c in uuid.chars() {
528                assert!(c.is_ascii_hexdigit() || c == '-');
529            }
530        }
531    }
532
533    #[test]
534    fn test_uuid_v4_uniqueness() {
535        // Generate multiple UUIDs and ensure they're unique
536        let generator = UuidV4Generator::new();
537        let mut rng = thread_rng();
538        let config = GeneratorConfig::default();
539
540        let mut uuids = std::collections::HashSet::new();
541        for _ in 0..100 {
542            let uuid = generator.generate(&mut rng, &config);
543            assert!(uuids.insert(uuid), "Generated duplicate UUID");
544        }
545    }
546}