Skip to main content

perfgate_validation/
lib.rs

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