yo-resp 0.3.20

The RESP2 and RESP3 codec: borrowed request frames in, wire bytes out, no allocation on the hot path.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
//! The connection and server commands.
//!
//! None of these touch a key. They are here because a client library sends most
//! of them before it sends anything else: a driver opens a socket, says `HELLO
//! 3`, maybe `SELECT 4`, asks `COMMAND DOCS` or `COMMAND COUNT` to build its
//! own routing table, and only then does any work. A server that answers `GET`
//! perfectly and `HELLO` badly is a server no client library can talk to, which
//! is why these land in the same milestone as the string commands rather than
//! after them.
//!
//! The replies were read off a running Redis 8.8 in both protocols. The shapes
//! are not obvious from the documentation: `HELLO` is a map on RESP3 and the
//! same pairs flattened on RESP2, `CONFIG GET` is the same, `INFO` is a
//! verbatim string on RESP3 and a bulk string on RESP2, and the flags in
//! `COMMAND INFO` are simple strings inside an array rather than bulk strings.

use super::args::{self, Args, is};
use super::table::{self, Spec};
use super::{DATABASES, Flow, Server, Session, backup, cpu};
use crate::proto::Proto;
use crate::reply::Out;
use core::fmt::Write;
use std::time::{SystemTime, UNIX_EPOCH};
use yo_common::num::parse_i64;
use yo_common::{Code, Error, Result, glob};
use yo_kv::Keyspace;
use yo_kv::access::Policy;

/// What we tell a client we are.
///
/// It is a lie and it is a deliberate one. Every client library in the world
/// branches on this pair to decide which commands exist, and a driver that
/// reads `yo` here falls back to its oldest code path or refuses to connect.
/// Divergence D-12 in `divergences.toml` says so, and the honest answer is in
/// the `yo_version` field of `INFO` next to this one.
const REPORTED_SERVER: &str = "redis";
/// The Redis version we answer 100 percent of, which is what `HELLO` reports.
///
/// [`super::backup`] writes it into the `redis-ver` aux field of the base file
/// it produces, so a server told to load one reads the same version out of the
/// file that a client reads off the connection.
pub(super) const REPORTED_VERSION: &str = "8.8.0";

/// The settings that are fixed for the life of the process.
///
/// `CONFIG SET` accepts a write to one of these that changes nothing and
/// refuses everything else rather than pretending to have taken it. A client
/// that sets `appendonly no` on a server that already has no append only file
/// gets an `OK` and is telling the truth; one that sets `appendonly yes` gets
/// told it cannot, which is better than an `OK` and no file.
const SETTINGS: &[(&str, &str)] = &[
    ("appendonly", "no"),
    ("appendfsync", "everysec"),
    // Where `BACKUP` writes, under `dir`. Fixed here where a real server takes
    // it at startup, because nothing in this build reads it from a file.
    ("backupdirname", backup::DIR_NAME),
    ("databases", "16"),
    ("io-threads", "1"),
    ("proto-max-bulk-len", "536870912"),
    ("save", ""),
    ("timeout", "0"),
];

/// Which number on the size ladder a settings name refers to.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Knob {
    SetIntsetEntries,
    SetListpackEntries,
    SetListpackValue,
    HashListpackEntries,
    HashListpackValue,
    MaxmemorySamples,
    LfuLogFactor,
    LfuDecayTime,
}

/// The settings that move the size ladder, which are the ones that really move.
///
/// These decide where a collection stops being a packed blob and becomes an
/// element table, so they decide what `OBJECT ENCODING` answers, and a client
/// that reads `OBJECT ENCODING` after setting one of these expects the two to
/// agree. That is the whole reason they are writable when nothing else here is.
///
/// The `ziplist` spellings are the names these had before Redis renamed them
/// and it still answers to both, so this does too. Two names, one number: a
/// `CONFIG SET hash-max-ziplist-entries 4` shows up under the listpack name
/// too, which was checked against 8.10.1 rather than assumed.
///
/// Moving one of these leaves every collection that already exists exactly as
/// it is, and only decides what the next write builds. Redis does the same, and
/// it is the reason `CONFIG SET set-max-listpack-entries 0` does not rewrite
/// the keyspace.
///
/// The three eviction numbers are in here too, which stretches the name a
/// little. They belong with these rather than with the immutable settings for
/// the same reason: a client that sets one and then reads `OBJECT FREQ` or
/// watches `evicted_keys` expects the two to agree. `maxmemory-samples` says how
/// many keys a round of sampling looks at, and the two `lfu` numbers set what
/// the counter under an LFU policy actually measures.
const LADDER: &[(&str, Knob)] = &[
    ("hash-max-listpack-entries", Knob::HashListpackEntries),
    ("hash-max-listpack-value", Knob::HashListpackValue),
    ("hash-max-ziplist-entries", Knob::HashListpackEntries),
    ("hash-max-ziplist-value", Knob::HashListpackValue),
    ("lfu-decay-time", Knob::LfuDecayTime),
    ("lfu-log-factor", Knob::LfuLogFactor),
    ("maxmemory-samples", Knob::MaxmemorySamples),
    ("set-max-intset-entries", Knob::SetIntsetEntries),
    ("set-max-listpack-entries", Knob::SetListpackEntries),
    ("set-max-listpack-value", Knob::SetListpackValue),
];

/// The setting that decides which way the access field on every record is read.
///
/// It is on its own rather than in [`SETTINGS`] or [`LADDER`] because it is the
/// only writable setting that is not a number, and rather than immutable because
/// it really moves: a client that sets it and then reads `OBJECT FREQ` expects
/// the two to agree, which is the same argument the size ladder makes.
///
/// Setting it changes nothing about the keys already stored. Whatever is in
/// their access field stays there and means something different from the moment
/// the policy changes, which is what the `OBJECT FREQ` error text warns about.
const MAXMEMORY_POLICY: &str = "maxmemory-policy";

/// How much the server is allowed to hold before it starts evicting.
///
/// Also on its own, and for the third different reason. It is not immutable,
/// it is not on the size ladder and it is the only setting whose value is not a
/// plain integer: a client writes `maxmemory 100mb` and means a hundred and
/// four million bytes, so it needs a parser of its own.
///
/// Zero means no limit, which is the default and is what makes the check in
/// front of every write one comparison. Setting it to a number smaller than
/// what the server is already holding is allowed and is a real thing to do: the
/// next write that would allocate evicts until it fits or is refused, which is
/// what the `maxmemory-policy` decides between.
const MAXMEMORY: &str = "maxmemory";

