Coverage Report

Created: 2025-08-28 06:34

/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