1use std::collections::BTreeMap;
12
13pub const C2S_KV_OPEN: u8 = 0x70;
16pub const C2S_KV_STOP: u8 = 0x71;
18pub const C2S_KV_ACK: u8 = 0x72;
20pub const C2S_KV_PUT: u8 = 0x73;
24pub const C2S_KV_FETCH: u8 = 0x74;
26
27pub const S2C_KV_OPENED: u8 = 0x70;
29pub const S2C_KV_UPDATE: u8 = 0x71;
31pub const S2C_KV_DONE: u8 = 0x72;
36pub const S2C_KV_VALUE: u8 = 0x73;
38pub const S2C_KV_CLOSED: u8 = 0x74;
43
44pub const FEATURE_KV: u32 = 1 << 9;
48
49pub const KV_ID_INVALID: u16 = 0xFFFF;
51
52pub const KV_MAX_KEY: usize = 256;
54
55pub const KV_PUT_NO_CAS: u8 = 1 << 0;
58pub const KV_PUT_DELETE: u8 = 1 << 1;
61pub const KV_PUT_DURABLE: u8 = 1 << 2;
63
64pub const KV_UPDATE_SNAPSHOT_END: u8 = 1 << 0;
67
68pub const KV_CLOSED_RESOURCE_LIMIT: u8 = 4;
75
76pub const KV_STATUS_OK: u8 = 0;
80pub const KV_STATUS_NOT_FOUND: u8 = 2;
81pub const KV_STATUS_PERMISSION: u8 = 4;
82pub const KV_STATUS_TOO_LARGE: u8 = 5;
83pub const KV_STATUS_BUDGET: u8 = 6;
84pub const KV_STATUS_INVALID: u8 = 7;
85pub const KV_STATUS_OTHER: u8 = 9;
86pub const KV_STATUS_CONFLICT: u8 = 11;
89
90pub fn kv_status_text(status: u8) -> &'static str {
92 match status {
93 KV_STATUS_OK => "ok",
94 KV_STATUS_NOT_FOUND => "not found",
95 KV_STATUS_PERMISSION => "permission denied",
96 KV_STATUS_TOO_LARGE => "too large",
97 KV_STATUS_BUDGET => "budget exhausted",
98 KV_STATUS_INVALID => "invalid request",
99 KV_STATUS_CONFLICT => "conflict",
100 _ => "error",
101 }
102}
103
104pub fn kv_key_valid(key: &str) -> bool {
107 !key.is_empty() && key.len() <= KV_MAX_KEY && !key.as_bytes().contains(&0)
108}
109
110pub const KV_RECORD_UPSERT: u8 = 0x01;
112pub const KV_RECORD_DELETE: u8 = 0x02;
113
114pub const KV_CONTENT_NONE: u8 = 0;
116pub const KV_CONTENT_FULL: u8 = 1;
117
118#[derive(Clone, Debug, PartialEq, Eq)]
120pub enum KvRecord<'a> {
121 Upsert {
122 key: &'a str,
123 hash: u128,
125 size: u32,
126 mtime_ns: u64,
127 value: Option<&'a [u8]>,
130 },
131 Delete {
132 key: &'a str,
133 },
134}
135
136pub fn append_kv_record(buf: &mut Vec<u8>, record: &KvRecord<'_>) {
138 let start = buf.len();
139 buf.extend_from_slice(&0u32.to_le_bytes()); match record {
141 KvRecord::Upsert {
142 key,
143 hash,
144 size,
145 mtime_ns,
146 value,
147 } => {
148 buf.push(KV_RECORD_UPSERT);
149 let kb = key.as_bytes();
150 buf.extend_from_slice(&(kb.len() as u16).to_le_bytes());
151 buf.extend_from_slice(kb);
152 buf.extend_from_slice(&hash.to_le_bytes());
153 buf.extend_from_slice(&size.to_le_bytes());
154 buf.extend_from_slice(&mtime_ns.to_le_bytes());
155 match value {
156 None => buf.push(KV_CONTENT_NONE),
157 Some(data) => {
158 buf.push(KV_CONTENT_FULL);
159 buf.extend_from_slice(&(data.len() as u32).to_le_bytes());
160 buf.extend_from_slice(data);
161 }
162 }
163 }
164 KvRecord::Delete { key } => {
165 buf.push(KV_RECORD_DELETE);
166 let kb = key.as_bytes();
167 buf.extend_from_slice(&(kb.len() as u16).to_le_bytes());
168 buf.extend_from_slice(kb);
169 }
170 }
171 let len = (buf.len() - start - 4) as u32;
172 buf[start..start + 4].copy_from_slice(&len.to_le_bytes());
173}
174
175pub struct KvRecordIter<'a> {
179 data: &'a [u8],
180}
181
182pub fn kv_records(data: &[u8]) -> KvRecordIter<'_> {
183 KvRecordIter { data }
184}
185
186fn take_key<'a>(body: &mut &'a [u8]) -> Option<&'a str> {
187 if body.len() < 2 {
188 return None;
189 }
190 let len = u16::from_le_bytes([body[0], body[1]]) as usize;
191 if body.len() < 2 + len {
192 return None;
193 }
194 let s = std::str::from_utf8(&body[2..2 + len]).ok()?;
195 *body = &body[2 + len..];
196 Some(s)
197}
198
199impl<'a> Iterator for KvRecordIter<'a> {
200 type Item = KvRecord<'a>;
201
202 fn next(&mut self) -> Option<KvRecord<'a>> {
203 loop {
204 if self.data.len() < 4 {
205 return None;
206 }
207 let rec_len =
208 u32::from_le_bytes([self.data[0], self.data[1], self.data[2], self.data[3]])
209 as usize;
210 if self.data.len() < 4 + rec_len || rec_len == 0 {
211 return None;
212 }
213 let mut body = &self.data[4..4 + rec_len];
214 self.data = &self.data[4 + rec_len..];
215 let kind = body[0];
216 body = &body[1..];
217 match kind {
218 KV_RECORD_UPSERT => {
219 let key = take_key(&mut body)?;
220 if body.len() < 16 + 4 + 8 + 1 {
221 return None;
222 }
223 let hash = u128::from_le_bytes(body[0..16].try_into().unwrap());
224 let size = u32::from_le_bytes(body[16..20].try_into().unwrap());
225 let mtime_ns = u64::from_le_bytes(body[20..28].try_into().unwrap());
226 let content_kind = body[28];
227 body = &body[29..];
228 let value = match content_kind {
229 KV_CONTENT_NONE => None,
230 KV_CONTENT_FULL => {
231 if body.len() < 4 {
232 return None;
233 }
234 let len = u32::from_le_bytes(body[0..4].try_into().unwrap()) as usize;
235 if body.len() < 4 + len {
236 return None;
237 }
238 Some(&body[4..4 + len])
239 }
240 _ => return None,
241 };
242 return Some(KvRecord::Upsert {
243 key,
244 hash,
245 size,
246 mtime_ns,
247 value,
248 });
249 }
250 KV_RECORD_DELETE => {
251 let key = take_key(&mut body)?;
252 return Some(KvRecord::Delete { key });
253 }
254 _ => continue, }
256 }
257 }
258}
259
260pub fn msg_kv_open(nonce: u16, flags: u8, inline_max: u32, prefix: &str) -> Vec<u8> {
265 let pb = prefix.as_bytes();
266 let mut msg = Vec::with_capacity(10 + pb.len());
267 msg.push(C2S_KV_OPEN);
268 msg.extend_from_slice(&nonce.to_le_bytes());
269 msg.push(flags);
270 msg.extend_from_slice(&inline_max.to_le_bytes());
271 msg.extend_from_slice(&(pb.len() as u16).to_le_bytes());
272 msg.extend_from_slice(pb);
273 msg
274}
275
276pub fn parse_kv_open(msg: &[u8]) -> Option<(u16, u8, u32, String)> {
278 if msg.len() < 10 || msg[0] != C2S_KV_OPEN {
279 return None;
280 }
281 let nonce = u16::from_le_bytes([msg[1], msg[2]]);
282 let flags = msg[3];
283 let inline_max = u32::from_le_bytes(msg[4..8].try_into().unwrap());
284 let prefix_len = u16::from_le_bytes([msg[8], msg[9]]) as usize;
285 let prefix = std::str::from_utf8(msg.get(10..10 + prefix_len)?)
286 .ok()?
287 .to_string();
288 Some((nonce, flags, inline_max, prefix))
289}
290
291pub fn msg_kv_stop(kv_id: u16) -> Vec<u8> {
292 let mut msg = Vec::with_capacity(3);
293 msg.push(C2S_KV_STOP);
294 msg.extend_from_slice(&kv_id.to_le_bytes());
295 msg
296}
297
298pub fn parse_kv_stop(msg: &[u8]) -> Option<u16> {
300 if msg.len() < 3 || msg[0] != C2S_KV_STOP {
301 return None;
302 }
303 Some(u16::from_le_bytes([msg[1], msg[2]]))
304}
305
306pub fn msg_kv_ack(kv_id: u16, update_id: u32) -> Vec<u8> {
307 let mut msg = Vec::with_capacity(7);
308 msg.push(C2S_KV_ACK);
309 msg.extend_from_slice(&kv_id.to_le_bytes());
310 msg.extend_from_slice(&update_id.to_le_bytes());
311 msg
312}
313
314pub fn parse_kv_ack(msg: &[u8]) -> Option<(u16, u32)> {
316 if msg.len() < 7 || msg[0] != C2S_KV_ACK {
317 return None;
318 }
319 let kv_id = u16::from_le_bytes([msg[1], msg[2]]);
320 let update_id = u32::from_le_bytes(msg[3..7].try_into().unwrap());
321 Some((kv_id, update_id))
322}
323
324#[derive(Clone, Debug, PartialEq, Eq)]
326pub struct KvPut {
327 pub nonce: u16,
328 pub flags: u8,
329 pub base: u128,
330 pub key: String,
331 pub value: Vec<u8>,
332}
333
334pub fn msg_kv_put(p: &KvPut) -> Vec<u8> {
335 let kb = p.key.as_bytes();
336 let compressed = lz4_flex::compress_prepend_size(&p.value);
337 let mut msg = Vec::with_capacity(22 + kb.len() + compressed.len());
338 msg.push(C2S_KV_PUT);
339 msg.extend_from_slice(&p.nonce.to_le_bytes());
340 msg.push(p.flags);
341 msg.extend_from_slice(&p.base.to_le_bytes());
342 msg.extend_from_slice(&(kb.len() as u16).to_le_bytes());
343 msg.extend_from_slice(kb);
344 msg.extend_from_slice(&compressed);
345 msg
346}
347
348pub fn kv_put_declared_value_len(msg: &[u8]) -> Option<usize> {
359 if msg.len() < 22 || msg[0] != C2S_KV_PUT {
360 return None;
361 }
362 let key_len = u16::from_le_bytes([msg[20], msg[21]]) as usize;
363 let value = msg.get(22 + key_len..)?;
364 let head = value.get(0..4)?;
365 Some(u32::from_le_bytes(head.try_into().unwrap()) as usize)
366}
367
368pub fn parse_kv_put(msg: &[u8]) -> Option<KvPut> {
369 if msg.len() < 22 || msg[0] != C2S_KV_PUT {
371 return None;
372 }
373 let nonce = u16::from_le_bytes([msg[1], msg[2]]);
374 let flags = msg[3];
375 let base = u128::from_le_bytes(msg[4..20].try_into().unwrap());
376 let key_len = u16::from_le_bytes([msg[20], msg[21]]) as usize;
377 let key = std::str::from_utf8(msg.get(22..22 + key_len)?)
378 .ok()?
379 .to_string();
380 let value = decompress_guarded(&msg[22 + key_len..])?;
381 Some(KvPut {
382 nonce,
383 flags,
384 base,
385 key,
386 value,
387 })
388}
389
390pub fn msg_kv_fetch(nonce: u16, key: &str) -> Vec<u8> {
391 let kb = key.as_bytes();
392 let mut msg = Vec::with_capacity(5 + kb.len());
393 msg.push(C2S_KV_FETCH);
394 msg.extend_from_slice(&nonce.to_le_bytes());
395 msg.extend_from_slice(&(kb.len() as u16).to_le_bytes());
396 msg.extend_from_slice(kb);
397 msg
398}
399
400pub fn parse_kv_fetch(msg: &[u8]) -> Option<(u16, String)> {
402 if msg.len() < 5 || msg[0] != C2S_KV_FETCH {
403 return None;
404 }
405 let nonce = u16::from_le_bytes([msg[1], msg[2]]);
406 let key_len = u16::from_le_bytes([msg[3], msg[4]]) as usize;
407 let key = std::str::from_utf8(msg.get(5..5 + key_len)?)
408 .ok()?
409 .to_string();
410 Some((nonce, key))
411}
412
413pub fn msg_kv_opened(nonce: u16, kv_id: u16, status: u8, detail: &str) -> Vec<u8> {
414 let db = detail.as_bytes();
415 let mut msg = Vec::with_capacity(8 + db.len());
416 msg.push(S2C_KV_OPENED);
417 msg.extend_from_slice(&nonce.to_le_bytes());
418 msg.extend_from_slice(&kv_id.to_le_bytes());
419 msg.push(status);
420 msg.extend_from_slice(&(db.len() as u16).to_le_bytes());
421 msg.extend_from_slice(db);
422 msg
423}
424
425pub fn parse_kv_opened(msg: &[u8]) -> Option<(u16, u16, u8, String)> {
427 if msg.len() < 8 || msg[0] != S2C_KV_OPENED {
428 return None;
429 }
430 let nonce = u16::from_le_bytes([msg[1], msg[2]]);
431 let kv_id = u16::from_le_bytes([msg[3], msg[4]]);
432 let status = msg[5];
433 let detail_len = u16::from_le_bytes([msg[6], msg[7]]) as usize;
434 let detail = String::from_utf8_lossy(msg.get(8..8 + detail_len)?).into_owned();
435 Some((nonce, kv_id, status, detail))
436}
437
438pub fn msg_kv_update(kv_id: u16, update_id: u32, flags: u8, records: &[u8]) -> Vec<u8> {
440 let compressed = lz4_flex::compress_prepend_size(records);
441 let mut msg = Vec::with_capacity(8 + compressed.len());
442 msg.push(S2C_KV_UPDATE);
443 msg.extend_from_slice(&kv_id.to_le_bytes());
444 msg.extend_from_slice(&update_id.to_le_bytes());
445 msg.push(flags);
446 msg.extend_from_slice(&compressed);
447 msg
448}
449
450pub fn msg_kv_done(nonce: u16, status: u8, hash: u128, mtime_ns: u64) -> Vec<u8> {
453 let mut msg = Vec::with_capacity(28);
454 msg.push(S2C_KV_DONE);
455 msg.extend_from_slice(&nonce.to_le_bytes());
456 msg.push(status);
457 msg.extend_from_slice(&hash.to_le_bytes());
458 msg.extend_from_slice(&mtime_ns.to_le_bytes());
459 msg
460}
461
462pub fn parse_kv_done(msg: &[u8]) -> Option<(u16, u8, u128, u64)> {
464 if msg.len() < 28 || msg[0] != S2C_KV_DONE {
465 return None;
466 }
467 let nonce = u16::from_le_bytes([msg[1], msg[2]]);
468 let status = msg[3];
469 let hash = u128::from_le_bytes(msg[4..20].try_into().unwrap());
470 let mtime_ns = u64::from_le_bytes(msg[20..28].try_into().unwrap());
471 Some((nonce, status, hash, mtime_ns))
472}
473
474pub fn msg_kv_value(nonce: u16, status: u8, hash: u128, data: &[u8]) -> Vec<u8> {
475 let compressed = lz4_flex::compress_prepend_size(data);
476 let mut msg = Vec::with_capacity(20 + compressed.len());
477 msg.push(S2C_KV_VALUE);
478 msg.extend_from_slice(&nonce.to_le_bytes());
479 msg.push(status);
480 msg.extend_from_slice(&hash.to_le_bytes());
481 msg.extend_from_slice(&compressed);
482 msg
483}
484
485pub fn parse_kv_value(msg: &[u8]) -> Option<(u16, u8, u128, Vec<u8>)> {
487 if msg.len() < 20 || msg[0] != S2C_KV_VALUE {
488 return None;
489 }
490 let nonce = u16::from_le_bytes([msg[1], msg[2]]);
491 let status = msg[3];
492 let hash = u128::from_le_bytes(msg[4..20].try_into().unwrap());
493 let data = decompress_guarded(&msg[20..])?;
494 Some((nonce, status, hash, data))
495}
496
497pub fn msg_kv_closed(kv_id: u16, reason: u8) -> Vec<u8> {
498 let mut msg = Vec::with_capacity(4);
499 msg.push(S2C_KV_CLOSED);
500 msg.extend_from_slice(&kv_id.to_le_bytes());
501 msg.push(reason);
502 msg
503}
504
505pub fn parse_kv_closed(msg: &[u8]) -> Option<(u16, u8)> {
507 if msg.len() < 4 || msg[0] != S2C_KV_CLOSED {
508 return None;
509 }
510 Some((u16::from_le_bytes([msg[1], msg[2]]), msg[3]))
511}
512
513fn decompress_guarded(data: &[u8]) -> Option<Vec<u8>> {
516 if data.len() < 4 {
517 return None;
518 }
519 let declared = u32::from_le_bytes(data[0..4].try_into().unwrap()) as usize;
520 if declared > crate::MAX_DECOMPRESSED {
521 return None;
522 }
523 lz4_flex::decompress_size_prepended(data).ok()
524}
525
526#[derive(Clone, Debug, PartialEq, Eq)]
532pub struct KvEntry {
533 pub hash: u128,
535 pub size: u32,
536 pub mtime_ns: u64,
537 pub value: Option<Vec<u8>>,
540}
541
542#[derive(Debug, Default)]
548pub struct KvMirror {
549 pub live: BTreeMap<String, KvEntry>,
550 pub snapshot_done: bool,
552}
553
554impl KvMirror {
555 pub fn new() -> Self {
556 Self::default()
557 }
558
559 pub fn apply_update(&mut self, msg: &[u8]) -> Option<u32> {
562 if msg.len() < 8 || msg[0] != S2C_KV_UPDATE {
563 return None;
564 }
565 let update_id = u32::from_le_bytes([msg[3], msg[4], msg[5], msg[6]]);
566 let flags = msg[7];
567 let records = decompress_guarded(&msg[8..])?;
568 for record in kv_records(&records) {
569 match record {
570 KvRecord::Upsert {
571 key,
572 hash,
573 size,
574 mtime_ns,
575 value,
576 } => {
577 self.live.insert(
578 key.to_string(),
579 KvEntry {
580 hash,
581 size,
582 mtime_ns,
583 value: value.map(|v| v.to_vec()),
584 },
585 );
586 }
587 KvRecord::Delete { key } => {
588 self.live.remove(key);
589 }
590 }
591 }
592 if flags & KV_UPDATE_SNAPSHOT_END != 0 {
593 self.snapshot_done = true;
594 }
595 Some(update_id)
596 }
597}
598
599#[cfg(test)]
600mod tests {
601 use super::*;
602
603 #[test]
604 fn kv_open_roundtrip_and_bytes() {
605 let m = msg_kv_open(7, 0, 4096, "editor/");
606 assert_eq!(
608 m,
609 vec![
610 0x70, 0x07, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x07, 0x00, 0x65, 0x64, 0x69, 0x74,
611 0x6F, 0x72, 0x2F
612 ]
613 );
614 let (nonce, flags, inline_max, prefix) = parse_kv_open(&m).unwrap();
615 assert_eq!(nonce, 7);
616 assert_eq!(flags, 0);
617 assert_eq!(inline_max, 4096);
618 assert_eq!(prefix, "editor/");
619 }
620
621 #[test]
622 fn kv_stop_ack_roundtrip() {
623 assert_eq!(parse_kv_stop(&msg_kv_stop(3)), Some(3));
624 assert_eq!(parse_kv_ack(&msg_kv_ack(3, 99)), Some((3, 99)));
625 }
626
627 #[test]
628 fn kv_put_roundtrip() {
629 let p = KvPut {
630 nonce: 21,
631 flags: KV_PUT_DURABLE,
632 base: 0xDEAD_BEEF,
633 key: "roots".to_string(),
634 value: b"main = /src/blit\n".to_vec(),
635 };
636 let out = parse_kv_put(&msg_kv_put(&p)).unwrap();
637 assert_eq!(out, p);
638 }
639
640 #[test]
641 fn kv_put_delete_empty_value() {
642 let p = KvPut {
643 nonce: 1,
644 flags: KV_PUT_DELETE,
645 base: 42,
646 key: "editor/buf//tmp/x".to_string(),
647 value: Vec::new(),
648 };
649 let out = parse_kv_put(&msg_kv_put(&p)).unwrap();
650 assert_eq!(out, p);
651 }
652
653 #[test]
654 fn kv_fetch_roundtrip() {
655 let m = msg_kv_fetch(5, "editor/open//x/y.rs");
656 assert_eq!(parse_kv_fetch(&m), Some((5, "editor/open//x/y.rs".into())));
657 }
658
659 #[test]
660 fn kv_opened_roundtrip() {
661 let m = msg_kv_opened(9, 2, KV_STATUS_OK, "");
662 assert_eq!(parse_kv_opened(&m), Some((9, 2, KV_STATUS_OK, "".into())));
663 let m = msg_kv_opened(9, KV_ID_INVALID, KV_STATUS_PERMISSION, "kv disabled");
664 assert_eq!(
665 parse_kv_opened(&m),
666 Some((9, KV_ID_INVALID, KV_STATUS_PERMISSION, "kv disabled".into()))
667 );
668 }
669
670 #[test]
671 fn kv_closed_roundtrip_and_bytes() {
672 let m = msg_kv_closed(3, KV_CLOSED_RESOURCE_LIMIT);
673 assert_eq!(m, vec![0x74, 0x03, 0x00, 0x04]);
675 assert_eq!(parse_kv_closed(&m), Some((3, KV_CLOSED_RESOURCE_LIMIT)));
676 assert_eq!(parse_kv_closed(&m[..3]), None);
677 assert_eq!(parse_kv_closed(&msg_kv_stop(3)), None);
678 }
679
680 #[test]
681 fn kv_done_value_roundtrip() {
682 let m = msg_kv_done(4, KV_STATUS_CONFLICT, 77, 123_456);
683 assert_eq!(
684 parse_kv_done(&m),
685 Some((4, KV_STATUS_CONFLICT, 77, 123_456))
686 );
687 let m = msg_kv_value(6, KV_STATUS_OK, 88, b"payload");
688 assert_eq!(
689 parse_kv_value(&m),
690 Some((6, KV_STATUS_OK, 88, b"payload".to_vec()))
691 );
692 }
693
694 #[test]
695 fn record_roundtrip_and_mirror() {
696 let mut buf = Vec::new();
697 append_kv_record(
698 &mut buf,
699 &KvRecord::Upsert {
700 key: "editor/open//a.rs",
701 hash: 11,
702 size: 2,
703 mtime_ns: 5,
704 value: Some(b"{}"),
705 },
706 );
707 append_kv_record(
708 &mut buf,
709 &KvRecord::Upsert {
710 key: "editor/buf//a.rs",
711 hash: 12,
712 size: 9_999_999,
713 mtime_ns: 6,
714 value: None, },
716 );
717 append_kv_record(&mut buf, &KvRecord::Delete { key: "roots" });
718 let records: Vec<_> = kv_records(&buf).collect();
719 assert_eq!(records.len(), 3);
720 assert_eq!(
721 records[0],
722 KvRecord::Upsert {
723 key: "editor/open//a.rs",
724 hash: 11,
725 size: 2,
726 mtime_ns: 5,
727 value: Some(b"{}"),
728 }
729 );
730
731 let mut mirror = KvMirror::new();
732 let msg = msg_kv_update(1, 10, KV_UPDATE_SNAPSHOT_END, &buf);
733 assert_eq!(mirror.apply_update(&msg), Some(10));
734 assert!(mirror.snapshot_done);
735 assert_eq!(mirror.live.len(), 2);
736 assert_eq!(
737 mirror.live.get("editor/open//a.rs").unwrap().value,
738 Some(b"{}".to_vec())
739 );
740 assert_eq!(mirror.live.get("editor/buf//a.rs").unwrap().value, None);
741
742 let mut buf2 = Vec::new();
744 append_kv_record(
745 &mut buf2,
746 &KvRecord::Delete {
747 key: "editor/open//a.rs",
748 },
749 );
750 let msg2 = msg_kv_update(1, 11, 0, &buf2);
751 assert_eq!(mirror.apply_update(&msg2), Some(11));
752 assert_eq!(mirror.live.len(), 1);
753 }
754
755 #[test]
756 fn unknown_record_kind_skipped() {
757 let mut buf = Vec::new();
758 buf.extend_from_slice(&4u32.to_le_bytes());
760 buf.push(0x7F);
761 buf.extend_from_slice(&[1, 2, 3]);
762 append_kv_record(&mut buf, &KvRecord::Delete { key: "k" });
763 let records: Vec<_> = kv_records(&buf).collect();
764 assert_eq!(records, vec![KvRecord::Delete { key: "k" }]);
765 }
766
767 #[test]
768 fn key_validity() {
769 assert!(kv_key_valid("roots"));
770 assert!(kv_key_valid("editor/buf//x/y.rs"));
771 assert!(!kv_key_valid(""));
772 assert!(!kv_key_valid(&"k".repeat(KV_MAX_KEY + 1)));
773 assert!(!kv_key_valid("a\0b"));
774 }
775}