1use std::fmt;
4use std::str::FromStr;
5
6use minicbor::{Decode, Encode};
7
8use crate::{Hash, StreamId, chain};
9
10#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Encode, Decode)]
31#[cbor(map)]
32pub struct Cursor {
33 #[n(0)]
35 pub stream: StreamId,
36 #[n(1)]
38 pub seq: u64,
39 #[n(2)]
41 pub hash: Hash,
42}
43
44impl Cursor {
45 pub fn start(stream: StreamId) -> Self {
48 Self { stream, seq: 0, hash: chain::genesis(&stream) }
49 }
50}
51
52#[derive(Clone, Debug, PartialEq, Eq)]
54pub struct ParseCursorError;
55
56impl fmt::Display for ParseCursorError {
57 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
58 f.write_str("a cursor is <position>.<stream>.<link>: a number, 32 and 64 lowercase hexadecimal digits")
59 }
60}
61
62impl std::error::Error for ParseCursorError {}
63
64impl fmt::Display for Cursor {
65 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66 write!(f, "{}.{}.{}", self.seq, self.stream, self.hash)
67 }
68}
69
70impl FromStr for Cursor {
71 type Err = ParseCursorError;
72
73 fn from_str(s: &str) -> Result<Self, Self::Err> {
74 let mut parts = s.split('.');
75 let (Some(seq), Some(stream), Some(hash), None) = (parts.next(), parts.next(), parts.next(), parts.next())
76 else {
77 return Err(ParseCursorError);
78 };
79 if seq.is_empty() || !seq.bytes().all(|b| b.is_ascii_digit()) || (seq.len() > 1 && seq.starts_with('0')) {
82 return Err(ParseCursorError);
83 }
84 Ok(Self {
85 seq: seq.parse().map_err(|_| ParseCursorError)?,
86 stream: stream.parse().map_err(|_| ParseCursorError)?,
87 hash: hash.parse().map_err(|_| ParseCursorError)?,
88 })
89 }
90}
91
92#[cfg(test)]
93mod tests {
94 use super::*;
95
96 fn sample() -> Cursor {
97 Cursor { stream: StreamId::from_bytes([0xab; 16]), seq: 42, hash: Hash::from_bytes([0xcd; 32]) }
98 }
99
100 #[test]
101 fn round_trips_through_text() {
102 let cursor = sample();
103 let text = cursor.to_string();
104 assert_eq!(text, format!("42.{}.{}", "ab".repeat(16), "cd".repeat(32)));
105 assert_eq!(text.parse::<Cursor>(), Ok(cursor));
106 }
107
108 #[test]
109 fn has_one_spelling() {
110 let canonical = sample().to_string();
111 for wrong in [
112 canonical.replacen("42", "+42", 1),
113 canonical.replacen("42", "042", 1),
114 canonical.replacen("42", "", 1),
115 canonical.to_uppercase(),
116 format!("{canonical}."),
117 format!("{canonical}.0"),
118 canonical.replacen('.', ":", 1),
119 "18446744073709551616".to_string() + &canonical[2..],
120 ] {
121 assert_eq!(wrong.parse::<Cursor>(), Err(ParseCursorError), "{wrong:?} should not parse");
122 }
123 }
124
125 #[test]
126 fn the_start_holds_the_genesis_link() {
127 let stream = StreamId::from_bytes([5; 16]);
128 let start = Cursor::start(stream);
129 assert_eq!(start.seq, 0);
130 assert_eq!(start.hash, chain::genesis(&stream));
131 }
132
133 #[test]
134 fn round_trips_through_cbor() {
135 let cursor = sample();
136 let bytes = minicbor::to_vec(cursor).unwrap();
137 assert_eq!(minicbor::decode::<Cursor>(&bytes).unwrap(), cursor);
138 }
139}