Skip to main content

keyhog_core/
json_selector.rs

1//! Bounded rooted selectors for detector-owned verification responses.
2
3use std::fmt;
4
5/// Maximum encoded selector length accepted from a detector specification.
6pub const MAX_SELECTOR_BYTES: usize = 1024;
7/// Maximum object-key and array-index segments in one selector.
8pub const MAX_SELECTOR_SEGMENTS: usize = 64;
9const MAX_ARRAY_INDEX: u64 = 1_000_000;
10const MAX_ERROR_SELECTOR_PREVIEW_BYTES: usize = 160;
11
12/// A syntax error in a detector verification response selector.
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct SelectorError {
15    selector_preview: String,
16    selector_bytes: usize,
17    offset: usize,
18    reason: &'static str,
19}
20
21impl SelectorError {
22    fn new(selector: &str, offset: usize, reason: &'static str) -> Self {
23        let preview_end = if selector.len() <= MAX_ERROR_SELECTOR_PREVIEW_BYTES {
24            selector.len()
25        } else {
26            let mut boundary = MAX_ERROR_SELECTOR_PREVIEW_BYTES;
27            while !selector.is_char_boundary(boundary) {
28                boundary -= 1;
29            }
30            boundary
31        };
32        Self {
33            selector_preview: selector[..preview_end].to_string(),
34            selector_bytes: selector.len(),
35            offset,
36            reason,
37        }
38    }
39}
40
41impl fmt::Display for SelectorError {
42    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
43        write!(
44            formatter,
45            "invalid response selector {:?}",
46            self.selector_preview
47        )?;
48        if self.selector_preview.len() < self.selector_bytes {
49            write!(formatter, " ({} bytes)", self.selector_bytes)?;
50        }
51        write!(
52            formatter,
53            " at byte {}: {}. Fix: use a `$`-rooted selector such as `$.data.account.email` or `$.orgs[0].name`",
54            self.offset, self.reason
55        )
56    }
57}
58
59impl std::error::Error for SelectorError {}
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62enum Segment<'a> {
63    Key(&'a str),
64    Index(usize),
65}
66
67fn visit_segments<'a>(
68    selector: &'a str,
69    mut visit: impl FnMut(Segment<'a>),
70) -> Result<(), SelectorError> {
71    let bytes = selector.as_bytes();
72    if bytes.len() > MAX_SELECTOR_BYTES {
73        return Err(SelectorError::new(
74            selector,
75            MAX_SELECTOR_BYTES,
76            "the selector exceeds the 1024-byte limit",
77        ));
78    }
79    if bytes.first() != Some(&b'$') {
80        return Err(SelectorError::new(
81            selector,
82            0,
83            "the selector must start with `$`",
84        ));
85    }
86    let mut cursor = 1;
87    let mut segment_count = 0usize;
88
89    let mut visit_bounded = |segment| {
90        segment_count += 1;
91        if segment_count > MAX_SELECTOR_SEGMENTS {
92            return false;
93        }
94        visit(segment);
95        true
96    };
97
98    while cursor < bytes.len() {
99        if bytes[cursor] == b'.' {
100            cursor += 1;
101            let key_start = cursor;
102            while cursor < bytes.len() && !matches!(bytes[cursor], b'.' | b'[' | b']') {
103                if !bytes[cursor].is_ascii_alphanumeric() && !matches!(bytes[cursor], b'_' | b'-') {
104                    return Err(SelectorError::new(
105                        selector,
106                        cursor,
107                        "bare object keys may contain only ASCII letters, digits, `_`, and `-`",
108                    ));
109                }
110                cursor += 1;
111            }
112            if cursor == key_start {
113                return Err(SelectorError::new(
114                    selector,
115                    cursor,
116                    "an object key is missing after `.`",
117                ));
118            }
119            if !visit_bounded(Segment::Key(&selector[key_start..cursor])) {
120                return Err(SelectorError::new(
121                    selector,
122                    cursor,
123                    "the selector exceeds the 64-segment limit",
124                ));
125            }
126            continue;
127        }
128        if bytes[cursor] != b'[' {
129            return Err(SelectorError::new(
130                selector,
131                cursor,
132                "expected `.` or `[` after the current segment",
133            ));
134        }
135        let bracket_start = cursor;
136        cursor += 1;
137        if cursor < bytes.len() && bytes[cursor] == b'"' {
138            cursor += 1;
139            let key_start = cursor;
140            while cursor < bytes.len() && bytes[cursor] != b'"' {
141                if bytes[cursor] == b'\\' || bytes[cursor].is_ascii_control() {
142                    return Err(SelectorError::new(
143                        selector,
144                        cursor,
145                        "quoted object keys cannot contain escapes or control characters",
146                    ));
147                }
148                cursor += 1;
149            }
150            if cursor == key_start {
151                return Err(SelectorError::new(
152                    selector,
153                    cursor,
154                    "a quoted object key cannot be empty",
155                ));
156            }
157            if cursor == bytes.len() || bytes[cursor] != b'"' {
158                return Err(SelectorError::new(
159                    selector,
160                    bracket_start,
161                    "a quoted object key is missing its closing quote",
162                ));
163            }
164            if !visit_bounded(Segment::Key(&selector[key_start..cursor])) {
165                return Err(SelectorError::new(
166                    selector,
167                    cursor,
168                    "the selector exceeds the 64-segment limit",
169                ));
170            }
171            cursor += 1;
172            if cursor == bytes.len() || bytes[cursor] != b']' {
173                return Err(SelectorError::new(
174                    selector,
175                    bracket_start,
176                    "a quoted object key is missing its closing `]`",
177                ));
178            }
179            cursor += 1;
180            continue;
181        }
182        let index_start = cursor;
183        while cursor < bytes.len() && bytes[cursor].is_ascii_digit() {
184            cursor += 1;
185        }
186        if cursor == index_start {
187            return Err(SelectorError::new(
188                selector,
189                cursor,
190                "an array index must contain decimal digits",
191            ));
192        }
193        if cursor == bytes.len() || bytes[cursor] != b']' {
194            return Err(SelectorError::new(
195                selector,
196                bracket_start,
197                "an array index is missing its closing `]`",
198            ));
199        }
200        if cursor - index_start > 1 && bytes[index_start] == b'0' {
201            return Err(SelectorError::new(
202                selector,
203                index_start,
204                "array indexes cannot contain leading zeroes",
205            ));
206        }
207        let index = selector[index_start..cursor].parse::<u64>().map_err(|_| {
208            SelectorError::new(selector, index_start, "the array index is too large")
209        })?;
210        if index > MAX_ARRAY_INDEX {
211            return Err(SelectorError::new(
212                selector,
213                index_start,
214                "the array index exceeds the 1000000 limit",
215            ));
216        }
217        if !visit_bounded(Segment::Index(index as usize)) {
218            return Err(SelectorError::new(
219                selector,
220                cursor,
221                "the selector exceeds the 64-segment limit",
222            ));
223        }
224        cursor += 1;
225    }
226    Ok(())
227}
228
229/// Validate the rooted response selector grammar used by detector TOMLs.
230pub fn validate(selector: &str) -> Result<(), SelectorError> {
231    visit_segments(selector, |_| {})
232}
233
234fn validate_at(errors: &mut Vec<String>, scope: std::fmt::Arguments<'_>, selector: Option<&str>) {
235    if let Some(selector) = selector {
236        if let Err(error) = validate(selector) {
237            errors.push(format!("{scope}: {error}"));
238        }
239    }
240}
241
242/// Validate every response selector and selector-dependent field in a detector.
243pub fn validate_detector_response_selectors(detector: &crate::DetectorSpec) -> Vec<String> {
244    let Some(verify) = &detector.verify else {
245        return Vec::new();
246    };
247    let mut errors = Vec::new();
248    if let Some(success) = &verify.success {
249        validate_at(
250            &mut errors,
251            format_args!("verify.success.json_path"),
252            success.json_path.as_deref(),
253        );
254        if success.equals.is_some() && success.json_path.is_none() {
255            errors.push(
256                "verify.success.equals requires verify.success.json_path so the expected value has an explicit response selector"
257                    .to_string(),
258            );
259        }
260    }
261    for (metadata_index, metadata) in verify.metadata.iter().enumerate() {
262        validate_at(
263            &mut errors,
264            format_args!("verify.metadata[{metadata_index}].json_path"),
265            Some(&metadata.json_path),
266        );
267    }
268    for (step_index, step) in verify.steps.iter().enumerate() {
269        validate_at(
270            &mut errors,
271            format_args!("verify.steps[{step_index}].success.json_path"),
272            step.success.json_path.as_deref(),
273        );
274        if step.success.equals.is_some() && step.success.json_path.is_none() {
275            errors.push(format!(
276                "verify.steps[{step_index}].success.equals requires verify.steps[{step_index}].success.json_path so the expected value has an explicit response selector"
277            ));
278        }
279        for (extract_index, metadata) in step.extract.iter().enumerate() {
280            validate_at(
281                &mut errors,
282                format_args!("verify.steps[{step_index}].extract[{extract_index}].json_path"),
283                Some(&metadata.json_path),
284            );
285        }
286    }
287    errors
288}
289
290/// Resolve a dotted selector against a JSON value.
291///
292/// A valid selector that does not exist returns `Ok(None)`. Invalid syntax
293/// returns an error so verifier configuration faults cannot look like dead
294/// credentials or missing metadata.
295pub fn select<'a>(
296    value: &'a serde_json::Value,
297    selector: &str,
298) -> Result<Option<&'a serde_json::Value>, SelectorError> {
299    let mut selected = Some(value);
300    visit_segments(selector, |segment| {
301        selected = match (selected, segment) {
302            (Some(serde_json::Value::Object(object)), Segment::Key(key)) => object.get(key),
303            (Some(serde_json::Value::Array(array)), Segment::Index(index)) => array.get(index),
304            _ => None,
305        };
306    })?;
307    Ok(selected)
308}