Skip to main content

perfgate_types/
validation.rs

1//! Validation functions for benchmark names and configuration.
2//!
3//! This module provides validation logic for validating benchmark names
4//! according to a strict set of rules.
5
6/// Maximum allowed length (in bytes) for a benchmark name.
7///
8/// # Examples
9///
10/// ```
11/// use perfgate_types::validation::BENCH_NAME_MAX_LEN;
12///
13/// // Exactly at the limit – accepted
14/// let name = "a".repeat(BENCH_NAME_MAX_LEN);
15/// assert!(perfgate_types::validation::validate_bench_name(&name).is_ok());
16///
17/// // One byte over – rejected
18/// let too_long = "a".repeat(BENCH_NAME_MAX_LEN + 1);
19/// assert!(perfgate_types::validation::validate_bench_name(&too_long).is_err());
20/// ```
21pub use crate::error::BENCH_NAME_MAX_LEN;
22
23/// Regex pattern describing the set of valid benchmark-name characters.
24///
25/// The pattern allows lowercase ASCII letters, digits, underscores,
26/// dots, hyphens, and forward slashes.
27///
28/// # Examples
29///
30/// ```
31/// use perfgate_types::validation::BENCH_NAME_PATTERN;
32///
33/// assert_eq!(BENCH_NAME_PATTERN, r"^[a-z0-9_.\-/]+$");
34/// assert!(BENCH_NAME_PATTERN.starts_with('^'));
35/// assert!(BENCH_NAME_PATTERN.ends_with('$'));
36/// ```
37pub use crate::error::BENCH_NAME_PATTERN;
38
39/// Error type returned when a benchmark name fails validation.
40///
41/// # Examples
42///
43/// ```
44/// use perfgate_types::validation::{validate_bench_name, ValidationError};
45///
46/// // Empty name yields `ValidationError::Empty`
47/// let err = validate_bench_name("").unwrap_err();
48/// assert!(matches!(err, ValidationError::Empty));
49/// assert_eq!(err.name(), "");
50///
51/// // Uppercase letters yield `ValidationError::InvalidCharacters`
52/// let err = validate_bench_name("MyBench").unwrap_err();
53/// assert!(matches!(err, ValidationError::InvalidCharacters { .. }));
54/// assert_eq!(err.name(), "MyBench");
55///
56/// // Path traversal yields `ValidationError::PathTraversal`
57/// let err = validate_bench_name("../escape").unwrap_err();
58/// assert!(matches!(err, ValidationError::PathTraversal { .. }));
59///
60/// // Trailing slash yields `ValidationError::EmptySegment`
61/// let err = validate_bench_name("bench/").unwrap_err();
62/// assert!(matches!(err, ValidationError::EmptySegment { .. }));
63/// ```
64pub use crate::error::ValidationError;
65
66/// Validate a benchmark name against the naming rules.
67///
68/// Returns `Ok(())` when the name is valid, or a [`ValidationError`]
69/// describing why the name was rejected.
70///
71/// # Rules
72///
73/// 1. Must not be empty.
74/// 2. Must not exceed [`BENCH_NAME_MAX_LEN`] bytes.
75/// 3. Only lowercase ASCII, digits, `_`, `.`, `-`, and `/` are allowed.
76/// 4. No empty path segments (leading, trailing, or consecutive `/`).
77/// 5. No `.` or `..` path segments (path traversal).
78///
79/// # Examples
80///
81/// ```
82/// use perfgate_types::validation::{validate_bench_name, ValidationError};
83///
84/// // ── Valid names ──────────────────────────────────────
85/// assert!(validate_bench_name("my-bench").is_ok());
86/// assert!(validate_bench_name("bench_v2").is_ok());
87/// assert!(validate_bench_name("path/to/bench").is_ok());
88/// assert!(validate_bench_name("bench.v1").is_ok());
89/// assert!(validate_bench_name("123").is_ok());
90///
91/// // ── Invalid names ───────────────────────────────────
92/// // Empty
93/// assert!(matches!(
94///     validate_bench_name(""),
95///     Err(ValidationError::Empty),
96/// ));
97///
98/// // Uppercase
99/// assert!(matches!(
100///     validate_bench_name("MyBench"),
101///     Err(ValidationError::InvalidCharacters { .. }),
102/// ));
103///
104/// // Path traversal
105/// assert!(matches!(
106///     validate_bench_name("../bench"),
107///     Err(ValidationError::PathTraversal { .. }),
108/// ));
109///
110/// // Trailing slash (empty segment)
111/// assert!(matches!(
112///     validate_bench_name("bench/"),
113///     Err(ValidationError::EmptySegment { .. }),
114/// ));
115/// ```
116pub use crate::error::validate_bench_name;
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121
122    #[test]
123    fn valid_names_basic() {
124        assert!(validate_bench_name("my-bench").is_ok());
125        assert!(validate_bench_name("bench_a").is_ok());
126        assert!(validate_bench_name("path/to/bench").is_ok());
127        assert!(validate_bench_name("bench.v2").is_ok());
128        assert!(validate_bench_name("a").is_ok());
129        assert!(validate_bench_name("123").is_ok());
130    }
131
132    #[test]
133    fn valid_names_with_dots() {
134        assert!(validate_bench_name("bench.v1").is_ok());
135        assert!(validate_bench_name("v1.2.3").is_ok());
136        assert!(validate_bench_name("bench.test.final").is_ok());
137    }
138
139    #[test]
140    fn valid_names_with_hyphens() {
141        assert!(validate_bench_name("my-bench-name").is_ok());
142        assert!(validate_bench_name("bench-v1-final").is_ok());
143    }
144
145    #[test]
146    fn valid_names_with_underscores() {
147        assert!(validate_bench_name("bench_name").is_ok());
148        assert!(validate_bench_name("my_bench_v2").is_ok());
149    }
150
151    #[test]
152    fn valid_names_with_slashes() {
153        assert!(validate_bench_name("path/to/bench").is_ok());
154        assert!(validate_bench_name("a/b/c").is_ok());
155        assert!(validate_bench_name("category/subcategory/bench").is_ok());
156    }
157
158    #[test]
159    fn valid_names_mixed_chars() {
160        assert!(validate_bench_name("my_bench-v1.2").is_ok());
161        assert!(validate_bench_name("path/to-bench_v2").is_ok());
162        assert!(validate_bench_name("a1-b2_c3.d4/e5").is_ok());
163    }
164
165    #[test]
166    fn valid_names_single_char() {
167        assert!(validate_bench_name("a").is_ok());
168        assert!(validate_bench_name("z").is_ok());
169        assert!(validate_bench_name("0").is_ok());
170        assert!(validate_bench_name("9").is_ok());
171    }
172
173    #[test]
174    fn valid_names_all_digits() {
175        assert!(validate_bench_name("12345").is_ok());
176        assert!(validate_bench_name("0").is_ok());
177    }
178
179    #[test]
180    fn invalid_empty() {
181        assert!(matches!(
182            validate_bench_name(""),
183            Err(ValidationError::Empty)
184        ));
185    }
186
187    #[test]
188    fn invalid_uppercase() {
189        assert!(matches!(
190            validate_bench_name("MyBench"),
191            Err(ValidationError::InvalidCharacters { .. })
192        ));
193        assert!(matches!(
194            validate_bench_name("BENCH"),
195            Err(ValidationError::InvalidCharacters { .. })
196        ));
197        assert!(matches!(
198            validate_bench_name("benchA"),
199            Err(ValidationError::InvalidCharacters { .. })
200        ));
201        assert!(matches!(
202            validate_bench_name("Bench"),
203            Err(ValidationError::InvalidCharacters { .. })
204        ));
205    }
206
207    #[test]
208    fn invalid_special_characters() {
209        assert!(matches!(
210            validate_bench_name("bench|name"),
211            Err(ValidationError::InvalidCharacters { .. })
212        ));
213        assert!(matches!(
214            validate_bench_name("bench name"),
215            Err(ValidationError::InvalidCharacters { .. })
216        ));
217        assert!(matches!(
218            validate_bench_name("bench@name"),
219            Err(ValidationError::InvalidCharacters { .. })
220        ));
221        assert!(matches!(
222            validate_bench_name("bench#name"),
223            Err(ValidationError::InvalidCharacters { .. })
224        ));
225        assert!(matches!(
226            validate_bench_name("bench$name"),
227            Err(ValidationError::InvalidCharacters { .. })
228        ));
229        assert!(matches!(
230            validate_bench_name("bench%name"),
231            Err(ValidationError::InvalidCharacters { .. })
232        ));
233        assert!(matches!(
234            validate_bench_name("bench!name"),
235            Err(ValidationError::InvalidCharacters { .. })
236        ));
237    }
238
239    #[test]
240    fn invalid_path_traversal() {
241        assert!(matches!(
242            validate_bench_name("../bench"),
243            Err(ValidationError::PathTraversal { .. })
244        ));
245        assert!(matches!(
246            validate_bench_name("bench/../x"),
247            Err(ValidationError::PathTraversal { .. })
248        ));
249        assert!(matches!(
250            validate_bench_name("./bench"),
251            Err(ValidationError::PathTraversal { .. })
252        ));
253        assert!(matches!(
254            validate_bench_name("bench/."),
255            Err(ValidationError::PathTraversal { .. })
256        ));
257        assert!(matches!(
258            validate_bench_name(".."),
259            Err(ValidationError::PathTraversal { .. })
260        ));
261        assert!(matches!(
262            validate_bench_name("."),
263            Err(ValidationError::PathTraversal { .. })
264        ));
265    }
266
267    #[test]
268    fn invalid_empty_segments() {
269        assert!(matches!(
270            validate_bench_name("/bench"),
271            Err(ValidationError::EmptySegment { .. })
272        ));
273        assert!(matches!(
274            validate_bench_name("bench/"),
275            Err(ValidationError::EmptySegment { .. })
276        ));
277        assert!(matches!(
278            validate_bench_name("bench//x"),
279            Err(ValidationError::EmptySegment { .. })
280        ));
281        assert!(matches!(
282            validate_bench_name("/"),
283            Err(ValidationError::EmptySegment { .. })
284        ));
285        assert!(matches!(
286            validate_bench_name("a//b"),
287            Err(ValidationError::EmptySegment { .. })
288        ));
289        assert!(matches!(
290            validate_bench_name("//"),
291            Err(ValidationError::EmptySegment { .. })
292        ));
293    }
294
295    #[test]
296    fn invalid_too_long() {
297        let name_64 = "a".repeat(BENCH_NAME_MAX_LEN);
298        assert!(validate_bench_name(&name_64).is_ok());
299
300        let name_65 = "a".repeat(BENCH_NAME_MAX_LEN + 1);
301        let result = validate_bench_name(&name_65);
302        assert!(matches!(result, Err(ValidationError::TooLong { .. })));
303        if let Err(ValidationError::TooLong { max_len, .. }) = result {
304            assert_eq!(max_len, BENCH_NAME_MAX_LEN);
305        }
306    }
307
308    #[test]
309    fn error_name_accessor() {
310        let err = validate_bench_name("INVALID").unwrap_err();
311        assert_eq!(err.name(), "INVALID");
312
313        let err = validate_bench_name("").unwrap_err();
314        assert_eq!(err.name(), "");
315
316        let err = validate_bench_name(&"x".repeat(100)).unwrap_err();
317        assert!(err.name().starts_with('x'));
318    }
319
320    #[test]
321    fn error_display() {
322        let err = ValidationError::Empty;
323        assert!(err.to_string().contains("must not be empty"));
324
325        let err = ValidationError::TooLong {
326            name: "test".to_string(),
327            max_len: 64,
328        };
329        assert!(err.to_string().contains("exceeds maximum length"));
330
331        let err = ValidationError::InvalidCharacters {
332            name: "TEST".to_string(),
333        };
334        assert!(err.to_string().contains("invalid characters"));
335
336        let err = ValidationError::EmptySegment {
337            name: "/test".to_string(),
338        };
339        assert!(err.to_string().contains("empty path segment"));
340
341        let err = ValidationError::PathTraversal {
342            name: "../test".to_string(),
343            segment: "..".to_string(),
344        };
345        assert!(err.to_string().contains("path traversal"));
346    }
347
348    // ── Boundary value tests ──────────────────────────────────────────
349
350    #[test]
351    fn boundary_exact_max_len() {
352        let name = "a".repeat(BENCH_NAME_MAX_LEN);
353        assert!(validate_bench_name(&name).is_ok());
354    }
355
356    #[test]
357    fn boundary_one_over_max_len() {
358        let name = "a".repeat(BENCH_NAME_MAX_LEN + 1);
359        assert!(matches!(
360            validate_bench_name(&name),
361            Err(ValidationError::TooLong { max_len, .. }) if max_len == BENCH_NAME_MAX_LEN
362        ));
363    }
364
365    #[test]
366    fn boundary_one_under_max_len() {
367        let name = "a".repeat(BENCH_NAME_MAX_LEN - 1);
368        assert!(validate_bench_name(&name).is_ok());
369    }
370
371    #[test]
372    fn boundary_single_char_all_valid() {
373        for c in b'a'..=b'z' {
374            assert!(validate_bench_name(&String::from(c as char)).is_ok());
375        }
376        for c in b'0'..=b'9' {
377            assert!(validate_bench_name(&String::from(c as char)).is_ok());
378        }
379        assert!(validate_bench_name("_").is_ok());
380        assert!(validate_bench_name("-").is_ok());
381    }
382
383    #[test]
384    fn boundary_single_dot_is_path_traversal() {
385        assert!(matches!(
386            validate_bench_name("."),
387            Err(ValidationError::PathTraversal { .. })
388        ));
389    }
390
391    #[test]
392    fn boundary_double_dot_is_path_traversal() {
393        assert!(matches!(
394            validate_bench_name(".."),
395            Err(ValidationError::PathTraversal { .. })
396        ));
397    }
398
399    #[test]
400    fn boundary_single_slash_is_empty_segment() {
401        assert!(matches!(
402            validate_bench_name("/"),
403            Err(ValidationError::EmptySegment { .. })
404        ));
405    }
406
407    #[test]
408    fn boundary_max_len_with_slashes() {
409        // Build a name of exactly BENCH_NAME_MAX_LEN using segments
410        let segment = "ab";
411        let sep = "/";
412        let seg_with_sep = segment.len() + sep.len(); // 3
413        let count = BENCH_NAME_MAX_LEN / seg_with_sep; // 21 segments with slashes
414        let remainder = BENCH_NAME_MAX_LEN - (count * seg_with_sep);
415        let mut name: String = (0..count).map(|_| format!("{segment}/")).collect();
416        name.push_str(&"a".repeat(remainder));
417        assert_eq!(name.len(), BENCH_NAME_MAX_LEN);
418        assert!(validate_bench_name(&name).is_ok());
419    }
420
421    // ── Unicode handling tests ────────────────────────────────────────
422
423    #[test]
424    fn unicode_emoji_rejected() {
425        assert!(matches!(
426            validate_bench_name("bench-🚀"),
427            Err(ValidationError::InvalidCharacters { .. })
428        ));
429        assert!(matches!(
430            validate_bench_name("🔥"),
431            Err(ValidationError::InvalidCharacters { .. })
432        ));
433        assert!(matches!(
434            validate_bench_name("a😀b"),
435            Err(ValidationError::InvalidCharacters { .. })
436        ));
437    }
438
439    #[test]
440    fn unicode_cjk_rejected() {
441        assert!(matches!(
442            validate_bench_name("ベンチ"),
443            Err(ValidationError::InvalidCharacters { .. })
444        ));
445        assert!(matches!(
446            validate_bench_name("bench-测试"),
447            Err(ValidationError::InvalidCharacters { .. })
448        ));
449        assert!(matches!(
450            validate_bench_name("벤치마크"),
451            Err(ValidationError::InvalidCharacters { .. })
452        ));
453    }
454
455    #[test]
456    fn unicode_rtl_rejected() {
457        assert!(matches!(
458            validate_bench_name("مقعد"),
459            Err(ValidationError::InvalidCharacters { .. })
460        ));
461        assert!(matches!(
462            validate_bench_name("bench-בדיקה"),
463            Err(ValidationError::InvalidCharacters { .. })
464        ));
465    }
466
467    #[test]
468    fn unicode_accented_rejected() {
469        assert!(matches!(
470            validate_bench_name("café"),
471            Err(ValidationError::InvalidCharacters { .. })
472        ));
473        assert!(matches!(
474            validate_bench_name("naïve"),
475            Err(ValidationError::InvalidCharacters { .. })
476        ));
477        assert!(matches!(
478            validate_bench_name("über"),
479            Err(ValidationError::InvalidCharacters { .. })
480        ));
481    }
482
483    #[test]
484    fn unicode_zero_width_and_bom_rejected() {
485        // Zero-width space U+200B
486        assert!(matches!(
487            validate_bench_name("bench\u{200B}name"),
488            Err(ValidationError::InvalidCharacters { .. })
489        ));
490        // BOM U+FEFF
491        assert!(matches!(
492            validate_bench_name("\u{FEFF}bench"),
493            Err(ValidationError::InvalidCharacters { .. })
494        ));
495    }
496
497    #[test]
498    fn unicode_multibyte_length_check() {
499        // 16 x 4-byte emoji = 64 bytes, but invalid chars rejected first
500        let name: String = "🔥".repeat(16);
501        assert_eq!(name.len(), 64);
502        assert!(matches!(
503            validate_bench_name(&name),
504            Err(ValidationError::InvalidCharacters { .. })
505        ));
506    }
507
508    // ── Empty input tests ─────────────────────────────────────────────
509
510    #[test]
511    fn empty_string_returns_empty_error() {
512        assert!(matches!(
513            validate_bench_name(""),
514            Err(ValidationError::Empty)
515        ));
516    }
517
518    #[test]
519    fn whitespace_only_rejected() {
520        assert!(matches!(
521            validate_bench_name(" "),
522            Err(ValidationError::InvalidCharacters { .. })
523        ));
524        assert!(matches!(
525            validate_bench_name("   "),
526            Err(ValidationError::InvalidCharacters { .. })
527        ));
528        assert!(matches!(
529            validate_bench_name("\t"),
530            Err(ValidationError::InvalidCharacters { .. })
531        ));
532        assert!(matches!(
533            validate_bench_name("\n"),
534            Err(ValidationError::InvalidCharacters { .. })
535        ));
536        assert!(matches!(
537            validate_bench_name("\r\n"),
538            Err(ValidationError::InvalidCharacters { .. })
539        ));
540    }
541
542    #[test]
543    fn null_byte_rejected() {
544        assert!(matches!(
545            validate_bench_name("\0"),
546            Err(ValidationError::InvalidCharacters { .. })
547        ));
548        assert!(matches!(
549            validate_bench_name("bench\0name"),
550            Err(ValidationError::InvalidCharacters { .. })
551        ));
552    }
553
554    // ── Negative / adversarial input tests ────────────────────────────
555
556    #[test]
557    fn hyphen_prefixed_names_are_valid() {
558        // Leading hyphen is allowed since '-' is a valid character
559        assert!(validate_bench_name("-1").is_ok());
560        assert!(validate_bench_name("-bench").is_ok());
561        assert!(validate_bench_name("--double").is_ok());
562    }
563
564    #[test]
565    fn control_characters_rejected() {
566        for c in 0x00u8..=0x1F {
567            let name = format!("bench{}name", c as char);
568            assert!(
569                validate_bench_name(&name).is_err(),
570                "control char 0x{c:02x} should be rejected"
571            );
572        }
573        // DEL character (0x7F)
574        assert!(matches!(
575            validate_bench_name("bench\x7Fname"),
576            Err(ValidationError::InvalidCharacters { .. })
577        ));
578    }
579
580    #[test]
581    fn backslash_rejected() {
582        assert!(matches!(
583            validate_bench_name("bench\\name"),
584            Err(ValidationError::InvalidCharacters { .. })
585        ));
586        assert!(matches!(
587            validate_bench_name("path\\to\\bench"),
588            Err(ValidationError::InvalidCharacters { .. })
589        ));
590    }
591
592    #[test]
593    fn path_traversal_in_middle_segment() {
594        assert!(matches!(
595            validate_bench_name("a/../b"),
596            Err(ValidationError::PathTraversal { .. })
597        ));
598        assert!(matches!(
599            validate_bench_name("a/./b"),
600            Err(ValidationError::PathTraversal { .. })
601        ));
602        assert!(matches!(
603            validate_bench_name("a/b/../c"),
604            Err(ValidationError::PathTraversal { .. })
605        ));
606    }
607
608    #[test]
609    fn triple_dot_segment_is_valid() {
610        // "..." is not "." or ".." so it should be valid
611        assert!(validate_bench_name("...").is_ok());
612        assert!(validate_bench_name("a/.../b").is_ok());
613    }
614
615    // ── Large input tests ─────────────────────────────────────────────
616
617    #[test]
618    fn large_string_over_max_len() {
619        let name = "a".repeat(1000);
620        assert!(matches!(
621            validate_bench_name(&name),
622            Err(ValidationError::TooLong { .. })
623        ));
624    }
625
626    #[test]
627    fn large_string_way_over_max_len() {
628        let name = "b".repeat(100_000);
629        let result = validate_bench_name(&name);
630        assert!(
631            matches!(result, Err(ValidationError::TooLong { max_len, .. }) if max_len == BENCH_NAME_MAX_LEN)
632        );
633    }
634
635    #[test]
636    fn large_string_with_invalid_chars_over_max_len() {
637        // TooLong is checked before InvalidCharacters
638        let name = "X".repeat(BENCH_NAME_MAX_LEN + 1);
639        assert!(matches!(
640            validate_bench_name(&name),
641            Err(ValidationError::TooLong { .. })
642        ));
643    }
644
645    #[test]
646    fn large_number_of_segments() {
647        // Many valid segments within max length: "a/a/a/..."
648        let segments: Vec<&str> = (0..32).map(|_| "a").collect();
649        let name = segments.join("/");
650        if name.len() <= BENCH_NAME_MAX_LEN {
651            assert!(validate_bench_name(&name).is_ok());
652        } else {
653            assert!(matches!(
654                validate_bench_name(&name),
655                Err(ValidationError::TooLong { .. })
656            ));
657        }
658    }
659
660    #[test]
661    fn large_segment_at_boundary() {
662        // Single segment of exactly max length
663        let name = "z".repeat(BENCH_NAME_MAX_LEN);
664        assert!(validate_bench_name(&name).is_ok());
665    }
666
667    #[test]
668    fn error_preserves_name_for_large_input() {
669        let name = "x".repeat(BENCH_NAME_MAX_LEN + 10);
670        if let Err(ValidationError::TooLong {
671            name: err_name,
672            max_len,
673        }) = validate_bench_name(&name)
674        {
675            assert_eq!(err_name, name);
676            assert_eq!(max_len, BENCH_NAME_MAX_LEN);
677        } else {
678            panic!("expected TooLong error");
679        }
680    }
681
682    #[test]
683    fn bench_name_max_len_constant_is_64() {
684        assert_eq!(BENCH_NAME_MAX_LEN, 64);
685    }
686
687    #[test]
688    fn bench_name_pattern_matches_expected() {
689        assert_eq!(BENCH_NAME_PATTERN, r"^[a-z0-9_.\-/]+$");
690    }
691}
692
693#[cfg(test)]
694mod property_tests {
695    use super::*;
696    use proptest::prelude::*;
697
698    prop_compose! {
699        fn valid_bench_char()(
700            c in any::<u8>()
701                .prop_map(|b| {
702                    if b.is_ascii_lowercase() || b.is_ascii_digit() {
703                        char::from(b)
704                    } else {
705                        ['_', '-'][(b as usize) % 2]
706                    }
707                })
708        ) -> char {
709            c
710        }
711    }
712
713    prop_compose! {
714        fn valid_segment_char()(
715            c in any::<u8>()
716                .prop_map(|b| {
717                    if b.is_ascii_lowercase() || b.is_ascii_digit() {
718                        char::from(b)
719                    } else {
720                        ['_', '.', '-'][(b as usize) % 3]
721                    }
722                })
723        ) -> char {
724            c
725        }
726    }
727
728    prop_compose! {
729        fn valid_segment()(s in proptest::collection::vec(valid_segment_char(), 1..10)) -> String {
730            let seg: String = s.into_iter().collect();
731            if seg == "." || seg == ".." {
732                "a".to_string()
733            } else {
734                seg
735            }
736        }
737    }
738
739    prop_compose! {
740        fn valid_bench_name()(
741            segments in proptest::collection::vec(valid_segment(), 1..5)
742        ) -> String {
743            segments.join("/")
744        }
745    }
746
747    fn is_invalid_chars_error(result: &std::result::Result<(), ValidationError>) -> bool {
748        matches!(result, Err(ValidationError::InvalidCharacters { .. }))
749    }
750
751    fn is_too_long_error(result: &std::result::Result<(), ValidationError>) -> bool {
752        matches!(result, Err(ValidationError::TooLong { .. }))
753    }
754
755    fn is_empty_error(result: &std::result::Result<(), ValidationError>) -> bool {
756        matches!(result, Err(ValidationError::Empty))
757    }
758
759    fn is_empty_segment_error(result: &std::result::Result<(), ValidationError>) -> bool {
760        matches!(result, Err(ValidationError::EmptySegment { .. }))
761    }
762
763    fn is_path_traversal_error(result: &std::result::Result<(), ValidationError>) -> bool {
764        matches!(result, Err(ValidationError::PathTraversal { .. }))
765    }
766
767    proptest! {
768        #[test]
769        fn valid_chars_produce_ok(name in valid_bench_name()) {
770            prop_assume!(name.len() <= BENCH_NAME_MAX_LEN);
771            prop_assert!(validate_bench_name(&name).is_ok());
772        }
773
774        #[test]
775        fn uppercase_always_fails(name in "[a-z0-9_\\-]{1,30}[A-Z][a-z0-9_\\-]{1,30}") {
776            prop_assume!(name.len() <= BENCH_NAME_MAX_LEN);
777            let result = validate_bench_name(&name);
778            prop_assert!(is_invalid_chars_error(&result),
779                "Expected InvalidCharacters error for name '{}' with uppercase, got {:?}", name, result);
780        }
781
782        #[test]
783        fn length_boundary(
784            len in BENCH_NAME_MAX_LEN.saturating_sub(1)..=BENCH_NAME_MAX_LEN.saturating_add(1)
785        ) {
786            let name: String = "a".repeat(len);
787            let result = validate_bench_name(&name);
788            if len <= BENCH_NAME_MAX_LEN && len > 0 {
789                prop_assert!(result.is_ok());
790            } else if len > BENCH_NAME_MAX_LEN {
791                prop_assert!(is_too_long_error(&result));
792            } else {
793                prop_assert!(is_empty_error(&result));
794            }
795        }
796
797        #[test]
798        fn empty_string_fails(name in "") {
799            let _ = name;
800            let result = validate_bench_name("");
801            prop_assert!(is_empty_error(&result));
802        }
803
804        #[test]
805        fn double_slash_fails(prefix in valid_segment(), suffix in valid_segment()) {
806            prop_assume!(prefix != "." && prefix != "..");
807            prop_assume!(suffix != "." && suffix != "..");
808            let name = format!("{prefix}//{suffix}");
809            let result = validate_bench_name(&name);
810            prop_assert!(is_empty_segment_error(&result));
811        }
812
813        #[test]
814        fn leading_slash_fails(name in valid_bench_name()) {
815            let name_with_leading = format!("/{name}");
816            let result = validate_bench_name(&name_with_leading);
817            prop_assert!(is_empty_segment_error(&result));
818        }
819
820        #[test]
821        fn trailing_slash_fails(name in valid_bench_name()) {
822            let name_with_trailing = format!("{name}/");
823            let result = validate_bench_name(&name_with_trailing);
824            prop_assert!(is_empty_segment_error(&result));
825        }
826
827        #[test]
828        fn dot_segment_fails(suffix in "[a-z0-9_-]+") {
829            let name = format!("./{suffix}");
830            prop_assume!(!suffix.is_empty());
831            let result = validate_bench_name(&name);
832            prop_assert!(is_path_traversal_error(&result));
833        }
834
835        #[test]
836        fn double_dot_segment_fails(suffix in "[a-z0-9_-]+") {
837            let name = format!("../{suffix}");
838            prop_assume!(!suffix.is_empty());
839            let result = validate_bench_name(&name);
840            prop_assert!(is_path_traversal_error(&result));
841        }
842
843        #[test]
844        fn valid_char_roundtrip(c in valid_bench_char()) {
845            let name: String = std::iter::repeat_n(c, 10).collect();
846            prop_assume!(name.len() <= BENCH_NAME_MAX_LEN);
847            prop_assert!(validate_bench_name(&name).is_ok());
848        }
849
850        #[test]
851        fn special_invalid_chars(c in any::<char>()) {
852            prop_assume!(!c.is_ascii_lowercase());
853            prop_assume!(!c.is_ascii_digit());
854            prop_assume!(c != '_');
855            prop_assume!(c != '.');
856            prop_assume!(c != '-');
857            prop_assume!(c != '/');
858            prop_assume!(c != '\0');
859
860            let name = format!("bench{}test", c);
861            let result = validate_bench_name(&name);
862            prop_assert!(is_invalid_chars_error(&result));
863        }
864    }
865}