Skip to main content

journal/
parse.rs

1use std::error::Error;
2
3pub type ParseError = Box<dyn Error + Send + Sync>;
4pub type ParsedCursor = (String, String, u64, u64);
5
6#[derive(Debug, Clone, PartialEq, Eq)]
7pub struct ParsedCursorLocation {
8    pub seqnum_id: String,
9    pub seqnum: u64,
10    pub seqnum_set: bool,
11    pub boot_id: String,
12    pub monotonic: u64,
13    pub monotonic_set: bool,
14    pub realtime: u64,
15    pub realtime_set: bool,
16    pub xor_hash: u64,
17    pub xor_hash_set: bool,
18}
19
20pub fn parse_match_string(s: &str) -> std::result::Result<Vec<u8>, ParseError> {
21    parse_match_bytes(s.as_bytes())
22}
23
24pub fn parse_cursor(cursor: &str) -> std::result::Result<ParsedCursor, ParseError> {
25    let location = parse_cursor_location(cursor, true)?;
26    Ok((
27        location.seqnum_id,
28        location.boot_id,
29        location.realtime,
30        location.seqnum,
31    ))
32}
33
34pub fn parse_cursor_location(
35    cursor: &str,
36    require_seek_component: bool,
37) -> std::result::Result<ParsedCursorLocation, ParseError> {
38    let mut seqnum_id = String::new();
39    let mut boot_id = String::new();
40    let mut realtime = None;
41    let mut monotonic = None;
42    let mut seqnum = None;
43    let mut xor_hash = None;
44    let mut legacy_cursor = false;
45
46    for part in cursor.split(';') {
47        let Some((key, value)) = part.split_once('=') else {
48            return Err("invalid cursor: malformed segment".into());
49        };
50        if key.is_empty() || value.is_empty() {
51            return Err("invalid cursor: empty segment".into());
52        }
53        match key {
54            "s" => seqnum_id = normalize_id(value),
55            // Legacy SDK cursor shape.
56            "j" => {
57                legacy_cursor = true;
58                boot_id = normalize_id(value);
59            }
60            "c" => {
61                legacy_cursor = true;
62                realtime = Some(u64::from_str_radix(value, 16)?);
63            }
64            "n" => {
65                legacy_cursor = true;
66                seqnum = Some(value.parse()?);
67            }
68            // Official systemd cursor shape.
69            "b" => boot_id = normalize_id(value),
70            "m" => monotonic = Some(u64::from_str_radix(value, 16)?),
71            "t" => realtime = Some(u64::from_str_radix(value, 16)?),
72            "i" => seqnum = Some(u64::from_str_radix(value, 16)?),
73            "x" => xor_hash = Some(u64::from_str_radix(value, 16)?),
74            _ => {}
75        }
76    }
77
78    if legacy_cursor {
79        if seqnum_id.is_empty() || boot_id.is_empty() || realtime.is_none() || seqnum.is_none() {
80            return Err("invalid cursor: incomplete legacy cursor".into());
81        }
82    } else {
83        let has_seqnum_cursor = !seqnum_id.is_empty() && seqnum.is_some();
84        let has_monotonic_cursor = !boot_id.is_empty() && monotonic.is_some();
85        let has_realtime_cursor = realtime.is_some();
86        if require_seek_component
87            && !(has_seqnum_cursor || has_monotonic_cursor || has_realtime_cursor)
88        {
89            return Err("invalid cursor: missing seek component".into());
90        }
91        if !require_seek_component
92            && seqnum_id.is_empty()
93            && seqnum.is_none()
94            && boot_id.is_empty()
95            && monotonic.is_none()
96            && realtime.is_none()
97            && xor_hash.is_none()
98        {
99            return Err("invalid cursor: missing cursor component".into());
100        }
101    }
102
103    Ok(ParsedCursorLocation {
104        seqnum_id,
105        boot_id,
106        realtime: realtime.unwrap_or(0),
107        realtime_set: realtime.is_some(),
108        monotonic: monotonic.unwrap_or(0),
109        monotonic_set: monotonic.is_some(),
110        seqnum: seqnum.unwrap_or(0),
111        seqnum_set: seqnum.is_some(),
112        xor_hash: xor_hash.unwrap_or(0),
113        xor_hash_set: xor_hash.is_some(),
114    })
115}
116
117fn normalize_id(value: &str) -> String {
118    value.replace('-', "").to_ascii_lowercase()
119}
120
121pub fn cursor_location_matches(got: &ParsedCursorLocation, want: &ParsedCursorLocation) -> bool {
122    let mut matched = false;
123    if !want.seqnum_id.is_empty() {
124        if got.seqnum_id != want.seqnum_id {
125            return false;
126        }
127        matched = true;
128    }
129    if want.seqnum_set {
130        if !got.seqnum_set || got.seqnum != want.seqnum {
131            return false;
132        }
133        matched = true;
134    }
135    if !want.boot_id.is_empty() {
136        if got.boot_id != want.boot_id {
137            return false;
138        }
139        matched = true;
140    }
141    if want.monotonic_set {
142        if !got.monotonic_set || got.monotonic != want.monotonic {
143            return false;
144        }
145        matched = true;
146    }
147    if want.realtime_set {
148        if !got.realtime_set || got.realtime != want.realtime {
149            return false;
150        }
151        matched = true;
152    }
153    if want.xor_hash_set {
154        if !got.xor_hash_set || got.xor_hash != want.xor_hash {
155            return false;
156        }
157        matched = true;
158    }
159    matched
160}
161
162pub fn cursor_location_at_or_after(
163    got: &ParsedCursorLocation,
164    want: &ParsedCursorLocation,
165) -> bool {
166    if !want.seqnum_id.is_empty() && want.seqnum_set && got.seqnum_id == want.seqnum_id {
167        if got.seqnum != want.seqnum {
168            return got.seqnum > want.seqnum;
169        }
170    }
171    if !want.boot_id.is_empty() && want.monotonic_set && got.boot_id == want.boot_id {
172        if got.monotonic != want.monotonic {
173            return got.monotonic > want.monotonic;
174        }
175    }
176    if want.realtime_set && got.realtime != want.realtime {
177        return got.realtime > want.realtime;
178    }
179    // systemd uses x= for exact cursor tests, but seek-cursor positioning
180    // treats sequence/monotonic/realtime components as the ordering key.
181    // A cursor with a mismatched x= still seeks to the row matching the
182    // other components; TestCursor remains exact and checks x= above.
183    true
184}
185
186pub fn parse_match_bytes(data: &[u8]) -> std::result::Result<Vec<u8>, ParseError> {
187    let eq = data.iter().position(|byte| *byte == b'=');
188    if eq.is_none() {
189        return Err("EINVAL: missing '=' separator".into());
190    }
191    let eq = eq.unwrap();
192    let key = &data[..eq];
193    if key.is_empty() || key[0].is_ascii_digit() {
194        return Err("EINVAL: invalid field name".into());
195    }
196    for byte in key {
197        if !byte.is_ascii_uppercase() && !byte.is_ascii_digit() && *byte != b'_' {
198            return Err("EINVAL: invalid field name".into());
199        }
200    }
201    return Ok(data.to_vec());
202}
203
204#[cfg(test)]
205mod tests {
206    use super::{
207        cursor_location_at_or_after, cursor_location_matches, parse_cursor, parse_cursor_location,
208    };
209
210    #[test]
211    fn parse_cursor_accepts_legacy_sdk_shape() {
212        let parsed = parse_cursor("s=abc123;j=def456;c=0000000000000001;n=42")
213            .expect("legacy cursor parses");
214        assert_eq!(parsed, ("abc123".to_string(), "def456".to_string(), 1, 42));
215    }
216
217    #[test]
218    fn parse_cursor_accepts_official_systemd_shape() {
219        let parsed =
220            parse_cursor("s=ABC123;i=2a;b=01234567-89ab-cdef-0123-456789abcdef;m=9;t=10;x=ff")
221                .expect("official cursor parses");
222        assert_eq!(
223            parsed,
224            (
225                "abc123".to_string(),
226                "0123456789abcdef0123456789abcdef".to_string(),
227                16,
228                42,
229            )
230        );
231    }
232
233    #[test]
234    fn parse_cursor_accepts_partial_systemd_shape() {
235        let parsed = parse_cursor("s=ABC123;i=2a").expect("partial seqnum cursor parses");
236        assert_eq!(parsed, ("abc123".to_string(), "".to_string(), 0, 42));
237
238        let parsed = parse_cursor("t=10").expect("partial realtime cursor parses");
239        assert_eq!(parsed, ("".to_string(), "".to_string(), 16, 0));
240    }
241
242    #[test]
243    fn cursor_seek_order_ignores_x_hash_but_exact_match_checks_it() {
244        let got = parse_cursor_location("s=abc;i=2;b=def;m=3;t=4;x=1", false)
245            .expect("current cursor parses");
246        let want = parse_cursor_location("s=abc;i=2;b=def;m=3;t=4;x=ffffffffffffffff", true)
247            .expect("target cursor parses");
248
249        assert!(
250            cursor_location_at_or_after(&got, &want),
251            "seek ordering should ignore mismatched x= when other components match"
252        );
253        assert!(
254            !cursor_location_matches(&got, &want),
255            "exact cursor matching must still include x="
256        );
257    }
258}