/// How much the server is allowed to keep on the file before it starts evicting.
///
/// The other half of the eviction inversion `14` section 4.1 describes, and the
/// only setting here that has no counterpart in Redis. `maxmemory` is a limit on
/// memory, and the right answer to a memory limit on a system with a file under
/// it is to move data to the file. Throwing data away is the right answer to a
/// limit on the file, and this is that limit.
///
/// Minus one is no limit and is the default, so a server that never sets this
/// grows until the disk is full and then refuses writes, which is what a
/// database does. Zero is a real setting and it means the file may hold nothing,
/// so migration cannot make room and eviction is all that is left, which is
/// Redis exactly and is the documented setting for a drop in cache.
const MAXSTORE: &str = "maxstore";

/// Where the server writes, which `BACKUP LIST` answers paths under.
///
/// On its own for a fourth reason: it is readable and not writable, and it is
/// not writable in a way of its own. Redis calls it a protected config, which
/// means `CONFIG SET dir` is refused with a sentence about protection rather
/// than about immutability unless the server was started with protected configs
/// enabled. That distinction is copied, because the two messages are what an
/// operator reads when a `CONFIG SET` does not take.
const DIR: &str = "dir";

/// How long a sealed backup is kept before it cleans itself up.
///
/// Seconds, and zero is the default and means it is kept until somebody says
/// `BACKUP CLEANUP`. Writable, since a backup taken by a script that then died
/// is exactly the thing this is for and setting it afterwards has to work.
const SEALED_TTL: &str = "backup-sealed-ttl";

/// Read a byte count the way `CONFIG SET maxmemory` reads one.
///
/// This is Redis's `memtoull`. Digits, then an optional unit that is not case
/// sensitive: nothing or `b` is bytes, `k` is a thousand and `kb` is a kibibyte,
/// and the same pairing again for `m` and `g`. The two spellings meaning
/// different numbers is a trap and it is Redis's trap, so it is repeated here
/// rather than tidied up.
///
/// A unit that overflows clamps rather than failing, which is upstream's
/// `ULLONG_MAX` arm. There is no sign: a leading minus is refused before the
/// digits are read, so `maxmemory -1` is not a very large number.
///
/// Public because `yodb serve` takes the same limits on the command line that
/// `CONFIG SET` takes at runtime, and a server that accepts `100mb` from one and
/// not the other, or reads it as a different number, is a server that gets
/// misconfigured. One parser, one answer.
#[must_use]
pub fn parse_memory(value: &[u8]) -> Option<u64> {
    let split = value
        .iter()
        .position(|b| !b.is_ascii_digit())
        .unwrap_or(value.len());
    let (digits, unit) = value.split_at(split);
    if digits.is_empty() {
        return None;
    }
    let mul: u64 = match unit {
        [] => 1,
        u if u.eq_ignore_ascii_case(b"b") => 1,
        u if u.eq_ignore_ascii_case(b"k") => 1000,
        u if u.eq_ignore_ascii_case(b"kb") => 1024,
        u if u.eq_ignore_ascii_case(b"m") => 1000 * 1000,
        u if u.eq_ignore_ascii_case(b"mb") => 1024 * 1024,
        u if u.eq_ignore_ascii_case(b"g") => 1000 * 1000 * 1000,
        u if u.eq_ignore_ascii_case(b"gb") => 1024 * 1024 * 1024,
        _ => return None,
    };
    let mut n: u64 = 0;
    for d in digits {
        n = n.saturating_mul(10).saturating_add(u64::from(d - b'0'));
    }
    Some(n.saturating_mul(mul))
}

/// Every policy name, joined the way `CONFIG SET` lists them when it refuses one.
///
/// This is a formatter and not a string because the error path should not touch
/// the allocator, and it walks [`Policy::ALL`] rather than spelling the ten names
/// out again so the two cannot drift apart. The order is the order in Redis's own
/// enum table, which is the whole reason `Policy::ALL` is written down.
struct PolicyNames;

impl core::fmt::Display for PolicyNames {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        for (at, policy) in Policy::ALL.iter().enumerate() {
            if at > 0 {
                f.write_str(", ")?;
            }
            f.write_str(policy.name())?;
        }
        Ok(())
    }
}

/// Run one connection or server command.
pub(super) fn execute(
    server: &Server,
    session: &mut Session,
    spec: &Spec,
    args: Args<'_>,
    out: &mut Out,
) -> Result<Flow> {
    match spec.name {
        // The arity in the table is a minimum of one, and a real server then
        // refuses a second argument as a wrong number of them.
        "ping" => {
            if args.len() > 2 {
                return Err(args::wrong_arity("ping"));
            }
            if args.len() == 2 {
                out.bulk(args.get(1));
            } else {
                out.simple(b"PONG");
            }
        }
        "echo" => out.bulk(args.get(1)),
        "hello" => hello(session, args, out)?,
        "select" => {
            let n = args.int(1)?;
            let ok = usize::try_from(n).is_ok_and(|n| n < DATABASES);
            if !ok {
                return Err(Error::new(Code::Invalid, "DB index is out of range"));
            }
            session.db = n as usize;
            out.ok();
        }
        "reset" => {
            // Everything a connection carries goes back to what it was when it
            // was opened, and that includes the protocol: a connection that
            // said `HELLO 3` is speaking RESP2 again after this.
            session.reset();
            out.set_proto(Proto::Resp2);
            out.simple(b"RESET");
        }
        // The reply goes out before the socket closes, which is why this is a
        // flow answer and not something the body does to the connection.
        "quit" => {
            out.ok();
            return Ok(Flow::Close);
        }
        "command" => command(args, out)?,
        "config" => config(server, args, out)?,
        "info" => info(server, args, out),
        // A key that is past its deadline and has not been read since is still
        // counted, which is what Redis does too: `DBSIZE` is the size of the
        // dictionary and not a walk over it. Redis has an active expiry cycle
        // that takes those keys out within a tick or so and we do not yet, so
        // the two servers disagree for as long as a dead key sits unread. That
        // gap closes with the maintenance slice rather than with a count here,
        // because a count here would be O(N) on a command that is O(1)
        // everywhere else.
        "dbsize" => out.int(server.dbs[session.db].len() as i64),
        "flushall" => {
            flush_mode(args)?;
            for db in &server.dbs {
                db.clear();
            }
            server.search.lock().clear();
            out.ok();
        }
        // The search indexes go too, and they go whichever database this is.
        // An index that only ever followed keys on database zero is dropped by
        // a `FLUSHDB` on database nine, which is measured against a real server
        // rather than reasoned about: the module hangs its callback on the
        // flush event without looking at which database flushed.
        "flushdb" => {
            flush_mode(args)?;
            server.dbs[session.db].clear();
            server.search.lock().clear();
            out.ok();
        }
        // Two databases change places and no key moves. What is in the stripes
        // is exchanged and the databases stay where they are, so this costs two
        // pointer sized writes per stripe whatever is in either of them, which
        // is what makes `SWAPDB` fast and dangerous at the same time.
        //
        // No connection is told. A client on database zero is still on database
        // zero and is now looking at what used to be database one, which is the
        // whole point of the command and is why Redis calls it dangerous. A
        // client parked in `BLPOP` remembers the database index it blocked on
        // and not the database, so it wakes up against the swapped in one, which
        // is Redis's behaviour and falls out of the index being what is stored.
        "swapdb" => {
            let first = db_index(args.get(1), "invalid first DB index")?;
            let second = db_index(args.get(2), "invalid second DB index")?;
            server.striped(first).swap_with(server.striped(second));
            out.ok();
        }
        "time" => time(out),
        "backup" => backup::execute(server, args, out)?,
        "shutdown" => return shutdown(server, args),
        _ => return Err(args::unknown_command(args)),
    }
    Ok(Flow::Continue)
}

