red_couch/
ascii.rs

1//! ASCII (text) protocol handler for the memcached text protocol.
2//!
3//! Implements all 19 supported ASCII commands.  Entered from
4//! `handle_conn()` in `lib.rs` after first-byte protocol detection.
5
6// ── Constants ────────────────────────────────────────────────────────
7
8/// Maximum command line length (bytes before \r\n).
9#[cfg(not(test))]
10const MAX_LINE_LEN: usize = 2048;
11
12/// Maximum key length (same as binary protocol).
13const MAX_KEY_LEN: usize = 250;
14
15/// Version string returned by the `version` command.
16#[cfg(not(test))]
17const VERSION_STRING: &str = "VERSION RedCouch 0.1.0";
18
19// ── Pure parser types ───────────────────────────────────────────────
20
21#[derive(Debug, PartialEq)]
22enum AsciiCmd<'a> {
23    Store {
24        cmd: StoreOp,
25        key: &'a [u8],
26        flags: u32,
27        exptime: u32,
28        bytes: u32,
29        noreply: bool,
30    },
31    Cas {
32        key: &'a [u8],
33        flags: u32,
34        exptime: u32,
35        bytes: u32,
36        cas_unique: u64,
37        noreply: bool,
38    },
39    AppendPrepend {
40        is_prepend: bool,
41        key: &'a [u8],
42        bytes: u32,
43        noreply: bool,
44    },
45    Retrieval {
46        cmd: RetrievalOp,
47        exptime: Option<u32>,
48        keys: Vec<&'a [u8]>,
49    },
50    Delete {
51        key: &'a [u8],
52        noreply: bool,
53    },
54    Counter {
55        is_decr: bool,
56        key: &'a [u8],
57        value: u64,
58        noreply: bool,
59    },
60    Touch {
61        key: &'a [u8],
62        exptime: u32,
63        noreply: bool,
64    },
65    FlushAll {
66        _delay: u32,
67        noreply: bool,
68    },
69    Version,
70    Stats {
71        args: Option<&'a str>,
72    },
73    Verbosity {
74        noreply: bool,
75    },
76    Quit,
77}
78
79#[derive(Debug, PartialEq, Clone, Copy)]
80enum StoreOp {
81    Set,
82    Add,
83    Replace,
84}
85
86/// Groups the item-metadata parameters for an ASCII store operation,
87/// keeping the `ascii_store` function under clippy's argument limit.
88#[cfg(not(test))]
89struct StoreParams {
90    op: StoreOp,
91    flags: u32,
92    exptime: u32,
93    cas: u64,
94    noreply: bool,
95}
96
97#[derive(Debug, PartialEq, Clone, Copy)]
98enum RetrievalOp {
99    Get,
100    Gets,
101    Gat,
102    Gats,
103}
104
105#[derive(Debug)]
106enum CmdParseResult<'a> {
107    Ok(AsciiCmd<'a>),
108    UnknownCommand,
109    ClientError(String),
110}
111
112// ── Line extraction ─────────────────────────────────────────────────
113
114/// Find line end in buffer. Returns `(line_end_idx, skip_past_terminator)`.
115/// Accepts both `\r\n` and bare `\n`.
116fn find_line_end(buf: &[u8]) -> Option<(usize, usize)> {
117    for i in 0..buf.len() {
118        if buf[i] == b'\n' {
119            if i > 0 && buf[i - 1] == b'\r' {
120                return Some((i - 1, i + 1));
121            }
122            return Some((i, i + 1));
123        }
124    }
125    None
126}
127
128// ── Validation / parsing helpers ────────────────────────────────────
129
130pub(crate) fn validate_key(key: &[u8]) -> Result<(), String> {
131    if key.is_empty() || key.len() > MAX_KEY_LEN {
132        return Err("bad command line format".into());
133    }
134    for &b in key {
135        if b <= 0x20 || b == 0x7F {
136            return Err("bad command line format".into());
137        }
138    }
139    Ok(())
140}
141
142fn parse_u32(s: &str) -> Result<u32, String> {
143    s.parse::<u32>()
144        .map_err(|_| "bad command line format".to_string())
145}
146
147fn parse_u64(s: &str) -> Result<u64, String> {
148    s.parse::<u64>()
149        .map_err(|_| "bad command line format".to_string())
150}
151
152// ── Individual command parsers ──────────────────────────────────────
153
154/// set/add/replace <key> <flags> <exptime> <bytes> [noreply]\r\n
155fn parse_store_cmd<'a>(cmd_name: &str, args: &[&'a str], _line: &'a [u8]) -> CmdParseResult<'a> {
156    // Expected: key flags exptime bytes [noreply]
157    if args.len() < 4 || args.len() > 5 {
158        return CmdParseResult::ClientError("bad command line format".into());
159    }
160    let key = args[0].as_bytes();
161    if let Err(e) = validate_key(key) {
162        return CmdParseResult::ClientError(e);
163    }
164    let flags = match parse_u32(args[1]) {
165        Ok(v) => v,
166        Err(e) => return CmdParseResult::ClientError(e),
167    };
168    let exptime = match parse_u32(args[2]) {
169        Ok(v) => v,
170        Err(e) => return CmdParseResult::ClientError(e),
171    };
172    let bytes = match parse_u32(args[3]) {
173        Ok(v) => v,
174        Err(e) => return CmdParseResult::ClientError(e),
175    };
176    let noreply = args.len() == 5 && args[4] == "noreply";
177    if args.len() == 5 && args[4] != "noreply" {
178        return CmdParseResult::ClientError("bad command line format".into());
179    }
180    let cmd = match cmd_name {
181        "set" => StoreOp::Set,
182        "add" => StoreOp::Add,
183        "replace" => StoreOp::Replace,
184        _ => unreachable!(),
185    };
186    CmdParseResult::Ok(AsciiCmd::Store {
187        cmd,
188        key,
189        flags,
190        exptime,
191        bytes,
192        noreply,
193    })
194}
195
196/// cas <key> <flags> <exptime> <bytes> <cas_unique> [noreply]\r\n
197fn parse_cas_cmd<'a>(args: &[&'a str], _line: &'a [u8]) -> CmdParseResult<'a> {
198    if args.len() < 5 || args.len() > 6 {
199        return CmdParseResult::ClientError("bad command line format".into());
200    }
201    let key = args[0].as_bytes();
202    if let Err(e) = validate_key(key) {
203        return CmdParseResult::ClientError(e);
204    }
205    let flags = match parse_u32(args[1]) {
206        Ok(v) => v,
207        Err(e) => return CmdParseResult::ClientError(e),
208    };
209    let exptime = match parse_u32(args[2]) {
210        Ok(v) => v,
211        Err(e) => return CmdParseResult::ClientError(e),
212    };
213    let bytes = match parse_u32(args[3]) {
214        Ok(v) => v,
215        Err(e) => return CmdParseResult::ClientError(e),
216    };
217    let cas_unique = match parse_u64(args[4]) {
218        Ok(v) => v,
219        Err(e) => return CmdParseResult::ClientError(e),
220    };
221    let noreply = args.len() == 6 && args[5] == "noreply";
222    if args.len() == 6 && args[5] != "noreply" {
223        return CmdParseResult::ClientError("bad command line format".into());
224    }
225    CmdParseResult::Ok(AsciiCmd::Cas {
226        key,
227        flags,
228        exptime,
229        bytes,
230        cas_unique,
231        noreply,
232    })
233}
234
235/// append/prepend <key> <bytes> [noreply]\r\n
236fn parse_append_prepend_cmd<'a>(
237    cmd_name: &str,
238    args: &[&'a str],
239    _line: &'a [u8],
240) -> CmdParseResult<'a> {
241    if args.len() < 2 || args.len() > 3 {
242        return CmdParseResult::ClientError("bad command line format".into());
243    }
244    let key = args[0].as_bytes();
245    if let Err(e) = validate_key(key) {
246        return CmdParseResult::ClientError(e);
247    }
248    let bytes = match parse_u32(args[1]) {
249        Ok(v) => v,
250        Err(e) => return CmdParseResult::ClientError(e),
251    };
252    let noreply = args.len() == 3 && args[2] == "noreply";
253    if args.len() == 3 && args[2] != "noreply" {
254        return CmdParseResult::ClientError("bad command line format".into());
255    }
256    let is_prepend = cmd_name == "prepend";
257    CmdParseResult::Ok(AsciiCmd::AppendPrepend {
258        is_prepend,
259        key,
260        bytes,
261        noreply,
262    })
263}
264
265/// get/gets <key>*\r\n
266fn parse_retrieval_cmd<'a>(
267    cmd_name: &str,
268    args: &[&'a str],
269    _line: &'a [u8],
270) -> CmdParseResult<'a> {
271    if args.is_empty() {
272        return CmdParseResult::ClientError("bad command line format".into());
273    }
274    let mut keys = Vec::with_capacity(args.len());
275    for &k in args {
276        let kb = k.as_bytes();
277        if let Err(e) = validate_key(kb) {
278            return CmdParseResult::ClientError(e);
279        }
280        keys.push(kb);
281    }
282    let cmd = if cmd_name == "gets" {
283        RetrievalOp::Gets
284    } else {
285        RetrievalOp::Get
286    };
287    CmdParseResult::Ok(AsciiCmd::Retrieval {
288        cmd,
289        exptime: None,
290        keys,
291    })
292}
293
294/// gat/gats <exptime> <key>*\r\n
295fn parse_gat_cmd<'a>(cmd_name: &str, args: &[&'a str], _line: &'a [u8]) -> CmdParseResult<'a> {
296    if args.len() < 2 {
297        return CmdParseResult::ClientError("bad command line format".into());
298    }
299    let exptime = match parse_u32(args[0]) {
300        Ok(v) => v,
301        Err(e) => return CmdParseResult::ClientError(e),
302    };
303    let mut keys = Vec::with_capacity(args.len() - 1);
304    for &k in &args[1..] {
305        let kb = k.as_bytes();
306        if let Err(e) = validate_key(kb) {
307            return CmdParseResult::ClientError(e);
308        }
309        keys.push(kb);
310    }
311    let cmd = if cmd_name == "gats" {
312        RetrievalOp::Gats
313    } else {
314        RetrievalOp::Gat
315    };
316    CmdParseResult::Ok(AsciiCmd::Retrieval {
317        cmd,
318        exptime: Some(exptime),
319        keys,
320    })
321}
322
323/// delete <key> [noreply]\r\n
324fn parse_delete_cmd<'a>(args: &[&'a str], _line: &'a [u8]) -> CmdParseResult<'a> {
325    if args.is_empty() || args.len() > 2 {
326        return CmdParseResult::ClientError("bad command line format".into());
327    }
328    let key = args[0].as_bytes();
329    if let Err(e) = validate_key(key) {
330        return CmdParseResult::ClientError(e);
331    }
332    let noreply = args.len() == 2 && args[1] == "noreply";
333    if args.len() == 2 && args[1] != "noreply" {
334        return CmdParseResult::ClientError("bad command line format".into());
335    }
336    CmdParseResult::Ok(AsciiCmd::Delete { key, noreply })
337}
338
339/// incr/decr <key> <value> [noreply]\r\n
340fn parse_counter_cmd<'a>(cmd_name: &str, args: &[&'a str], _line: &'a [u8]) -> CmdParseResult<'a> {
341    if args.len() < 2 || args.len() > 3 {
342        return CmdParseResult::ClientError("bad command line format".into());
343    }
344    let key = args[0].as_bytes();
345    if let Err(e) = validate_key(key) {
346        return CmdParseResult::ClientError(e);
347    }
348    let value = match parse_u64(args[1]) {
349        Ok(v) => v,
350        Err(e) => return CmdParseResult::ClientError(e),
351    };
352    let noreply = args.len() == 3 && args[2] == "noreply";
353    if args.len() == 3 && args[2] != "noreply" {
354        return CmdParseResult::ClientError("bad command line format".into());
355    }
356    let is_decr = cmd_name == "decr";
357    CmdParseResult::Ok(AsciiCmd::Counter {
358        is_decr,
359        key,
360        value,
361        noreply,
362    })
363}
364
365/// touch <key> <exptime> [noreply]\r\n
366fn parse_touch_cmd<'a>(args: &[&'a str], _line: &'a [u8]) -> CmdParseResult<'a> {
367    if args.len() < 2 || args.len() > 3 {
368        return CmdParseResult::ClientError("bad command line format".into());
369    }
370    let key = args[0].as_bytes();
371    if let Err(e) = validate_key(key) {
372        return CmdParseResult::ClientError(e);
373    }
374    let exptime = match parse_u32(args[1]) {
375        Ok(v) => v,
376        Err(e) => return CmdParseResult::ClientError(e),
377    };
378    let noreply = args.len() == 3 && args[2] == "noreply";
379    if args.len() == 3 && args[2] != "noreply" {
380        return CmdParseResult::ClientError("bad command line format".into());
381    }
382    CmdParseResult::Ok(AsciiCmd::Touch {
383        key,
384        exptime,
385        noreply,
386    })
387}
388
389/// flush_all [delay] [noreply]\r\n
390fn parse_flush_cmd<'a>(args: &[&'a str]) -> CmdParseResult<'a> {
391    let mut delay = 0u32;
392    let mut noreply = false;
393    for &a in args {
394        if a == "noreply" {
395            noreply = true;
396        } else if let Ok(d) = a.parse::<u32>() {
397            delay = d;
398        } else {
399            return CmdParseResult::ClientError("bad command line format".into());
400        }
401    }
402    CmdParseResult::Ok(AsciiCmd::FlushAll {
403        _delay: delay,
404        noreply,
405    })
406}
407
408/// Top-level command line parser.
409fn parse_command_line(line: &[u8]) -> CmdParseResult<'_> {
410    let line_str = match std::str::from_utf8(line) {
411        Ok(s) => s,
412        Err(_) => return CmdParseResult::ClientError("bad command line format".into()),
413    };
414    let tokens: Vec<&str> = line_str.split_whitespace().collect();
415    if tokens.is_empty() {
416        return CmdParseResult::UnknownCommand;
417    }
418    match tokens[0] {
419        "set" | "add" | "replace" => parse_store_cmd(tokens[0], &tokens[1..], line),
420        "cas" => parse_cas_cmd(&tokens[1..], line),
421        "append" | "prepend" => parse_append_prepend_cmd(tokens[0], &tokens[1..], line),
422        "get" | "gets" => parse_retrieval_cmd(tokens[0], &tokens[1..], line),
423        "gat" | "gats" => parse_gat_cmd(tokens[0], &tokens[1..], line),
424        "delete" => parse_delete_cmd(&tokens[1..], line),
425        "incr" | "decr" => parse_counter_cmd(tokens[0], &tokens[1..], line),
426        "touch" => parse_touch_cmd(&tokens[1..], line),
427        "flush_all" => parse_flush_cmd(&tokens[1..]),
428        "version" => CmdParseResult::Ok(AsciiCmd::Version),
429        "stats" => {
430            let args_str = if tokens.len() > 1 {
431                // Find start of the args portion in the original line.
432                let cmd_end = line_str.find(char::is_whitespace).unwrap_or(line_str.len());
433                let rest = line_str[cmd_end..].trim();
434                if rest.is_empty() { None } else { Some(rest) }
435            } else {
436                None
437            };
438            CmdParseResult::Ok(AsciiCmd::Stats { args: args_str })
439        }
440        "verbosity" => {
441            let noreply = tokens.last().map(|t| *t == "noreply").unwrap_or(false);
442            CmdParseResult::Ok(AsciiCmd::Verbosity { noreply })
443        }
444        "quit" => CmdParseResult::Ok(AsciiCmd::Quit),
445        _ => CmdParseResult::UnknownCommand,
446    }
447}
448
449// ═══════════════════════════════════════════════════════════════════
450// Runtime connection handler (compiled out in unit-test builds)
451// ═══════════════════════════════════════════════════════════════════
452
453#[cfg(not(test))]
454use crate::meta::{
455    MetaCmd, MetaFlag, MetaParseResult, get_flag_token, has_flag, parse_meta_command,
456    validate_ma_flags, validate_md_flags, validate_me_flags, validate_mg_flags, validate_mn_flags,
457    validate_ms_flags, write_meta_flag_echo,
458};
459#[cfg(not(test))]
460use crate::protocol::MAX_BODY_LEN;
461#[cfg(not(test))]
462use crate::{
463    Br, BridgeErr, CAS_COUNTER_KEY, MAX_CONNECTIONS, SCRIPT_APPEND, SCRIPT_COUNT_ITEMS,
464    SCRIPT_COUNTER, SCRIPT_DELETE, SCRIPT_FLUSH, SCRIPT_GAT, SCRIPT_GET, SCRIPT_META_GET,
465    SCRIPT_PREPEND, SCRIPT_STORE, SCRIPT_TOUCH, STARTUP_INSTANT, STAT_AUTH_CMDS, STAT_AUTH_ERRORS,
466    STAT_CAS_BADVAL, STAT_CAS_HITS, STAT_CAS_MISSES, STAT_CMD_FLUSH, STAT_CMD_GET, STAT_CMD_SET,
467    STAT_CMD_TOUCH, STAT_CURR_CONNECTIONS, STAT_DECR_HITS, STAT_DECR_MISSES, STAT_DELETE_HITS,
468    STAT_DELETE_MISSES, STAT_GET_HITS, STAT_GET_MISSES, STAT_INCR_HITS, STAT_INCR_MISSES,
469    STAT_REJECTED_CONNECTIONS, STAT_TOTAL_CONNECTIONS, eval_int, eval_lua, eval_str, hex_decode,
470    is_redis_error, make_redis_key, with_ctx,
471};
472#[cfg(not(test))]
473use bytes::{Buf, BytesMut};
474#[cfg(not(test))]
475use redis_module::RedisValue;
476#[cfg(not(test))]
477use std::io::Read;
478#[cfg(not(test))]
479use std::net::TcpStream;
480#[cfg(not(test))]
481use std::sync::atomic::Ordering;
482
483/// ASCII connection handler — entered after protocol detection.
484/// `buf` already contains the first bytes read by `handle_conn`.
485#[cfg(not(test))]
486pub(crate) fn handle_ascii_conn(sock: &mut TcpStream, buf: &mut BytesMut) -> Br<()> {
487    let mut out = Vec::with_capacity(4096);
488
489    loop {
490        // Try to extract a complete line from the buffer.
491        if let Some((line_end, skip)) = find_line_end(buf) {
492            if line_end > MAX_LINE_LEN {
493                out.extend_from_slice(b"CLIENT_ERROR line too long\r\n");
494                buf.advance(skip);
495                flush_out(sock, &mut out)?;
496                continue;
497            }
498            // Copy line bytes so we release the immutable borrow on buf
499            // before calling buf.advance() or read_data_block().
500            let line_bytes = buf[..line_end].to_vec();
501            buf.advance(skip);
502            // Skip empty lines (blank lines between commands).
503            if line_bytes.is_empty() || line_bytes.iter().all(|b| b.is_ascii_whitespace()) {
504                continue;
505            }
506
507            // ── Text-path prefix routing ──────────────────────────
508            // Meta protocol commands use two-letter prefixes (mg, ms,
509            // md, ma, mn, me) followed by a space.  Route them to the
510            // meta protocol handlers; classic ASCII commands fall through.
511            if is_meta_command(&line_bytes) {
512                match parse_meta_command(&line_bytes) {
513                    MetaParseResult::Ok(cmd) => {
514                        dispatch_meta_cmd(cmd, None, &mut out)?;
515                    }
516                    MetaParseResult::NeedData(cmd, datalen) => {
517                        if datalen > MAX_BODY_LEN {
518                            out.extend_from_slice(b"CLIENT_ERROR object too large for cache\r\n");
519                            drain_data_block(sock, buf, datalen)?;
520                        } else {
521                            match read_data_block(sock, buf, datalen) {
522                                Ok(data) => {
523                                    dispatch_meta_cmd(cmd, Some(&data), &mut out)?;
524                                }
525                                Err(_) => {
526                                    out.extend_from_slice(b"CLIENT_ERROR bad data chunk\r\n");
527                                }
528                            }
529                        }
530                    }
531                    MetaParseResult::ClientError(msg) => {
532                        out.extend_from_slice(b"CLIENT_ERROR ");
533                        out.extend_from_slice(msg.as_bytes());
534                        out.extend_from_slice(b"\r\n");
535                    }
536                }
537                flush_out(sock, &mut out)?;
538                continue;
539            }
540
541            let cmd = parse_command_line(&line_bytes);
542
543            match cmd {
544                CmdParseResult::Ok(ascii_cmd) => {
545                    if needs_data_block(&ascii_cmd) {
546                        let byte_count = data_block_len(&ascii_cmd);
547                        if byte_count > MAX_BODY_LEN {
548                            let nr = is_noreply(&ascii_cmd);
549                            if !nr {
550                                out.extend_from_slice(
551                                    b"CLIENT_ERROR object too large for cache\r\n",
552                                );
553                            }
554                            drain_data_block(sock, buf, byte_count)?;
555                            flush_out(sock, &mut out)?;
556                            continue;
557                        }
558                        let data = match read_data_block(sock, buf, byte_count) {
559                            Ok(d) => d,
560                            Err(_) => {
561                                let nr = is_noreply(&ascii_cmd);
562                                if !nr {
563                                    out.extend_from_slice(b"CLIENT_ERROR bad data chunk\r\n");
564                                }
565                                flush_out(sock, &mut out)?;
566                                continue;
567                            }
568                        };
569                        dispatch_cmd(ascii_cmd, Some(&data), &mut out)?;
570                    } else {
571                        dispatch_cmd(ascii_cmd, None, &mut out)?;
572                    }
573                }
574                CmdParseResult::UnknownCommand => {
575                    out.extend_from_slice(b"ERROR\r\n");
576                }
577                CmdParseResult::ClientError(msg) => {
578                    out.extend_from_slice(b"CLIENT_ERROR ");
579                    out.extend_from_slice(msg.as_bytes());
580                    out.extend_from_slice(b"\r\n");
581                }
582            }
583
584            flush_out(sock, &mut out)?;
585            continue;
586        }
587
588        // No complete line yet — check buffer size guard.
589        if buf.len() > MAX_LINE_LEN + 2 {
590            // Line too long — cannot find \n within limit.
591            out.extend_from_slice(b"CLIENT_ERROR line too long\r\n");
592            flush_out(sock, &mut out)?;
593            return Ok(());
594        }
595
596        // Read more data.
597        let mut tmp = [0u8; 16384];
598        match sock.read(&mut tmp) {
599            Ok(0) => return Ok(()),
600            Ok(n) => buf.extend_from_slice(&tmp[..n]),
601            Err(ref e)
602                if e.kind() == std::io::ErrorKind::WouldBlock
603                    || e.kind() == std::io::ErrorKind::TimedOut =>
604            {
605                return Err(std::io::Error::from(std::io::ErrorKind::TimedOut).into());
606            }
607            Err(e) => return Err(e.into()),
608        }
609    }
610}
611
612// ── Helper functions for the connection handler ─────────────────────
613
614#[cfg(not(test))]
615fn flush_out(sock: &mut TcpStream, out: &mut Vec<u8>) -> Br<()> {
616    if !out.is_empty() {
617        use std::io::Write;
618        sock.write_all(out)?;
619        out.clear();
620    }
621    Ok(())
622}
623
624/// Check if a line is a meta protocol command.
625/// Meta commands use two-letter prefixes: mg, ms, md, ma, mn, me
626/// followed by a space (or end of line for mn/me which can be bare).
627fn is_meta_command(line: &[u8]) -> bool {
628    if line.len() < 2 {
629        return false;
630    }
631    let prefix = &line[..2];
632    let is_meta_prefix = prefix == b"mg"
633        || prefix == b"ms"
634        || prefix == b"md"
635        || prefix == b"ma"
636        || prefix == b"mn"
637        || prefix == b"me";
638    if !is_meta_prefix {
639        return false;
640    }
641    // Must be followed by space, \t, or end of line (for bare mn/me).
642    line.len() == 2 || line[2] == b' ' || line[2] == b'\t'
643}
644
645/// Does this command need a data block after the command line?
646fn needs_data_block(cmd: &AsciiCmd<'_>) -> bool {
647    matches!(
648        cmd,
649        AsciiCmd::Store { .. } | AsciiCmd::Cas { .. } | AsciiCmd::AppendPrepend { .. }
650    )
651}
652
653/// Get the data block length declared by the command.
654fn data_block_len(cmd: &AsciiCmd<'_>) -> u32 {
655    match cmd {
656        AsciiCmd::Store { bytes, .. } => *bytes,
657        AsciiCmd::Cas { bytes, .. } => *bytes,
658        AsciiCmd::AppendPrepend { bytes, .. } => *bytes,
659        _ => 0,
660    }
661}
662
663/// Check if noreply is set on this command.
664fn is_noreply(cmd: &AsciiCmd<'_>) -> bool {
665    match cmd {
666        AsciiCmd::Store { noreply, .. } => *noreply,
667        AsciiCmd::Cas { noreply, .. } => *noreply,
668        AsciiCmd::AppendPrepend { noreply, .. } => *noreply,
669        AsciiCmd::Delete { noreply, .. } => *noreply,
670        AsciiCmd::Counter { noreply, .. } => *noreply,
671        AsciiCmd::Touch { noreply, .. } => *noreply,
672        AsciiCmd::FlushAll { noreply, .. } => *noreply,
673        AsciiCmd::Verbosity { noreply, .. } => *noreply,
674        _ => false,
675    }
676}
677
678/// Read exactly `byte_count` bytes of data plus trailing \r\n from
679/// the socket/buffer.  Returns the data bytes (without the \r\n).
680#[cfg(not(test))]
681fn read_data_block(
682    sock: &mut TcpStream,
683    buf: &mut BytesMut,
684    byte_count: u32,
685) -> Result<Vec<u8>, ()> {
686    let need = byte_count as usize + 2; // data + \r\n
687    // Read until we have enough.
688    while buf.len() < need {
689        let mut tmp = [0u8; 16384];
690        match sock.read(&mut tmp) {
691            Ok(0) => return Err(()),
692            Ok(n) => buf.extend_from_slice(&tmp[..n]),
693            Err(_) => return Err(()),
694        }
695    }
696    let data = buf[..byte_count as usize].to_vec();
697    // Validate trailing \r\n (or bare \n).
698    let trail = &buf[byte_count as usize..byte_count as usize + 2];
699    let valid_terminator = trail == b"\r\n" || (trail[0] == b'\n'); // bare \n + whatever follows
700    if !valid_terminator {
701        buf.advance(need);
702        return Err(());
703    }
704    let skip = if trail[0] == b'\r' { 2 } else { 1 };
705    buf.advance(byte_count as usize + skip);
706    Ok(data)
707}
708
709/// Drain a data block we don't intend to use (e.g., oversized).
710#[cfg(not(test))]
711fn drain_data_block(sock: &mut TcpStream, buf: &mut BytesMut, byte_count: u32) -> Br<()> {
712    let need = byte_count as usize + 2;
713    while buf.len() < need {
714        let mut tmp = [0u8; 16384];
715        match sock.read(&mut tmp) {
716            Ok(0) => return Ok(()),
717            Ok(n) => buf.extend_from_slice(&tmp[..n]),
718            Err(e) => return Err(e.into()),
719        }
720    }
721    buf.advance(need);
722    Ok(())
723}
724
725// ── Command dispatch ────────────────────────────────────────────────
726
727#[cfg(not(test))]
728fn dispatch_cmd(cmd: AsciiCmd<'_>, data: Option<&[u8]>, out: &mut Vec<u8>) -> Br<()> {
729    match cmd {
730        AsciiCmd::Store {
731            cmd,
732            key,
733            flags,
734            exptime,
735            noreply,
736            ..
737        } => ascii_store(
738            &StoreParams {
739                op: cmd,
740                flags,
741                exptime,
742                cas: 0,
743                noreply,
744            },
745            key,
746            data.unwrap_or(&[]),
747            out,
748        ),
749        AsciiCmd::Cas {
750            key,
751            flags,
752            exptime,
753            cas_unique,
754            noreply,
755            ..
756        } => ascii_store(
757            &StoreParams {
758                op: StoreOp::Set,
759                flags,
760                exptime,
761                cas: cas_unique,
762                noreply,
763            },
764            key,
765            data.unwrap_or(&[]),
766            out,
767        ),
768        AsciiCmd::AppendPrepend {
769            is_prepend,
770            key,
771            noreply,
772            ..
773        } => ascii_append_prepend(is_prepend, key, data.unwrap_or(&[]), noreply, out),
774        AsciiCmd::Retrieval { cmd, exptime, keys } => ascii_retrieval(cmd, exptime, &keys, out),
775        AsciiCmd::Delete { key, noreply } => ascii_delete(key, noreply, out),
776        AsciiCmd::Counter {
777            is_decr,
778            key,
779            value,
780            noreply,
781        } => ascii_counter(is_decr, key, value, noreply, out),
782        AsciiCmd::Touch {
783            key,
784            exptime,
785            noreply,
786        } => ascii_touch(key, exptime, noreply, out),
787        AsciiCmd::FlushAll { noreply, .. } => ascii_flush(noreply, out),
788        AsciiCmd::Version => {
789            out.extend_from_slice(VERSION_STRING.as_bytes());
790            out.extend_from_slice(b"\r\n");
791            Ok(())
792        }
793        AsciiCmd::Stats { args } => ascii_stats(args, out),
794        AsciiCmd::Verbosity { noreply } => {
795            if !noreply {
796                out.extend_from_slice(b"OK\r\n");
797            }
798            Ok(())
799        }
800        AsciiCmd::Quit => {
801            // Return an error to signal connection close.
802            Err(BridgeErr::Io(std::io::Error::new(
803                std::io::ErrorKind::ConnectionAborted,
804                "quit",
805            )))
806        }
807    }
808}
809
810// ── Meta protocol dispatch ─────────────────────────────────────────
811
812/// Dispatch a parsed meta command to the appropriate handler.
813#[cfg(not(test))]
814fn dispatch_meta_cmd(cmd: MetaCmd<'_>, data: Option<&[u8]>, out: &mut Vec<u8>) -> Br<()> {
815    match cmd {
816        MetaCmd::Noop { flags } => {
817            if let Err(e) = validate_mn_flags(&flags) {
818                out.extend_from_slice(b"CLIENT_ERROR ");
819                out.extend_from_slice(e.as_bytes());
820                out.extend_from_slice(b"\r\n");
821                return Ok(());
822            }
823            meta_noop(&flags, out)
824        }
825        MetaCmd::Get { key, flags } => {
826            if let Err(e) = validate_mg_flags(&flags) {
827                out.extend_from_slice(b"CLIENT_ERROR ");
828                out.extend_from_slice(e.as_bytes());
829                out.extend_from_slice(b"\r\n");
830                return Ok(());
831            }
832            meta_get(key, &flags, out)
833        }
834        MetaCmd::Set { key, flags, .. } => {
835            if let Err(e) = validate_ms_flags(&flags) {
836                out.extend_from_slice(b"CLIENT_ERROR ");
837                out.extend_from_slice(e.as_bytes());
838                out.extend_from_slice(b"\r\n");
839                return Ok(());
840            }
841            meta_set(key, data.unwrap_or(&[]), &flags, out)
842        }
843        MetaCmd::Delete { key, flags } => {
844            if let Err(e) = validate_md_flags(&flags) {
845                out.extend_from_slice(b"CLIENT_ERROR ");
846                out.extend_from_slice(e.as_bytes());
847                out.extend_from_slice(b"\r\n");
848                return Ok(());
849            }
850            meta_delete(key, &flags, out)
851        }
852        MetaCmd::Arithmetic { key, flags } => {
853            if let Err(e) = validate_ma_flags(&flags) {
854                out.extend_from_slice(b"CLIENT_ERROR ");
855                out.extend_from_slice(e.as_bytes());
856                out.extend_from_slice(b"\r\n");
857                return Ok(());
858            }
859            meta_arithmetic(key, &flags, out)
860        }
861        MetaCmd::Debug { key, flags } => {
862            if let Err(e) = validate_me_flags(&flags) {
863                out.extend_from_slice(b"CLIENT_ERROR ");
864                out.extend_from_slice(e.as_bytes());
865                out.extend_from_slice(b"\r\n");
866                return Ok(());
867            }
868            // me is unsupported — always return EN (not found).
869            let quiet = has_flag(&flags, b'q');
870            if !quiet {
871                out.extend_from_slice(b"EN");
872                write_meta_flag_echo(out, &flags, key);
873                out.extend_from_slice(b"\r\n");
874            }
875            Ok(())
876        }
877    }
878}
879
880// ── Individual command handlers ─────────────────────────────────────
881
882/// Handle set/add/replace/cas.
883#[cfg(not(test))]
884fn ascii_store(params: &StoreParams, key: &[u8], value: &[u8], out: &mut Vec<u8>) -> Br<()> {
885    STAT_CMD_SET.fetch_add(1, Ordering::Relaxed);
886    let rk = make_redis_key(key);
887    let op_name = match params.op {
888        StoreOp::Set => "set",
889        StoreOp::Add => "add",
890        StoreOp::Replace => "replace",
891    };
892    let cas_str = params.cas.to_string();
893    let flags_str = params.flags.to_string();
894    let expiry_str = params.exptime.to_string();
895
896    let reply = with_ctx(|ctx| {
897        let keys_and_args: &[&[u8]] = &[
898            b"2",
899            rk.as_slice(),
900            CAS_COUNTER_KEY.as_bytes(),
901            op_name.as_bytes(),
902            value,
903            flags_str.as_bytes(),
904            cas_str.as_bytes(),
905            expiry_str.as_bytes(),
906        ];
907        eval_lua(ctx, &SCRIPT_STORE, keys_and_args)
908    })
909    .map_err(|e| BridgeErr::Redis(e.to_string()))?;
910
911    let noreply = params.noreply;
912
913    if is_redis_error(&reply) {
914        if !noreply {
915            out.extend_from_slice(b"SERVER_ERROR internal\r\n");
916        }
917        return Ok(());
918    }
919
920    let (status_code, _new_cas) = match &reply {
921        RedisValue::Array(arr) if arr.len() >= 2 => {
922            let st = eval_int(&arr[0]);
923            let cas_s = eval_str(&arr[1]);
924            let cas_val: u64 = cas_s.parse().unwrap_or(0);
925            (st, cas_val)
926        }
927        _ => {
928            if !noreply {
929                out.extend_from_slice(b"SERVER_ERROR internal\r\n");
930            }
931            return Ok(());
932        }
933    };
934
935    let is_cas_op = params.cas != 0;
936    match status_code {
937        0 => {
938            if is_cas_op {
939                STAT_CAS_HITS.fetch_add(1, Ordering::Relaxed);
940            }
941            if !noreply {
942                out.extend_from_slice(b"STORED\r\n");
943            }
944        }
945        -1 => {
946            // NOT_FOUND (replace on missing, or CAS on missing)
947            if is_cas_op {
948                STAT_CAS_MISSES.fetch_add(1, Ordering::Relaxed);
949            }
950            if !noreply {
951                if is_cas_op {
952                    out.extend_from_slice(b"NOT_FOUND\r\n");
953                } else {
954                    out.extend_from_slice(b"NOT_STORED\r\n");
955                }
956            }
957        }
958        -2 => {
959            // KEY_EXISTS (add on existing, or CAS mismatch)
960            if is_cas_op {
961                STAT_CAS_BADVAL.fetch_add(1, Ordering::Relaxed);
962            }
963            if !noreply {
964                if is_cas_op {
965                    out.extend_from_slice(b"EXISTS\r\n");
966                } else {
967                    out.extend_from_slice(b"NOT_STORED\r\n");
968                }
969            }
970        }
971        _ => {
972            if !noreply {
973                out.extend_from_slice(b"SERVER_ERROR internal\r\n");
974            }
975        }
976    }
977    Ok(())
978}
979
980/// Handle get/gets/gat/gats (multi-key retrieval).
981#[cfg(not(test))]
982fn ascii_retrieval(
983    cmd: RetrievalOp,
984    exptime: Option<u32>,
985    keys: &[&[u8]],
986    out: &mut Vec<u8>,
987) -> Br<()> {
988    let include_cas = matches!(cmd, RetrievalOp::Gets | RetrievalOp::Gats);
989    let is_gat = matches!(cmd, RetrievalOp::Gat | RetrievalOp::Gats);
990
991    for &key in keys {
992        STAT_CMD_GET.fetch_add(1, Ordering::Relaxed);
993        if is_gat {
994            STAT_CMD_TOUCH.fetch_add(1, Ordering::Relaxed);
995        }
996
997        let rk = make_redis_key(key);
998
999        let reply = if is_gat {
1000            let exp_str = exptime.unwrap_or(0).to_string();
1001            with_ctx(|ctx| {
1002                let keys_and_args: &[&[u8]] = &[
1003                    b"2",
1004                    rk.as_slice(),
1005                    CAS_COUNTER_KEY.as_bytes(),
1006                    exp_str.as_bytes(),
1007                ];
1008                eval_lua(ctx, &SCRIPT_GAT, keys_and_args)
1009            })
1010            .map_err(|e| BridgeErr::Redis(e.to_string()))?
1011        } else {
1012            with_ctx(|ctx| {
1013                let keys_and_args: &[&[u8]] = &[b"1", rk.as_slice()];
1014                eval_lua(ctx, &SCRIPT_GET, keys_and_args)
1015            })
1016            .map_err(|e| BridgeErr::Redis(e.to_string()))?
1017        };
1018
1019        if is_redis_error(&reply) {
1020            continue;
1021        }
1022
1023        let (status, hex_val, flags_str, cas_str) = match &reply {
1024            RedisValue::Array(arr) if arr.len() >= 4 => (
1025                eval_int(&arr[0]),
1026                eval_str(&arr[1]),
1027                eval_str(&arr[2]),
1028                eval_str(&arr[3]),
1029            ),
1030            _ => continue,
1031        };
1032
1033        if status == -1 {
1034            STAT_GET_MISSES.fetch_add(1, Ordering::Relaxed);
1035            continue;
1036        }
1037        STAT_GET_HITS.fetch_add(1, Ordering::Relaxed);
1038
1039        let value = hex_decode(&hex_val);
1040        // VALUE <key> <flags> <bytes> [<cas>]\r\n<data>\r\n
1041        out.extend_from_slice(b"VALUE ");
1042        out.extend_from_slice(key);
1043        out.push(b' ');
1044        out.extend_from_slice(flags_str.as_bytes());
1045        out.push(b' ');
1046        out.extend_from_slice(value.len().to_string().as_bytes());
1047        if include_cas {
1048            out.push(b' ');
1049            out.extend_from_slice(cas_str.as_bytes());
1050        }
1051        out.extend_from_slice(b"\r\n");
1052        out.extend_from_slice(&value);
1053        out.extend_from_slice(b"\r\n");
1054    }
1055    out.extend_from_slice(b"END\r\n");
1056    Ok(())
1057}
1058
1059/// Handle delete.
1060#[cfg(not(test))]
1061fn ascii_delete(key: &[u8], noreply: bool, out: &mut Vec<u8>) -> Br<()> {
1062    let rk = make_redis_key(key);
1063    // ASCII delete never uses CAS — always non-CAS path.
1064    // Use direct DEL for the fast non-CAS bypass.
1065    let reply = with_ctx(|ctx| ctx.call("DEL", &[rk.as_slice()]))
1066        .map_err(|e| BridgeErr::Redis(e.to_string()))?;
1067
1068    if is_redis_error(&reply) {
1069        if !noreply {
1070            out.extend_from_slice(b"SERVER_ERROR internal\r\n");
1071        }
1072        return Ok(());
1073    }
1074
1075    // DEL returns the number of keys deleted: 1 = found+deleted, 0 = not found.
1076    let deleted_count = match &reply {
1077        RedisValue::Integer(n) => *n,
1078        _ => {
1079            if !noreply {
1080                out.extend_from_slice(b"SERVER_ERROR internal\r\n");
1081            }
1082            return Ok(());
1083        }
1084    };
1085
1086    if deleted_count > 0 {
1087        STAT_DELETE_HITS.fetch_add(1, Ordering::Relaxed);
1088        if !noreply {
1089            out.extend_from_slice(b"DELETED\r\n");
1090        }
1091    } else {
1092        STAT_DELETE_MISSES.fetch_add(1, Ordering::Relaxed);
1093        if !noreply {
1094            out.extend_from_slice(b"NOT_FOUND\r\n");
1095        }
1096    }
1097    Ok(())
1098}
1099
1100/// Handle incr/decr.
1101#[cfg(not(test))]
1102fn ascii_counter(
1103    is_decr: bool,
1104    key: &[u8],
1105    delta: u64,
1106    noreply: bool,
1107    out: &mut Vec<u8>,
1108) -> Br<()> {
1109    let rk = make_redis_key(key);
1110    let delta_str = delta.to_string();
1111    let is_decr_str = if is_decr { "1" } else { "0" };
1112    // ASCII incr/decr: if key doesn't exist, return NOT_FOUND.
1113    // We use expiry=4294967295 (0xFFFFFFFF) to signal "do not create".
1114    let initial_str = "0";
1115    let expiry_str = "4294967295";
1116
1117    let reply = with_ctx(|ctx| {
1118        let keys_and_args: &[&[u8]] = &[
1119            b"2",
1120            rk.as_slice(),
1121            CAS_COUNTER_KEY.as_bytes(),
1122            delta_str.as_bytes(),
1123            is_decr_str.as_bytes(),
1124            initial_str.as_bytes(),
1125            expiry_str.as_bytes(),
1126        ];
1127        eval_lua(ctx, &SCRIPT_COUNTER, keys_and_args)
1128    })
1129    .map_err(|e| BridgeErr::Redis(e.to_string()))?;
1130
1131    if is_redis_error(&reply) {
1132        if !noreply {
1133            out.extend_from_slice(b"SERVER_ERROR internal\r\n");
1134        }
1135        return Ok(());
1136    }
1137
1138    let (status, value_str, _cas) = match &reply {
1139        RedisValue::Array(arr) if arr.len() >= 3 => {
1140            (eval_int(&arr[0]), eval_str(&arr[1]), eval_str(&arr[2]))
1141        }
1142        _ => {
1143            if !noreply {
1144                out.extend_from_slice(b"SERVER_ERROR internal\r\n");
1145            }
1146            return Ok(());
1147        }
1148    };
1149
1150    match status {
1151        0 => {
1152            if is_decr {
1153                STAT_DECR_HITS.fetch_add(1, Ordering::Relaxed);
1154            } else {
1155                STAT_INCR_HITS.fetch_add(1, Ordering::Relaxed);
1156            }
1157            if !noreply {
1158                out.extend_from_slice(value_str.as_bytes());
1159                out.extend_from_slice(b"\r\n");
1160            }
1161        }
1162        -1 => {
1163            if is_decr {
1164                STAT_DECR_MISSES.fetch_add(1, Ordering::Relaxed);
1165            } else {
1166                STAT_INCR_MISSES.fetch_add(1, Ordering::Relaxed);
1167            }
1168            if !noreply {
1169                out.extend_from_slice(b"NOT_FOUND\r\n");
1170            }
1171        }
1172        -3 => {
1173            // Non-numeric value.
1174            if !noreply {
1175                out.extend_from_slice(
1176                    b"CLIENT_ERROR cannot increment or decrement non-numeric value\r\n",
1177                );
1178            }
1179        }
1180        _ => {
1181            if !noreply {
1182                out.extend_from_slice(b"SERVER_ERROR internal\r\n");
1183            }
1184        }
1185    }
1186    Ok(())
1187}
1188
1189/// Handle touch.
1190#[cfg(not(test))]
1191fn ascii_touch(key: &[u8], exptime: u32, noreply: bool, out: &mut Vec<u8>) -> Br<()> {
1192    STAT_CMD_TOUCH.fetch_add(1, Ordering::Relaxed);
1193    let rk = make_redis_key(key);
1194    let exp_str = exptime.to_string();
1195
1196    let reply = with_ctx(|ctx| {
1197        let keys_and_args: &[&[u8]] = &[
1198            b"2",
1199            rk.as_slice(),
1200            CAS_COUNTER_KEY.as_bytes(),
1201            exp_str.as_bytes(),
1202        ];
1203        eval_lua(ctx, &SCRIPT_TOUCH, keys_and_args)
1204    })
1205    .map_err(|e| BridgeErr::Redis(e.to_string()))?;
1206
1207    if is_redis_error(&reply) {
1208        if !noreply {
1209            out.extend_from_slice(b"SERVER_ERROR internal\r\n");
1210        }
1211        return Ok(());
1212    }
1213
1214    let status = match &reply {
1215        RedisValue::Array(arr) if !arr.is_empty() => eval_int(&arr[0]),
1216        _ => {
1217            if !noreply {
1218                out.extend_from_slice(b"SERVER_ERROR internal\r\n");
1219            }
1220            return Ok(());
1221        }
1222    };
1223
1224    match status {
1225        0 => {
1226            if !noreply {
1227                out.extend_from_slice(b"TOUCHED\r\n");
1228            }
1229        }
1230        _ => {
1231            if !noreply {
1232                out.extend_from_slice(b"NOT_FOUND\r\n");
1233            }
1234        }
1235    }
1236    Ok(())
1237}
1238
1239/// Handle append/prepend.
1240#[cfg(not(test))]
1241fn ascii_append_prepend(
1242    is_prepend: bool,
1243    key: &[u8],
1244    value: &[u8],
1245    noreply: bool,
1246    out: &mut Vec<u8>,
1247) -> Br<()> {
1248    STAT_CMD_SET.fetch_add(1, Ordering::Relaxed);
1249    let rk = make_redis_key(key);
1250    let script = if is_prepend {
1251        &SCRIPT_PREPEND
1252    } else {
1253        &SCRIPT_APPEND
1254    };
1255
1256    let reply = with_ctx(|ctx| {
1257        let keys_and_args: &[&[u8]] =
1258            &[b"2", rk.as_slice(), CAS_COUNTER_KEY.as_bytes(), value, b"0"];
1259        eval_lua(ctx, script, keys_and_args)
1260    })
1261    .map_err(|e| BridgeErr::Redis(e.to_string()))?;
1262
1263    if is_redis_error(&reply) {
1264        if !noreply {
1265            out.extend_from_slice(b"SERVER_ERROR internal\r\n");
1266        }
1267        return Ok(());
1268    }
1269
1270    let status = match &reply {
1271        RedisValue::Array(arr) if !arr.is_empty() => eval_int(&arr[0]),
1272        _ => {
1273            if !noreply {
1274                out.extend_from_slice(b"SERVER_ERROR internal\r\n");
1275            }
1276            return Ok(());
1277        }
1278    };
1279
1280    match status {
1281        0 => {
1282            if !noreply {
1283                out.extend_from_slice(b"STORED\r\n");
1284            }
1285        }
1286        -5 => {
1287            // NOT_STORED — key does not exist.
1288            if !noreply {
1289                out.extend_from_slice(b"NOT_STORED\r\n");
1290            }
1291        }
1292        _ => {
1293            if !noreply {
1294                out.extend_from_slice(b"SERVER_ERROR internal\r\n");
1295            }
1296        }
1297    }
1298    Ok(())
1299}
1300
1301/// Handle flush_all.
1302#[cfg(not(test))]
1303fn ascii_flush(noreply: bool, out: &mut Vec<u8>) -> Br<()> {
1304    STAT_CMD_FLUSH.fetch_add(1, Ordering::Relaxed);
1305
1306    // Flush via EVALSHA (NOSCRIPT fallback) — same as binary flush handler.
1307    with_ctx(|ctx| {
1308        let keys_and_args: &[&[u8]] = &[b"0"];
1309        eval_lua(ctx, &SCRIPT_FLUSH, keys_and_args)
1310    })
1311    .map_err(|e| BridgeErr::Redis(e.to_string()))?;
1312
1313    if !noreply {
1314        out.extend_from_slice(b"OK\r\n");
1315    }
1316    Ok(())
1317}
1318
1319// ── Meta protocol handlers ─────────────────────────────────────────
1320
1321/// Handle mn (meta noop).
1322#[cfg(not(test))]
1323fn meta_noop(flags: &[MetaFlag], out: &mut Vec<u8>) -> Br<()> {
1324    out.extend_from_slice(b"MN");
1325    if let Some(opaque) = get_flag_token(flags, b'O') {
1326        out.push(b' ');
1327        out.push(b'O');
1328        out.extend_from_slice(opaque.as_bytes());
1329    }
1330    out.extend_from_slice(b"\r\n");
1331    Ok(())
1332}
1333
1334/// Handle mg (meta get).
1335#[cfg(not(test))]
1336fn meta_get(key: &[u8], flags: &[MetaFlag], out: &mut Vec<u8>) -> Br<()> {
1337    STAT_CMD_GET.fetch_add(1, Ordering::Relaxed);
1338
1339    let rk = make_redis_key(key);
1340    let want_ttl_update = get_flag_token(flags, b'T');
1341
1342    let reply = if let Some(ttl_str) = want_ttl_update {
1343        // Use GAT to update TTL, then we'll get a second call for TTL info.
1344        // Actually, we need the extended meta get that also returns TTL.
1345        // Use LUA_META_GET after touch.
1346        let exp_str = ttl_str.to_string();
1347        with_ctx(|ctx| {
1348            // First touch to update TTL.
1349            let touch_keys_and_args: &[&[u8]] = &[
1350                b"2",
1351                rk.as_slice(),
1352                CAS_COUNTER_KEY.as_bytes(),
1353                exp_str.as_bytes(),
1354            ];
1355            eval_lua(ctx, &SCRIPT_TOUCH, touch_keys_and_args)
1356        })
1357        .map_err(|e| BridgeErr::Redis(e.to_string()))?;
1358        // Then get with TTL info.
1359        with_ctx(|ctx| {
1360            let keys_and_args: &[&[u8]] = &[b"1", rk.as_slice()];
1361            eval_lua(ctx, &SCRIPT_META_GET, keys_and_args)
1362        })
1363        .map_err(|e| BridgeErr::Redis(e.to_string()))?
1364    } else {
1365        with_ctx(|ctx| {
1366            let keys_and_args: &[&[u8]] = &[b"1", rk.as_slice()];
1367            eval_lua(ctx, &SCRIPT_META_GET, keys_and_args)
1368        })
1369        .map_err(|e| BridgeErr::Redis(e.to_string()))?
1370    };
1371
1372    if is_redis_error(&reply) {
1373        out.extend_from_slice(b"SERVER_ERROR internal\r\n");
1374        return Ok(());
1375    }
1376
1377    // Parse: {status, hex_value, flags_string, cas_string, ttl, size}
1378    let (status, hex_val, item_flags, cas_str, ttl, size) = match &reply {
1379        RedisValue::Array(arr) if arr.len() >= 6 => {
1380            let st = eval_int(&arr[0]);
1381            let hv = eval_str(&arr[1]);
1382            let fl = eval_str(&arr[2]);
1383            let cs = eval_str(&arr[3]);
1384            let t = eval_int(&arr[4]);
1385            let sz = eval_int(&arr[5]);
1386            (st, hv, fl, cs, t, sz)
1387        }
1388        _ => {
1389            out.extend_from_slice(b"SERVER_ERROR internal\r\n");
1390            return Ok(());
1391        }
1392    };
1393
1394    let quiet = has_flag(flags, b'q');
1395    if status == -1 {
1396        STAT_GET_MISSES.fetch_add(1, Ordering::Relaxed);
1397        if !quiet {
1398            out.extend_from_slice(b"EN");
1399            write_meta_flag_echo(out, flags, key);
1400            out.extend_from_slice(b"\r\n");
1401        }
1402        return Ok(());
1403    }
1404
1405    STAT_GET_HITS.fetch_add(1, Ordering::Relaxed);
1406
1407    let value = hex_decode(&hex_val);
1408    let want_value = has_flag(flags, b'v');
1409
1410    if want_value {
1411        // VA <size> [flags]\r\n<data>\r\n
1412        out.extend_from_slice(b"VA ");
1413        out.extend_from_slice(value.len().to_string().as_bytes());
1414    } else {
1415        // HD [flags]\r\n
1416        out.extend_from_slice(b"HD");
1417    }
1418
1419    // Append requested metadata flags.
1420    if has_flag(flags, b'c') {
1421        out.push(b' ');
1422        out.push(b'c');
1423        out.extend_from_slice(cas_str.as_bytes());
1424    }
1425    if has_flag(flags, b'f') {
1426        out.push(b' ');
1427        out.push(b'f');
1428        out.extend_from_slice(item_flags.as_bytes());
1429    }
1430    if has_flag(flags, b's') {
1431        out.push(b' ');
1432        out.push(b's');
1433        out.extend_from_slice(size.to_string().as_bytes());
1434    }
1435    if has_flag(flags, b't') {
1436        out.push(b' ');
1437        out.push(b't');
1438        // ttl: -1 means no expiry, positive means seconds remaining.
1439        let ttl_val = if ttl == -1 { -1 } else { ttl };
1440        out.extend_from_slice(ttl_val.to_string().as_bytes());
1441    }
1442    write_meta_flag_echo(out, flags, key);
1443    out.extend_from_slice(b"\r\n");
1444
1445    if want_value {
1446        out.extend_from_slice(&value);
1447        out.extend_from_slice(b"\r\n");
1448    }
1449    Ok(())
1450}
1451
1452/// Handle ms (meta set).
1453#[cfg(not(test))]
1454fn meta_set(key: &[u8], data: &[u8], flags: &[MetaFlag], out: &mut Vec<u8>) -> Br<()> {
1455    STAT_CMD_SET.fetch_add(1, Ordering::Relaxed);
1456
1457    let quiet = has_flag(flags, b'q');
1458    let mode = get_flag_token(flags, b'M').unwrap_or("S");
1459    let item_flags = get_flag_token(flags, b'F').unwrap_or("0");
1460    let ttl = get_flag_token(flags, b'T').unwrap_or("0");
1461    let cas = get_flag_token(flags, b'C').unwrap_or("0");
1462
1463    match mode {
1464        "A" | "P" => {
1465            // Append/Prepend mode.
1466            let is_prepend = mode == "P";
1467            let script = if is_prepend {
1468                &SCRIPT_PREPEND
1469            } else {
1470                &SCRIPT_APPEND
1471            };
1472            let rk = make_redis_key(key);
1473            let reply = with_ctx(|ctx| {
1474                let keys_and_args: &[&[u8]] = &[
1475                    b"2",
1476                    rk.as_slice(),
1477                    CAS_COUNTER_KEY.as_bytes(),
1478                    data,
1479                    cas.as_bytes(),
1480                ];
1481                eval_lua(ctx, script, keys_and_args)
1482            })
1483            .map_err(|e| BridgeErr::Redis(e.to_string()))?;
1484
1485            if is_redis_error(&reply) {
1486                if !quiet {
1487                    out.extend_from_slice(b"SERVER_ERROR internal\r\n");
1488                }
1489                return Ok(());
1490            }
1491
1492            let (status, cas_val) = match &reply {
1493                RedisValue::Array(arr) if arr.len() >= 2 => (eval_int(&arr[0]), eval_str(&arr[1])),
1494                _ => {
1495                    if !quiet {
1496                        out.extend_from_slice(b"SERVER_ERROR internal\r\n");
1497                    }
1498                    return Ok(());
1499                }
1500            };
1501
1502            if !quiet {
1503                match status {
1504                    0 => {
1505                        out.extend_from_slice(b"HD");
1506                        if has_flag(flags, b'c') || cas != "0" {
1507                            out.push(b' ');
1508                            out.push(b'c');
1509                            out.extend_from_slice(cas_val.as_bytes());
1510                        }
1511                        write_meta_flag_echo(out, flags, key);
1512                        out.extend_from_slice(b"\r\n");
1513                    }
1514                    -5 => {
1515                        // NOT_FOUND for append/prepend.
1516                        out.extend_from_slice(b"NS");
1517                        write_meta_flag_echo(out, flags, key);
1518                        out.extend_from_slice(b"\r\n");
1519                    }
1520                    -2 => {
1521                        // CAS mismatch.
1522                        out.extend_from_slice(b"EX");
1523                        write_meta_flag_echo(out, flags, key);
1524                        out.extend_from_slice(b"\r\n");
1525                    }
1526                    _ => {
1527                        out.extend_from_slice(b"SERVER_ERROR internal\r\n");
1528                    }
1529                }
1530            }
1531        }
1532        "S" | "E" | "R" => {
1533            // S (set), E (add), R (replace) modes.
1534            let op_name = match mode {
1535                "E" => "add",
1536                "R" => "replace",
1537                _ => "set",
1538            };
1539            let rk = make_redis_key(key);
1540            let cas_str = cas.to_string();
1541            let flags_str = item_flags.to_string();
1542            let expiry_str = ttl.to_string();
1543
1544            let reply = with_ctx(|ctx| {
1545                let keys_and_args: &[&[u8]] = &[
1546                    b"2",
1547                    rk.as_slice(),
1548                    CAS_COUNTER_KEY.as_bytes(),
1549                    op_name.as_bytes(),
1550                    data,
1551                    flags_str.as_bytes(),
1552                    cas_str.as_bytes(),
1553                    expiry_str.as_bytes(),
1554                ];
1555                eval_lua(ctx, &SCRIPT_STORE, keys_and_args)
1556            })
1557            .map_err(|e| BridgeErr::Redis(e.to_string()))?;
1558
1559            if is_redis_error(&reply) {
1560                if !quiet {
1561                    out.extend_from_slice(b"SERVER_ERROR internal\r\n");
1562                }
1563                return Ok(());
1564            }
1565
1566            let (status, new_cas) = match &reply {
1567                RedisValue::Array(arr) if arr.len() >= 2 => (eval_int(&arr[0]), eval_str(&arr[1])),
1568                _ => {
1569                    if !quiet {
1570                        out.extend_from_slice(b"SERVER_ERROR internal\r\n");
1571                    }
1572                    return Ok(());
1573                }
1574            };
1575
1576            if !quiet {
1577                match status {
1578                    0 => {
1579                        STAT_CAS_HITS.fetch_add(1, Ordering::Relaxed);
1580                        out.extend_from_slice(b"HD");
1581                        if has_flag(flags, b'c') || cas != "0" {
1582                            out.push(b' ');
1583                            out.push(b'c');
1584                            out.extend_from_slice(new_cas.as_bytes());
1585                        }
1586                        write_meta_flag_echo(out, flags, key);
1587                        out.extend_from_slice(b"\r\n");
1588                    }
1589                    -1 => {
1590                        // NOT_FOUND (replace on missing key, or CAS on missing key).
1591                        out.extend_from_slice(b"NF");
1592                        write_meta_flag_echo(out, flags, key);
1593                        out.extend_from_slice(b"\r\n");
1594                    }
1595                    -2 => {
1596                        // KEY_EXISTS (add on existing, or CAS mismatch).
1597                        if cas != "0" {
1598                            STAT_CAS_BADVAL.fetch_add(1, Ordering::Relaxed);
1599                            out.extend_from_slice(b"EX");
1600                        } else {
1601                            out.extend_from_slice(b"NS");
1602                        }
1603                        write_meta_flag_echo(out, flags, key);
1604                        out.extend_from_slice(b"\r\n");
1605                    }
1606                    _ => {
1607                        out.extend_from_slice(b"SERVER_ERROR internal\r\n");
1608                    }
1609                }
1610            }
1611        }
1612        _ => {
1613            // Unreachable: validate_ms_flags rejects unknown modes before dispatch.
1614            if !quiet {
1615                out.extend_from_slice(b"CLIENT_ERROR unsupported ms mode\r\n");
1616            }
1617        }
1618    }
1619    Ok(())
1620}
1621
1622/// Handle md (meta delete).
1623#[cfg(not(test))]
1624fn meta_delete(key: &[u8], flags: &[MetaFlag], out: &mut Vec<u8>) -> Br<()> {
1625    let rk = make_redis_key(key);
1626    let cas = get_flag_token(flags, b'C').unwrap_or("0");
1627    let quiet = has_flag(flags, b'q');
1628
1629    // Non-CAS DELETE bypass: when CAS is "0", use direct DEL instead of Lua.
1630    let (reply, is_direct_del) = if cas == "0" {
1631        let r = with_ctx(|ctx| ctx.call("DEL", &[rk.as_slice()]))
1632            .map_err(|e| BridgeErr::Redis(e.to_string()))?;
1633        (r, true)
1634    } else {
1635        let r = with_ctx(|ctx| {
1636            let keys_and_args: &[&[u8]] = &[b"1", rk.as_slice(), cas.as_bytes()];
1637            eval_lua(ctx, &SCRIPT_DELETE, keys_and_args)
1638        })
1639        .map_err(|e| BridgeErr::Redis(e.to_string()))?;
1640        (r, false)
1641    };
1642
1643    if is_redis_error(&reply) {
1644        if !quiet {
1645            out.extend_from_slice(b"SERVER_ERROR internal\r\n");
1646        }
1647        return Ok(());
1648    }
1649
1650    // Direct DEL returns count (1=deleted, 0=not found).
1651    // Lua DELETE returns status (0=OK, -1=NOT_FOUND, -2=CAS mismatch).
1652    let status = match &reply {
1653        RedisValue::Integer(n) => {
1654            if is_direct_del {
1655                // Map DEL count to Lua-compatible status codes.
1656                if *n > 0 { 0 } else { -1 }
1657            } else {
1658                *n
1659            }
1660        }
1661        _ => {
1662            if !quiet {
1663                out.extend_from_slice(b"SERVER_ERROR internal\r\n");
1664            }
1665            return Ok(());
1666        }
1667    };
1668
1669    match status {
1670        0 => {
1671            STAT_DELETE_HITS.fetch_add(1, Ordering::Relaxed);
1672            if !quiet {
1673                out.extend_from_slice(b"HD");
1674                write_meta_flag_echo(out, flags, key);
1675                out.extend_from_slice(b"\r\n");
1676            }
1677        }
1678        -1 => {
1679            STAT_DELETE_MISSES.fetch_add(1, Ordering::Relaxed);
1680            if !quiet {
1681                out.extend_from_slice(b"NF");
1682                write_meta_flag_echo(out, flags, key);
1683                out.extend_from_slice(b"\r\n");
1684            }
1685        }
1686        -2 => {
1687            // CAS mismatch.
1688            if !quiet {
1689                out.extend_from_slice(b"EX");
1690                write_meta_flag_echo(out, flags, key);
1691                out.extend_from_slice(b"\r\n");
1692            }
1693        }
1694        _ => {
1695            if !quiet {
1696                out.extend_from_slice(b"SERVER_ERROR internal\r\n");
1697            }
1698        }
1699    }
1700    Ok(())
1701}
1702
1703/// Handle ma (meta arithmetic).
1704#[cfg(not(test))]
1705fn meta_arithmetic(key: &[u8], flags: &[MetaFlag], out: &mut Vec<u8>) -> Br<()> {
1706    let rk = make_redis_key(key);
1707    let quiet = has_flag(flags, b'q');
1708
1709    let delta_str = get_flag_token(flags, b'D').unwrap_or("1");
1710    let mode = get_flag_token(flags, b'M').unwrap_or("I");
1711    // Mode validation already done in validate_ma_flags; only I/D reach here.
1712    let is_decr = mode == "D";
1713    let is_decr_str = if is_decr { "1" } else { "0" };
1714    let initial = get_flag_token(flags, b'J').unwrap_or("0");
1715    // N flag = TTL for auto-vivification. Without N, use 4294967295 to signal "do not create".
1716    let expiry = get_flag_token(flags, b'N').unwrap_or("4294967295");
1717
1718    let reply = with_ctx(|ctx| {
1719        let keys_and_args: &[&[u8]] = &[
1720            b"2",
1721            rk.as_slice(),
1722            CAS_COUNTER_KEY.as_bytes(),
1723            delta_str.as_bytes(),
1724            is_decr_str.as_bytes(),
1725            initial.as_bytes(),
1726            expiry.as_bytes(),
1727        ];
1728        eval_lua(ctx, &SCRIPT_COUNTER, keys_and_args)
1729    })
1730    .map_err(|e| BridgeErr::Redis(e.to_string()))?;
1731
1732    if is_redis_error(&reply) {
1733        if !quiet {
1734            out.extend_from_slice(b"SERVER_ERROR internal\r\n");
1735        }
1736        return Ok(());
1737    }
1738
1739    let (status, value_str, cas_str) = match &reply {
1740        RedisValue::Array(arr) if arr.len() >= 3 => {
1741            let st = eval_int(&arr[0]);
1742            let val = eval_str(&arr[1]);
1743            let cas_s = eval_str(&arr[2]);
1744            (st, val, cas_s)
1745        }
1746        _ => {
1747            if !quiet {
1748                out.extend_from_slice(b"SERVER_ERROR internal\r\n");
1749            }
1750            return Ok(());
1751        }
1752    };
1753
1754    if is_decr {
1755        if status == 0 {
1756            STAT_DECR_HITS.fetch_add(1, Ordering::Relaxed);
1757        } else {
1758            STAT_DECR_MISSES.fetch_add(1, Ordering::Relaxed);
1759        }
1760    } else {
1761        if status == 0 {
1762            STAT_INCR_HITS.fetch_add(1, Ordering::Relaxed);
1763        } else {
1764            STAT_INCR_MISSES.fetch_add(1, Ordering::Relaxed);
1765        }
1766    }
1767
1768    match status {
1769        0 => {
1770            let want_value = has_flag(flags, b'v');
1771            if !quiet {
1772                if want_value {
1773                    out.extend_from_slice(b"VA ");
1774                    out.extend_from_slice(value_str.len().to_string().as_bytes());
1775                } else {
1776                    out.extend_from_slice(b"HD");
1777                }
1778                if has_flag(flags, b'c') {
1779                    out.push(b' ');
1780                    out.push(b'c');
1781                    out.extend_from_slice(cas_str.as_bytes());
1782                }
1783                write_meta_flag_echo(out, flags, key);
1784                out.extend_from_slice(b"\r\n");
1785                if want_value {
1786                    out.extend_from_slice(value_str.as_bytes());
1787                    out.extend_from_slice(b"\r\n");
1788                }
1789            }
1790        }
1791        -1 => {
1792            // NOT_FOUND — key doesn't exist and N flag not present.
1793            if !quiet {
1794                out.extend_from_slice(b"NF");
1795                write_meta_flag_echo(out, flags, key);
1796                out.extend_from_slice(b"\r\n");
1797            }
1798        }
1799        -3 => {
1800            // NON_NUMERIC — value is not a number.
1801            if !quiet {
1802                out.extend_from_slice(
1803                    b"CLIENT_ERROR cannot increment or decrement non-numeric value\r\n",
1804                );
1805            }
1806        }
1807        _ => {
1808            if !quiet {
1809                out.extend_from_slice(b"SERVER_ERROR internal\r\n");
1810            }
1811        }
1812    }
1813    Ok(())
1814}
1815
1816/// Handle stats [args].
1817#[cfg(not(test))]
1818fn ascii_stats(args: Option<&str>, out: &mut Vec<u8>) -> Br<()> {
1819    match args {
1820        None | Some("") => {
1821            // General stats — same counters as binary stat handler.
1822            let uptime = STARTUP_INSTANT
1823                .get()
1824                .map(|t| t.elapsed().as_secs())
1825                .unwrap_or(0);
1826            let pid = std::process::id();
1827
1828            let curr_items: u64 = match with_ctx(|ctx| {
1829                let keys_and_args: &[&[u8]] = &[b"0"];
1830                eval_lua(ctx, &SCRIPT_COUNT_ITEMS, keys_and_args)
1831            }) {
1832                Ok(RedisValue::Integer(n)) => n as u64,
1833                _ => 0,
1834            };
1835
1836            let stats: Vec<(&str, String)> = vec![
1837                ("pid", pid.to_string()),
1838                ("uptime", uptime.to_string()),
1839                (
1840                    "time",
1841                    std::time::SystemTime::now()
1842                        .duration_since(std::time::UNIX_EPOCH)
1843                        .map(|d| d.as_secs())
1844                        .unwrap_or(0)
1845                        .to_string(),
1846                ),
1847                ("version", "RedCouch 0.1.0".to_string()),
1848                ("curr_items", curr_items.to_string()),
1849                (
1850                    "curr_connections",
1851                    STAT_CURR_CONNECTIONS.load(Ordering::Relaxed).to_string(),
1852                ),
1853                (
1854                    "total_connections",
1855                    STAT_TOTAL_CONNECTIONS.load(Ordering::Relaxed).to_string(),
1856                ),
1857                ("cmd_get", STAT_CMD_GET.load(Ordering::Relaxed).to_string()),
1858                ("cmd_set", STAT_CMD_SET.load(Ordering::Relaxed).to_string()),
1859                (
1860                    "cmd_flush",
1861                    STAT_CMD_FLUSH.load(Ordering::Relaxed).to_string(),
1862                ),
1863                (
1864                    "cmd_touch",
1865                    STAT_CMD_TOUCH.load(Ordering::Relaxed).to_string(),
1866                ),
1867                (
1868                    "get_hits",
1869                    STAT_GET_HITS.load(Ordering::Relaxed).to_string(),
1870                ),
1871                (
1872                    "get_misses",
1873                    STAT_GET_MISSES.load(Ordering::Relaxed).to_string(),
1874                ),
1875                (
1876                    "delete_hits",
1877                    STAT_DELETE_HITS.load(Ordering::Relaxed).to_string(),
1878                ),
1879                (
1880                    "delete_misses",
1881                    STAT_DELETE_MISSES.load(Ordering::Relaxed).to_string(),
1882                ),
1883                (
1884                    "incr_hits",
1885                    STAT_INCR_HITS.load(Ordering::Relaxed).to_string(),
1886                ),
1887                (
1888                    "incr_misses",
1889                    STAT_INCR_MISSES.load(Ordering::Relaxed).to_string(),
1890                ),
1891                (
1892                    "decr_hits",
1893                    STAT_DECR_HITS.load(Ordering::Relaxed).to_string(),
1894                ),
1895                (
1896                    "decr_misses",
1897                    STAT_DECR_MISSES.load(Ordering::Relaxed).to_string(),
1898                ),
1899                (
1900                    "cas_hits",
1901                    STAT_CAS_HITS.load(Ordering::Relaxed).to_string(),
1902                ),
1903                (
1904                    "cas_misses",
1905                    STAT_CAS_MISSES.load(Ordering::Relaxed).to_string(),
1906                ),
1907                (
1908                    "cas_badval",
1909                    STAT_CAS_BADVAL.load(Ordering::Relaxed).to_string(),
1910                ),
1911                (
1912                    "auth_cmds",
1913                    STAT_AUTH_CMDS.load(Ordering::Relaxed).to_string(),
1914                ),
1915                (
1916                    "auth_errors",
1917                    STAT_AUTH_ERRORS.load(Ordering::Relaxed).to_string(),
1918                ),
1919                (
1920                    "rejected_connections",
1921                    STAT_REJECTED_CONNECTIONS
1922                        .load(Ordering::Relaxed)
1923                        .to_string(),
1924                ),
1925                ("max_connections", MAX_CONNECTIONS.to_string()),
1926            ];
1927
1928            for (name, value) in &stats {
1929                out.extend_from_slice(b"STAT ");
1930                out.extend_from_slice(name.as_bytes());
1931                out.push(b' ');
1932                out.extend_from_slice(value.as_bytes());
1933                out.extend_from_slice(b"\r\n");
1934            }
1935        }
1936        // All unsupported stat groups → empty END.
1937        Some(_) => {}
1938    }
1939    out.extend_from_slice(b"END\r\n");
1940    Ok(())
1941}
1942
1943// ═══════════════════════════════════════════════════════════════════
1944// Unit tests — pure parser logic only (no Redis)
1945// ═══════════════════════════════════════════════════════════════════
1946
1947#[cfg(test)]
1948mod tests {
1949    use super::*;
1950
1951    // ── find_line_end ───────────────────────────────────────────────
1952
1953    #[test]
1954    fn line_end_crlf() {
1955        let buf = b"get foo\r\n";
1956        assert_eq!(find_line_end(buf), Some((7, 9)));
1957    }
1958
1959    #[test]
1960    fn line_end_bare_lf() {
1961        let buf = b"get foo\n";
1962        assert_eq!(find_line_end(buf), Some((7, 8)));
1963    }
1964
1965    #[test]
1966    fn line_end_no_terminator() {
1967        let buf = b"get foo";
1968        assert_eq!(find_line_end(buf), None);
1969    }
1970
1971    #[test]
1972    fn line_end_empty() {
1973        assert_eq!(find_line_end(b""), None);
1974    }
1975
1976    #[test]
1977    fn line_end_just_crlf() {
1978        assert_eq!(find_line_end(b"\r\n"), Some((0, 2)));
1979    }
1980
1981    // ── validate_key ────────────────────────────────────────────────
1982
1983    #[test]
1984    fn key_valid() {
1985        assert!(validate_key(b"mykey").is_ok());
1986    }
1987
1988    #[test]
1989    fn key_empty() {
1990        assert!(validate_key(b"").is_err());
1991    }
1992
1993    #[test]
1994    fn key_too_long() {
1995        let long_key = vec![b'a'; MAX_KEY_LEN + 1];
1996        assert!(validate_key(&long_key).is_err());
1997    }
1998
1999    #[test]
2000    fn key_max_length_ok() {
2001        let key = vec![b'a'; MAX_KEY_LEN];
2002        assert!(validate_key(&key).is_ok());
2003    }
2004
2005    #[test]
2006    fn key_with_space() {
2007        assert!(validate_key(b"has space").is_err());
2008    }
2009
2010    #[test]
2011    fn key_with_control_char() {
2012        assert!(validate_key(b"has\x01ctrl").is_err());
2013    }
2014
2015    // ── parse_command_line: storage ─────────────────────────────────
2016
2017    #[test]
2018    fn parse_set_basic() {
2019        match parse_command_line(b"set mykey 0 60 5") {
2020            CmdParseResult::Ok(AsciiCmd::Store {
2021                cmd,
2022                key,
2023                flags,
2024                exptime,
2025                bytes,
2026                noreply,
2027            }) => {
2028                assert_eq!(cmd, StoreOp::Set);
2029                assert_eq!(key, b"mykey");
2030                assert_eq!(flags, 0);
2031                assert_eq!(exptime, 60);
2032                assert_eq!(bytes, 5);
2033                assert!(!noreply);
2034            }
2035            other => panic!("unexpected: {other:?}"),
2036        }
2037    }
2038
2039    #[test]
2040    fn parse_set_noreply() {
2041        match parse_command_line(b"set k 1 0 3 noreply") {
2042            CmdParseResult::Ok(AsciiCmd::Store { noreply, .. }) => assert!(noreply),
2043            other => panic!("unexpected: {other:?}"),
2044        }
2045    }
2046
2047    #[test]
2048    fn parse_add() {
2049        match parse_command_line(b"add k 0 0 1") {
2050            CmdParseResult::Ok(AsciiCmd::Store {
2051                cmd: StoreOp::Add, ..
2052            }) => {}
2053            other => panic!("unexpected: {other:?}"),
2054        }
2055    }
2056
2057    #[test]
2058    fn parse_replace() {
2059        match parse_command_line(b"replace k 0 0 1") {
2060            CmdParseResult::Ok(AsciiCmd::Store {
2061                cmd: StoreOp::Replace,
2062                ..
2063            }) => {}
2064            other => panic!("unexpected: {other:?}"),
2065        }
2066    }
2067
2068    // ── parse_command_line: CAS ─────────────────────────────────────
2069
2070    #[test]
2071    fn parse_cas_basic() {
2072        match parse_command_line(b"cas k 0 0 5 12345") {
2073            CmdParseResult::Ok(AsciiCmd::Cas {
2074                key,
2075                cas_unique,
2076                noreply,
2077                ..
2078            }) => {
2079                assert_eq!(key, b"k");
2080                assert_eq!(cas_unique, 12345);
2081                assert!(!noreply);
2082            }
2083            other => panic!("unexpected: {other:?}"),
2084        }
2085    }
2086
2087    #[test]
2088    fn parse_cas_noreply() {
2089        match parse_command_line(b"cas k 0 0 5 100 noreply") {
2090            CmdParseResult::Ok(AsciiCmd::Cas { noreply, .. }) => assert!(noreply),
2091            other => panic!("unexpected: {other:?}"),
2092        }
2093    }
2094
2095    // ── parse_command_line: append/prepend ──────────────────────────
2096
2097    #[test]
2098    fn parse_append() {
2099        match parse_command_line(b"append k 5") {
2100            CmdParseResult::Ok(AsciiCmd::AppendPrepend {
2101                is_prepend,
2102                key,
2103                bytes,
2104                noreply,
2105            }) => {
2106                assert!(!is_prepend);
2107                assert_eq!(key, b"k");
2108                assert_eq!(bytes, 5);
2109                assert!(!noreply);
2110            }
2111            other => panic!("unexpected: {other:?}"),
2112        }
2113    }
2114
2115    #[test]
2116    fn parse_prepend_noreply() {
2117        match parse_command_line(b"prepend k 3 noreply") {
2118            CmdParseResult::Ok(AsciiCmd::AppendPrepend {
2119                is_prepend,
2120                noreply,
2121                ..
2122            }) => {
2123                assert!(is_prepend);
2124                assert!(noreply);
2125            }
2126            other => panic!("unexpected: {other:?}"),
2127        }
2128    }
2129
2130    // ── parse_command_line: retrieval ───────────────────────────────
2131
2132    #[test]
2133    fn parse_get_single() {
2134        match parse_command_line(b"get foo") {
2135            CmdParseResult::Ok(AsciiCmd::Retrieval { cmd, exptime, keys }) => {
2136                assert_eq!(cmd, RetrievalOp::Get);
2137                assert!(exptime.is_none());
2138                assert_eq!(keys, vec![b"foo".as_slice()]);
2139            }
2140            other => panic!("unexpected: {other:?}"),
2141        }
2142    }
2143
2144    #[test]
2145    fn parse_get_multi() {
2146        match parse_command_line(b"get a b c") {
2147            CmdParseResult::Ok(AsciiCmd::Retrieval { keys, .. }) => {
2148                assert_eq!(keys.len(), 3);
2149            }
2150            other => panic!("unexpected: {other:?}"),
2151        }
2152    }
2153
2154    #[test]
2155    fn parse_gets() {
2156        match parse_command_line(b"gets k") {
2157            CmdParseResult::Ok(AsciiCmd::Retrieval {
2158                cmd: RetrievalOp::Gets,
2159                ..
2160            }) => {}
2161            other => panic!("unexpected: {other:?}"),
2162        }
2163    }
2164
2165    #[test]
2166    fn parse_gat() {
2167        match parse_command_line(b"gat 60 k1 k2") {
2168            CmdParseResult::Ok(AsciiCmd::Retrieval { cmd, exptime, keys }) => {
2169                assert_eq!(cmd, RetrievalOp::Gat);
2170                assert_eq!(exptime, Some(60));
2171                assert_eq!(keys.len(), 2);
2172            }
2173            other => panic!("unexpected: {other:?}"),
2174        }
2175    }
2176
2177    #[test]
2178    fn parse_gats() {
2179        match parse_command_line(b"gats 0 k") {
2180            CmdParseResult::Ok(AsciiCmd::Retrieval {
2181                cmd: RetrievalOp::Gats,
2182                ..
2183            }) => {}
2184            other => panic!("unexpected: {other:?}"),
2185        }
2186    }
2187
2188    // ── parse_command_line: delete/counter/touch ────────────────────
2189
2190    #[test]
2191    fn parse_delete() {
2192        match parse_command_line(b"delete mykey") {
2193            CmdParseResult::Ok(AsciiCmd::Delete { key, noreply }) => {
2194                assert_eq!(key, b"mykey");
2195                assert!(!noreply);
2196            }
2197            other => panic!("unexpected: {other:?}"),
2198        }
2199    }
2200
2201    #[test]
2202    fn parse_delete_noreply() {
2203        match parse_command_line(b"delete k noreply") {
2204            CmdParseResult::Ok(AsciiCmd::Delete { noreply, .. }) => assert!(noreply),
2205            other => panic!("unexpected: {other:?}"),
2206        }
2207    }
2208
2209    #[test]
2210    fn parse_incr() {
2211        match parse_command_line(b"incr counter 5") {
2212            CmdParseResult::Ok(AsciiCmd::Counter {
2213                is_decr,
2214                key,
2215                value,
2216                noreply,
2217            }) => {
2218                assert!(!is_decr);
2219                assert_eq!(key, b"counter");
2220                assert_eq!(value, 5);
2221                assert!(!noreply);
2222            }
2223            other => panic!("unexpected: {other:?}"),
2224        }
2225    }
2226
2227    #[test]
2228    fn parse_decr_noreply() {
2229        match parse_command_line(b"decr c 10 noreply") {
2230            CmdParseResult::Ok(AsciiCmd::Counter {
2231                is_decr, noreply, ..
2232            }) => {
2233                assert!(is_decr);
2234                assert!(noreply);
2235            }
2236            other => panic!("unexpected: {other:?}"),
2237        }
2238    }
2239
2240    #[test]
2241    fn parse_touch() {
2242        match parse_command_line(b"touch k 300") {
2243            CmdParseResult::Ok(AsciiCmd::Touch {
2244                key,
2245                exptime,
2246                noreply,
2247            }) => {
2248                assert_eq!(key, b"k");
2249                assert_eq!(exptime, 300);
2250                assert!(!noreply);
2251            }
2252            other => panic!("unexpected: {other:?}"),
2253        }
2254    }
2255
2256    // ── parse_command_line: admin commands ──────────────────────────
2257
2258    #[test]
2259    fn parse_flush_all() {
2260        match parse_command_line(b"flush_all") {
2261            CmdParseResult::Ok(AsciiCmd::FlushAll { _delay, noreply }) => {
2262                assert_eq!(_delay, 0);
2263                assert!(!noreply);
2264            }
2265            other => panic!("unexpected: {other:?}"),
2266        }
2267    }
2268
2269    #[test]
2270    fn parse_flush_all_delay_noreply() {
2271        match parse_command_line(b"flush_all 30 noreply") {
2272            CmdParseResult::Ok(AsciiCmd::FlushAll { _delay, noreply }) => {
2273                assert_eq!(_delay, 30);
2274                assert!(noreply);
2275            }
2276            other => panic!("unexpected: {other:?}"),
2277        }
2278    }
2279
2280    #[test]
2281    fn parse_version() {
2282        assert!(matches!(
2283            parse_command_line(b"version"),
2284            CmdParseResult::Ok(AsciiCmd::Version)
2285        ));
2286    }
2287
2288    #[test]
2289    fn parse_stats_bare() {
2290        match parse_command_line(b"stats") {
2291            CmdParseResult::Ok(AsciiCmd::Stats { args }) => assert!(args.is_none()),
2292            other => panic!("unexpected: {other:?}"),
2293        }
2294    }
2295
2296    #[test]
2297    fn parse_stats_with_arg() {
2298        match parse_command_line(b"stats items") {
2299            CmdParseResult::Ok(AsciiCmd::Stats { args }) => {
2300                assert_eq!(args, Some("items"));
2301            }
2302            other => panic!("unexpected: {other:?}"),
2303        }
2304    }
2305
2306    #[test]
2307    fn parse_verbosity() {
2308        match parse_command_line(b"verbosity 2") {
2309            CmdParseResult::Ok(AsciiCmd::Verbosity { noreply }) => assert!(!noreply),
2310            other => panic!("unexpected: {other:?}"),
2311        }
2312    }
2313
2314    #[test]
2315    fn parse_quit() {
2316        assert!(matches!(
2317            parse_command_line(b"quit"),
2318            CmdParseResult::Ok(AsciiCmd::Quit)
2319        ));
2320    }
2321
2322    // ── parse_command_line: error cases ─────────────────────────────
2323
2324    #[test]
2325    fn parse_unknown_command() {
2326        assert!(matches!(
2327            parse_command_line(b"foobar key"),
2328            CmdParseResult::UnknownCommand
2329        ));
2330    }
2331
2332    #[test]
2333    fn parse_set_missing_args() {
2334        match parse_command_line(b"set k 0") {
2335            CmdParseResult::ClientError(msg) => assert!(!msg.is_empty()),
2336            other => panic!("expected ClientError, got {other:?}"),
2337        }
2338    }
2339
2340    #[test]
2341    fn parse_set_bad_flags() {
2342        assert!(matches!(
2343            parse_command_line(b"set k notanum 0 5"),
2344            CmdParseResult::ClientError(_)
2345        ));
2346    }
2347
2348    #[test]
2349    fn parse_get_no_keys() {
2350        assert!(matches!(
2351            parse_command_line(b"get"),
2352            CmdParseResult::ClientError(_)
2353        ));
2354    }
2355
2356    #[test]
2357    fn parse_incr_bad_value() {
2358        assert!(matches!(
2359            parse_command_line(b"incr k -5"),
2360            CmdParseResult::ClientError(_)
2361        ));
2362    }
2363
2364    #[test]
2365    fn parse_set_bad_noreply_token() {
2366        assert!(matches!(
2367            parse_command_line(b"set k 0 0 5 garbage"),
2368            CmdParseResult::ClientError(_)
2369        ));
2370    }
2371
2372    #[test]
2373    fn parse_delete_bad_extra() {
2374        assert!(matches!(
2375            parse_command_line(b"delete k extra"),
2376            CmdParseResult::ClientError(_)
2377        ));
2378    }
2379
2380    // ── needs_data_block / data_block_len / is_noreply ─────────────
2381
2382    #[test]
2383    fn data_block_for_store() {
2384        let cmd = AsciiCmd::Store {
2385            cmd: StoreOp::Set,
2386            key: b"k",
2387            flags: 0,
2388            exptime: 0,
2389            bytes: 10,
2390            noreply: false,
2391        };
2392        assert!(needs_data_block(&cmd));
2393        assert_eq!(data_block_len(&cmd), 10);
2394        assert!(!is_noreply(&cmd));
2395    }
2396
2397    #[test]
2398    fn no_data_block_for_get() {
2399        let cmd = AsciiCmd::Retrieval {
2400            cmd: RetrievalOp::Get,
2401            exptime: None,
2402            keys: vec![b"k"],
2403        };
2404        assert!(!needs_data_block(&cmd));
2405    }
2406
2407    // ── append/prepend correct wire format (no flags/exptime) ──────
2408
2409    #[test]
2410    fn append_no_flags_exptime() {
2411        // This must fail: append does NOT take flags/exptime.
2412        assert!(matches!(
2413            parse_command_line(b"append k 0 0 5"),
2414            CmdParseResult::ClientError(_),
2415        ));
2416    }
2417
2418    #[test]
2419    fn prepend_no_flags_exptime() {
2420        assert!(matches!(
2421            parse_command_line(b"prepend k 0 0 5"),
2422            CmdParseResult::ClientError(_),
2423        ));
2424    }
2425
2426    // ── is_meta_command (prefix-based text-path routing) ───────────
2427
2428    #[test]
2429    fn meta_get_is_meta() {
2430        assert!(is_meta_command(b"mg mykey"));
2431    }
2432
2433    #[test]
2434    fn meta_set_is_meta() {
2435        assert!(is_meta_command(b"ms mykey 5"));
2436    }
2437
2438    #[test]
2439    fn meta_delete_is_meta() {
2440        assert!(is_meta_command(b"md mykey"));
2441    }
2442
2443    #[test]
2444    fn meta_arithmetic_is_meta() {
2445        assert!(is_meta_command(b"ma mykey"));
2446    }
2447
2448    #[test]
2449    fn meta_noop_bare_is_meta() {
2450        assert!(is_meta_command(b"mn"));
2451    }
2452
2453    #[test]
2454    fn meta_debug_is_meta() {
2455        assert!(is_meta_command(b"me mykey"));
2456    }
2457
2458    #[test]
2459    fn classic_get_not_meta() {
2460        assert!(!is_meta_command(b"get foo"));
2461    }
2462
2463    #[test]
2464    fn classic_set_not_meta() {
2465        assert!(!is_meta_command(b"set foo 0 0 5"));
2466    }
2467
2468    #[test]
2469    fn short_line_not_meta() {
2470        assert!(!is_meta_command(b"m"));
2471    }
2472
2473    #[test]
2474    fn empty_line_not_meta() {
2475        assert!(!is_meta_command(b""));
2476    }
2477
2478    #[test]
2479    fn mg_without_space_not_meta() {
2480        // "mgx" is not a meta command — must be followed by space or end.
2481        assert!(!is_meta_command(b"mgx foo"));
2482    }
2483
2484    // ── parse_command_line: additional error paths ──────────────────
2485
2486    #[test]
2487    fn parse_empty_line() {
2488        assert!(matches!(
2489            parse_command_line(b""),
2490            CmdParseResult::UnknownCommand
2491        ));
2492    }
2493
2494    #[test]
2495    fn parse_whitespace_only() {
2496        assert!(matches!(
2497            parse_command_line(b"   "),
2498            CmdParseResult::UnknownCommand
2499        ));
2500    }
2501
2502    #[test]
2503    fn parse_non_utf8_line() {
2504        assert!(matches!(
2505            parse_command_line(b"set \xff\xfe 0 0 5"),
2506            CmdParseResult::ClientError(_)
2507        ));
2508    }
2509
2510    // ── cas error paths ─────────────────────────────────────────────
2511
2512    #[test]
2513    fn parse_cas_too_few_args() {
2514        assert!(matches!(
2515            parse_command_line(b"cas k 0 0"),
2516            CmdParseResult::ClientError(_)
2517        ));
2518    }
2519
2520    #[test]
2521    fn parse_cas_too_many_args() {
2522        assert!(matches!(
2523            parse_command_line(b"cas k 0 0 5 100 noreply extra"),
2524            CmdParseResult::ClientError(_)
2525        ));
2526    }
2527
2528    #[test]
2529    fn parse_cas_bad_cas_value() {
2530        assert!(matches!(
2531            parse_command_line(b"cas k 0 0 5 notanum"),
2532            CmdParseResult::ClientError(_)
2533        ));
2534    }
2535
2536    #[test]
2537    fn parse_cas_bad_noreply_token() {
2538        assert!(matches!(
2539            parse_command_line(b"cas k 0 0 5 100 garbage"),
2540            CmdParseResult::ClientError(_)
2541        ));
2542    }
2543
2544    // ── counter error paths ─────────────────────────────────────────
2545
2546    #[test]
2547    fn parse_incr_too_few_args() {
2548        assert!(matches!(
2549            parse_command_line(b"incr k"),
2550            CmdParseResult::ClientError(_)
2551        ));
2552    }
2553
2554    #[test]
2555    fn parse_decr_too_many_args() {
2556        assert!(matches!(
2557            parse_command_line(b"decr k 5 noreply extra"),
2558            CmdParseResult::ClientError(_)
2559        ));
2560    }
2561
2562    #[test]
2563    fn parse_incr_bad_noreply_token() {
2564        assert!(matches!(
2565            parse_command_line(b"incr k 5 garbage"),
2566            CmdParseResult::ClientError(_)
2567        ));
2568    }
2569
2570    // ── touch error paths ───────────────────────────────────────────
2571
2572    #[test]
2573    fn parse_touch_too_few_args() {
2574        assert!(matches!(
2575            parse_command_line(b"touch k"),
2576            CmdParseResult::ClientError(_)
2577        ));
2578    }
2579
2580    #[test]
2581    fn parse_touch_too_many_args() {
2582        assert!(matches!(
2583            parse_command_line(b"touch k 60 noreply extra"),
2584            CmdParseResult::ClientError(_)
2585        ));
2586    }
2587
2588    #[test]
2589    fn parse_touch_bad_exptime() {
2590        assert!(matches!(
2591            parse_command_line(b"touch k notanum"),
2592            CmdParseResult::ClientError(_)
2593        ));
2594    }
2595
2596    #[test]
2597    fn parse_touch_bad_noreply_token() {
2598        assert!(matches!(
2599            parse_command_line(b"touch k 60 garbage"),
2600            CmdParseResult::ClientError(_)
2601        ));
2602    }
2603
2604    #[test]
2605    fn parse_touch_noreply() {
2606        match parse_command_line(b"touch k 60 noreply") {
2607            CmdParseResult::Ok(AsciiCmd::Touch { noreply, .. }) => assert!(noreply),
2608            other => panic!("unexpected: {other:?}"),
2609        }
2610    }
2611
2612    // ── gat/gats error paths ────────────────────────────────────────
2613
2614    #[test]
2615    fn parse_gat_no_keys() {
2616        assert!(matches!(
2617            parse_command_line(b"gat 60"),
2618            CmdParseResult::ClientError(_)
2619        ));
2620    }
2621
2622    #[test]
2623    fn parse_gat_bad_exptime() {
2624        assert!(matches!(
2625            parse_command_line(b"gat notanum k"),
2626            CmdParseResult::ClientError(_)
2627        ));
2628    }
2629
2630    #[test]
2631    fn parse_gats_no_keys() {
2632        assert!(matches!(
2633            parse_command_line(b"gats 0"),
2634            CmdParseResult::ClientError(_)
2635        ));
2636    }
2637
2638    // ── flush_all error paths ───────────────────────────────────────
2639
2640    #[test]
2641    fn parse_flush_bad_arg() {
2642        assert!(matches!(
2643            parse_command_line(b"flush_all notanum"),
2644            CmdParseResult::ClientError(_)
2645        ));
2646    }
2647
2648    // ── delete error paths ──────────────────────────────────────────
2649
2650    #[test]
2651    fn parse_delete_no_key() {
2652        assert!(matches!(
2653            parse_command_line(b"delete"),
2654            CmdParseResult::ClientError(_)
2655        ));
2656    }
2657
2658    #[test]
2659    fn parse_delete_too_many_args() {
2660        assert!(matches!(
2661            parse_command_line(b"delete k noreply extra"),
2662            CmdParseResult::ClientError(_)
2663        ));
2664    }
2665
2666    // ── set error paths: bad exptime, bad bytes ─────────────────────
2667
2668    #[test]
2669    fn parse_set_bad_exptime() {
2670        assert!(matches!(
2671            parse_command_line(b"set k 0 notanum 5"),
2672            CmdParseResult::ClientError(_)
2673        ));
2674    }
2675
2676    #[test]
2677    fn parse_set_bad_bytes() {
2678        assert!(matches!(
2679            parse_command_line(b"set k 0 0 notanum"),
2680            CmdParseResult::ClientError(_)
2681        ));
2682    }
2683
2684    #[test]
2685    fn parse_set_too_many_args() {
2686        assert!(matches!(
2687            parse_command_line(b"set k 0 0 5 noreply extra"),
2688            CmdParseResult::ClientError(_)
2689        ));
2690    }
2691
2692    // ── append/prepend error paths ──────────────────────────────────
2693
2694    #[test]
2695    fn parse_append_too_few_args() {
2696        assert!(matches!(
2697            parse_command_line(b"append k"),
2698            CmdParseResult::ClientError(_)
2699        ));
2700    }
2701
2702    #[test]
2703    fn parse_append_too_many_args() {
2704        assert!(matches!(
2705            parse_command_line(b"append k 5 noreply extra"),
2706            CmdParseResult::ClientError(_)
2707        ));
2708    }
2709
2710    #[test]
2711    fn parse_append_bad_bytes() {
2712        assert!(matches!(
2713            parse_command_line(b"append k notanum"),
2714            CmdParseResult::ClientError(_)
2715        ));
2716    }
2717
2718    #[test]
2719    fn parse_append_bad_noreply_token() {
2720        assert!(matches!(
2721            parse_command_line(b"append k 5 garbage"),
2722            CmdParseResult::ClientError(_)
2723        ));
2724    }
2725
2726    // ── verbosity noreply ───────────────────────────────────────────
2727
2728    #[test]
2729    fn parse_verbosity_noreply() {
2730        match parse_command_line(b"verbosity 2 noreply") {
2731            CmdParseResult::Ok(AsciiCmd::Verbosity { noreply }) => assert!(noreply),
2732            other => panic!("unexpected: {other:?}"),
2733        }
2734    }
2735
2736    // ── gets with multiple keys ─────────────────────────────────────
2737
2738    #[test]
2739    fn parse_gets_multi() {
2740        match parse_command_line(b"gets a b c") {
2741            CmdParseResult::Ok(AsciiCmd::Retrieval {
2742                cmd: RetrievalOp::Gets,
2743                keys,
2744                ..
2745            }) => assert_eq!(keys.len(), 3),
2746            other => panic!("unexpected: {other:?}"),
2747        }
2748    }
2749
2750    #[test]
2751    fn parse_gets_no_keys() {
2752        assert!(matches!(
2753            parse_command_line(b"gets"),
2754            CmdParseResult::ClientError(_)
2755        ));
2756    }
2757
2758    // ── data_block_len and is_noreply for additional variants ───────
2759
2760    #[test]
2761    fn data_block_for_cas() {
2762        let cmd = AsciiCmd::Cas {
2763            key: b"k",
2764            flags: 0,
2765            exptime: 0,
2766            bytes: 7,
2767            cas_unique: 1,
2768            noreply: true,
2769        };
2770        assert!(needs_data_block(&cmd));
2771        assert_eq!(data_block_len(&cmd), 7);
2772        assert!(is_noreply(&cmd));
2773    }
2774
2775    #[test]
2776    fn data_block_for_append_prepend() {
2777        let cmd = AsciiCmd::AppendPrepend {
2778            is_prepend: true,
2779            key: b"k",
2780            bytes: 3,
2781            noreply: false,
2782        };
2783        assert!(needs_data_block(&cmd));
2784        assert_eq!(data_block_len(&cmd), 3);
2785        assert!(!is_noreply(&cmd));
2786    }
2787
2788    #[test]
2789    fn is_noreply_for_non_noreply_variants() {
2790        assert!(!is_noreply(&AsciiCmd::Version));
2791        assert!(!is_noreply(&AsciiCmd::Quit));
2792        assert!(!is_noreply(&AsciiCmd::Stats { args: None }));
2793        assert!(!is_noreply(&AsciiCmd::Retrieval {
2794            cmd: RetrievalOp::Get,
2795            exptime: None,
2796            keys: vec![b"k"],
2797        }));
2798    }
2799
2800    #[test]
2801    fn data_block_len_zero_for_non_store() {
2802        assert_eq!(
2803            data_block_len(&AsciiCmd::Delete {
2804                key: b"k",
2805                noreply: false
2806            }),
2807            0
2808        );
2809    }
2810
2811    // ── key validation edge cases ───────────────────────────────────
2812
2813    #[test]
2814    fn key_with_tab() {
2815        assert!(validate_key(b"has\ttab").is_err());
2816    }
2817
2818    #[test]
2819    fn key_with_del() {
2820        assert!(validate_key(b"has\x7fchar").is_err());
2821    }
2822
2823    #[test]
2824    fn key_high_bytes_ok() {
2825        // Bytes > 0x7F are valid in keys (binary-safe above ASCII range).
2826        assert!(validate_key(b"\x80\xff").is_ok());
2827    }
2828
2829    // ── meta tab separator ──────────────────────────────────────────
2830
2831    #[test]
2832    fn meta_with_tab_is_meta() {
2833        assert!(is_meta_command(b"mg\tkey"));
2834    }
2835}