1use reddb_types::encoding::base64_encode;
50
51pub const TOPOLOGY_WIRE_VERSION_V1: u8 = 0x01;
57
58pub const MAX_KNOWN_TOPOLOGY_VERSION: u8 = TOPOLOGY_WIRE_VERSION_V1;
62
63pub const TOPOLOGY_HEADER_SIZE: usize = 1 + 4;
65
66#[derive(Debug, Clone, PartialEq, Eq)]
71pub struct Topology {
72 pub epoch: u64,
73 pub primary: Endpoint,
74 pub replicas: Vec<ReplicaInfo>,
75}
76
77#[derive(Debug, Clone, PartialEq, Eq)]
78pub struct Endpoint {
79 pub addr: String,
80 pub region: String,
81}
82
83#[derive(Debug, Clone, PartialEq, Eq)]
84pub struct ReplicaInfo {
85 pub addr: String,
86 pub region: String,
87 pub healthy: bool,
88 pub lag_ms: u32,
89 pub last_applied_lsn: u64,
90 pub rebootstrapping: bool,
103}
104
105#[derive(Debug, Clone, PartialEq, Eq)]
110pub enum TopologyError {
111 Truncated,
112 BodyLengthMismatch { declared: u32, available: usize },
113 InvalidUtf8,
114 StringTooLong { declared: u32, remaining: usize },
115}
116
117impl std::fmt::Display for TopologyError {
118 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
119 match self {
120 Self::Truncated => write!(f, "topology blob truncated (< 5-byte header)"),
121 Self::BodyLengthMismatch {
122 declared,
123 available,
124 } => write!(
125 f,
126 "topology body length mismatch: declared {declared}, available {available}"
127 ),
128 Self::InvalidUtf8 => write!(f, "topology string field is not valid UTF-8"),
129 Self::StringTooLong {
130 declared,
131 remaining,
132 } => write!(
133 f,
134 "topology string length {declared} exceeds remaining body bytes {remaining}"
135 ),
136 }
137 }
138}
139
140impl std::error::Error for TopologyError {}
141
142pub fn encode_topology(topology: &Topology) -> Vec<u8> {
147 let mut body = Vec::with_capacity(estimate_body_size(topology));
148 body.extend_from_slice(&topology.epoch.to_le_bytes());
149 write_str(&mut body, &topology.primary.addr);
150 write_str(&mut body, &topology.primary.region);
151 body.extend_from_slice(&(topology.replicas.len() as u32).to_le_bytes());
152 for r in &topology.replicas {
153 write_str(&mut body, &r.addr);
154 write_str(&mut body, &r.region);
155 body.push(if r.healthy { 1 } else { 0 });
156 body.extend_from_slice(&r.lag_ms.to_le_bytes());
157 body.extend_from_slice(&r.last_applied_lsn.to_le_bytes());
158 }
159 for r in &topology.replicas {
165 body.push(if r.rebootstrapping { 1 } else { 0 });
166 }
167
168 let mut out = Vec::with_capacity(TOPOLOGY_HEADER_SIZE + body.len());
169 out.push(TOPOLOGY_WIRE_VERSION_V1);
170 out.extend_from_slice(&(body.len() as u32).to_le_bytes());
171 out.extend_from_slice(&body);
172 out
173}
174
175pub fn decode_topology(bytes: &[u8]) -> Result<Option<Topology>, TopologyError> {
185 if bytes.len() < TOPOLOGY_HEADER_SIZE {
186 return Err(TopologyError::Truncated);
187 }
188 let version = bytes[0];
189 let declared_len = u32::from_le_bytes([bytes[1], bytes[2], bytes[3], bytes[4]]);
190 let body = &bytes[TOPOLOGY_HEADER_SIZE..];
191 if (body.len() as u64) < declared_len as u64 {
192 return Err(TopologyError::BodyLengthMismatch {
193 declared: declared_len,
194 available: body.len(),
195 });
196 }
197 let body = &body[..declared_len as usize];
198
199 if version > MAX_KNOWN_TOPOLOGY_VERSION {
200 return Ok(None);
202 }
203
204 let mut cur = Cursor::new(body);
206 let epoch = cur.read_u64()?;
207 let primary_addr = cur.read_str()?;
208 let primary_region = cur.read_str()?;
209 let replica_count = cur.read_u32()? as usize;
210 let mut replicas = Vec::with_capacity(replica_count);
211 for _ in 0..replica_count {
212 let addr = cur.read_str()?;
213 let region = cur.read_str()?;
214 let healthy = cur.read_u8()? != 0;
215 let lag_ms = cur.read_u32()?;
216 let last_applied_lsn = cur.read_u64()?;
217 replicas.push(ReplicaInfo {
218 addr,
219 region,
220 healthy,
221 lag_ms,
222 last_applied_lsn,
223 rebootstrapping: false,
227 });
228 }
229 for r in replicas.iter_mut() {
236 if cur.remaining() == 0 {
237 break;
238 }
239 r.rebootstrapping = cur.read_u8()? != 0;
240 }
241 Ok(Some(Topology {
242 epoch,
243 primary: Endpoint {
244 addr: primary_addr,
245 region: primary_region,
246 },
247 replicas,
248 }))
249}
250
251fn estimate_body_size(t: &Topology) -> usize {
252 let endpoint = |e: &Endpoint| 4 + e.addr.len() + 4 + e.region.len();
253 let mut n = 8 + endpoint(&t.primary) + 4;
254 for r in &t.replicas {
255 n += 4 + r.addr.len() + 4 + r.region.len() + 1 + 4 + 8;
256 }
257 n += t.replicas.len();
259 n
260}
261
262fn write_str(buf: &mut Vec<u8>, s: &str) {
263 buf.extend_from_slice(&(s.len() as u32).to_le_bytes());
264 buf.extend_from_slice(s.as_bytes());
265}
266
267struct Cursor<'a> {
268 buf: &'a [u8],
269 pos: usize,
270}
271
272impl<'a> Cursor<'a> {
273 fn new(buf: &'a [u8]) -> Self {
274 Self { buf, pos: 0 }
275 }
276
277 fn remaining(&self) -> usize {
278 self.buf.len() - self.pos
279 }
280
281 fn read_u8(&mut self) -> Result<u8, TopologyError> {
282 if self.remaining() < 1 {
283 return Err(TopologyError::Truncated);
284 }
285 let v = self.buf[self.pos];
286 self.pos += 1;
287 Ok(v)
288 }
289
290 fn read_u32(&mut self) -> Result<u32, TopologyError> {
291 if self.remaining() < 4 {
292 return Err(TopologyError::Truncated);
293 }
294 let bytes = &self.buf[self.pos..self.pos + 4];
295 self.pos += 4;
296 Ok(u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
297 }
298
299 fn read_u64(&mut self) -> Result<u64, TopologyError> {
300 if self.remaining() < 8 {
301 return Err(TopologyError::Truncated);
302 }
303 let bytes = &self.buf[self.pos..self.pos + 8];
304 self.pos += 8;
305 Ok(u64::from_le_bytes([
306 bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
307 ]))
308 }
309
310 fn read_str(&mut self) -> Result<String, TopologyError> {
311 let len = self.read_u32()?;
312 if (len as usize) > self.remaining() {
313 return Err(TopologyError::StringTooLong {
314 declared: len,
315 remaining: self.remaining(),
316 });
317 }
318 let bytes = &self.buf[self.pos..self.pos + len as usize];
319 self.pos += len as usize;
320 let s = std::str::from_utf8(bytes)
321 .map_err(|_| TopologyError::InvalidUtf8)?
322 .to_string();
323 Ok(s)
324 }
325}
326
327pub fn encode_topology_for_hello_ack(topology: &Topology) -> String {
345 base64_encode(&encode_topology(topology))
346}
347
348pub fn decode_topology_from_hello_ack(field: &str) -> Result<Option<Topology>, TopologyError> {
358 let Some(bytes) = base64_decode(field) else {
359 return Ok(None);
363 };
364 decode_topology(&bytes)
365}
366
367fn base64_decode(input: &str) -> Option<Vec<u8>> {
368 let trimmed = input.trim_end_matches('=');
369 let mut out = Vec::with_capacity(trimmed.len() * 3 / 4);
370 let mut buf = 0u32;
371 let mut bits = 0u8;
372 for ch in trimmed.bytes() {
373 let v: u32 = match ch {
374 b'A'..=b'Z' => (ch - b'A') as u32,
375 b'a'..=b'z' => (ch - b'a' + 26) as u32,
376 b'0'..=b'9' => (ch - b'0' + 52) as u32,
377 b'+' => 62,
378 b'/' => 63,
379 _ => return None,
380 };
381 buf = (buf << 6) | v;
382 bits += 6;
383 if bits >= 8 {
384 bits -= 8;
385 out.push(((buf >> bits) & 0xFF) as u8);
386 }
387 }
388 Some(out)
389}
390
391#[cfg(test)]
392mod tests {
393 use super::*;
394
395 fn fixture() -> Topology {
396 Topology {
397 epoch: 0xDEAD_BEEF_CAFE_BABE,
398 primary: Endpoint {
399 addr: "primary.example.com:5050".into(),
400 region: "us-east-1".into(),
401 },
402 replicas: vec![
403 ReplicaInfo {
404 addr: "replica-a.example.com:5050".into(),
405 region: "us-east-1".into(),
406 healthy: true,
407 lag_ms: 12,
408 last_applied_lsn: 4242,
409 rebootstrapping: false,
410 },
411 ReplicaInfo {
412 addr: "replica-b.example.com:5050".into(),
413 region: "us-west-2".into(),
414 healthy: false,
415 lag_ms: 999,
416 last_applied_lsn: 4100,
417 rebootstrapping: true,
418 },
419 ],
420 }
421 }
422
423 #[test]
424 fn round_trip_v1() {
425 let t = fixture();
426 let bytes = encode_topology(&t);
427 let decoded = decode_topology(&bytes).expect("decode").expect("v1 known");
428 assert_eq!(decoded, t);
429 }
430
431 #[test]
432 fn empty_replicas_round_trip() {
433 let t = Topology {
434 epoch: 1,
435 primary: Endpoint {
436 addr: "p:5050".into(),
437 region: "r".into(),
438 },
439 replicas: vec![],
440 };
441 let bytes = encode_topology(&t);
442 let decoded = decode_topology(&bytes).expect("decode").expect("v1");
443 assert_eq!(decoded, t);
444 }
445
446 #[test]
447 fn unknown_version_tag_returns_none() {
448 let mut bytes = encode_topology(&fixture());
452 bytes[0] = 0xFE; let decoded = decode_topology(&bytes).expect("decode");
454 assert!(
455 decoded.is_none(),
456 "unknown version tag must drop cleanly, got {decoded:?}"
457 );
458 }
459
460 #[test]
461 fn truncated_header_errors() {
462 assert!(matches!(
463 decode_topology(&[0x01, 0x00]),
464 Err(TopologyError::Truncated)
465 ));
466 }
467
468 #[test]
469 fn body_length_mismatch_errors() {
470 let bytes = vec![0x01, 0xFF, 0xFF, 0xFF, 0xFF, 0x00];
472 assert!(matches!(
473 decode_topology(&bytes),
474 Err(TopologyError::BodyLengthMismatch { .. })
475 ));
476 }
477
478 #[test]
479 fn version_tag_is_pinned_to_0x01() {
480 assert_eq!(TOPOLOGY_WIRE_VERSION_V1, 0x01);
484 }
485
486 #[test]
487 fn rebootstrapping_flag_round_trips_per_replica() {
488 let t = fixture();
491 let decoded = decode_topology(&encode_topology(&t))
492 .expect("decode")
493 .expect("v1 known");
494 assert!(!decoded.replicas[0].rebootstrapping);
495 assert!(decoded.replicas[1].rebootstrapping);
496 }
497
498 #[test]
499 fn legacy_blob_without_trailing_block_defaults_rebootstrapping_false() {
500 let t = fixture();
506 let full = encode_topology(&t);
507 let trailing = t.replicas.len();
508 let body_len = (full.len() - TOPOLOGY_HEADER_SIZE - trailing) as u32;
509 let mut legacy = Vec::new();
510 legacy.push(TOPOLOGY_WIRE_VERSION_V1);
511 legacy.extend_from_slice(&body_len.to_le_bytes());
512 legacy.extend_from_slice(&full[TOPOLOGY_HEADER_SIZE..full.len() - trailing]);
513 let decoded = decode_topology(&legacy).expect("decode").expect("v1 known");
514 assert_eq!(decoded.replicas.len(), 2);
515 assert!(decoded.replicas.iter().all(|r| !r.rebootstrapping));
516 assert_eq!(decoded.replicas[0].last_applied_lsn, 4242);
519 assert_eq!(decoded.replicas[1].lag_ms, 999);
520 }
521
522 #[test]
523 fn hello_ack_round_trip_via_base64() {
524 let t = fixture();
528 let field = encode_topology_for_hello_ack(&t);
529 let decoded = decode_topology_from_hello_ack(&field)
530 .expect("decode")
531 .expect("v1 known");
532 assert_eq!(decoded, t);
533 }
534
535 #[test]
536 fn hello_ack_inner_bytes_match_grpc_bytes() {
537 let t = fixture();
542 let canonical = encode_topology(&t);
543 let field = encode_topology_for_hello_ack(&t);
544 let recovered = base64_decode(&field).expect("base64");
545 assert_eq!(recovered, canonical);
546 }
547
548 #[test]
549 fn hello_ack_unknown_version_tag_drops_cleanly() {
550 let mut bytes = encode_topology(&fixture());
554 bytes[0] = 0x99;
555 let field = base64_encode(&bytes);
556 let decoded = decode_topology_from_hello_ack(&field).expect("decode");
557 assert!(decoded.is_none());
558 }
559
560 #[test]
561 fn hello_ack_malformed_base64_drops_cleanly() {
562 let decoded = decode_topology_from_hello_ack("@not base64@").expect("decode");
565 assert!(decoded.is_none());
566 }
567
568 #[test]
569 fn old_hello_ack_without_topology_field_is_backwards_compat() {
570 let json = br#"{"version":1,"auth":"bearer","features":3,"server":"reddb/0.2.9"}"#;
578 let v: serde_json_check::Value = serde_json_check::from_slice(json).expect("valid JSON");
579 let topo_field = v.find_string("topology");
580 let topology = match topo_field {
581 None => None,
582 Some(s) => decode_topology_from_hello_ack(&s).expect("decode"),
583 };
584 assert!(
585 topology.is_none(),
586 "an old HelloAck without `topology` must produce None"
587 );
588 }
589
590 mod serde_json_check {
594 pub enum Value {
595 Object(Vec<(String, Value)>),
596 String(String),
597 Other,
598 }
599
600 impl Value {
601 pub fn find_string(&self, key: &str) -> Option<String> {
602 match self {
603 Value::Object(map) => map.iter().find_map(|(k, v)| {
604 if k == key {
605 if let Value::String(s) = v {
606 Some(s.clone())
607 } else {
608 None
609 }
610 } else {
611 None
612 }
613 }),
614 _ => None,
615 }
616 }
617 }
618
619 pub fn from_slice(bytes: &[u8]) -> Result<Value, &'static str> {
620 let s = std::str::from_utf8(bytes).map_err(|_| "utf8")?;
621 let mut p = Parser { src: s, pos: 0 };
622 p.skip_ws();
623 let v = p.parse_value()?;
624 Ok(v)
625 }
626
627 struct Parser<'a> {
628 src: &'a str,
629 pos: usize,
630 }
631
632 impl<'a> Parser<'a> {
633 fn rest(&self) -> &'a str {
634 &self.src[self.pos..]
635 }
636 fn bump(&mut self, n: usize) {
637 self.pos += n;
638 }
639 fn skip_ws(&mut self) {
640 while let Some(c) = self.rest().chars().next() {
641 if c.is_whitespace() {
642 self.bump(c.len_utf8());
643 } else {
644 break;
645 }
646 }
647 }
648 fn parse_value(&mut self) -> Result<Value, &'static str> {
649 self.skip_ws();
650 let head = self.rest().chars().next().ok_or("eof")?;
651 match head {
652 '{' => self.parse_object(),
653 '"' => self.parse_string().map(Value::String),
654 _ => {
655 self.skip_until_top_level_comma_or_close();
660 Ok(Value::Other)
661 }
662 }
663 }
664 fn skip_until_top_level_comma_or_close(&mut self) {
665 let mut depth = 0i32;
666 while let Some(c) = self.rest().chars().next() {
667 match c {
668 '"' => {
669 let _ = self.parse_string();
670 continue;
671 }
672 '{' | '[' => {
673 depth += 1;
674 self.bump(1);
675 }
676 '}' | ']' => {
677 if depth == 0 {
678 return;
679 }
680 depth -= 1;
681 self.bump(1);
682 }
683 ',' if depth == 0 => return,
684 _ => self.bump(c.len_utf8()),
685 }
686 }
687 }
688 fn parse_object(&mut self) -> Result<Value, &'static str> {
689 self.bump(1); let mut map = Vec::new();
691 loop {
692 self.skip_ws();
693 if self.rest().starts_with('}') {
694 self.bump(1);
695 return Ok(Value::Object(map));
696 }
697 let key = self.parse_string()?;
698 self.skip_ws();
699 if !self.rest().starts_with(':') {
700 return Err("expected ':'");
701 }
702 self.bump(1);
703 let val = self.parse_value()?;
704 map.push((key, val));
705 self.skip_ws();
706 match self.rest().chars().next() {
707 Some(',') => {
708 self.bump(1);
709 continue;
710 }
711 Some('}') => {
712 self.bump(1);
713 return Ok(Value::Object(map));
714 }
715 _ => return Err("expected ',' or '}'"),
716 }
717 }
718 }
719 fn parse_string(&mut self) -> Result<String, &'static str> {
720 if !self.rest().starts_with('"') {
721 return Err("expected '\"'");
722 }
723 self.bump(1);
724 let start = self.pos;
725 while let Some(c) = self.rest().chars().next() {
726 if c == '"' {
727 let s = self.src[start..self.pos].to_string();
728 self.bump(1);
729 return Ok(s);
730 }
731 if c == '\\' {
732 self.bump(c.len_utf8());
733 }
734 self.bump(c.len_utf8());
735 }
736 Err("unterminated string")
737 }
738 }
739 }
740
741 #[test]
742 fn header_layout_first_byte_is_version_then_le_length() {
743 let t = fixture();
748 let bytes = encode_topology(&t);
749 assert_eq!(bytes[0], TOPOLOGY_WIRE_VERSION_V1);
750 let declared = u32::from_le_bytes([bytes[1], bytes[2], bytes[3], bytes[4]]);
751 assert_eq!(declared as usize, bytes.len() - TOPOLOGY_HEADER_SIZE);
752 }
753}