/// `TIME`, which is two bulk strings and not one integer.
///
/// Seconds first and then microseconds within that second, both written out as
/// decimal text, which is a shape nobody would choose today and is the shape
/// every client library parses.
///
/// It reads the wall clock rather than the coarse clock the keyspace uses. The
/// coarse one is a cached millisecond that a background tick refreshes, which is
/// the right trade for deciding whether a key has expired and the wrong one for
/// a command whose entire job is to say what time it is. A client that calls
/// `TIME` twice in a row and gets the same microsecond has been lied to.
fn time(out: &mut Out) {
    let now = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap_or_default();
    out.array(2);
    out.bulk(now.as_secs().to_string().as_bytes());
    out.bulk(now.subsec_micros().to_string().as_bytes());
}

// ---------------------------------------------------------------- SHUTDOWN

/// `SHUTDOWN [NOSAVE | SAVE] [NOW] [FORCE] [ABORT]`.
///
/// On success this writes nothing at all and the connection closes under the
/// client, which is what a server that has stopped looks like from the outside
/// and is what every client library already expects. There is no `OK`, because
/// an `OK` would be a promise made by a process that is about to not exist.
///
/// The flags are taken and none of them changes what happens, which is the same
/// answer `SAVE` gets from `CONFIG GET`: this server has no save points and no
/// snapshot to write, so saving and not saving are the same act. What durability
/// there is belongs to the file underneath and is already on disk by the time a
/// command returns, so there is nothing for `SAVE` to do and nothing for
/// `NOSAVE` to skip. `NOW` and `FORCE` are about not waiting for replicas and
/// about going anyway when a save failed, and neither has anything to wait for
/// or to fail here.
///
/// # Errors
///
/// [`Code::Invalid`] for a word that is not one of the five, for `SAVE` and
/// `NOSAVE` in the same call, and for `ABORT` alongside any other flag, all of
/// which is what 8.10.1 says. `ABORT` on its own gets Redis's message for a
/// cancel with nothing to cancel, and here that is not a state that can be
/// reached rather than one that happens to be empty: a shutdown is decided and
/// done inside one turn of the loop, so there is never a window in which one is
/// in progress and a second client could call it off.
fn shutdown(server: &Server, args: Args<'_>) -> Result<Flow> {
    let (mut save, mut nosave, mut abort, mut other) = (false, false, false, false);
    for at in 1..args.len() {
        let arg = args.get(at);
        match () {
            () if is(arg, b"save") => save = true,
            () if is(arg, b"nosave") => nosave = true,
            () if is(arg, b"abort") => abort = true,
            () if is(arg, b"now") || is(arg, b"force") => other = true,
            () => return Err(args::syntax()),
        }
    }
    // Repeating one is fine and contradicting yourself is not, and `ABORT` says
    // to do nothing so it cannot be combined with a word about how to do it.
    if (save && nosave) || (abort && (save || nosave || other)) {
        return Err(args::syntax());
    }
    if abort {
        return Err(Error::new(Code::Invalid, "No shutdown in progress."));
    }
    server.stop();
    // Closing is what stops anything the client pipelined behind this from
    // being answered by a server that is on its way out.
    Ok(Flow::Close)
}

// ------------------------------------------------------------------- FLUSH

/// Check the optional `ASYNC` or `SYNC` on `FLUSHALL` and `FLUSHDB`.
///
/// Both are accepted and neither changes anything. On a real server the choice
/// is whether the freeing happens on the connection's thread or on the lazy
/// free thread, and either way the keyspace is empty before the `OK` goes out.
/// That is the whole of what a client can observe, and it is the same here,
/// so taking the word and ignoring it is answering the question rather than
/// pretending to.
///
/// # Errors
///
/// [`Code::Invalid`] for a third argument, or for a second that is neither
/// word, which is what Redis says about both.
fn flush_mode(args: Args<'_>) -> Result<()> {
    if args.len() == 1 {
        return Ok(());
    }
    if args.len() > 2 || !(is(args.get(1), b"async") || is(args.get(1), b"sync")) {
        return Err(args::syntax());
    }
    Ok(())
}

/// One of `SWAPDB`'s two database indexes, with Redis's two different
/// complaints about it.
///
/// A word that is not a number, or a number too big to be a database index on a
/// server that stores the index in a C `int`, gets the caller's message, which
/// says which of the two arguments was wrong. A number that is a plausible index
/// and is not one of ours gets the same out of range message `SELECT` gives. The
/// split looks arbitrary and it is Redis's, and the reason for it is that the
/// first check happens while reading the argument and the second happens inside
/// the swap, so only the first one knows which argument it was looking at.
fn db_index(arg: &[u8], bad: &'static str) -> Result<usize> {
    let n = parse_i64(arg)
        .filter(|n| i32::try_from(*n).is_ok())
        .ok_or_else(|| Error::new(Code::Invalid, bad))?;
    usize::try_from(n)
        .ok()
        .filter(|n| *n < DATABASES)
        .ok_or_else(|| Error::new(Code::Invalid, "DB index is out of range"))
}

