Line data Source code
1 : /*-------------------------------------------------------------------------
2 : *
3 : * test_json_parser_incremental.c
4 : * Test program for incremental JSON parser
5 : *
6 : * Copyright (c) 2024-2025, PostgreSQL Global Development Group
7 : *
8 : * IDENTIFICATION
9 : * src/test/modules/test_json_parser/test_json_parser_incremental.c
10 : *
11 : * This program tests incremental parsing of json. The input is fed into
12 : * the parser in very small chunks. In practice you would normally use
13 : * much larger chunks, but doing this makes it more likely that the
14 : * full range of increment handling, especially in the lexer, is exercised.
15 : *
16 : * If the "-c SIZE" option is provided, that chunk size is used instead
17 : * of the default of 60.
18 : *
19 : * If the "-r SIZE" option is provided, a range of chunk sizes from SIZE down to
20 : * 1 are run sequentially. A null byte is printed to the streams after each
21 : * iteration.
22 : *
23 : * If the -s flag is given, the program does semantic processing. This should
24 : * just mirror back the json, albeit with white space changes.
25 : *
26 : * If the -o flag is given, the JSONLEX_CTX_OWNS_TOKENS flag is set. (This can
27 : * be used in combination with a leak sanitizer; without the option, the parser
28 : * may leak memory with invalid JSON.)
29 : *
30 : * The argument specifies the file containing the JSON input.
31 : *
32 : *-------------------------------------------------------------------------
33 : */
34 :
35 : #include "postgres_fe.h"
36 :
37 : #include <stdio.h>
38 : #include <sys/types.h>
39 : #include <sys/stat.h>
40 : #include <unistd.h>
41 :
42 : #include "common/jsonapi.h"
43 : #include "common/logging.h"
44 : #include "lib/stringinfo.h"
45 : #include "mb/pg_wchar.h"
46 : #include "pg_getopt.h"
47 :
48 : #define BUFSIZE 6000
49 : #define DEFAULT_CHUNK_SIZE 60
50 :
51 : typedef struct DoState
52 : {
53 : JsonLexContext *lex;
54 : bool elem_is_first;
55 : StringInfo buf;
56 : } DoState;
57 :
58 : static void usage(const char *progname);
59 : static void escape_json(StringInfo buf, const char *str);
60 :
61 : /* semantic action functions for parser */
62 : static JsonParseErrorType do_object_start(void *state);
63 : static JsonParseErrorType do_object_end(void *state);
64 : static JsonParseErrorType do_object_field_start(void *state, char *fname, bool isnull);
65 : static JsonParseErrorType do_object_field_end(void *state, char *fname, bool isnull);
66 : static JsonParseErrorType do_array_start(void *state);
67 : static JsonParseErrorType do_array_end(void *state);
68 : static JsonParseErrorType do_array_element_start(void *state, bool isnull);
69 : static JsonParseErrorType do_array_element_end(void *state, bool isnull);
70 : static JsonParseErrorType do_scalar(void *state, char *token, JsonTokenType tokentype);
71 :
72 : static JsonSemAction sem = {
73 : .object_start = do_object_start,
74 : .object_end = do_object_end,
75 : .object_field_start = do_object_field_start,
76 : .object_field_end = do_object_field_end,
77 : .array_start = do_array_start,
78 : .array_end = do_array_end,
79 : .array_element_start = do_array_element_start,
80 : .array_element_end = do_array_element_end,
81 : .scalar = do_scalar
82 : };
83 :
84 : static bool lex_owns_tokens = false;
85 :
86 : int
87 840 : main(int argc, char **argv)
88 : {
89 : char buff[BUFSIZE];
90 : FILE *json_file;
91 : JsonParseErrorType result;
92 : JsonLexContext *lex;
93 : StringInfoData json;
94 : int n_read;
95 840 : size_t chunk_size = DEFAULT_CHUNK_SIZE;
96 840 : bool run_chunk_ranges = false;
97 : struct stat statbuf;
98 840 : const JsonSemAction *testsem = &nullSemAction;
99 : char *testfile;
100 : int c;
101 840 : bool need_strings = false;
102 840 : int ret = 0;
103 :
104 840 : pg_logging_init(argv[0]);
105 :
106 840 : lex = calloc(1, sizeof(JsonLexContext));
107 840 : if (!lex)
108 0 : pg_fatal("out of memory");
109 :
110 2940 : while ((c = getopt(argc, argv, "r:c:os")) != -1)
111 : {
112 1260 : switch (c)
113 : {
114 312 : case 'r': /* chunk range */
115 312 : run_chunk_ranges = true;
116 : /* fall through */
117 832 : case 'c': /* chunk size */
118 832 : chunk_size = strtou64(optarg, NULL, 10);
119 832 : if (chunk_size > BUFSIZE)
120 0 : pg_fatal("chunk size cannot exceed %d", BUFSIZE);
121 832 : break;
122 420 : case 'o': /* switch token ownership */
123 420 : lex_owns_tokens = true;
124 420 : break;
125 8 : case 's': /* do semantic processing */
126 8 : testsem = &sem;
127 8 : sem.semstate = palloc(sizeof(struct DoState));
128 8 : ((struct DoState *) sem.semstate)->lex = lex;
129 8 : ((struct DoState *) sem.semstate)->buf = makeStringInfo();
130 8 : need_strings = true;
131 8 : break;
132 : }
133 : }
134 :
135 840 : if (optind < argc)
136 : {
137 832 : testfile = argv[optind];
138 832 : optind++;
139 : }
140 : else
141 : {
142 8 : usage(argv[0]);
143 8 : exit(1);
144 : }
145 :
146 832 : initStringInfo(&json);
147 :
148 832 : if ((json_file = fopen(testfile, PG_BINARY_R)) == NULL)
149 0 : pg_fatal("error opening input: %m");
150 :
151 832 : if (fstat(fileno(json_file), &statbuf) != 0)
152 0 : pg_fatal("error statting input: %m");
153 :
154 : do
155 : {
156 : /*
157 : * This outer loop only repeats in -r mode. Reset the parse state and
158 : * our position in the input file for the inner loop, which performs
159 : * the incremental parsing.
160 : */
161 3920 : off_t bytes_left = statbuf.st_size;
162 3920 : size_t to_read = chunk_size;
163 :
164 3920 : makeJsonLexContextIncremental(lex, PG_UTF8, need_strings);
165 3920 : setJsonLexContextOwnsTokens(lex, lex_owns_tokens);
166 :
167 3920 : rewind(json_file);
168 3920 : resetStringInfo(&json);
169 :
170 : for (;;)
171 : {
172 : /* We will break when there's nothing left to read */
173 :
174 745416 : if (bytes_left < to_read)
175 2624 : to_read = bytes_left;
176 :
177 745416 : n_read = fread(buff, 1, to_read, json_file);
178 745416 : if (n_read < to_read)
179 0 : pg_fatal("error reading input file: %d", ferror(json_file));
180 :
181 745416 : appendBinaryStringInfo(&json, buff, n_read);
182 :
183 : /*
184 : * Append some trailing junk to the buffer passed to the parser.
185 : * This helps us ensure that the parser does the right thing even
186 : * if the chunk isn't terminated with a '\0'.
187 : */
188 745416 : appendStringInfoString(&json, "1+23 trailing junk");
189 745416 : bytes_left -= n_read;
190 745416 : if (bytes_left > 0)
191 : {
192 741840 : result = pg_parse_json_incremental(lex, testsem,
193 741840 : json.data, n_read,
194 : false);
195 741840 : if (result != JSON_INCOMPLETE)
196 : {
197 344 : fprintf(stderr, "%s\n", json_errdetail(result, lex));
198 344 : ret = 1;
199 344 : goto cleanup;
200 : }
201 741496 : resetStringInfo(&json);
202 : }
203 : else
204 : {
205 3576 : result = pg_parse_json_incremental(lex, testsem,
206 3576 : json.data, n_read,
207 : true);
208 3576 : if (result != JSON_SUCCESS)
209 : {
210 1560 : fprintf(stderr, "%s\n", json_errdetail(result, lex));
211 1560 : ret = 1;
212 1560 : goto cleanup;
213 : }
214 2016 : if (!need_strings)
215 2008 : printf("SUCCESS!\n");
216 2016 : break;
217 : }
218 : }
219 :
220 3920 : cleanup:
221 3920 : freeJsonLexContext(lex);
222 :
223 : /*
224 : * In -r mode, separate output with nulls so that the calling test can
225 : * split it up, decrement the chunk size, and loop back to the top.
226 : * All other modes immediately fall out of the loop and exit.
227 : */
228 3920 : if (run_chunk_ranges)
229 : {
230 3400 : fputc('\0', stdout);
231 3400 : fputc('\0', stderr);
232 : }
233 3920 : } while (run_chunk_ranges && (--chunk_size > 0));
234 :
235 832 : fclose(json_file);
236 832 : free(json.data);
237 832 : free(lex);
238 :
239 832 : return ret;
240 : }
241 :
242 : /*
243 : * The semantic routines here essentially just output the same json, except
244 : * for white space. We could pretty print it but there's no need for our
245 : * purposes. The result should be able to be fed to any JSON processor
246 : * such as jq for validation.
247 : */
248 :
249 : static JsonParseErrorType
250 320 : do_object_start(void *state)
251 : {
252 320 : DoState *_state = (DoState *) state;
253 :
254 320 : printf("{\n");
255 320 : _state->elem_is_first = true;
256 :
257 320 : return JSON_SUCCESS;
258 : }
259 :
260 : static JsonParseErrorType
261 320 : do_object_end(void *state)
262 : {
263 320 : DoState *_state = (DoState *) state;
264 :
265 320 : printf("\n}\n");
266 320 : _state->elem_is_first = false;
267 :
268 320 : return JSON_SUCCESS;
269 : }
270 :
271 : static JsonParseErrorType
272 1248 : do_object_field_start(void *state, char *fname, bool isnull)
273 : {
274 1248 : DoState *_state = (DoState *) state;
275 :
276 1248 : if (!_state->elem_is_first)
277 968 : printf(",\n");
278 1248 : resetStringInfo(_state->buf);
279 1248 : escape_json(_state->buf, fname);
280 1248 : printf("%s: ", _state->buf->data);
281 1248 : _state->elem_is_first = false;
282 :
283 1248 : return JSON_SUCCESS;
284 : }
285 :
286 : static JsonParseErrorType
287 1248 : do_object_field_end(void *state, char *fname, bool isnull)
288 : {
289 1248 : if (!lex_owns_tokens)
290 624 : free(fname);
291 :
292 1248 : return JSON_SUCCESS;
293 : }
294 :
295 : static JsonParseErrorType
296 88 : do_array_start(void *state)
297 : {
298 88 : DoState *_state = (DoState *) state;
299 :
300 88 : printf("[\n");
301 88 : _state->elem_is_first = true;
302 :
303 88 : return JSON_SUCCESS;
304 : }
305 :
306 : static JsonParseErrorType
307 88 : do_array_end(void *state)
308 : {
309 88 : DoState *_state = (DoState *) state;
310 :
311 88 : printf("\n]\n");
312 88 : _state->elem_is_first = false;
313 :
314 88 : return JSON_SUCCESS;
315 : }
316 :
317 : static JsonParseErrorType
318 240 : do_array_element_start(void *state, bool isnull)
319 : {
320 240 : DoState *_state = (DoState *) state;
321 :
322 240 : if (!_state->elem_is_first)
323 152 : printf(",\n");
324 240 : _state->elem_is_first = false;
325 :
326 240 : return JSON_SUCCESS;
327 : }
328 :
329 : static JsonParseErrorType
330 240 : do_array_element_end(void *state, bool isnull)
331 : {
332 : /* nothing to do */
333 :
334 240 : return JSON_SUCCESS;
335 : }
336 :
337 : static JsonParseErrorType
338 1088 : do_scalar(void *state, char *token, JsonTokenType tokentype)
339 : {
340 1088 : DoState *_state = (DoState *) state;
341 :
342 1088 : if (tokentype == JSON_TOKEN_STRING)
343 : {
344 848 : resetStringInfo(_state->buf);
345 848 : escape_json(_state->buf, token);
346 848 : printf("%s", _state->buf->data);
347 : }
348 : else
349 240 : printf("%s", token);
350 :
351 1088 : if (!lex_owns_tokens)
352 544 : free(token);
353 :
354 1088 : return JSON_SUCCESS;
355 : }
356 :
357 :
358 : /* copied from backend code */
359 : static void
360 2096 : escape_json(StringInfo buf, const char *str)
361 : {
362 : const char *p;
363 :
364 2096 : appendStringInfoCharMacro(buf, '"');
365 32944 : for (p = str; *p; p++)
366 : {
367 30848 : switch (*p)
368 : {
369 8 : case '\b':
370 8 : appendStringInfoString(buf, "\\b");
371 8 : break;
372 8 : case '\f':
373 8 : appendStringInfoString(buf, "\\f");
374 8 : break;
375 8 : case '\n':
376 8 : appendStringInfoString(buf, "\\n");
377 8 : break;
378 8 : case '\r':
379 8 : appendStringInfoString(buf, "\\r");
380 8 : break;
381 8 : case '\t':
382 8 : appendStringInfoString(buf, "\\t");
383 8 : break;
384 0 : case '"':
385 0 : appendStringInfoString(buf, "\\\"");
386 0 : break;
387 8 : case '\\':
388 8 : appendStringInfoString(buf, "\\\\");
389 8 : break;
390 30800 : default:
391 30800 : if ((unsigned char) *p < ' ')
392 8 : appendStringInfo(buf, "\\u%04x", (int) *p);
393 : else
394 30792 : appendStringInfoCharMacro(buf, *p);
395 30800 : break;
396 : }
397 : }
398 2096 : appendStringInfoCharMacro(buf, '"');
399 2096 : }
400 :
401 : static void
402 8 : usage(const char *progname)
403 : {
404 8 : fprintf(stderr, "Usage: %s [OPTION ...] testfile\n", progname);
405 8 : fprintf(stderr, "Options:\n");
406 8 : fprintf(stderr, " -c chunksize size of piece fed to parser (default 64)\n");
407 8 : fprintf(stderr, " -o set JSONLEX_CTX_OWNS_TOKENS for leak checking\n");
408 8 : fprintf(stderr, " -s do semantic processing\n");
409 :
410 8 : }
|