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 "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 "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 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}