// ------------------------------------------------------------------- HELLO

/// `HELLO [protover [AUTH username password] [SETNAME name]]`.
fn hello(session: &mut Session, args: Args<'_>, out: &mut Out) -> Result<()> {
    if args.len() > 1 {
        let v = parse_i64(args.get(1)).ok_or_else(|| {
            Error::new(
                Code::Invalid,
                "Protocol version is not an integer or out of range",
            )
        })?;
        let Some(proto) = Proto::from_version(v) else {
            // `NOPROTO` rather than `ERR`, and it is the one error in this file
            // written straight into the buffer: the prefix is part of what the
            // client branches on, and it is the only place in the engine that
            // needs this one.
            out.error(b"NOPROTO unsupported protocol version");
            return Ok(());
        };
        let mut i = 2;
        while i < args.len() {
            let o = args.get(i);
            if is(o, b"AUTH") && i + 2 < args.len() {
                // No password is configured, so the default user is `nopass`
                // and any password for it is the right one, which is how a
                // real server with no `requirepass` behaves. Any other user
                // does not exist.
                if !is(args.get(i + 1), b"default") {
                    out.error(b"WRONGPASS invalid username-password pair or user is disabled.");
                    return Ok(());
                }
                i += 3;
            } else if is(o, b"SETNAME") && i + 1 < args.len() {
                session.set_name(args.get(i + 1));
                i += 2;
            } else {
                return Err(yo_alloc::allow(|| {
                    Error::fmt(
                        Code::Invalid,
                        format_args!(
                            "Syntax error in HELLO option '{}'",
                            String::from_utf8_lossy(o)
                        ),
                    )
                }));
            }
        }
        // The reply is written in the protocol that was just agreed, not the
        // one the request arrived in.
        out.set_proto(proto);
    }

    let proto = out.proto().version();
    out.map(7);
    out.bulk(b"server");
    out.bulk(REPORTED_SERVER.as_bytes());
    out.bulk(b"version");
    out.bulk(REPORTED_VERSION.as_bytes());
    out.bulk(b"proto");
    out.int(proto);
    out.bulk(b"id");
    out.int(session.id as i64);
    out.bulk(b"mode");
    out.bulk(b"standalone");
    out.bulk(b"role");
    out.bulk(b"master");
    out.bulk(b"modules");
    out.array(0);
    Ok(())
}

// ----------------------------------------------------------------- COMMAND

/// `COMMAND [COUNT|LIST|INFO|DOCS|GETKEYS|HELP]`.
fn command(args: Args<'_>, out: &mut Out) -> Result<()> {
    if args.len() == 1 {
        out.array(table::COMMANDS.len());
        for spec in table::COMMANDS {
            write_spec(out, spec);
        }
        return Ok(());
    }
    let sub = args.get(1);
    if is(sub, b"COUNT") {
        out.int(table::COMMANDS.len() as i64);
    } else if is(sub, b"INFO") {
        if args.len() == 2 {
            out.array(table::COMMANDS.len());
            for spec in table::COMMANDS {
                write_spec(out, spec);
            }
        } else {
            out.array(args.len() - 2);
            for i in 2..args.len() {
                match table::lookup(args.get(i)) {
                    Some(spec) => write_spec(out, spec),
                    // A name nobody has heard of is a null in the list rather
                    // than an error, so one bad name in a batch does not cost
                    // the client the other answers. It is the plain null and
                    // not the array one, which on RESP2 is the difference
                    // between `$-1` and `*-1` and is what a real server sends.
                    None => out.nil(),
                }
            }
        }
    } else if is(sub, b"LIST") {
        list(args, out)?;
    } else if is(sub, b"DOCS") {
        docs(args, out);
    } else if is(sub, b"GETKEYS") {
        getkeys(args, out)?;
    } else if is(sub, b"HELP") {
        help(out, COMMAND_HELP);
    } else {
        return Err(args::unknown_subcommand(sub, "COMMAND"));
    }
    Ok(())
}

/// `COMMAND LIST [FILTERBY MODULE m|ACLCAT c|PATTERN p]`.
fn list(args: Args<'_>, out: &mut Out) -> Result<()> {
    if args.len() == 2 {
        out.array(table::COMMANDS.len());
        for spec in table::COMMANDS {
            out.bulk(spec.name.as_bytes());
        }
        return Ok(());
    }
    if args.len() != 5 || !is(args.get(2), b"FILTERBY") {
        return Err(args::syntax());
    }
    let (how, what) = (args.get(3), args.get(4));
    let keep = |spec: &Spec| {
        if is(how, b"MODULE") {
            // Nothing here came from a module, so every filter by one is empty.
            false
        } else if is(how, b"ACLCAT") {
            spec.acl
                .iter()
                .any(|c| c.len() == what.len() + 1 && c.as_bytes()[1..].eq_ignore_ascii_case(what))
        } else {
            glob::matches(what, spec.name.as_bytes())
        }
    };
    if !is(how, b"MODULE") && !is(how, b"ACLCAT") && !is(how, b"PATTERN") {
        return Err(args::syntax());
    }
    out.array(table::COMMANDS.iter().filter(|s| keep(s)).count());
    for spec in table::COMMANDS.iter().filter(|s| keep(s)) {
        out.bulk(spec.name.as_bytes());
    }
    Ok(())
}

/// `COMMAND DOCS [name ...]`.
///
/// The arguments field a real server sends is left out. It describes the shape
/// of every option of every command in a form nothing but `redis-cli`'s hinting
/// reads, and getting it wrong would be worse than not sending it, since a
/// client that finds the field trusts it.
fn docs(args: Args<'_>, out: &mut Out) {
    if args.len() == 2 {
        out.map(table::COMMANDS.len());
        for spec in table::COMMANDS {
            write_docs(out, spec);
        }
        return;
    }
    let found = (2..args.len())
        .filter(|&i| table::lookup(args.get(i)).is_some())
        .count();
    out.map(found);
    for i in 2..args.len() {
        if let Some(spec) = table::lookup(args.get(i)) {
            write_docs(out, spec);
        }
    }
}

