Skip to main content

libmagic_rs/parser/
types.rs

1// Copyright (c) 2025-2026 the libmagic-rs contributors
2// SPDX-License-Identifier: Apache-2.0
3
4//! Type keyword parsing for magic file types
5//!
6//! This module handles parsing and classification of magic file type keywords
7//! (byte, short, long, quad, string, etc.) into their corresponding [`TypeKind`]
8//! representations. It extracts the type keyword recognition from the grammar
9//! module to keep type-specific logic cohesive and manageable as new types are
10//! added.
11
12use nom::{IResult, Parser, branch::alt, bytes::complete::tag};
13
14use crate::parser::ast::{Endianness, MetaType, PStringLengthWidth, StringFlags, TypeKind};
15
16/// Error returned by [`type_keyword_to_kind`] when the supplied keyword is
17/// not a recognized magic type keyword.
18///
19/// This is a tight, structured error surfaced from a pure mapping function
20/// that has no access to line-number context. Callers that *do* have line
21/// context (e.g. the grammar layer wrapping a higher-level parse) can
22/// convert it into a richer [`crate::error::ParseError`] variant if needed.
23/// The struct is `#[non_exhaustive]` so future fields (e.g. suggested
24/// alternatives) can be added without a major version bump.
25///
26/// # Examples
27///
28/// ```
29/// use libmagic_rs::parser::types::{type_keyword_to_kind, UnknownTypeKeyword};
30///
31/// let err = type_keyword_to_kind("notarealtype").unwrap_err();
32/// assert_eq!(err.keyword, "notarealtype");
33/// ```
34#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
35#[non_exhaustive]
36#[error("unknown type keyword: {keyword}")]
37pub struct UnknownTypeKeyword {
38    /// The keyword string that was not recognized.
39    pub keyword: String,
40}
41
42/// Parse a type keyword from magic file input
43///
44/// Recognizes all supported type keywords and returns the matched keyword string.
45/// Type keywords are organized by bit width (64, 32, 16, 8 bits) with longest
46/// prefixes matched first within each group to avoid ambiguous partial matches.
47///
48/// # Supported Keywords
49///
50/// - 64-bit: `ubequad`, `ulequad`, `uquad`, `bequad`, `lequad`, `quad`
51/// - 32-bit: `ubelong`, `ulelong`, `ulong`, `belong`, `lelong`, `long`, `beid3`, `leid3`
52/// - 16-bit: `ubeshort`, `uleshort`, `ushort`, `beshort`, `leshort`, `short`
53/// - 8-bit: `ubyte`, `byte`
54/// - String: `pstring`, `string`
55///
56/// # Examples
57///
58/// ```
59/// use libmagic_rs::parser::types::parse_type_keyword;
60///
61/// let (rest, keyword) = parse_type_keyword("bequad rest").unwrap();
62/// assert_eq!(keyword, "bequad");
63/// assert_eq!(rest, " rest");
64/// ```
65///
66/// # Errors
67///
68/// Returns a nom parsing error if the input doesn't start with a known type keyword.
69pub fn parse_type_keyword(input: &str) -> IResult<&str, &str> {
70    alt((
71        // 64-bit types (6 branches)
72        alt((
73            tag("ubequad"),
74            tag("ulequad"),
75            tag("uquad"),
76            tag("bequad"),
77            tag("lequad"),
78            tag("quad"),
79        )),
80        // 32-bit types (6 branches)
81        alt((
82            tag("ubelong"),
83            tag("ulelong"),
84            tag("ulong"),
85            tag("belong"),
86            tag("lelong"),
87            tag("long"),
88            tag("beid3"),
89            tag("leid3"),
90        )),
91        // 16-bit types (6 branches)
92        alt((
93            tag("ubeshort"),
94            tag("uleshort"),
95            tag("ushort"),
96            tag("beshort"),
97            tag("leshort"),
98            tag("short"),
99        )),
100        // 8-bit types (2 branches)
101        alt((tag("ubyte"), tag("byte"))),
102        // Float/double types (6 branches)
103        alt((
104            tag("bedouble"),
105            tag("ledouble"),
106            tag("double"),
107            tag("befloat"),
108            tag("lefloat"),
109            tag("float"),
110        )),
111        // Date types -- 32-bit (date) and 64-bit (qdate)
112        alt((
113            tag("beqldate"),
114            tag("leqldate"),
115            tag("beqdate"),
116            tag("leqdate"),
117            tag("qldate"),
118            tag("qdate"),
119            tag("beldate"),
120            tag("leldate"),
121            tag("bedate"),
122            tag("ldate"),
123            tag("ledate"),
124            tag("date"),
125        )),
126        // String types (and regex/search, which share the string-type family).
127        //
128        // `lestring16`/`bestring16` are listed before `string` because nom
129        // tries each tag in order and we need the longer keyword to win when
130        // both could plausibly match -- in practice the prefixes (`lestring`,
131        // `bestring`) don't collide with anything else, but ordering by
132        // length is the safer pattern as more keywords are added.
133        alt((
134            tag("lestring16"),
135            tag("bestring16"),
136            tag("pstring"),
137            tag("search"),
138            tag("regex"),
139            tag("string"),
140        )),
141        // Meta / control-flow directives. `indirect` is listed first so the
142        // longest match is tried before `default`, `clear`, `name`, `use`;
143        // none of these collide with other supported keywords.
144        //
145        // `offset` is recognized here so the parser can accept magic files
146        // that use it (e.g. `searchbug.magic`). It maps to
147        // `TypeKind::Meta(MetaType::Offset)` and is fully evaluated by the
148        // engine: the resolved offset is emitted as `Value::Uint(position)`
149        // and participates in printf-style format substitution.
150        alt((
151            tag("indirect"),
152            tag("default"),
153            tag("offset"),
154            tag("clear"),
155            tag("name"),
156            tag("use"),
157        )),
158    ))
159    .parse(input)
160}
161
162/// Convert a type keyword string to its corresponding [`TypeKind`]
163///
164/// Maps a previously parsed type keyword (from [`parse_type_keyword`]) to the
165/// appropriate `TypeKind` variant with correct endianness and signedness settings.
166///
167/// # Conventions
168///
169/// - Unprefixed types are signed (libmagic default): `byte`, `short`, `long`, `quad`
170/// - `u` prefix indicates unsigned: `ubyte`, `ushort`, `ulong`, `uquad`
171/// - `be` prefix indicates big-endian: `beshort`, `belong`, `bequad`
172/// - `le` prefix indicates little-endian: `leshort`, `lelong`, `lequad`
173/// - No endian prefix means native endianness
174///
175/// Returns `Ok(None)` for `regex` and `search`, which cannot be constructed
176/// from the keyword alone -- they require suffix parsing (flags/count
177/// for regex, mandatory `NonZeroUsize` range for search) that only
178/// happens in `parser::grammar::parse_type_and_operator`. Callers that
179/// need a complete `TypeKind::Regex` or `TypeKind::Search` must build
180/// it directly in the grammar layer, not via this function.
181///
182/// # Examples
183///
184/// ```
185/// use libmagic_rs::parser::types::type_keyword_to_kind;
186/// use libmagic_rs::parser::ast::{TypeKind, Endianness};
187///
188/// assert_eq!(type_keyword_to_kind("byte"), Ok(Some(TypeKind::Byte { signed: true })));
189/// assert_eq!(type_keyword_to_kind("ubyte"), Ok(Some(TypeKind::Byte { signed: false })));
190/// assert_eq!(
191///     type_keyword_to_kind("beshort"),
192///     Ok(Some(TypeKind::Short { endian: Endianness::Big, signed: true }))
193/// );
194/// // regex/search require suffix parsing, so the keyword alone returns Ok(None).
195/// assert_eq!(type_keyword_to_kind("regex"), Ok(None));
196/// assert_eq!(type_keyword_to_kind("search"), Ok(None));
197/// // Unknown keywords return a structured error.
198/// assert!(type_keyword_to_kind("bogus").is_err());
199/// ```
200///
201/// # Returns
202///
203/// * `Ok(Some(TypeKind))` for fully-specified keywords (byte, short, long,
204///   quad, float, double, date, qdate, string, pstring and all their
205///   variants).
206/// * `Ok(None)` for suffix-required keywords (`regex`, `search`) which
207///   cannot be converted from the keyword alone -- the grammar layer
208///   builds their `TypeKind` directly after parsing the suffix.
209/// * `Err(UnknownTypeKeyword)` if `type_name` is not a recognized
210///   keyword. Under normal control flow this only happens when a caller
211///   bypasses [`parse_type_keyword`] (which is the only supported way
212///   to produce valid input for this function).
213///
214/// # Errors
215///
216/// Returns [`UnknownTypeKeyword`] when `type_name` is not one of the
217/// keywords recognized by [`parse_type_keyword`]. This replaces a prior
218/// `unreachable!` panic; library code must never panic on untrusted
219/// input, and the structured error lets callers translate the failure
220/// into their own error type (e.g. a nom parse error or a richer
221/// `ParseError::InvalidType`).
222pub fn type_keyword_to_kind(type_name: &str) -> Result<Option<TypeKind>, UnknownTypeKeyword> {
223    // `regex` and `search` cannot be constructed from the keyword alone.
224    // They require suffix parsing (flags/count for regex, mandatory
225    // `NonZeroUsize` range for search) which only happens in
226    // `parse_type_and_operator` in grammar/mod.rs. Returning `None`
227    // here makes the "keyword alone isn't enough" invariant
228    // type-enforced instead of relying on a placeholder that the
229    // grammar layer is expected to overwrite.
230    //
231    // `name` and `use` also return `Ok(None)` because their identifier
232    // suffix is parsed in the grammar layer, following the same
233    // "keyword alone isn't enough" pattern.
234    if matches!(type_name, "regex" | "search" | "name" | "use") {
235        return Ok(None);
236    }
237
238    // Meta / control-flow directives with no trailing operand are fully
239    // specified by the keyword alone. `offset` maps to
240    // `MetaType::Offset` which the engine evaluates by emitting the
241    // resolved file position as `Value::Uint` for format substitution.
242    match type_name {
243        "default" => return Ok(Some(TypeKind::Meta(MetaType::Default))),
244        "clear" => return Ok(Some(TypeKind::Meta(MetaType::Clear))),
245        "indirect" => return Ok(Some(TypeKind::Meta(MetaType::Indirect))),
246        "offset" => return Ok(Some(TypeKind::Meta(MetaType::Offset))),
247        _ => {}
248    }
249
250    if let Some(kind) = byte_family(type_name)
251        .or_else(|| short_family(type_name))
252        .or_else(|| long_family(type_name))
253        .or_else(|| quad_family(type_name))
254        .or_else(|| float_family(type_name))
255        .or_else(|| double_family(type_name))
256        .or_else(|| date_family(type_name))
257        .or_else(|| qdate_family(type_name))
258        .or_else(|| string_family(type_name))
259        .or_else(|| string16_family(type_name))
260    {
261        return Ok(Some(kind));
262    }
263
264    Err(UnknownTypeKeyword {
265        keyword: type_name.to_string(),
266    })
267}
268
269/// Map a byte-family keyword (`byte`, `ubyte`) to its `TypeKind`.
270fn byte_family(name: &str) -> Option<TypeKind> {
271    match name {
272        "byte" => Some(TypeKind::Byte { signed: true }),
273        "ubyte" => Some(TypeKind::Byte { signed: false }),
274        _ => None,
275    }
276}
277
278/// Map a short-family keyword (`short`/`ushort`/`beshort`/...) to its `TypeKind`.
279fn short_family(name: &str) -> Option<TypeKind> {
280    let (endian, signed) = match name {
281        "short" => (Endianness::Native, true),
282        "ushort" => (Endianness::Native, false),
283        "leshort" => (Endianness::Little, true),
284        "uleshort" => (Endianness::Little, false),
285        "beshort" => (Endianness::Big, true),
286        "ubeshort" => (Endianness::Big, false),
287        _ => return None,
288    };
289    Some(TypeKind::Short { endian, signed })
290}
291
292/// Map a long-family keyword (`long`/`ulong`/`belong`/...) to its `TypeKind`.
293fn long_family(name: &str) -> Option<TypeKind> {
294    let (endian, signed) = match name {
295        "long" => (Endianness::Native, true),
296        "ulong" => (Endianness::Native, false),
297        "lelong" => (Endianness::Little, true),
298        "ulelong" => (Endianness::Little, false),
299        "belong" => (Endianness::Big, true),
300        "ubelong" => (Endianness::Big, false),
301        "beid3" => {
302            return Some(TypeKind::Id3 {
303                endian: Endianness::Big,
304            });
305        }
306        "leid3" => {
307            return Some(TypeKind::Id3 {
308                endian: Endianness::Little,
309            });
310        }
311        _ => return None,
312    };
313    Some(TypeKind::Long { endian, signed })
314}
315
316/// Map a quad-family keyword (`quad`/`uquad`/`bequad`/...) to its `TypeKind`.
317fn quad_family(name: &str) -> Option<TypeKind> {
318    let (endian, signed) = match name {
319        "quad" => (Endianness::Native, true),
320        "uquad" => (Endianness::Native, false),
321        "lequad" => (Endianness::Little, true),
322        "ulequad" => (Endianness::Little, false),
323        "bequad" => (Endianness::Big, true),
324        "ubequad" => (Endianness::Big, false),
325        _ => return None,
326    };
327    Some(TypeKind::Quad { endian, signed })
328}
329
330/// Map a float-family keyword (`float`/`befloat`/`lefloat`) to its `TypeKind`.
331fn float_family(name: &str) -> Option<TypeKind> {
332    let endian = match name {
333        "float" => Endianness::Native,
334        "befloat" => Endianness::Big,
335        "lefloat" => Endianness::Little,
336        _ => return None,
337    };
338    Some(TypeKind::Float { endian })
339}
340
341/// Map a double-family keyword (`double`/`bedouble`/`ledouble`) to its `TypeKind`.
342fn double_family(name: &str) -> Option<TypeKind> {
343    let endian = match name {
344        "double" => Endianness::Native,
345        "bedouble" => Endianness::Big,
346        "ledouble" => Endianness::Little,
347        _ => return None,
348    };
349    Some(TypeKind::Double { endian })
350}
351
352/// Map a 32-bit date keyword (`date`/`ldate`/`bedate`/...) to its `TypeKind`.
353fn date_family(name: &str) -> Option<TypeKind> {
354    let (endian, utc) = match name {
355        "date" => (Endianness::Native, true),
356        "ldate" => (Endianness::Native, false),
357        "bedate" => (Endianness::Big, true),
358        "beldate" => (Endianness::Big, false),
359        "ledate" => (Endianness::Little, true),
360        "leldate" => (Endianness::Little, false),
361        _ => return None,
362    };
363    Some(TypeKind::Date { endian, utc })
364}
365
366/// Map a 64-bit date keyword (`qdate`/`qldate`/`beqdate`/...) to its `TypeKind`.
367fn qdate_family(name: &str) -> Option<TypeKind> {
368    let (endian, utc) = match name {
369        "qdate" => (Endianness::Native, true),
370        "qldate" => (Endianness::Native, false),
371        "beqdate" => (Endianness::Big, true),
372        "beqldate" => (Endianness::Big, false),
373        "leqdate" => (Endianness::Little, true),
374        "leqldate" => (Endianness::Little, false),
375        _ => return None,
376    };
377    Some(TypeKind::QDate { endian, utc })
378}
379
380/// Map a string-family keyword (`string`, `pstring`) to its `TypeKind`.
381///
382/// `pstring` defaults to a 1-byte length prefix; the grammar layer
383/// overwrites `length_width` / `length_includes_itself` from any
384/// trailing `/B`/`/H`/`/h`/`/L`/`/l`/`/J` suffix.
385fn string_family(name: &str) -> Option<TypeKind> {
386    match name {
387        "string" => Some(TypeKind::String {
388            max_length: None,
389            flags: StringFlags::default(),
390        }),
391        "pstring" => Some(TypeKind::PString {
392            max_length: None,
393            length_width: PStringLengthWidth::OneByte,
394            length_includes_itself: false,
395        }),
396        _ => None,
397    }
398}
399
400/// Map a 16-bit string keyword (`lestring16`/`bestring16`) to its `TypeKind`.
401///
402/// magic(5) defines only the explicitly-endian forms; bare `string16` is not
403/// a valid keyword.
404fn string16_family(name: &str) -> Option<TypeKind> {
405    match name {
406        "lestring16" => Some(TypeKind::String16 {
407            endian: Endianness::Little,
408        }),
409        "bestring16" => Some(TypeKind::String16 {
410            endian: Endianness::Big,
411        }),
412        _ => None,
413    }
414}
415
416#[cfg(test)]
417mod tests {
418    use super::*;
419    use crate::parser::ast::Endianness;
420
421    // ============================================================
422    // parse_type_keyword tests
423    // ============================================================
424
425    #[test]
426    fn test_parse_type_keyword_byte_variants() {
427        assert_eq!(parse_type_keyword("byte rest"), Ok((" rest", "byte")));
428        assert_eq!(parse_type_keyword("ubyte rest"), Ok((" rest", "ubyte")));
429    }
430
431    #[test]
432    fn test_parse_type_keyword_short_variants() {
433        let cases = [
434            ("short", "short"),
435            ("ushort", "ushort"),
436            ("leshort", "leshort"),
437            ("uleshort", "uleshort"),
438            ("beshort", "beshort"),
439            ("ubeshort", "ubeshort"),
440        ];
441        for (input, expected) in cases {
442            let input_with_rest = format!("{input} rest");
443            let (rest, keyword) = parse_type_keyword(&input_with_rest).unwrap();
444            assert_eq!(keyword, expected, "Failed for input: {input}");
445            assert_eq!(rest, " rest", "Wrong remaining for input: {input}");
446        }
447    }
448
449    #[test]
450    fn test_parse_type_keyword_long_variants() {
451        let cases = ["long", "ulong", "lelong", "ulelong", "belong", "ubelong"];
452        for input in cases {
453            let input_with_rest = format!("{input} rest");
454            let (rest, keyword) = parse_type_keyword(&input_with_rest).unwrap();
455            assert_eq!(keyword, input, "Failed for: {input}");
456            assert_eq!(rest, " rest");
457        }
458    }
459
460    #[test]
461    fn test_parse_type_keyword_quad_variants() {
462        let cases = ["quad", "uquad", "lequad", "ulequad", "bequad", "ubequad"];
463        for input in cases {
464            let input_with_rest = format!("{input} rest");
465            let (rest, keyword) = parse_type_keyword(&input_with_rest).unwrap();
466            assert_eq!(keyword, input, "Failed for: {input}");
467            assert_eq!(rest, " rest");
468        }
469    }
470
471    #[test]
472    fn test_parse_type_keyword_string() {
473        assert_eq!(parse_type_keyword("string rest"), Ok((" rest", "string")));
474    }
475
476    #[test]
477    fn test_parse_type_keyword_unknown() {
478        assert!(parse_type_keyword("unknown rest").is_err());
479    }
480
481    #[test]
482    fn test_parse_type_keyword_empty() {
483        assert!(parse_type_keyword("").is_err());
484    }
485
486    // ============================================================
487    // type_keyword_to_kind tests
488    // ============================================================
489
490    #[test]
491    fn test_type_keyword_to_kind_byte() {
492        assert_eq!(
493            type_keyword_to_kind("byte"),
494            Ok(Some(TypeKind::Byte { signed: true }))
495        );
496        assert_eq!(
497            type_keyword_to_kind("ubyte"),
498            Ok(Some(TypeKind::Byte { signed: false }))
499        );
500    }
501
502    #[test]
503    fn test_type_keyword_to_kind_short_endianness() {
504        assert_eq!(
505            type_keyword_to_kind("short"),
506            Ok(Some(TypeKind::Short {
507                endian: Endianness::Native,
508                signed: true
509            }))
510        );
511        assert_eq!(
512            type_keyword_to_kind("leshort"),
513            Ok(Some(TypeKind::Short {
514                endian: Endianness::Little,
515                signed: true
516            }))
517        );
518        assert_eq!(
519            type_keyword_to_kind("beshort"),
520            Ok(Some(TypeKind::Short {
521                endian: Endianness::Big,
522                signed: true
523            }))
524        );
525    }
526
527    #[test]
528    fn test_type_keyword_to_kind_unsigned_variants() {
529        assert_eq!(
530            type_keyword_to_kind("ushort"),
531            Ok(Some(TypeKind::Short {
532                endian: Endianness::Native,
533                signed: false
534            }))
535        );
536        assert_eq!(
537            type_keyword_to_kind("ulong"),
538            Ok(Some(TypeKind::Long {
539                endian: Endianness::Native,
540                signed: false
541            }))
542        );
543        assert_eq!(
544            type_keyword_to_kind("uquad"),
545            Ok(Some(TypeKind::Quad {
546                endian: Endianness::Native,
547                signed: false
548            }))
549        );
550    }
551
552    #[test]
553    fn test_type_keyword_to_kind_signed_defaults() {
554        // libmagic types are signed by default
555        assert_eq!(
556            type_keyword_to_kind("long"),
557            Ok(Some(TypeKind::Long {
558                endian: Endianness::Native,
559                signed: true
560            }))
561        );
562        assert_eq!(
563            type_keyword_to_kind("quad"),
564            Ok(Some(TypeKind::Quad {
565                endian: Endianness::Native,
566                signed: true
567            }))
568        );
569    }
570
571    #[test]
572    fn test_type_keyword_to_kind_string() {
573        assert_eq!(
574            type_keyword_to_kind("string"),
575            Ok(Some(TypeKind::String {
576                max_length: None,
577                flags: StringFlags::default()
578            }))
579        );
580    }
581
582    #[test]
583    fn test_parse_type_keyword_pstring() {
584        assert_eq!(parse_type_keyword("pstring rest"), Ok((" rest", "pstring")));
585    }
586
587    #[test]
588    fn test_type_keyword_to_kind_pstring() {
589        assert_eq!(
590            type_keyword_to_kind("pstring"),
591            Ok(Some(TypeKind::PString {
592                max_length: None,
593                length_width: PStringLengthWidth::OneByte,
594                length_includes_itself: false
595            }))
596        );
597    }
598
599    #[test]
600    fn test_type_keyword_to_kind_regex_and_search_return_none() {
601        // regex and search require suffix parsing (flags/count/range)
602        // that only happens in grammar/mod.rs. The keyword-to-kind
603        // function deliberately returns Ok(None) for them so callers
604        // are forced to use the grammar layer's direct construction.
605        assert_eq!(type_keyword_to_kind("regex"), Ok(None));
606        assert_eq!(type_keyword_to_kind("search"), Ok(None));
607    }
608
609    #[test]
610    fn test_type_keyword_to_kind_unknown_returns_err() {
611        // Unknown keywords produce a structured error instead of a
612        // panic. This path is not reachable through `parse_type_keyword`
613        // (which rejects unknown keywords before this function runs),
614        // but it is reachable if a caller constructs the input string
615        // directly, so the error must be representable.
616        let err = type_keyword_to_kind("nonexistent").expect_err("unknown keyword must return Err");
617        assert_eq!(err.keyword, "nonexistent");
618        // And the Display impl mentions the keyword for debuggability.
619        assert!(err.to_string().contains("nonexistent"));
620    }
621
622    #[test]
623    fn test_pstring_keyword_defaults_to_one_byte_width() {
624        // pstring keyword alone should produce OneByte length_width
625        // (suffix parsing is handled by grammar/mod.rs, not types.rs)
626        let kind = type_keyword_to_kind("pstring")
627            .expect("pstring is a known keyword")
628            .expect("pstring maps to Some(TypeKind)");
629        match kind {
630            TypeKind::PString {
631                max_length,
632                length_width,
633                length_includes_itself: _,
634            } => {
635                assert_eq!(
636                    max_length, None,
637                    "pstring default should have no max_length"
638                );
639                assert_eq!(
640                    length_width,
641                    PStringLengthWidth::OneByte,
642                    "pstring default should be OneByte"
643                );
644            }
645            _ => panic!("Expected TypeKind::PString, got {kind:?}"),
646        }
647    }
648
649    #[test]
650    fn test_pstring_keyword_does_not_consume_suffix() {
651        // parse_type_keyword should only consume "pstring", leaving suffix for grammar
652        let (rest, keyword) = parse_type_keyword("pstring/H =value").unwrap();
653        assert_eq!(keyword, "pstring");
654        assert_eq!(
655            rest, "/H =value",
656            "Suffix should remain unconsumed by type keyword parser"
657        );
658    }
659
660    #[test]
661    fn test_pstring_keyword_boundary() {
662        // pstring at exact boundary (no trailing input)
663        let (rest, keyword) = parse_type_keyword("pstring").unwrap();
664        assert_eq!(keyword, "pstring");
665        assert_eq!(rest, "");
666    }
667
668    #[test]
669    fn test_pstring_before_operator() {
670        // pstring followed by whitespace then operator
671        let (rest, keyword) = parse_type_keyword("pstring =hello").unwrap();
672        assert_eq!(keyword, "pstring");
673        assert_eq!(rest, " =hello");
674    }
675
676    #[test]
677    fn test_parse_type_keyword_string16_variants() {
678        let (rest, kw) = parse_type_keyword("lestring16 rest").unwrap();
679        assert_eq!(kw, "lestring16");
680        assert_eq!(rest, " rest");
681
682        let (rest, kw) = parse_type_keyword("bestring16 rest").unwrap();
683        assert_eq!(kw, "bestring16");
684        assert_eq!(rest, " rest");
685    }
686
687    #[test]
688    fn test_string16_keyword_to_kind() {
689        assert_eq!(
690            type_keyword_to_kind("lestring16"),
691            Ok(Some(TypeKind::String16 {
692                endian: Endianness::Little,
693            }))
694        );
695        assert_eq!(
696            type_keyword_to_kind("bestring16"),
697            Ok(Some(TypeKind::String16 {
698                endian: Endianness::Big,
699            }))
700        );
701    }
702
703    #[test]
704    fn test_roundtrip_all_keywords() {
705        // Verify that every keyword parsed by parse_type_keyword can be
706        // converted to a TypeKind by type_keyword_to_kind. Regex and
707        // search are excluded from the conversion side because they
708        // require suffix parsing in grammar/mod.rs and deliberately
709        // return None from `type_keyword_to_kind`; the keyword
710        // parser still recognizes them.
711        let convertible_keywords = [
712            "byte",
713            "ubyte",
714            "short",
715            "ushort",
716            "leshort",
717            "uleshort",
718            "beshort",
719            "ubeshort",
720            "long",
721            "ulong",
722            "lelong",
723            "ulelong",
724            "belong",
725            "beid3",
726            "leid3",
727            "ubelong",
728            "quad",
729            "uquad",
730            "lequad",
731            "ulequad",
732            "bequad",
733            "ubequad",
734            "float",
735            "befloat",
736            "lefloat",
737            "double",
738            "bedouble",
739            "ledouble",
740            "date",
741            "ldate",
742            "bedate",
743            "beldate",
744            "ledate",
745            "leldate",
746            "qdate",
747            "qldate",
748            "beqdate",
749            "beqldate",
750            "leqdate",
751            "leqldate",
752            "pstring",
753            "string",
754            "lestring16",
755            "bestring16",
756            "default",
757            "clear",
758            "indirect",
759            "offset",
760        ];
761        for keyword in convertible_keywords {
762            let (rest, parsed) = parse_type_keyword(keyword).unwrap();
763            assert_eq!(rest, "", "Keyword {keyword} should consume all input");
764            assert!(
765                type_keyword_to_kind(parsed).is_ok_and(|o| o.is_some()),
766                "{keyword} should map to Ok(Some(TypeKind))"
767            );
768        }
769        // regex, search, name, and use are recognized by parse_type_keyword
770        // but require grammar-layer suffix parsing (flags/count/range or an
771        // identifier) to construct their TypeKind. Verify both sides of
772        // this split invariant.
773        for keyword in ["regex", "search", "name", "use"] {
774            let (rest, parsed) = parse_type_keyword(keyword).unwrap();
775            assert_eq!(rest, "", "Keyword {keyword} should consume all input");
776            assert_eq!(
777                type_keyword_to_kind(parsed),
778                Ok(None),
779                "{keyword} should return Ok(None) from keyword-to-kind"
780            );
781        }
782    }
783}