Skip to main content

libmagic_rs/
build_helpers.rs

1// Copyright (c) 2025-2026 the libmagic-rs contributors
2// SPDX-License-Identifier: Apache-2.0
3
4/// Build-time helpers for compiling magic rules.
5///
6/// This module contains functionality used by the build script to parse magic files
7/// and generate Rust code for built-in rules. It is extracted into a library module
8/// to enable comprehensive testing of the build process, including error cases.
9///
10/// Serialization logic is provided by [`crate::parser::codegen`], which is shared
11/// with `build.rs` to avoid duplication.
12use crate::error::ParseError;
13use crate::parser::parse_text_magic_file;
14
15// Re-export codegen functions used by tests
16#[cfg(test)]
17use crate::parser::codegen::{
18    format_byte_vec, format_number, generate_builtin_rules, serialize_children,
19    serialize_endianness, serialize_offset_spec, serialize_operator, serialize_type_kind,
20    serialize_value,
21};
22
23/// Parses a magic file and generates Rust code for the built-in rules.
24///
25/// This function wraps the parsing and code generation steps, providing a testable
26/// interface for the build script logic.
27///
28/// # Errors
29///
30/// Returns a `ParseError` if the magic file content is invalid or malformed.
31pub fn parse_and_generate_builtin_rules(magic_content: &str) -> Result<String, ParseError> {
32    let parsed = parse_text_magic_file(magic_content)?;
33    Ok(crate::parser::codegen::generate_builtin_rules(
34        &parsed.rules,
35    ))
36}
37
38/// Formats a parse error for display in build script output.
39///
40/// This function converts a `ParseError` into a human-readable message suitable
41/// for display when the build script fails.
42#[must_use]
43pub fn format_parse_error(error: &ParseError) -> String {
44    match error {
45        ParseError::InvalidSyntax { line, message } => {
46            format!("Error parsing builtin_rules.magic at line {line}: {message}")
47        }
48        ParseError::UnsupportedFeature { line, feature } => {
49            format!("Error parsing builtin_rules.magic at line {line}: {feature}")
50        }
51        ParseError::InvalidOffset { line, offset } => {
52            format!("Error parsing builtin_rules.magic at line {line}: {offset}")
53        }
54        ParseError::InvalidType { line, type_spec } => {
55            format!("Error parsing builtin_rules.magic at line {line}: {type_spec}")
56        }
57        ParseError::InvalidOperator { line, operator } => {
58            format!("Error parsing builtin_rules.magic at line {line}: {operator}")
59        }
60        ParseError::InvalidValue { line, value } => {
61            format!("Error parsing builtin_rules.magic at line {line}: {value}")
62        }
63        ParseError::UnsupportedFormat {
64            line,
65            format_type,
66            message,
67        } => format!("Error parsing builtin_rules.magic at line {line}: {format_type} {message}"),
68        ParseError::IoError(err) => {
69            format!("Error parsing builtin_rules.magic: I/O error: {err}")
70        }
71    }
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77    use crate::parser::ast::{
78        Endianness, IndirectAdjustmentOp, MagicRule, OffsetSpec, Operator, SearchFlags,
79        StringFlags, TypeKind, Value,
80    };
81    use crate::parser::codegen::format_string_literal;
82
83    #[test]
84    fn test_format_parse_error_invalid_syntax() {
85        let error = ParseError::InvalidSyntax {
86            line: 42,
87            message: "expected offset".to_string(),
88        };
89        let formatted = format_parse_error(&error);
90        assert!(formatted.contains("line 42"));
91        assert!(formatted.contains("expected offset"));
92        assert!(formatted.contains("builtin_rules.magic"));
93    }
94
95    #[test]
96    fn test_format_parse_error_unsupported_feature() {
97        let error = ParseError::UnsupportedFeature {
98            line: 10,
99            feature: "regex patterns".to_string(),
100        };
101        let formatted = format_parse_error(&error);
102        assert!(formatted.contains("line 10"));
103        assert!(formatted.contains("regex patterns"));
104    }
105
106    #[test]
107    fn test_format_parse_error_invalid_offset() {
108        let error = ParseError::InvalidOffset {
109            line: 5,
110            offset: "invalid offset spec".to_string(),
111        };
112        let formatted = format_parse_error(&error);
113        assert!(formatted.contains("line 5"));
114        assert!(formatted.contains("invalid offset spec"));
115    }
116
117    #[test]
118    fn test_format_parse_error_invalid_type() {
119        let error = ParseError::InvalidType {
120            line: 7,
121            type_spec: "unknown type".to_string(),
122        };
123        let formatted = format_parse_error(&error);
124        assert!(formatted.contains("line 7"));
125        assert!(formatted.contains("unknown type"));
126    }
127
128    #[test]
129    fn test_format_parse_error_invalid_operator() {
130        let error = ParseError::InvalidOperator {
131            line: 12,
132            operator: "bad operator".to_string(),
133        };
134        let formatted = format_parse_error(&error);
135        assert!(formatted.contains("line 12"));
136        assert!(formatted.contains("bad operator"));
137    }
138
139    #[test]
140    fn test_format_parse_error_invalid_value() {
141        let error = ParseError::InvalidValue {
142            line: 15,
143            value: "malformed value".to_string(),
144        };
145        let formatted = format_parse_error(&error);
146        assert!(formatted.contains("line 15"));
147        assert!(formatted.contains("malformed value"));
148    }
149
150    #[test]
151    fn test_serialize_offset_spec_absolute() {
152        let offset = OffsetSpec::Absolute(42);
153        let serialized = serialize_offset_spec(&offset);
154        assert_eq!(serialized, "OffsetSpec::Absolute(42)");
155    }
156
157    #[test]
158    fn test_serialize_offset_spec_relative() {
159        let offset = OffsetSpec::Relative(-10);
160        let serialized = serialize_offset_spec(&offset);
161        assert_eq!(serialized, "OffsetSpec::Relative(-10)");
162    }
163
164    #[test]
165    fn test_serialize_offset_spec_from_end() {
166        let offset = OffsetSpec::FromEnd(-16);
167        let serialized = serialize_offset_spec(&offset);
168        assert_eq!(serialized, "OffsetSpec::FromEnd(-16)");
169    }
170
171    #[test]
172    fn test_serialize_type_kind_byte() {
173        let signed = TypeKind::Byte { signed: true };
174        assert_eq!(
175            serialize_type_kind(&signed),
176            "TypeKind::Byte { signed: true }"
177        );
178        let unsigned = TypeKind::Byte { signed: false };
179        assert_eq!(
180            serialize_type_kind(&unsigned),
181            "TypeKind::Byte { signed: false }"
182        );
183    }
184
185    #[test]
186    fn test_serialize_type_kind_short() {
187        let typ = TypeKind::Short {
188            endian: Endianness::Little,
189            signed: false,
190        };
191        let serialized = serialize_type_kind(&typ);
192        assert!(serialized.contains("TypeKind::Short"));
193        assert!(serialized.contains("Endianness::Little"));
194        assert!(serialized.contains("signed: false"));
195    }
196
197    #[test]
198    fn test_serialize_type_kind_long() {
199        let typ = TypeKind::Long {
200            endian: Endianness::Big,
201            signed: true,
202        };
203        let serialized = serialize_type_kind(&typ);
204        assert!(serialized.contains("TypeKind::Long"));
205        assert!(serialized.contains("Endianness::Big"));
206        assert!(serialized.contains("signed: true"));
207    }
208
209    #[test]
210    fn test_serialize_type_kind_quad() {
211        let typ = TypeKind::Quad {
212            endian: Endianness::Little,
213            signed: true,
214        };
215        let serialized = serialize_type_kind(&typ);
216        assert!(serialized.contains("TypeKind::Quad"));
217        assert!(serialized.contains("Endianness::Little"));
218        assert!(serialized.contains("signed: true"));
219
220        let typ2 = TypeKind::Quad {
221            endian: Endianness::Big,
222            signed: false,
223        };
224        let serialized2 = serialize_type_kind(&typ2);
225        assert!(serialized2.contains("TypeKind::Quad"));
226        assert!(serialized2.contains("Endianness::Big"));
227        assert!(serialized2.contains("signed: false"));
228    }
229
230    #[test]
231    fn test_serialize_type_kind_id3() {
232        let cases = [
233            (
234                TypeKind::Id3 {
235                    endian: Endianness::Little,
236                },
237                "TypeKind::Id3 { endian: Endianness::Little }",
238            ),
239            (
240                TypeKind::Id3 {
241                    endian: Endianness::Big,
242                },
243                "TypeKind::Id3 { endian: Endianness::Big }",
244            ),
245        ];
246        for (typ, expected) in &cases {
247            assert_eq!(serialize_type_kind(typ), *expected);
248        }
249
250        // audio:308's `(6.I+10)` pointer, as build-time codegen emits it.
251        let spec = OffsetSpec::Indirect {
252            base_offset: 6,
253            base_relative: false,
254            pointer_type: TypeKind::Id3 {
255                endian: Endianness::Big,
256            },
257            adjustment: 10,
258            adjustment_op: IndirectAdjustmentOp::Add,
259            result_relative: false,
260            endian: Endianness::Big,
261        };
262        assert_eq!(
263            serialize_offset_spec(&spec),
264            "OffsetSpec::Indirect { base_offset: 6, base_relative: false, \
265             pointer_type: TypeKind::Id3 { endian: Endianness::Big }, adjustment: 10, \
266             adjustment_op: IndirectAdjustmentOp::Add, result_relative: false, \
267             endian: Endianness::Big }"
268        );
269    }
270
271    #[test]
272    fn test_serialize_type_kind_float() {
273        let cases = [
274            (
275                TypeKind::Float {
276                    endian: Endianness::Native,
277                },
278                "TypeKind::Float { endian: Endianness::Native }",
279            ),
280            (
281                TypeKind::Float {
282                    endian: Endianness::Little,
283                },
284                "TypeKind::Float { endian: Endianness::Little }",
285            ),
286            (
287                TypeKind::Float {
288                    endian: Endianness::Big,
289                },
290                "TypeKind::Float { endian: Endianness::Big }",
291            ),
292        ];
293        for (typ, expected) in &cases {
294            assert_eq!(serialize_type_kind(typ), *expected);
295        }
296    }
297
298    #[test]
299    fn test_serialize_type_kind_double() {
300        let cases = [
301            (
302                TypeKind::Double {
303                    endian: Endianness::Native,
304                },
305                "TypeKind::Double { endian: Endianness::Native }",
306            ),
307            (
308                TypeKind::Double {
309                    endian: Endianness::Little,
310                },
311                "TypeKind::Double { endian: Endianness::Little }",
312            ),
313            (
314                TypeKind::Double {
315                    endian: Endianness::Big,
316                },
317                "TypeKind::Double { endian: Endianness::Big }",
318            ),
319        ];
320        for (typ, expected) in &cases {
321            assert_eq!(serialize_type_kind(typ), *expected);
322        }
323    }
324
325    #[test]
326    fn test_serialize_value_float() {
327        // Positive finite literal
328        let serialized = serialize_value(&Value::Float(3.125));
329        assert_eq!(serialized, "Value::Float(3.125)");
330
331        // Negative finite literal
332        let serialized = serialize_value(&Value::Float(-1.0));
333        assert_eq!(serialized, "Value::Float(-1.0)");
334
335        // Non-finite values produce valid Rust expressions
336        assert_eq!(
337            serialize_value(&Value::Float(f64::NAN)),
338            "Value::Float(f64::NAN)"
339        );
340        assert_eq!(
341            serialize_value(&Value::Float(f64::INFINITY)),
342            "Value::Float(f64::INFINITY)"
343        );
344        assert_eq!(
345            serialize_value(&Value::Float(f64::NEG_INFINITY)),
346            "Value::Float(f64::NEG_INFINITY)"
347        );
348    }
349
350    #[test]
351    fn test_serialize_type_kind_string() {
352        let typ1 = TypeKind::String {
353            max_length: None,
354            flags: StringFlags::default(),
355        };
356        let serialized1 = serialize_type_kind(&typ1);
357        assert_eq!(
358            serialized1,
359            "TypeKind::String { max_length: None, flags: crate::parser::ast::StringFlags::default() }"
360        );
361
362        let typ2 = TypeKind::String {
363            max_length: Some(256),
364            flags: StringFlags::default(),
365        };
366        let serialized2 = serialize_type_kind(&typ2);
367        assert_eq!(
368            serialized2,
369            "TypeKind::String { max_length: Some(256), flags: crate::parser::ast::StringFlags::default() }"
370        );
371
372        // Non-default flags emit the builder chain.
373        let typ3 = TypeKind::String {
374            max_length: None,
375            flags: StringFlags::default().with_ignore_lowercase(true),
376        };
377        let serialized3 = serialize_type_kind(&typ3);
378        assert_eq!(
379            serialized3,
380            "TypeKind::String { max_length: None, flags: crate::parser::ast::StringFlags::default().with_ignore_lowercase(true) }"
381        );
382    }
383
384    #[test]
385    fn test_serialize_type_kind_pstring() {
386        use crate::parser::ast::PStringLengthWidth;
387        let typ1 = TypeKind::PString {
388            max_length: None,
389            length_width: PStringLengthWidth::OneByte,
390            length_includes_itself: false,
391        };
392        let serialized1 = serialize_type_kind(&typ1);
393        assert_eq!(
394            serialized1,
395            "TypeKind::PString { max_length: None, length_width: PStringLengthWidth::OneByte, length_includes_itself: false }"
396        );
397
398        let typ2 = TypeKind::PString {
399            max_length: Some(128),
400            length_width: PStringLengthWidth::FourByteLE,
401            length_includes_itself: false,
402        };
403        let serialized2 = serialize_type_kind(&typ2);
404        assert_eq!(
405            serialized2,
406            "TypeKind::PString { max_length: Some(128), length_width: PStringLengthWidth::FourByteLE, length_includes_itself: false }"
407        );
408    }
409
410    #[test]
411    fn test_serialize_operator() {
412        assert_eq!(serialize_operator(&Operator::Equal), "Operator::Equal");
413        assert_eq!(
414            serialize_operator(&Operator::NotEqual),
415            "Operator::NotEqual"
416        );
417        assert_eq!(
418            serialize_operator(&Operator::LessThan),
419            "Operator::LessThan"
420        );
421        assert_eq!(
422            serialize_operator(&Operator::GreaterThan),
423            "Operator::GreaterThan"
424        );
425        assert_eq!(
426            serialize_operator(&Operator::LessEqual),
427            "Operator::LessEqual"
428        );
429        assert_eq!(
430            serialize_operator(&Operator::GreaterEqual),
431            "Operator::GreaterEqual"
432        );
433        assert_eq!(
434            serialize_operator(&Operator::BitwiseAnd),
435            "Operator::BitwiseAnd"
436        );
437        assert_eq!(
438            serialize_operator(&Operator::BitwiseAndMask(0xFF)),
439            "Operator::BitwiseAndMask(255)"
440        );
441    }
442
443    #[test]
444    fn test_serialize_value_uint() {
445        let value = Value::Uint(12345);
446        let serialized = serialize_value(&value);
447        assert_eq!(serialized, "Value::Uint(12_345)");
448    }
449
450    #[test]
451    fn test_serialize_value_int() {
452        let value = Value::Int(-100);
453        let serialized = serialize_value(&value);
454        assert!(serialized.contains("Value::Int"));
455    }
456
457    #[test]
458    fn test_serialize_value_bytes() {
459        let value = Value::Bytes(vec![0x7F, 0x45, 0x4C, 0x46]);
460        let serialized = serialize_value(&value);
461        assert_eq!(serialized, "Value::Bytes(vec![0x7f, 0x45, 0x4c, 0x46])");
462    }
463
464    #[test]
465    fn test_serialize_value_string() {
466        let value = Value::String("test".to_string());
467        let serialized = serialize_value(&value);
468        assert!(serialized.contains("Value::String"));
469        assert!(serialized.contains("test"));
470    }
471
472    #[test]
473    fn test_format_number_small() {
474        assert_eq!(format_number(42), "42");
475        assert_eq!(format_number(999), "999");
476        assert_eq!(format_number(9999), "9999");
477    }
478
479    #[test]
480    fn test_format_number_large() {
481        assert_eq!(format_number(10000), "10_000");
482        assert_eq!(format_number(123_456), "123_456");
483        assert_eq!(format_number(1_234_567_890), "1_234_567_890");
484    }
485
486    #[test]
487    fn test_serialize_endianness() {
488        assert_eq!(
489            serialize_endianness(Endianness::Little),
490            "Endianness::Little"
491        );
492        assert_eq!(serialize_endianness(Endianness::Big), "Endianness::Big");
493        assert_eq!(
494            serialize_endianness(Endianness::Native),
495            "Endianness::Native"
496        );
497    }
498
499    #[test]
500    fn test_format_byte_vec_empty() {
501        let result = format_byte_vec(&[]);
502        assert_eq!(result, "vec![]");
503    }
504
505    #[test]
506    fn test_format_byte_vec_single() {
507        let result = format_byte_vec(&[0x42]);
508        assert_eq!(result, "vec![0x42]");
509    }
510
511    #[test]
512    fn test_format_byte_vec_multiple() {
513        let result = format_byte_vec(&[0x12, 0x34, 0x56]);
514        assert_eq!(result, "vec![0x12, 0x34, 0x56]");
515    }
516
517    #[test]
518    fn test_format_string_literal() {
519        assert_eq!(format_string_literal("hello"), "\"hello\"");
520        assert_eq!(format_string_literal("test\n"), "\"test\\n\"");
521        assert_eq!(format_string_literal("quote\"here"), "\"quote\\\"here\"");
522    }
523
524    #[test]
525    fn test_generate_builtin_rules_empty() {
526        let rules: Vec<MagicRule> = vec![];
527        let generated = generate_builtin_rules(&rules);
528
529        assert!(generated.contains("LazyLock<Vec<MagicRule>>"));
530        assert!(generated.contains("vec![]") || generated.contains("vec!["));
531        assert!(generated.contains("use crate::parser::ast"));
532        assert!(generated.contains("use std::sync::LazyLock"));
533    }
534
535    #[test]
536    fn test_generate_builtin_rules_single_rule() {
537        let rule = MagicRule {
538            offset: OffsetSpec::Absolute(0),
539            typ: TypeKind::Byte { signed: true },
540            op: Operator::Equal,
541            value: Value::Uint(0x7F),
542            message: "test".to_string(),
543            children: vec![],
544            level: 0,
545            strength_modifier: None,
546            value_transform: None,
547        };
548
549        let generated = generate_builtin_rules(&[rule]);
550
551        assert!(generated.contains("OffsetSpec::Absolute(0)"));
552        assert!(generated.contains("TypeKind::Byte { signed: true }"));
553        assert!(generated.contains("Operator::Equal"));
554        assert!(generated.contains("Value::Uint(127)"));
555        assert!(generated.contains("test"));
556        assert!(generated.contains("level: 0"));
557    }
558
559    #[test]
560    fn test_serialize_children_empty() {
561        let result = serialize_children(&[], 4);
562        assert_eq!(result, "Vec::new()");
563    }
564
565    #[test]
566    fn test_serialize_children_with_nested_rule() {
567        let child = MagicRule {
568            offset: OffsetSpec::Absolute(4),
569            typ: TypeKind::Byte { signed: true },
570            op: Operator::Equal,
571            value: Value::Uint(1),
572            message: "child".to_string(),
573            children: vec![],
574            level: 1,
575            strength_modifier: None,
576            value_transform: None,
577        };
578
579        let result = serialize_children(&[child], 4);
580
581        assert!(result.contains("vec!["));
582        assert!(result.contains("OffsetSpec::Absolute(4)"));
583        assert!(result.contains("level: 1"));
584        assert!(result.contains("child"));
585    }
586
587    // Tests for invalid magic file parsing failure path
588    #[test]
589    fn test_parse_and_generate_invalid_syntax() {
590        let invalid_magic = "this is not valid magic syntax";
591        let result = parse_and_generate_builtin_rules(invalid_magic);
592
593        assert!(result.is_err());
594        let error = result.unwrap_err();
595        let formatted = format_parse_error(&error);
596        assert!(formatted.contains("builtin_rules.magic"));
597    }
598
599    #[test]
600    fn test_parse_and_generate_invalid_offset() {
601        let invalid_magic = "999999999999999999999 byte =0x7F ELF";
602        let result = parse_and_generate_builtin_rules(invalid_magic);
603
604        assert!(result.is_err());
605        let error = result.unwrap_err();
606        let formatted = format_parse_error(&error);
607        assert!(formatted.contains("builtin_rules.magic"));
608    }
609
610    #[test]
611    fn test_parse_and_generate_invalid_type() {
612        let invalid_magic = "0 invalidtype =0x7F test";
613        let result = parse_and_generate_builtin_rules(invalid_magic);
614
615        assert!(result.is_err());
616        let error = result.unwrap_err();
617        let formatted = format_parse_error(&error);
618        assert!(formatted.contains("builtin_rules.magic"));
619    }
620
621    #[test]
622    fn test_parse_and_generate_empty_input() {
623        let empty_magic = "";
624        let result = parse_and_generate_builtin_rules(empty_magic);
625
626        // Empty input should succeed with no rules
627        assert!(result.is_ok());
628        let generated = result.unwrap();
629        assert!(generated.contains("vec![]") || generated.contains("vec!["));
630    }
631
632    #[test]
633    fn test_parse_and_generate_valid_magic() {
634        let valid_magic = "0 byte =0x7F ELF executable";
635        let result = parse_and_generate_builtin_rules(valid_magic);
636
637        assert!(result.is_ok());
638        let generated = result.unwrap();
639        assert!(generated.contains("OffsetSpec::Absolute(0)"));
640        assert!(generated.contains("TypeKind::Byte { signed: true }"));
641        assert!(generated.contains("Value::Uint(127)"));
642        assert!(generated.contains("ELF executable"));
643    }
644
645    #[test]
646    fn test_parse_and_generate_malformed_value() {
647        let invalid_magic = "0 byte =notahexvalue test";
648        let result = parse_and_generate_builtin_rules(invalid_magic);
649
650        assert!(result.is_err());
651        let error = result.unwrap_err();
652        let formatted = format_parse_error(&error);
653        assert!(formatted.contains("builtin_rules.magic"));
654    }
655
656    /// Regression test for PR #215 Copilot review comment: the codegen
657    /// for `TypeKind::Regex` and `TypeKind::Search` must NOT emit
658    /// `.expect("nonzero")` (a panic marker banned by AGENTS.md for
659    /// library code, which is what the generated `builtin_rules.rs`
660    /// becomes). Instead it must emit
661    /// `.unwrap_or(NonZero::<..>::MIN)`, which preserves the invariant
662    /// expression without introducing a panic path.
663    #[test]
664    fn test_serialize_regex_codegen_has_no_expect_panic_marker() {
665        use crate::parser::ast::{RegexCount, RegexFlags};
666        use std::num::NonZeroU32;
667
668        let cases = [
669            TypeKind::Regex {
670                flags: RegexFlags::default(),
671                count: RegexCount::Bytes(NonZeroU32::new(256).unwrap()),
672            },
673            TypeKind::Regex {
674                flags: RegexFlags::default(),
675                count: RegexCount::Lines(Some(NonZeroU32::new(3).unwrap())),
676            },
677        ];
678        for typ in &cases {
679            let generated = serialize_type_kind(typ);
680            assert!(
681                !generated.contains(".expect("),
682                "serialize_type_kind must not emit .expect() (AGENTS.md panic-marker ban); got:\n{generated}"
683            );
684            assert!(
685                generated.contains(".unwrap_or(::std::num::NonZeroU32::MIN)"),
686                "serialize_type_kind must emit .unwrap_or(NonZeroU32::MIN); got:\n{generated}"
687            );
688        }
689    }
690
691    #[test]
692    fn test_serialize_search_codegen_has_no_expect_panic_marker() {
693        use std::num::NonZeroUsize;
694
695        // Bounded `search/512`: `range` is `Option<NonZeroUsize>`, so
696        // `NonZeroUsize::new(512)` already yields `Some(_)` -- no panicking
697        // conversion and no `.unwrap_or` fallback is needed (the old
698        // `NonZeroUsize`-typed field required `.unwrap_or(MIN)`; the Option
699        // field does not).
700        let typ = TypeKind::Search {
701            range: NonZeroUsize::new(512),
702            flags: SearchFlags::default(),
703        };
704        let generated = serialize_type_kind(&typ);
705        assert!(
706            !generated.contains(".expect(") && !generated.contains(".unwrap()"),
707            "serialize_type_kind must not emit a panic marker (AGENTS.md ban); got:\n{generated}"
708        );
709        assert!(
710            generated.contains("range: ::std::num::NonZeroUsize::new(512)"),
711            "bounded search must serialize range as NonZeroUsize::new(N) (an Option); got:\n{generated}"
712        );
713
714        // Bare `search` (`range: None`) must serialize as a literal `None`.
715        let bare = TypeKind::Search {
716            range: None,
717            flags: SearchFlags::default(),
718        };
719        let bare_gen = serialize_type_kind(&bare);
720        assert!(
721            bare_gen.contains("range: None"),
722            "bare search must serialize range as None (scan-to-EOF); got:\n{bare_gen}"
723        );
724    }
725
726    /// Round-trip: a `TypeKind::Search` with default flags must emit a
727    /// fully-explicit `SearchFlags { ... }` struct literal in which every
728    /// field is set to `false`. U4 requirement: no `..Default::default()`
729    /// because the generated `builtin_rules.rs` is compiled by a separate
730    /// build-script compilation unit that does not import `Default`.
731    #[test]
732    fn test_serialize_type_kind_search_default_flags() {
733        use std::num::NonZeroUsize;
734
735        let typ = TypeKind::Search {
736            range: NonZeroUsize::new(256),
737            flags: SearchFlags::default(),
738        };
739        let generated = serialize_type_kind(&typ);
740
741        // Every field must appear explicitly with `false`.
742        for field in [
743            "compact_whitespace: false",
744            "compact_optional_whitespace: false",
745            "ignore_lowercase: false",
746            "ignore_uppercase: false",
747            "text_test: false",
748            "trim: false",
749            "bin_test: false",
750            "full_word: false",
751            "start_anchor: false",
752        ] {
753            assert!(
754                generated.contains(field),
755                "generated codegen missing `{field}` field; got:\n{generated}"
756            );
757        }
758
759        // No `..Default::default()` shortcut allowed.
760        assert!(
761            !generated.contains("..Default::default()"),
762            "generated codegen must not use `..Default::default()`; got:\n{generated}"
763        );
764
765        // Fully-qualified path so the build script does not need an import.
766        assert!(
767            generated.contains("crate::parser::ast::SearchFlags"),
768            "generated codegen must use fully-qualified path; got:\n{generated}"
769        );
770    }
771
772    /// Round-trip: a `TypeKind::Search` with non-default flags must emit
773    /// the corresponding `true` values for the set fields and `false` for
774    /// the rest. Exercises `start_anchor` (search-only `/s`) and
775    /// `ignore_lowercase` (`/c`) together.
776    #[test]
777    fn test_serialize_type_kind_search_with_flags() {
778        use std::num::NonZeroUsize;
779
780        let typ = TypeKind::Search {
781            range: NonZeroUsize::new(1024),
782            flags: SearchFlags::default()
783                .with_start_anchor(true)
784                .with_ignore_lowercase(true),
785        };
786        let generated = serialize_type_kind(&typ);
787
788        assert!(
789            generated.contains("start_anchor: true"),
790            "expected start_anchor: true; got:\n{generated}"
791        );
792        assert!(
793            generated.contains("ignore_lowercase: true"),
794            "expected ignore_lowercase: true; got:\n{generated}"
795        );
796
797        // Unset flags must still appear with `false`.
798        for field in [
799            "compact_whitespace: false",
800            "compact_optional_whitespace: false",
801            "ignore_uppercase: false",
802            "text_test: false",
803            "trim: false",
804            "bin_test: false",
805            "full_word: false",
806        ] {
807            assert!(
808                generated.contains(field),
809                "expected `{field}` in codegen; got:\n{generated}"
810            );
811        }
812
813        // Range value must still round-trip.
814        assert!(
815            generated.contains("1024"),
816            "expected range literal 1024; got:\n{generated}"
817        );
818    }
819
820    /// Verify the emitted codegen has no panic markers across every
821    /// `SearchFlags` permutation that combines default and non-default
822    /// flags. Extends the original `_default_only` regression test from
823    /// U1's placeholder pass to cover non-default flags too.
824    #[test]
825    fn test_search_codegen_has_no_panic_markers() {
826        use std::num::NonZeroUsize;
827
828        let cases = [
829            SearchFlags::default(),
830            SearchFlags::default().with_start_anchor(true),
831            SearchFlags::default().with_ignore_lowercase(true),
832            SearchFlags::default()
833                .with_compact_whitespace(true)
834                .with_full_word(true),
835            SearchFlags::default()
836                .with_start_anchor(true)
837                .with_ignore_lowercase(true)
838                .with_ignore_uppercase(true)
839                .with_compact_whitespace(true)
840                .with_compact_optional_whitespace(true)
841                .with_text_test(true)
842                .with_trim(true)
843                .with_bin_test(true)
844                .with_full_word(true),
845        ];
846
847        for flags in cases {
848            let typ = TypeKind::Search {
849                range: NonZeroUsize::new(64),
850                flags,
851            };
852            let generated = serialize_type_kind(&typ);
853
854            for forbidden in [".unwrap()", ".expect(", "panic!(", "unreachable!", "todo!"] {
855                assert!(
856                    !generated.contains(forbidden),
857                    "emitted codegen contains forbidden panic marker `{forbidden}`; got:\n{generated}"
858                );
859            }
860
861            // `range` is now `Option<NonZeroUsize>`, so `NonZeroUsize::new(64)`
862            // already yields `Some(_)` -- no `.unwrap_or(MIN)` fallback is
863            // needed (or emitted) any more. The panic-marker checks above are
864            // the real safety property.
865            assert!(
866                generated.contains("range: ::std::num::NonZeroUsize::new(64)"),
867                "bounded search must serialize range as an Option-yielding NonZeroUsize::new(N); got:\n{generated}"
868            );
869        }
870    }
871}