/// One command's documentation, as the name and then the map about it.
fn write_docs(out: &mut Out, spec: &Spec) {
    out.bulk(spec.name.as_bytes());
    out.map(4);
    out.bulk(b"summary");
    out.bulk(spec.summary.as_bytes());
    out.bulk(b"since");
    out.bulk(spec.since.as_bytes());
    out.bulk(b"group");
    out.bulk(spec.group.as_bytes());
    out.bulk(b"complexity");
    out.bulk(spec.complexity.as_bytes());
}

/// `COMMAND GETKEYS <full command>`.
///
/// This is how a cluster aware client routes a command it does not have a rule
/// for, so a wrong answer here is a client that sends a write to the wrong
/// node. The generic path is the first, last and step triple from the table.
fn getkeys(args: Args<'_>, out: &mut Out) -> Result<()> {
    if args.len() < 3 {
        return Err(args::wrong_arity_sub("command", "getkeys"));
    }
    let inner = args.get(2);
    let spec = table::lookup(inner)
        .ok_or_else(|| Error::new(Code::Unsupported, "Invalid command specified"))?;
    let argc = args.len() - 2;
    if !table::arity_ok(spec, argc) {
        return Err(Error::new(
            Code::Invalid,
            "Invalid number of arguments specified for command",
        ));
    }
    // Three commands here keep their keys somewhere the triple cannot describe,
    // behind a count of how many there are. That is why a real server marks them
    // `movablekeys` and why a client has to ask this question about them at all.
    // `MSETEX` counts pairs and the two joined time series reads count single
    // keys, so the step is the only thing that differs between them.
    if let Some(step) = match spec.name {
        "msetex" => Some(2),
        "ts.nrange" | "ts.nrevrange" => Some(1),
        _ => None,
    } {
        let n = parse_i64(args.get(3))
            .filter(|&n| n > 0)
            .and_then(|n| usize::try_from(n).ok())
            .filter(|&n| 4 + step * n <= args.len())
            .ok_or_else(|| Error::new(Code::Invalid, "Invalid arguments specified for command"))?;
        out.array(n);
        for i in 0..n {
            out.bulk(args.get(4 + step * i));
        }
        return Ok(());
    }
    if spec.first_key == 0 {
        return Err(Error::new(
            Code::Invalid,
            "The command has no key arguments",
        ));
    }
    let last = if spec.last_key < 0 {
        (argc as i64) + i64::from(spec.last_key)
    } else {
        i64::from(spec.last_key)
    };
    let step = i64::from(spec.step).max(1);
    let first = i64::from(spec.first_key);
    let count = if last < first {
        0
    } else {
        ((last - first) / step + 1) as usize
    };
    out.array(count);
    for i in 0..count {
        out.bulk(args.get(2 + (first + (i as i64) * step) as usize));
    }
    Ok(())
}

/// One command, in the ten field shape `COMMAND INFO` has had since 7.0.
///
/// The tips, the key specs and the subcommands are all empty. The triple above
/// them says where the keys are for everything in this table except `MSETEX`,
/// `TS.NRANGE` and `TS.NREVRANGE`, which is what `COMMAND GETKEYS` is for, and
/// divergence D-13 says so.
fn write_spec(out: &mut Out, spec: &Spec) {
    out.array(10);
    out.bulk(spec.name.as_bytes());
    out.int(i64::from(spec.arity));
    out.array(spec.flags.len());
    for f in spec.flags {
        out.simple(f.as_bytes());
    }
    out.int(i64::from(spec.first_key));
    out.int(i64::from(spec.last_key));
    out.int(i64::from(spec.step));
    out.array(spec.acl.len());
    for a in spec.acl {
        out.simple(a.as_bytes());
    }
    out.array(0);
    out.array(0);
    out.array(0);
}

// ------------------------------------------------------------------ CONFIG

/// What a ladder setting is set to now.
fn read_knob(db: &Keyspace, knob: Knob) -> usize {
    match knob {
        Knob::SetIntsetEntries => db.limits().max_intset_entries,
        Knob::SetListpackEntries => db.limits().max_listpack_entries,
        Knob::SetListpackValue => db.limits().max_listpack_value,
        Knob::HashListpackEntries => db.hash_limits().max_listpack_entries,
        Knob::HashListpackValue => db.hash_limits().max_listpack_value,
        Knob::MaxmemorySamples => db.samples(),
        Knob::LfuLogFactor => db.lfu().log_factor as usize,
        Knob::LfuDecayTime => db.lfu().decay_minutes as usize,
    }
}

/// Move one ladder setting on one database.
fn write_knob(db: &mut Keyspace, knob: Knob, n: usize) {
    let mut set = *db.limits();
    let mut hash = *db.hash_limits();
    let mut lfu = db.lfu();
    match knob {
        Knob::SetIntsetEntries => set.max_intset_entries = n,
        Knob::SetListpackEntries => set.max_listpack_entries = n,
        Knob::SetListpackValue => set.max_listpack_value = n,
        Knob::HashListpackEntries => hash.max_listpack_entries = n,
        Knob::HashListpackValue => hash.max_listpack_value = n,
        Knob::MaxmemorySamples => db.set_samples(n),
        // Saturating rather than wrapping, because these two are read as `u32`
        // and a client is free to send a number that does not fit. Redis clamps
        // `lfu-log-factor` and `lfu-decay-time` to the same width.
        Knob::LfuLogFactor => lfu.log_factor = u32::try_from(n).unwrap_or(u32::MAX),
        Knob::LfuDecayTime => lfu.decay_minutes = u32::try_from(n).unwrap_or(u32::MAX),
    }
    db.set_limits(set);
    db.set_hash_limits(hash);
    db.set_lfu(lfu);
}

/// The two things a real server says about a number it will not take.
///
/// Both name the setting the client typed and not the one it is an alias for,
/// so `hash-max-ziplist-entries` comes back saying `hash-max-ziplist-entries`.
/// A value past the range of an `i64` is the parse complaint and not the range
/// one, which is upstream reading it before it checks it.
fn bad_setting(name: &str, parsed: bool) -> Error {
    if parsed {
        Error::fmt(
            Code::Invalid,
            format_args!(
                "CONFIG SET failed (possibly related to argument '{name}') - argument must be between 0 and 9223372036854775807 inclusive"
            ),
        )
    } else {
        Error::fmt(
            Code::Invalid,
            format_args!(
                "CONFIG SET failed (possibly related to argument '{name}') - argument couldn't be parsed into an integer"
            ),
        )
    }
}

