Skip to main content

efema_proto/
cursor.rs

1//! Where a reader stands in a stream.
2
3use std::fmt;
4use std::str::FromStr;
5
6use minicbor::{Decode, Encode};
7
8use crate::{Hash, StreamId, chain};
9
10/// A reader's place in a stream: which stream, how far, and what the history
11/// up to there was.
12///
13/// The position is the last entry the reader has; reading "after" the cursor
14/// returns the entries that follow it. The identity and the link make the
15/// cursor checkable: the relay answers a cursor only if the stream is the same
16/// incarnation and its chain has the same link at that position. Otherwise it
17/// says which of the two failed, and the reader knows its copy and the relay's
18/// have parted - rather than reading on from a place that no longer exists.
19///
20/// The text form, used in URLs, is `<position>.<stream>.<link>`:
21///
22/// ```
23/// use efema_proto::{Cursor, StreamId};
24///
25/// let start = Cursor::start(StreamId::from_bytes([0xab; 16]));
26/// let text = start.to_string();
27/// assert!(text.starts_with("0.abababab"));
28/// assert_eq!(text.parse::<Cursor>().unwrap(), start);
29/// ```
30#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Encode, Decode)]
31#[cbor(map)]
32pub struct Cursor {
33    /// The stream incarnation this cursor belongs to.
34    #[n(0)]
35    pub stream: StreamId,
36    /// The position of the last entry the reader has; 0 before the first.
37    #[n(1)]
38    pub seq: u64,
39    /// The chain link at `seq`.
40    #[n(2)]
41    pub hash: Hash,
42}
43
44impl Cursor {
45    /// The cursor before the first entry of `stream`: the reader knows the
46    /// stream and has read nothing from it yet.
47    pub fn start(stream: StreamId) -> Self {
48        Self { stream, seq: 0, hash: chain::genesis(&stream) }
49    }
50}
51
52/// Why a text is not a cursor.
53#[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        // Digits only: `u64::from_str` would also take a leading `+`, and a
80        // second spelling of the same cursor is one more thing to compare.
81        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}