vv_agent/checkpoint/canonical/
json_pointer.rs1use super::*;
2
3pub fn resolve_json_pointer<'a>(value: &'a Value, pointer: &str) -> CheckpointResult<&'a Value> {
4 let tokens = pointer_tokens(pointer)?;
5 let mut current = value;
6 for token in tokens {
7 current = match current {
8 Value::Object(object) => object.get(&token).ok_or_else(|| {
9 CheckpointError::new(
10 "checkpoint_credential_slot_unresolved",
11 format!("JSON pointer {pointer} does not resolve"),
12 )
13 })?,
14 Value::Array(array) => {
15 let index = token.parse::<usize>().map_err(|_| {
16 CheckpointError::new(
17 "checkpoint_credential_slot_unresolved",
18 format!("JSON pointer {pointer} has an invalid array index"),
19 )
20 })?;
21 array.get(index).ok_or_else(|| {
22 CheckpointError::new(
23 "checkpoint_credential_slot_unresolved",
24 format!("JSON pointer {pointer} does not resolve"),
25 )
26 })?
27 }
28 _ => {
29 return Err(CheckpointError::new(
30 "checkpoint_credential_slot_unresolved",
31 format!("JSON pointer {pointer} does not resolve"),
32 ));
33 }
34 };
35 }
36 Ok(current)
37}
38
39pub fn set_json_pointer(
40 value: &mut Value,
41 pointer: &str,
42 replacement: Value,
43) -> CheckpointResult<()> {
44 let tokens = pointer_tokens(pointer)?;
45 if tokens.is_empty() {
46 *value = replacement;
47 return Ok(());
48 }
49 let mut current = value;
50 for token in &tokens[..tokens.len() - 1] {
51 current = match current {
52 Value::Object(object) => object.get_mut(token).ok_or_else(|| {
53 CheckpointError::new(
54 "checkpoint_credential_slot_unresolved",
55 format!("JSON pointer {pointer} does not resolve"),
56 )
57 })?,
58 Value::Array(array) => {
59 let index = token.parse::<usize>().map_err(|_| {
60 CheckpointError::new(
61 "checkpoint_credential_slot_unresolved",
62 format!("JSON pointer {pointer} has an invalid array index"),
63 )
64 })?;
65 array.get_mut(index).ok_or_else(|| {
66 CheckpointError::new(
67 "checkpoint_credential_slot_unresolved",
68 format!("JSON pointer {pointer} does not resolve"),
69 )
70 })?
71 }
72 _ => {
73 return Err(CheckpointError::new(
74 "checkpoint_credential_slot_unresolved",
75 format!("JSON pointer {pointer} does not resolve"),
76 ));
77 }
78 };
79 }
80 let last = tokens.last().expect("non-empty pointer tokens");
81 match current {
82 Value::Object(object) => {
83 if !object.contains_key(last) {
84 return Err(CheckpointError::new(
85 "checkpoint_credential_slot_unresolved",
86 format!("JSON pointer {pointer} does not resolve"),
87 ));
88 }
89 object.insert(last.clone(), replacement);
90 }
91 Value::Array(array) => {
92 let index = last.parse::<usize>().map_err(|_| {
93 CheckpointError::new(
94 "checkpoint_credential_slot_unresolved",
95 format!("JSON pointer {pointer} has an invalid array index"),
96 )
97 })?;
98 let Some(item) = array.get_mut(index) else {
99 return Err(CheckpointError::new(
100 "checkpoint_credential_slot_unresolved",
101 format!("JSON pointer {pointer} does not resolve"),
102 ));
103 };
104 *item = replacement;
105 }
106 _ => {
107 return Err(CheckpointError::new(
108 "checkpoint_credential_slot_unresolved",
109 format!("JSON pointer {pointer} does not resolve"),
110 ));
111 }
112 }
113 Ok(())
114}
115
116pub(in crate::checkpoint) fn validate_pointer(pointer: &str) -> CheckpointResult<()> {
117 pointer_tokens(pointer).map(|_| ())
118}
119
120fn pointer_tokens(pointer: &str) -> CheckpointResult<Vec<String>> {
121 if pointer.is_empty() {
122 return Ok(Vec::new());
123 }
124 if !pointer.starts_with('/') {
125 return Err(CheckpointError::new(
126 "checkpoint_credential_slots_invalid",
127 "JSON pointer must be empty or start with '/'",
128 ));
129 }
130 pointer
131 .split('/')
132 .skip(1)
133 .map(|raw| {
134 let mut token = String::with_capacity(raw.len());
135 let bytes = raw.as_bytes();
136 let mut index = 0;
137 while index < bytes.len() {
138 if bytes[index] != b'~' {
139 let character = raw[index..].chars().next().expect("valid UTF-8");
140 token.push(character);
141 index += character.len_utf8();
142 continue;
143 }
144 if index + 1 >= bytes.len() {
145 return Err(CheckpointError::new(
146 "checkpoint_credential_slots_invalid",
147 "JSON pointer contains an invalid escape",
148 ));
149 }
150 match bytes[index + 1] {
151 b'0' => token.push('~'),
152 b'1' => token.push('/'),
153 _ => {
154 return Err(CheckpointError::new(
155 "checkpoint_credential_slots_invalid",
156 "JSON pointer contains an invalid escape",
157 ));
158 }
159 }
160 index += 2;
161 }
162 Ok(token)
163 })
164 .collect()
165}