/// `CONFIG GET|SET|RESETSTAT|REWRITE|HELP`.
fn config(server: &Server, args: Args<'_>, out: &mut Out) -> Result<()> {
    let sub = args.get(1);
    if is(sub, b"GET") {
        if args.len() < 3 {
            return Err(args::wrong_arity_sub("config", "get"));
        }
        let wanted =
            |name: &str| (2..args.len()).any(|i| glob::matches(args.get(i), name.as_bytes()));
        // A setting that two patterns both ask for is sent once, which is what
        // makes this a count of settings rather than a count of matches. The
        // two spellings of a ladder setting are two settings by that rule, so
        // `CONFIG GET hash-max-*` sends the listpack name and the ziplist name
        // and the same number under both, which is what a real server does.
        let fixed = SETTINGS.iter().filter(|(k, _)| wanted(k));
        let ladder = LADDER.iter().filter(|(k, _)| wanted(k));
        let policy = wanted(MAXMEMORY_POLICY);
        let limit = wanted(MAXMEMORY);
        let store = wanted(MAXSTORE);
        let where_ = wanted(DIR);
        let ttl = wanted(SEALED_TTL);
        out.map(
            fixed.clone().count()
                + ladder.clone().count()
                + usize::from(policy)
                + usize::from(limit)
                + usize::from(store)
                + usize::from(where_)
                + usize::from(ttl),
        );
        for (k, v) in fixed {
            out.bulk(k.as_bytes());
            out.bulk(v.as_bytes());
        }
        for (k, knob) in ladder {
            out.bulk(k.as_bytes());
            out.bulk_int(read_knob(&server.settings(), *knob) as i64);
        }
        if policy {
            out.bulk(MAXMEMORY_POLICY.as_bytes());
            out.bulk(server.settings().policy().name().as_bytes());
        }
        if limit {
            // Back as a plain number of bytes whatever the client typed to set
            // it, which is what a real server does: `CONFIG SET maxmemory 1gb`
            // reads back as 1073741824.
            out.bulk(MAXMEMORY.as_bytes());
            out.bulk_int(server.maxmemory() as i64);
        }
        if store {
            // Minus one for no limit, and a plain number of bytes otherwise.
            // Zero cannot mean no limit here the way it does for `maxmemory`,
            // because zero is the setting that says the file holds nothing.
            out.bulk(MAXSTORE.as_bytes());
            out.bulk_int(server.maxstore().map_or(-1, |n| n as i64));
        }
        if where_ {
            // Absolute, which is what a real server answers too: it resolves the
            // directory at startup and reports the resolved one, so a client can
            // tell where the files are without knowing where the process was
            // launched from.
            out.bulk(DIR.as_bytes());
            yo_alloc::allow(|| out.bulk(server.dir().to_string_lossy().as_bytes()));
        }
        if ttl {
            out.bulk(SEALED_TTL.as_bytes());
            out.bulk_int(server.backup().ttl() as i64);
        }
    } else if is(sub, b"SET") {
        // Too few is a wrong number of arguments and an odd number is a syntax
        // error, which is not the same sentence and is not the same rule. A
        // real server counts the pairs after it has decided there is at least
        // one, so `CONFIG SET appendonly` is an arity error and `CONFIG SET
        // appendonly no maxmemory` is a syntax one.
        if args.len() < 4 {
            return Err(args::wrong_arity_sub("config", "set"));
        }
        if !args.len().is_multiple_of(2) {
            return Err(args::syntax());
        }
        // Every pair is checked before any of them is applied, because a real
        // server takes the whole `CONFIG SET` or none of it. `CONFIG SET
        // hash-max-listpack-entries 7 set-max-listpack-entries abc` leaves the
        // hash setting where it was, which was checked rather than assumed.
        let mut writes = [None; 16];
        let mut count = 0;
        let mut policy = None;
        let mut limit = None;
        let mut store = None;
        let mut ttl = None;
        let mut i = 2;
        while i < args.len() {
            let (name, value) = (args.get(i), args.get(i + 1));
            i += 2;
            if is(name, MAXMEMORY.as_bytes()) {
                let Some(bytes) = parse_memory(value) else {
                    return Err(Error::fmt(
                        Code::Invalid,
                        format_args!(
                            "CONFIG SET failed (possibly related to argument '{MAXMEMORY}') - argument must be a memory value"
                        ),
                    ));
                };
                limit = Some(bytes);
                continue;
            }
            if is(name, MAXSTORE.as_bytes()) {
                // `-1` before the memory parser sees it, because that parser
                // refuses a sign and should keep refusing one: `maxmemory -1`
                // is not a very large number and never was.
                let parsed = if value == b"-1" {
                    Some(None)
                } else {
                    parse_memory(value).map(Some)
                };
                let Some(bytes) = parsed else {
                    return Err(Error::fmt(
                        Code::Invalid,
                        format_args!(
                            "CONFIG SET failed (possibly related to argument '{MAXSTORE}') - argument must be a memory value or -1"
                        ),
                    ));
                };
                store = Some(bytes);
                continue;
            }
            if is(name, MAXMEMORY_POLICY.as_bytes()) {
                // Named twice in one command, the last one wins, which is the
                // same rule the ladder settings follow and is what a real server
                // does with any setting repeated in a single `CONFIG SET`.
                let Some(p) = Policy::parse(value) else {
                    return Err(Error::fmt(
                        Code::Invalid,
                        format_args!(
                            "CONFIG SET failed (possibly related to argument '{MAXMEMORY_POLICY}') - argument(s) must be one of the following: {PolicyNames}"
                        ),
                    ));
                };
                policy = Some(p);
                continue;
            }
            if is(name, DIR.as_bytes()) {
                // Refused whatever the value is, including the one it is already
                // set to, which is the one place a setting here does not take
                // the write that changes nothing. That is the reference's
                // answer: a protected config is refused before anybody looks at
                // what was asked for.
                return Err(Error::fmt(
                    Code::Unsupported,
                    format_args!(
                        "CONFIG SET failed (possibly related to argument '{DIR}') - can't set protected config"
                    ),
                ));
            }
            if is(name, SEALED_TTL.as_bytes()) {
                let Some(n) = parse_i64(value).filter(|&n| n >= 0) else {
                    return Err(bad_setting(SEALED_TTL, parse_i64(value).is_some()));
                };
                ttl = Some(n as u64);
                continue;
            }
            if let Some((k, knob)) = LADDER.iter().find(|(k, _)| is(name, k.as_bytes())) {
                let Some(n) = parse_i64(value).filter(|&n| n >= 0) else {
                    return Err(bad_setting(k, parse_i64(value).is_some()));
                };
                if count == writes.len() {
                    // Sixteen pairs is more than the ten names there are, so
                    // getting here means a name was given twice enough times to
                    // fill it, and the last one would have won anyway.
                    return Err(args::syntax());
                }
                writes[count] = Some((*knob, n as usize));
                count += 1;
                continue;
            }
            let Some((k, v)) = SETTINGS.iter().find(|(k, _)| is(name, k.as_bytes())) else {
                return Err(yo_alloc::allow(|| {
                    Error::fmt(
                        Code::Invalid,
                        format_args!(
                            "Unknown option or number of arguments for CONFIG SET - '{}'",
                            String::from_utf8_lossy(name)
                        ),
                    )
                }));
            };
            if value != v.as_bytes() {
                return Err(Error::fmt(
                    Code::Unsupported,
                    format_args!(
                        "CONFIG SET failed (possibly related to argument '{k}') - can't set immutable config"
                    ),
                ));
            }
        }
        // Every stripe of every database, because these are one server wide
        // number in Redis and the fact that a `Keyspace` carries its own copy is
        // ours and not the client's problem. A stripe that missed one would put
        // a key in a different shape from the same key on the stripe next to it,
        // which `OBJECT ENCODING` would then answer differently for depending on
        // where the key happened to land.
        // The whole database is held while its stripes are set rather than one
        // stripe at a time, for the same reason they all get the same number: a
        // client that read `OBJECT ENCODING` in the middle of a half done change
        // would be told two different things about two keys depending on nothing
        // it can see.
        for (knob, n) in writes.iter().flatten() {
            for at in 0..DATABASES {
                let db = server.striped(at);
                let mut held = db.hold_many(0..db.width());
                for i in 0..db.width() {
                    write_knob(held.stripe_mut(i), *knob, *n);
                }
            }
        }
        if let Some(p) = policy {
            for at in 0..DATABASES {
                let db = server.striped(at);
                let mut held = db.hold_many(0..db.width());
                for i in 0..db.width() {
                    held.stripe_mut(i).set_policy(p);
                }
            }
        }
        if let Some(seconds) = ttl {
            server.backup().set_ttl(seconds);
        }
        // Last, so that a `CONFIG SET maxmemory 1mb maxmemory-policy allkeys-lru`
        // has the policy in place before the limit that will act on it. The two
        // in the other order would run the first eviction under whatever the
        // policy used to be, which for a fresh server is `noeviction` and would
        // refuse the next write instead of making room for it.
        if let Some(bytes) = store {
            server.set_maxstore(bytes);
        }
        if let Some(bytes) = limit {
            server.set_maxmemory(bytes);
        }
        out.ok();
    } else if is(sub, b"RESETSTAT") {
        server.reset_stats();
        out.ok();
    } else if is(sub, b"REWRITE") {
        return Err(Error::new(
            Code::Unsupported,
            "The server is running without a config file",
        ));
    } else if is(sub, b"HELP") {
        help(out, CONFIG_HELP);
    } else {
        return Err(args::unknown_subcommand(sub, "CONFIG"));
    }
    Ok(())
}

