red_couch/
meta.rs

1//! Meta protocol parser and types for memcached meta commands.
2//!
3//! Meta commands use two-letter prefixes (mg, ms, md, ma, mn, me) and
4//! a flag-based system.  This module contains the pure parser and types;
5//! runtime handlers live in `ascii.rs` alongside the classic ASCII handlers.
6
7use super::ascii::validate_key;
8
9// ── Meta command types ──────────────────────────────────────────────
10
11/// A parsed meta protocol flag with optional token argument.
12#[derive(Debug, PartialEq, Clone)]
13pub(crate) struct MetaFlag {
14    pub ch: u8,
15    pub token: Option<String>,
16}
17
18/// Parsed meta command.
19#[derive(Debug, PartialEq)]
20pub(crate) enum MetaCmd<'a> {
21    /// mg <key> [flags]*
22    Get { key: &'a [u8], flags: Vec<MetaFlag> },
23    /// ms <key> <datalen> [flags]*
24    Set {
25        key: &'a [u8],
26        datalen: u32,
27        flags: Vec<MetaFlag>,
28    },
29    /// md <key> [flags]*
30    Delete { key: &'a [u8], flags: Vec<MetaFlag> },
31    /// ma <key> [flags]*
32    Arithmetic { key: &'a [u8], flags: Vec<MetaFlag> },
33    /// mn [flags]*
34    Noop { flags: Vec<MetaFlag> },
35    /// me <key> [flags]*
36    Debug { key: &'a [u8], flags: Vec<MetaFlag> },
37}
38
39#[derive(Debug)]
40pub(crate) enum MetaParseResult<'a> {
41    Ok(MetaCmd<'a>),
42    ClientError(String),
43    /// ms needs a data block of this size
44    NeedData(MetaCmd<'a>, u32),
45}
46
47// ── Flag parsing ────────────────────────────────────────────────────
48
49/// Parse meta flags from a slice of whitespace-separated tokens.
50/// Each flag is a single character optionally followed by a token argument
51/// (attached directly, no space).  E.g., "v", "T300", "Oopaque123".
52pub(crate) fn parse_meta_flags(tokens: &[&str]) -> Result<Vec<MetaFlag>, String> {
53    let mut flags = Vec::new();
54    for &tok in tokens {
55        if tok.is_empty() {
56            continue;
57        }
58        let bytes = tok.as_bytes();
59        let ch = bytes[0];
60        let token = if bytes.len() > 1 {
61            Some(
62                std::str::from_utf8(&bytes[1..])
63                    .map_err(|_| "bad flag token encoding".to_string())?
64                    .to_string(),
65            )
66        } else {
67            None
68        };
69        flags.push(MetaFlag { ch, token });
70    }
71    Ok(flags)
72}
73
74/// Check if a flag character is in the set.
75pub(crate) fn has_flag(flags: &[MetaFlag], ch: u8) -> bool {
76    flags.iter().any(|f| f.ch == ch)
77}
78
79/// Get the token for a flag, if present.
80pub(crate) fn get_flag_token(flags: &[MetaFlag], ch: u8) -> Option<&str> {
81    flags
82        .iter()
83        .find(|f| f.ch == ch)
84        .and_then(|f| f.token.as_deref())
85}
86
87// ── Supported flag validation ───────────────────────────────────────
88
89/// Flags that are silently ignored on all commands (proxy hints).
90const IGNORED_FLAGS: &[u8] = b"PL";
91
92/// Validate flags for mg. Returns error message if unsupported flag found.
93pub(crate) fn validate_mg_flags(flags: &[MetaFlag]) -> Result<(), String> {
94    const SUPPORTED: &[u8] = b"vcfksOqtT";
95    for f in flags {
96        if SUPPORTED.contains(&f.ch) || IGNORED_FLAGS.contains(&f.ch) {
97            continue;
98        }
99        return Err(format!("unsupported meta flag '{}'", f.ch as char));
100    }
101    validate_numeric_tokens(flags, b"T")?;
102    Ok(())
103}
104
105/// Validate flags for ms. Also validates M mode token and numeric tokens.
106pub(crate) fn validate_ms_flags(flags: &[MetaFlag]) -> Result<(), String> {
107    const SUPPORTED: &[u8] = b"FTCqOkM";
108    for f in flags {
109        if SUPPORTED.contains(&f.ch) || IGNORED_FLAGS.contains(&f.ch) {
110            continue;
111        }
112        return Err(format!("unsupported meta flag '{}'", f.ch as char));
113    }
114    // M requires a token; bare M is rejected.
115    validate_mode_token_present(flags)?;
116    // Validate M mode token: only S, E, A, P, R allowed.
117    if let Some(mode) = get_flag_token(flags, b'M') {
118        match mode {
119            "S" | "E" | "A" | "P" | "R" => {}
120            _ => return Err(format!("unsupported ms mode '{mode}'")),
121        }
122        // Append/Prepend don't support F (client flags) or T (TTL).
123        if (mode == "A" || mode == "P") && (has_flag(flags, b'F') || has_flag(flags, b'T')) {
124            return Err(format!("flags F/T not supported with ms mode '{mode}'"));
125        }
126    }
127    validate_numeric_tokens(flags, b"FTC")?;
128    Ok(())
129}
130
131/// Validate flags for md.
132pub(crate) fn validate_md_flags(flags: &[MetaFlag]) -> Result<(), String> {
133    const SUPPORTED: &[u8] = b"CqOk";
134    for f in flags {
135        if SUPPORTED.contains(&f.ch) || IGNORED_FLAGS.contains(&f.ch) {
136            continue;
137        }
138        return Err(format!("unsupported meta flag '{}'", f.ch as char));
139    }
140    validate_numeric_tokens(flags, b"C")?;
141    Ok(())
142}
143
144/// Validate flags for ma.
145pub(crate) fn validate_ma_flags(flags: &[MetaFlag]) -> Result<(), String> {
146    const SUPPORTED: &[u8] = b"DJNqOkvcM";
147    for f in flags {
148        if SUPPORTED.contains(&f.ch) || IGNORED_FLAGS.contains(&f.ch) {
149            continue;
150        }
151        return Err(format!("unsupported meta flag '{}'", f.ch as char));
152    }
153    // M requires a token; bare M is rejected.
154    validate_mode_token_present(flags)?;
155    // Validate M mode token: only I, D allowed.
156    if let Some(mode) = get_flag_token(flags, b'M') {
157        match mode {
158            "I" | "D" => {}
159            _ => return Err(format!("unsupported ma mode '{mode}'")),
160        }
161    }
162    validate_numeric_tokens(flags, b"DJN")?;
163    Ok(())
164}
165
166/// Validate flags for mn. Only O (opaque) is supported.
167pub(crate) fn validate_mn_flags(flags: &[MetaFlag]) -> Result<(), String> {
168    const SUPPORTED: &[u8] = b"O";
169    for f in flags {
170        if SUPPORTED.contains(&f.ch) || IGNORED_FLAGS.contains(&f.ch) {
171            continue;
172        }
173        return Err(format!("unsupported meta flag '{}'", f.ch as char));
174    }
175    Ok(())
176}
177
178/// Validate flags for me. Only O, k, q are supported.
179pub(crate) fn validate_me_flags(flags: &[MetaFlag]) -> Result<(), String> {
180    const SUPPORTED: &[u8] = b"Okq";
181    for f in flags {
182        if SUPPORTED.contains(&f.ch) || IGNORED_FLAGS.contains(&f.ch) {
183            continue;
184        }
185        return Err(format!("unsupported meta flag '{}'", f.ch as char));
186    }
187    Ok(())
188}
189
190/// Validate that flag tokens expected to be numeric have a valid unsigned integer token.
191/// Bare flags (no token) are rejected — numeric flags require an explicit value.
192fn validate_numeric_tokens(flags: &[MetaFlag], numeric_flags: &[u8]) -> Result<(), String> {
193    for f in flags {
194        if numeric_flags.contains(&f.ch) {
195            match &f.token {
196                Some(tok) => {
197                    if tok.parse::<u64>().is_err() {
198                        return Err(format!(
199                            "bad numeric value '{}' for flag '{}'",
200                            tok, f.ch as char
201                        ));
202                    }
203                }
204                None => {
205                    return Err(format!("flag '{}' requires a numeric token", f.ch as char));
206                }
207            }
208        }
209    }
210    Ok(())
211}
212
213/// Validate that an M (mode) flag has an explicit token. Bare M is rejected.
214fn validate_mode_token_present(flags: &[MetaFlag]) -> Result<(), String> {
215    for f in flags {
216        if f.ch == b'M' && f.token.is_none() {
217            return Err("flag 'M' requires a mode token".to_string());
218        }
219    }
220    Ok(())
221}
222
223// ── Command parsing ─────────────────────────────────────────────────
224
225/// Parse a meta command line (already stripped of \r\n).
226pub(crate) fn parse_meta_command(line: &[u8]) -> MetaParseResult<'_> {
227    let line_str = match std::str::from_utf8(line) {
228        Ok(s) => s,
229        Err(_) => return MetaParseResult::ClientError("bad command line format".into()),
230    };
231    let tokens: Vec<&str> = line_str.split_whitespace().collect();
232    if tokens.is_empty() {
233        return MetaParseResult::ClientError("bad command line format".into());
234    }
235
236    match tokens[0] {
237        "mn" => {
238            let flags = match parse_meta_flags(&tokens[1..]) {
239                Ok(f) => f,
240                Err(e) => return MetaParseResult::ClientError(e),
241            };
242            MetaParseResult::Ok(MetaCmd::Noop { flags })
243        }
244        "mg" | "me" | "md" | "ma" => {
245            if tokens.len() < 2 {
246                return MetaParseResult::ClientError("bad command line format".into());
247            }
248            let key = tokens[1].as_bytes();
249            if let Err(e) = validate_key(key) {
250                return MetaParseResult::ClientError(e);
251            }
252            let flags = match parse_meta_flags(&tokens[2..]) {
253                Ok(f) => f,
254                Err(e) => return MetaParseResult::ClientError(e),
255            };
256            match tokens[0] {
257                "mg" => MetaParseResult::Ok(MetaCmd::Get { key, flags }),
258                "me" => MetaParseResult::Ok(MetaCmd::Debug { key, flags }),
259                "md" => MetaParseResult::Ok(MetaCmd::Delete { key, flags }),
260                "ma" => MetaParseResult::Ok(MetaCmd::Arithmetic { key, flags }),
261                _ => unreachable!(),
262            }
263        }
264        "ms" => parse_meta_set(&tokens),
265        _ => MetaParseResult::ClientError("bad command line format".into()),
266    }
267}
268
269/// Parse `ms <key> <datalen> [flags]*`.
270fn parse_meta_set<'a>(tokens: &[&'a str]) -> MetaParseResult<'a> {
271    if tokens.len() < 3 {
272        return MetaParseResult::ClientError("bad command line format".into());
273    }
274    let key = tokens[1].as_bytes();
275    if let Err(e) = validate_key(key) {
276        return MetaParseResult::ClientError(e);
277    }
278    let datalen = match tokens[2].parse::<u32>() {
279        Ok(n) => n,
280        Err(_) => return MetaParseResult::ClientError("bad command line format".into()),
281    };
282    let flags = match parse_meta_flags(&tokens[3..]) {
283        Ok(f) => f,
284        Err(e) => return MetaParseResult::ClientError(e),
285    };
286    MetaParseResult::NeedData(
287        MetaCmd::Set {
288            key,
289            datalen,
290            flags,
291        },
292        datalen,
293    )
294}
295
296// ── Response helpers ────────────────────────────────────────────────
297
298/// Write the common flag echo suffix for a meta response.
299/// This writes the O (opaque) and k (key) flag echoes.
300pub(crate) fn write_meta_flag_echo(out: &mut Vec<u8>, flags: &[MetaFlag], key: &[u8]) {
301    if let Some(opaque) = get_flag_token(flags, b'O') {
302        out.push(b' ');
303        out.push(b'O');
304        out.extend_from_slice(opaque.as_bytes());
305    }
306    if has_flag(flags, b'k') {
307        out.push(b' ');
308        out.push(b'k');
309        out.extend_from_slice(key);
310    }
311}
312
313// ── Unit tests ──────────────────────────────────────────────────────
314
315#[cfg(test)]
316mod tests {
317    use super::*;
318
319    #[test]
320    fn parse_mn() {
321        match parse_meta_command(b"mn") {
322            MetaParseResult::Ok(MetaCmd::Noop { flags }) => assert!(flags.is_empty()),
323            other => panic!("expected Noop, got {other:?}"),
324        }
325    }
326
327    #[test]
328    fn parse_mn_with_opaque() {
329        match parse_meta_command(b"mn Otoken123") {
330            MetaParseResult::Ok(MetaCmd::Noop { flags }) => {
331                assert_eq!(flags.len(), 1);
332                assert_eq!(flags[0].ch, b'O');
333                assert_eq!(flags[0].token.as_deref(), Some("token123"));
334            }
335            other => panic!("expected Noop with O flag, got {other:?}"),
336        }
337    }
338
339    #[test]
340    fn parse_mg_basic() {
341        match parse_meta_command(b"mg mykey v f c") {
342            MetaParseResult::Ok(MetaCmd::Get { key, flags }) => {
343                assert_eq!(key, b"mykey");
344                assert_eq!(flags.len(), 3);
345                assert!(has_flag(&flags, b'v'));
346                assert!(has_flag(&flags, b'f'));
347                assert!(has_flag(&flags, b'c'));
348            }
349            other => panic!("expected Get, got {other:?}"),
350        }
351    }
352
353    #[test]
354    fn parse_mg_with_ttl_update() {
355        match parse_meta_command(b"mg mykey v T300") {
356            MetaParseResult::Ok(MetaCmd::Get { key, flags }) => {
357                assert_eq!(key, b"mykey");
358                assert!(has_flag(&flags, b'v'));
359                assert_eq!(get_flag_token(&flags, b'T'), Some("300"));
360            }
361            other => panic!("expected Get, got {other:?}"),
362        }
363    }
364
365    #[test]
366    fn parse_ms_basic() {
367        match parse_meta_command(b"ms mykey 5 F123 T300") {
368            MetaParseResult::NeedData(
369                MetaCmd::Set {
370                    key,
371                    datalen,
372                    flags,
373                },
374                5,
375            ) => {
376                assert_eq!(key, b"mykey");
377                assert_eq!(datalen, 5);
378                assert_eq!(get_flag_token(&flags, b'F'), Some("123"));
379                assert_eq!(get_flag_token(&flags, b'T'), Some("300"));
380            }
381            other => panic!("expected Set NeedData, got {other:?}"),
382        }
383    }
384
385    #[test]
386    fn parse_md_basic() {
387        match parse_meta_command(b"md mykey") {
388            MetaParseResult::Ok(MetaCmd::Delete { key, flags }) => {
389                assert_eq!(key, b"mykey");
390                assert!(flags.is_empty());
391            }
392            other => panic!("expected Delete, got {other:?}"),
393        }
394    }
395
396    #[test]
397    fn parse_ma_basic() {
398        match parse_meta_command(b"ma counter D10 MI") {
399            MetaParseResult::Ok(MetaCmd::Arithmetic { key, flags }) => {
400                assert_eq!(key, b"counter");
401                assert_eq!(get_flag_token(&flags, b'D'), Some("10"));
402                assert_eq!(get_flag_token(&flags, b'M'), Some("I"));
403            }
404            other => panic!("expected Arithmetic, got {other:?}"),
405        }
406    }
407
408    #[test]
409    fn parse_me_basic() {
410        match parse_meta_command(b"me mykey") {
411            MetaParseResult::Ok(MetaCmd::Debug { key, .. }) => {
412                assert_eq!(key, b"mykey");
413            }
414            other => panic!("expected Debug, got {other:?}"),
415        }
416    }
417
418    #[test]
419    fn flag_validation_mg() {
420        let flags = parse_meta_flags(&["v", "f", "c", "k", "s", "T300", "t", "q", "Oabc"]).unwrap();
421        assert!(validate_mg_flags(&flags).is_ok());
422
423        let bad = parse_meta_flags(&["v", "N30"]).unwrap();
424        assert!(validate_mg_flags(&bad).is_err());
425    }
426
427    #[test]
428    fn flag_validation_ms() {
429        let flags = parse_meta_flags(&["F123", "T300", "C5", "q", "k", "MS"]).unwrap();
430        assert!(validate_ms_flags(&flags).is_ok());
431
432        let bad = parse_meta_flags(&["v"]).unwrap();
433        assert!(validate_ms_flags(&bad).is_err());
434    }
435
436    #[test]
437    fn flag_validation_ms_rejects_unknown_mode() {
438        let bad = parse_meta_flags(&["MX"]).unwrap();
439        assert!(validate_ms_flags(&bad).is_err());
440    }
441
442    #[test]
443    fn flag_validation_ms_rejects_bad_numeric() {
444        let bad = parse_meta_flags(&["Tabc"]).unwrap();
445        assert!(validate_ms_flags(&bad).is_err());
446    }
447
448    #[test]
449    fn flag_validation_md() {
450        let flags = parse_meta_flags(&["C5", "q", "k"]).unwrap();
451        assert!(validate_md_flags(&flags).is_ok());
452
453        let bad = parse_meta_flags(&["I"]).unwrap();
454        assert!(validate_md_flags(&bad).is_err());
455    }
456
457    #[test]
458    fn flag_validation_md_rejects_bad_cas() {
459        let bad = parse_meta_flags(&["Cabc"]).unwrap();
460        assert!(validate_md_flags(&bad).is_err());
461    }
462
463    #[test]
464    fn flag_validation_ma() {
465        let flags = parse_meta_flags(&["D10", "J0", "N300", "q", "v", "c", "MI"]).unwrap();
466        assert!(validate_ma_flags(&flags).is_ok());
467    }
468
469    #[test]
470    fn flag_validation_ma_rejects_unknown_mode() {
471        let bad = parse_meta_flags(&["MX"]).unwrap();
472        assert!(validate_ma_flags(&bad).is_err());
473    }
474
475    #[test]
476    fn flag_validation_ma_rejects_t_flag() {
477        // t flag removed from ma — not accurately implementable.
478        let bad = parse_meta_flags(&["t"]).unwrap();
479        assert!(validate_ma_flags(&bad).is_err());
480    }
481
482    #[test]
483    fn flag_validation_ma_rejects_bad_delta() {
484        let bad = parse_meta_flags(&["Dabc"]).unwrap();
485        assert!(validate_ma_flags(&bad).is_err());
486    }
487
488    #[test]
489    fn flag_validation_mn() {
490        let flags = parse_meta_flags(&["Oabc"]).unwrap();
491        assert!(validate_mn_flags(&flags).is_ok());
492
493        let bad = parse_meta_flags(&["v"]).unwrap();
494        assert!(validate_mn_flags(&bad).is_err());
495    }
496
497    #[test]
498    fn flag_validation_me() {
499        let flags = parse_meta_flags(&["Oabc", "k", "q"]).unwrap();
500        assert!(validate_me_flags(&flags).is_ok());
501
502        let bad = parse_meta_flags(&["v"]).unwrap();
503        assert!(validate_me_flags(&bad).is_err());
504    }
505
506    #[test]
507    fn flag_validation_mg_rejects_bad_ttl() {
508        let bad = parse_meta_flags(&["Tabc"]).unwrap();
509        assert!(validate_mg_flags(&bad).is_err());
510    }
511
512    #[test]
513    fn reject_bare_numeric_flags() {
514        // Bare T (no token) must be rejected.
515        let bad = parse_meta_flags(&["T"]).unwrap();
516        assert!(validate_mg_flags(&bad).is_err());
517
518        // Bare F on ms must be rejected.
519        let bad = parse_meta_flags(&["F", "MS"]).unwrap();
520        assert!(validate_ms_flags(&bad).is_err());
521
522        // Bare C on md must be rejected.
523        let bad = parse_meta_flags(&["C"]).unwrap();
524        assert!(validate_md_flags(&bad).is_err());
525
526        // Bare D on ma must be rejected.
527        let bad = parse_meta_flags(&["D", "MI"]).unwrap();
528        assert!(validate_ma_flags(&bad).is_err());
529    }
530
531    #[test]
532    fn reject_bare_m_flag() {
533        // Bare M (no mode token) on ms must be rejected.
534        let bad = parse_meta_flags(&["M"]).unwrap();
535        assert!(validate_ms_flags(&bad).is_err());
536
537        // Bare M on ma must be rejected.
538        let bad = parse_meta_flags(&["M"]).unwrap();
539        assert!(validate_ma_flags(&bad).is_err());
540    }
541
542    #[test]
543    fn reject_ft_on_ms_append_prepend() {
544        // F on ms M=A must be rejected.
545        let bad = parse_meta_flags(&["MA", "F123"]).unwrap();
546        assert!(validate_ms_flags(&bad).is_err());
547
548        // T on ms M=P must be rejected.
549        let bad = parse_meta_flags(&["MP", "T300"]).unwrap();
550        assert!(validate_ms_flags(&bad).is_err());
551
552        // Both F and T on ms M=A must be rejected.
553        let bad = parse_meta_flags(&["MA", "F0", "T0"]).unwrap();
554        assert!(validate_ms_flags(&bad).is_err());
555
556        // But F and T on ms M=S is fine.
557        let ok = parse_meta_flags(&["MS", "F0", "T0"]).unwrap();
558        assert!(validate_ms_flags(&ok).is_ok());
559    }
560
561    #[test]
562    fn ignored_proxy_flags() {
563        let flags = parse_meta_flags(&["v", "P", "L"]).unwrap();
564        assert!(validate_mg_flags(&flags).is_ok());
565    }
566
567    #[test]
568    fn missing_key_error() {
569        match parse_meta_command(b"mg") {
570            MetaParseResult::ClientError(msg) => assert!(!msg.is_empty()),
571            other => panic!("expected error, got {other:?}"),
572        }
573    }
574
575    #[test]
576    fn write_flag_echo_opaque_and_key() {
577        let flags = parse_meta_flags(&["v", "Otoken42", "k"]).unwrap();
578        let mut out = Vec::new();
579        write_meta_flag_echo(&mut out, &flags, b"mykey");
580        assert_eq!(&out, b" Otoken42 kmykey");
581    }
582
583    #[test]
584    fn write_flag_echo_empty() {
585        let flags = parse_meta_flags(&["v"]).unwrap();
586        let mut out = Vec::new();
587        write_meta_flag_echo(&mut out, &flags, b"mykey");
588        assert!(out.is_empty());
589    }
590
591    // ── parse_meta_command error/edge paths ─────────────────────────
592
593    #[test]
594    fn parse_meta_empty_line() {
595        assert!(matches!(
596            parse_meta_command(b""),
597            MetaParseResult::ClientError(_)
598        ));
599    }
600
601    #[test]
602    fn parse_meta_unknown_prefix() {
603        assert!(matches!(
604            parse_meta_command(b"mx mykey"),
605            MetaParseResult::ClientError(_)
606        ));
607    }
608
609    #[test]
610    fn parse_md_missing_key() {
611        assert!(matches!(
612            parse_meta_command(b"md"),
613            MetaParseResult::ClientError(_)
614        ));
615    }
616
617    #[test]
618    fn parse_ma_missing_key() {
619        assert!(matches!(
620            parse_meta_command(b"ma"),
621            MetaParseResult::ClientError(_)
622        ));
623    }
624
625    #[test]
626    fn parse_me_missing_key() {
627        assert!(matches!(
628            parse_meta_command(b"me"),
629            MetaParseResult::ClientError(_)
630        ));
631    }
632
633    #[test]
634    fn parse_ms_missing_datalen() {
635        assert!(matches!(
636            parse_meta_command(b"ms mykey"),
637            MetaParseResult::ClientError(_)
638        ));
639    }
640
641    #[test]
642    fn parse_ms_bad_datalen() {
643        assert!(matches!(
644            parse_meta_command(b"ms mykey notanum"),
645            MetaParseResult::ClientError(_)
646        ));
647    }
648
649    #[test]
650    fn parse_ms_missing_key_and_datalen() {
651        assert!(matches!(
652            parse_meta_command(b"ms"),
653            MetaParseResult::ClientError(_)
654        ));
655    }
656
657    #[test]
658    fn parse_meta_non_utf8() {
659        assert!(matches!(
660            parse_meta_command(b"mg \xff\xfe"),
661            MetaParseResult::ClientError(_)
662        ));
663    }
664
665    // ── parse_meta_flags standalone ─────────────────────────────────
666
667    #[test]
668    fn parse_meta_flags_empty_tokens() {
669        let flags = parse_meta_flags(&[]).unwrap();
670        assert!(flags.is_empty());
671    }
672
673    #[test]
674    fn parse_meta_flags_empty_string_tokens() {
675        let flags = parse_meta_flags(&["", ""]).unwrap();
676        assert!(flags.is_empty());
677    }
678
679    #[test]
680    fn parse_meta_flags_multiple() {
681        let flags = parse_meta_flags(&["v", "T300", "q", "Oopaque"]).unwrap();
682        assert_eq!(flags.len(), 4);
683        assert_eq!(flags[0].ch, b'v');
684        assert!(flags[0].token.is_none());
685        assert_eq!(flags[1].ch, b'T');
686        assert_eq!(flags[1].token.as_deref(), Some("300"));
687    }
688
689    // ── has_flag / get_flag_token standalone ─────────────────────────
690
691    #[test]
692    fn has_flag_returns_false_for_absent() {
693        let flags = parse_meta_flags(&["v", "f"]).unwrap();
694        assert!(!has_flag(&flags, b'q'));
695    }
696
697    #[test]
698    fn get_flag_token_returns_none_for_absent() {
699        let flags = parse_meta_flags(&["v"]).unwrap();
700        assert!(get_flag_token(&flags, b'T').is_none());
701    }
702
703    #[test]
704    fn get_flag_token_returns_none_for_bare_flag() {
705        let flags = parse_meta_flags(&["v"]).unwrap();
706        assert!(get_flag_token(&flags, b'v').is_none());
707    }
708
709    // ── write_meta_flag_echo edge cases ─────────────────────────────
710
711    #[test]
712    fn write_flag_echo_opaque_only() {
713        let flags = parse_meta_flags(&["Otest"]).unwrap();
714        let mut out = Vec::new();
715        write_meta_flag_echo(&mut out, &flags, b"k");
716        assert_eq!(&out, b" Otest");
717    }
718
719    #[test]
720    fn write_flag_echo_key_only() {
721        let flags = parse_meta_flags(&["k"]).unwrap();
722        let mut out = Vec::new();
723        write_meta_flag_echo(&mut out, &flags, b"mykey");
724        assert_eq!(&out, b" kmykey");
725    }
726
727    // ── ms valid mode variants ──────────────────────────────────────
728
729    #[test]
730    fn ms_mode_e_accepted() {
731        let flags = parse_meta_flags(&["ME"]).unwrap();
732        assert!(validate_ms_flags(&flags).is_ok());
733    }
734
735    #[test]
736    fn ms_mode_r_accepted() {
737        let flags = parse_meta_flags(&["MR"]).unwrap();
738        assert!(validate_ms_flags(&flags).is_ok());
739    }
740
741    #[test]
742    fn ma_mode_d_accepted() {
743        let flags = parse_meta_flags(&["MD"]).unwrap();
744        assert!(validate_ma_flags(&flags).is_ok());
745    }
746}