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}