// -------------------------------------------------------------------- INFO

/// `INFO [section ...]`.
///
/// Every number in here is one this layer can actually answer. There is no
/// `rdb_last_save_time` because there is no save, and a field that is not there
/// is a client falling back rather than a client believing a zero.
///
/// The `CPU` section used to be missing for the same reason and is here now,
/// because nothing measured it and then something did. It is one `getrusage`
/// call in [`super::cpu`], and the reason it went in is that Redis's own
/// `unit/info-command` tests fail without it: a monitoring tool graphs
/// processor time against wall clock to decide whether a server is busy or
/// waiting, so an absent field there is a real hole and not a tidy omission.
fn info(server: &Server, args: Args<'_>, out: &mut Out) {
    // Redis keeps two lists: the sections a bare `INFO` hands back, and the ones
    // that have to be asked for by name or by `all`. `commandstats` is in the
    // second, along with `latencystats` and `errorstats`, because they grow with
    // the number of distinct commands a server has seen and a monitoring tool
    // polling `INFO` every second does not want them.
    //
    // `unit/info-command` is exactly this distinction written down: it asks for
    // `INFO default` and insists `rejected_calls` is not in the answer, then
    // asks for `INFO all` and insists that it is.
    let named = |section: &str| (1..args.len()).any(|i| is(args.get(i), section.as_bytes()));
    let everything = (1..args.len()).any(|i| {
        let a = args.get(i);
        is(a, b"all") || is(a, b"everything")
    });
    let by_default = args.len() == 1 || (1..args.len()).any(|i| is(args.get(i), b"default"));
    let want = |section: &str| by_default || everything || named(section);
    let extra = |section: &str| everything || named(section);
    // One string, built once and written once. It allocates, which is allowed
    // here and nowhere near the commands that count: `INFO` is a monitoring
    // call and it is not on the path M2 is measured on.
    let text = yo_alloc::allow(|| {
        let mut s = String::with_capacity(1024);
        if want("server") {
            let _ = write!(
                s,
                "# Server\r\nredis_version:{REPORTED_VERSION}\r\nyo_version:{}\r\n\
                 redis_mode:standalone\r\narch_bits:{}\r\nprocess_id:0\r\n\
                 run_id:0000000000000000000000000000000000000000\r\ntcp_port:0\r\n\
                 uptime_in_seconds:{}\r\nio_threads_active:0\r\n\r\n",
                env!("CARGO_PKG_VERSION"),
                usize::BITS,
                server.uptime_secs(),
            );
        }
        if want("clients") {
            let _ = write!(
                s,
                "# Clients\r\nconnected_clients:{}\r\nblocked_clients:{}\r\n\
                 cluster_connections:0\r\n\r\n",
                server.totals().clients,
                server.parked(),
            );
        }
        if want("memory") {
            // Both the cap and the quarter of it, because the quarter is an
            // empirical number and somebody surprised by it should be able to
            // see what it was a quarter of without reading the source. The
            // reasoning is written out in `cap`.
            let cap = crate::cap::cap();
            let compact = server.compaction();
            // Read out of its stripe before the write, because an argument list
            // keeps every temporary in it alive until the whole call is over
            // and one of the other arguments walks that same stripe.
            let policy = server.settings().policy().name();
            let _ = write!(
                s,
                "# Memory\r\nused_memory:{}\r\nused_memory_dataset:{}\r\n\
                 used_memory_overhead:{}\r\nmem_arena_bytes:{}\r\n\
                 mem_arena_segments:{}\r\nmem_compact_walked:{}\r\n\
                 mem_compact_moved:{}\r\nmem_compact_bytes:{}\r\n\
                 mem_index_bytes:{}\r\n\
                 mem_client_buffers:{}\r\ntotal_system_memory:{}\r\n\
                 mem_cgroup_limit:{}\r\nmem_limit:{}\r\nmem_budget:{}\r\n\
                 maxmemory:{}\r\nmaxmemory_policy:{}\r\n\
                 maxstore:{}\r\nyo_store_bytes:{}\r\nyo_memory_regime:{}\r\n\r\n",
                server.memory_bytes(),
                server.dataset_bytes(),
                server.memory_bytes() - server.dataset_bytes(),
                server.arena_bytes(),
                server.segment_count(),
                compact.walked,
                compact.moved,
                compact.bytes,
                server.index_bytes(),
                server.conn_bytes(),
                cap.host.unwrap_or(0),
                cap.cgroup.unwrap_or(0),
                cap.limit().unwrap_or(0),
                cap.budget(),
                server.maxmemory(),
                policy,
                server.maxstore().map_or(-1, |n| n as i64),
                server.store_bytes(),
                server.regime(),
            );
        }
        if want("stats") {
            // The cold counters live here and not in the memory section,
            // because they are totals since the server started and everything
            // in that section is a level right now. `yo_cold_faults` over the
            // point reads a run issued is the ratio G9 is a gate on, and it
            // cannot be worked out from outside the server.
            let cold = server.cold_stats();
            let totals = server.totals();
            let _ = write!(
                s,
                "# Stats\r\ntotal_connections_received:{}\r\n\
                 total_commands_processed:{}\r\nexpired_keys:{}\r\n\
                 evicted_keys:{}\r\nyo_cold_demoted:{}\r\nyo_cold_promoted:{}\r\n\
                 yo_cold_faults:{}\r\nyo_cold_served:{}\r\nyo_cold_bytes_out:{}\r\n\
                 yo_cold_bytes_in:{}\r\n\r\n",
                totals.connections,
                totals.commands,
                server.expired_keys(),
                server.evicted_keys(),
                cold.demoted,
                cold.promoted,
                cold.faults,
                cold.served,
                cold.bytes_out,
                cold.bytes_in,
            );
        }
        if want("cpu") {
            // Two of Redis's six are not here. `used_cpu_sys_main_thread` and
            // `used_cpu_user_main_thread` need `RUSAGE_THREAD`, which is Linux
            // only, and reporting the process totals under a name that says
            // main thread would be right on a single threaded server and wrong
            // on the one this becomes.
            if let Some(u) = cpu::usage() {
                let _ = write!(
                    s,
                    "# CPU\r\nused_cpu_sys:{:.6}\r\nused_cpu_user:{:.6}\r\n\
                     used_cpu_sys_children:{:.6}\r\nused_cpu_user_children:{:.6}\r\n\r\n",
                    u.sys, u.user, u.sys_children, u.user_children,
                );
            }
        }
        if want("replication") {
            // Four fields out of Redis's dozen, and the eight that are missing
            // all describe the replication backlog, which is a thing that does
            // not exist here rather than a thing that is empty. The four that
            // are here are true of a server with no replica attached: it is the
            // master, nobody is following it, no failover is in progress and
            // nothing has been written to a stream that does not exist, which is
            // an offset of zero.
            s.push_str(
                "# Replication\r\nrole:master\r\nconnected_slaves:0\r\n\
                 master_failover_state:no-failover\r\nmaster_repl_offset:0\r\n\r\n",
            );
        }
        if extra("commandstats") {
            s.push_str("# Commandstats\r\n");
            for (name, row) in server.command_stats() {
                let _ = write!(
                    s,
                    "cmdstat_{name}:calls={},rejected_calls={},failed_calls={}\r\n",
                    row.calls, row.rejected, row.failed,
                );
            }
            s.push_str("\r\n");
        }
        if want("keyspace") {
            s.push_str("# Keyspace\r\n");
            for i in 0..DATABASES {
                let keys = server.dbs[i].len();
                if keys > 0 {
                    // `avg_ttl` is still a zero, and Redis reports a zero there
                    // too on a server that has never run its active expiry
                    // cycle, because the number is a running estimate that cycle
                    // produces rather than something anybody measures on demand.
                    let expires = server.dbs[i].expires();
                    let _ = write!(s, "db{i}:keys={keys},expires={expires},avg_ttl=0\r\n");
                }
            }
            s.push_str("\r\n");
        }
        s
    });
    out.verbatim(b"txt", text.as_bytes());
}

