/src/valijson/include/valijson/internal/json_pointer.hpp
Line | Count | Source (jump to first uncovered line) |
1 | | #pragma once |
2 | | |
3 | | #include <algorithm> |
4 | | #include <cerrno> |
5 | | #include <cstddef> |
6 | | #include <cstring> |
7 | | #include <stdexcept> |
8 | | #include <string> |
9 | | |
10 | | #include <valijson/internal/adapter.hpp> |
11 | | #include <valijson/internal/optional.hpp> |
12 | | #include <valijson/exceptions.hpp> |
13 | | |
14 | | #ifdef _MSC_VER |
15 | | #pragma warning( push ) |
16 | | #pragma warning( disable : 4702 ) |
17 | | #endif |
18 | | |
19 | | namespace valijson { |
20 | | namespace internal { |
21 | | namespace json_pointer { |
22 | | |
23 | | /** |
24 | | * @brief Replace all occurrences of `search` with `replace`. Modifies `subject` in place. |
25 | | * |
26 | | * @param subject string to operate on |
27 | | * @param search string to search |
28 | | * @param replace replacement string |
29 | | */ |
30 | | inline void replaceAllInPlace(std::string& subject, const char* search, |
31 | | const char* replace) |
32 | 243k | { |
33 | 243k | size_t pos = 0; |
34 | | |
35 | 245k | while((pos = subject.find(search, pos)) != std::string::npos) { |
36 | 2.39k | subject.replace(pos, strlen(search), replace); |
37 | 2.39k | pos += strlen(replace); |
38 | 2.39k | } |
39 | 243k | } |
40 | | |
41 | | /** |
42 | | * @brief Return the char value corresponding to a 2-digit hexadecimal string |
43 | | * |
44 | | * @throws std::runtime_error for strings that are not exactly two characters |
45 | | * in length and for strings that contain non-hexadecimal characters |
46 | | * |
47 | | * @return decoded char value corresponding to the hexadecimal string |
48 | | */ |
49 | | inline char decodePercentEncodedChar(const std::string &digits) |
50 | 17.2k | { |
51 | 17.2k | if (digits.length() != 2) { |
52 | 806 | throwRuntimeError("Failed to decode %-encoded character '" + |
53 | 806 | digits + "' due to unexpected number of characters; " |
54 | 806 | "expected two characters"); |
55 | 806 | } |
56 | | |
57 | 17.2k | errno = 0; |
58 | 17.2k | const char *begin = digits.c_str(); |
59 | 17.2k | char *end = nullptr; |
60 | 17.2k | const unsigned long value = strtoul(begin, &end, 16); |
61 | 17.2k | if (end != begin && *end != '\0') { |
62 | 120 | throwRuntimeError("Failed to decode %-encoded character '" + |
63 | 120 | digits + "'"); |
64 | 120 | } |
65 | | |
66 | 17.2k | return char(value); |
67 | 17.2k | } |
68 | | |
69 | | /** |
70 | | * @brief Extract and transform the token between two iterators |
71 | | * |
72 | | * This function is responsible for extracting a JSON Reference token from |
73 | | * between two iterators, and performing any necessary transformations, before |
74 | | * returning the resulting string. Its main purpose is to replace the escaped |
75 | | * character sequences defined in the RFC-6901 (JSON Pointer), and to decode |
76 | | * %-encoded character sequences defined in RFC-3986 (URI). |
77 | | * |
78 | | * The encoding used in RFC-3986 should be familiar to many developers, but |
79 | | * the escaped character sequences used in JSON Pointers may be less so. From |
80 | | * the JSON Pointer specification (RFC 6901, April 2013): |
81 | | * |
82 | | * Evaluation of each reference token begins by decoding any escaped |
83 | | * character sequence. This is performed by first transforming any |
84 | | * occurrence of the sequence '~1' to '/', and then transforming any |
85 | | * occurrence of the sequence '~0' to '~'. By performing the |
86 | | * substitutions in this order, an implementation avoids the error of |
87 | | * turning '~01' first into '~1' and then into '/', which would be |
88 | | * incorrect (the string '~01' correctly becomes '~1' after |
89 | | * transformation). |
90 | | * |
91 | | * @param begin iterator pointing to beginning of a token |
92 | | * @param end iterator pointing to one character past the end of the token |
93 | | * |
94 | | * @return string with escaped character sequences replaced |
95 | | * |
96 | | */ |
97 | | inline std::string extractReferenceToken(std::string::const_iterator begin, |
98 | | std::string::const_iterator end) |
99 | 121k | { |
100 | 121k | std::string token(begin, end); |
101 | | |
102 | | // Replace JSON Pointer-specific escaped character sequences |
103 | 121k | replaceAllInPlace(token, "~1", "/"); |
104 | 121k | replaceAllInPlace(token, "~0", "~"); |
105 | | |
106 | | // Replace %-encoded character sequences with their actual characters |
107 | 137k | for (size_t n = token.find('%'); n != std::string::npos; |
108 | 121k | n = token.find('%', n + 1)) { |
109 | | |
110 | 17.2k | #if VALIJSON_USE_EXCEPTIONS |
111 | 17.2k | try { |
112 | 17.2k | #endif |
113 | 17.2k | const char c = decodePercentEncodedChar(token.substr(n + 1, 2)); |
114 | 17.2k | token.replace(n, 3, 1, c); |
115 | 17.2k | #if VALIJSON_USE_EXCEPTIONS |
116 | 17.2k | } catch (const std::runtime_error &e) { |
117 | 926 | throwRuntimeError( |
118 | 926 | std::string(e.what()) + "; in token: " + token); |
119 | 926 | } |
120 | 17.2k | #endif |
121 | 17.2k | } |
122 | | |
123 | 121k | return token; |
124 | 121k | } |
125 | | |
126 | | /** |
127 | | * @brief Recursively locate the value referenced by a JSON Pointer |
128 | | * |
129 | | * This function takes both a string reference and an iterator to the beginning |
130 | | * of the substring that is being resolved. This iterator is expected to point |
131 | | * to the beginning of a reference token, whose length will be determined by |
132 | | * searching for the next delimiter ('/' or '\0'). A reference token must be |
133 | | * at least one character in length to be considered valid. |
134 | | * |
135 | | * Once the next reference token has been identified, it will be used either as |
136 | | * an array index or as the name of an object member. The validity of a |
137 | | * reference token depends on the type of the node currently being traversed, |
138 | | * and the applicability of the token to that node. For example, an array can |
139 | | * only be dereferenced by a non-negative integral index. |
140 | | * |
141 | | * Once the next node has been identified, the length of the remaining portion |
142 | | * of the JSON Pointer will be used to determine whether recursion should |
143 | | * terminate. |
144 | | * |
145 | | * @param node current node in recursive evaluation of JSON Pointer |
146 | | * @param jsonPointer string containing complete JSON Pointer |
147 | | * @param jsonPointerItr string iterator pointing the beginning of the next |
148 | | * reference token |
149 | | * |
150 | | * @return an instance of AdapterType that wraps the dereferenced node |
151 | | */ |
152 | | template<typename AdapterType> |
153 | | inline AdapterType resolveJsonPointer( |
154 | | const AdapterType &node, |
155 | | const std::string &jsonPointer, |
156 | | const std::string::const_iterator jsonPointerItr) |
157 | 134k | { |
158 | | // TODO: This function will probably need to implement support for |
159 | | // fetching documents referenced by JSON Pointers, similar to the |
160 | | // populateSchema function. |
161 | | |
162 | 134k | const std::string::const_iterator jsonPointerEnd = jsonPointer.end(); |
163 | | |
164 | | // Terminate recursion if all reference tokens have been consumed |
165 | 134k | if (jsonPointerItr == jsonPointerEnd) { |
166 | 11.4k | return node; |
167 | 11.4k | } |
168 | | |
169 | | // Reference tokens must begin with a leading slash |
170 | 122k | if (*jsonPointerItr != '/') { |
171 | 932 | throwRuntimeError("Expected reference token to begin with " |
172 | 932 | "leading slash; remaining tokens: " + |
173 | 932 | std::string(jsonPointerItr, jsonPointerEnd)); |
174 | 932 | } |
175 | | |
176 | | // Find iterator that points to next slash or newline character; this is |
177 | | // one character past the end of the current reference token |
178 | 122k | std::string::const_iterator jsonPointerNext = |
179 | 122k | std::find(jsonPointerItr + 1, jsonPointerEnd, '/'); |
180 | | |
181 | | // Extract the next reference token |
182 | 122k | const std::string referenceToken = extractReferenceToken( |
183 | 122k | jsonPointerItr + 1, jsonPointerNext); |
184 | | |
185 | | // Empty reference tokens should be ignored |
186 | 122k | if (referenceToken.empty()) { |
187 | 110k | return resolveJsonPointer(node, jsonPointer, jsonPointerNext); |
188 | | |
189 | 110k | } else if (node.isArray()) { |
190 | 2.69k | if (referenceToken == "-") { |
191 | 0 | throwRuntimeError("Hyphens cannot be used as array indices " |
192 | 0 | "since the requested array element does not yet exist"); |
193 | 0 | } |
194 | | |
195 | 2.69k | #if VALIJSON_USE_EXCEPTIONS |
196 | 2.69k | try { |
197 | 2.69k | #endif |
198 | | // Fragment must be non-negative integer |
199 | 2.69k | const uint64_t index = std::stoul(referenceToken); |
200 | 2.69k | typedef typename AdapterType::Array Array; |
201 | 2.69k | const Array arr = node.asArray(); |
202 | 2.69k | typename Array::const_iterator itr = arr.begin(); |
203 | 2.69k | const uint64_t arrSize = arr.size(); |
204 | | |
205 | 2.69k | if (arrSize == 0 || index > arrSize - 1) { |
206 | 75 | throwRuntimeError("Expected reference token to identify " |
207 | 75 | "an element in the current array, but array index is " |
208 | 75 | "out of bounds; actual token: " + referenceToken); |
209 | 75 | } |
210 | | |
211 | 2.69k | if (index > static_cast<uint64_t>(std::numeric_limits<std::ptrdiff_t>::max())) { |
212 | 0 | throwRuntimeError("Array index out of bounds; hard " |
213 | 0 | "limit is " + std::to_string( |
214 | 0 | std::numeric_limits<std::ptrdiff_t>::max())); |
215 | 0 | } |
216 | | |
217 | 2.69k | itr.advance(static_cast<std::ptrdiff_t>(index)); |
218 | | |
219 | | // Recursively process the remaining tokens |
220 | 2.69k | return resolveJsonPointer(*itr, jsonPointer, jsonPointerNext); |
221 | | |
222 | 2.69k | #if VALIJSON_USE_EXCEPTIONS |
223 | 2.69k | } catch (std::invalid_argument &) { |
224 | 56 | throwRuntimeError("Expected reference token to contain a " |
225 | 56 | "non-negative integer to identify an element in the " |
226 | 56 | "current array; actual token: " + referenceToken); |
227 | 56 | } |
228 | 2.69k | #endif |
229 | 9.41k | } else if (node.maybeObject()) { |
230 | | // Fragment must identify a member of the candidate object |
231 | 7.26k | typedef typename AdapterType::Object Object; |
232 | | |
233 | 7.26k | const Object object = node.asObject(); |
234 | 7.26k | typename Object::const_iterator itr = object.find( |
235 | 7.26k | referenceToken); |
236 | 7.26k | if (itr == object.end()) { |
237 | 1.82k | throwRuntimeError("Expected reference token to identify an " |
238 | 1.82k | "element in the current object; " |
239 | 1.82k | "actual token: " + referenceToken); |
240 | 1.82k | abort(); |
241 | 1.82k | } |
242 | | |
243 | | // Recursively process the remaining tokens |
244 | 5.44k | return resolveJsonPointer(itr->second, jsonPointer, jsonPointerNext); |
245 | 7.26k | } |
246 | | |
247 | 2.14k | throwRuntimeError("Expected end of JSON Pointer, but at least " |
248 | 2.14k | "one reference token has not been processed; remaining tokens: " + |
249 | 2.14k | std::string(jsonPointerNext, jsonPointerEnd)); |
250 | 2.14k | abort(); |
251 | 122k | } |
252 | | |
253 | | /** |
254 | | * @brief Return the JSON Value referenced by a JSON Pointer |
255 | | * |
256 | | * @param rootNode node to use as root for JSON Pointer resolution |
257 | | * @param jsonPointer string containing JSON Pointer |
258 | | * |
259 | | * @return an instance AdapterType in the specified document |
260 | | */ |
261 | | template<typename AdapterType> |
262 | | inline AdapterType resolveJsonPointer( |
263 | | const AdapterType &rootNode, |
264 | | const std::string &jsonPointer) |
265 | 15.5k | { |
266 | 15.5k | return resolveJsonPointer(rootNode, jsonPointer, jsonPointer.begin()); |
267 | 15.5k | } |
268 | | |
269 | | } // namespace json_pointer |
270 | | } // namespace internal |
271 | | } // namespace valijson |
272 | | |
273 | | #ifdef _MSC_VER |
274 | | #pragma warning( pop ) |
275 | | #endif |