// -------------------------------------------------------------------- help

/// The `HELP` reply, which is an array of simple strings on both protocols.
pub(super) fn help(out: &mut Out, lines: &[&str]) {
    out.array(lines.len());
    for line in lines {
        out.simple(line.as_bytes());
    }
}

/// What `COMMAND HELP` says.
const COMMAND_HELP: &[&str] = &[
    "COMMAND <subcommand> [<arg> [value] [opt] ...]. Subcommands are:",
    "(no subcommand)",
    "    Return details about all commands.",
    "COUNT",
    "    Return the total number of commands in this server.",
    "LIST [FILTERBY <MODULE <module-name>|ACLCAT <category>|PATTERN <pattern>>]",
    "    Return a list of all commands in this server.",
    "INFO [<command-name> ...]",
    "    Return details about multiple commands.",
    "DOCS [<command-name> ...]",
    "    Return documentation details about multiple commands.",
    "GETKEYS <full-command>",
    "    Return the keys from a full command.",
    "HELP",
    "    Print this help.",
];

/// What `CONFIG HELP` says.
const CONFIG_HELP: &[&str] = &[
    "CONFIG <subcommand> [<arg> [value] [opt] ...]. Subcommands are:",
    "GET <pattern>",
    "    Return parameters matching the glob-like <pattern> and their values.",
    "SET <directive> <value>",
    "    Set the configuration <directive> to <value>.",
    "RESETSTAT",
    "    Reset statistics reported by the INFO command.",
    "REWRITE",
    "    Rewrite the configuration file.",
    "HELP",
    "    Print this help.",
];