Age Owner Branch data TLA 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
597 andrew@dunslane.net 87 :CBC 420 : 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;
567 95 : 420 : size_t chunk_size = DEFAULT_CHUNK_SIZE;
27 jchampion@postgresql 96 : 420 : bool run_chunk_ranges = false;
97 : : struct stat statbuf;
448 heikki.linnakangas@i 98 : 420 : const JsonSemAction *testsem = &nullSemAction;
99 : : char *testfile;
100 : : int c;
597 andrew@dunslane.net 101 : 420 : bool need_strings = false;
335 102 : 420 : int ret = 0;
103 : :
552 104 : 420 : pg_logging_init(argv[0]);
105 : :
188 dgustafsson@postgres 106 : 420 : lex = calloc(1, sizeof(JsonLexContext));
107 [ - + ]: 420 : if (!lex)
188 dgustafsson@postgres 108 :UBC 0 : pg_fatal("out of memory");
109 : :
27 jchampion@postgresql 110 [ + + ]:CBC 1470 : while ((c = getopt(argc, argv, "r:c:os")) != -1)
111 : : {
597 andrew@dunslane.net 112 [ - + + + : 630 : switch (c)
+ ]
113 : : {
27 jchampion@postgresql 114 : 156 : case 'r': /* chunk range */
115 : 156 : run_chunk_ranges = true;
116 : : /* fall through */
117 : 416 : case 'c': /* chunk size */
499 noah@leadboat.com 118 : 416 : chunk_size = strtou64(optarg, NULL, 10);
567 andrew@dunslane.net 119 [ - + ]: 416 : if (chunk_size > BUFSIZE)
553 andrew@dunslane.net 120 :UBC 0 : pg_fatal("chunk size cannot exceed %d", BUFSIZE);
597 andrew@dunslane.net 121 :CBC 416 : break;
335 122 : 210 : case 'o': /* switch token ownership */
123 : 210 : lex_owns_tokens = true;
124 : 210 : break;
597 125 : 4 : case 's': /* do semantic processing */
126 : 4 : testsem = &sem;
127 : 4 : sem.semstate = palloc(sizeof(struct DoState));
188 dgustafsson@postgres 128 : 4 : ((struct DoState *) sem.semstate)->lex = lex;
597 andrew@dunslane.net 129 : 4 : ((struct DoState *) sem.semstate)->buf = makeStringInfo();
130 : 4 : need_strings = true;
131 : 4 : break;
132 : : }
133 : : }
134 : :
135 [ + + ]: 420 : if (optind < argc)
136 : : {
335 137 : 416 : testfile = argv[optind];
597 138 : 416 : optind++;
139 : : }
140 : : else
141 : : {
142 : 4 : usage(argv[0]);
143 : 4 : exit(1);
144 : : }
145 : :
146 : 416 : initStringInfo(&json);
147 : :
476 148 [ - + ]: 416 : if ((json_file = fopen(testfile, PG_BINARY_R)) == NULL)
553 andrew@dunslane.net 149 :UBC 0 : pg_fatal("error opening input: %m");
150 : :
553 andrew@dunslane.net 151 [ - + ]:CBC 416 : if (fstat(fileno(json_file), &statbuf) != 0)
553 andrew@dunslane.net 152 :UBC 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 : : */
27 jchampion@postgresql 161 :CBC 1960 : off_t bytes_left = statbuf.st_size;
162 : 1960 : size_t to_read = chunk_size;
163 : :
164 : 1960 : makeJsonLexContextIncremental(lex, PG_UTF8, need_strings);
165 : 1960 : setJsonLexContextOwnsTokens(lex, lex_owns_tokens);
166 : :
167 : 1960 : rewind(json_file);
168 : 1960 : resetStringInfo(&json);
169 : :
170 : : for (;;)
171 : : {
172 : : /* We will break when there's nothing left to read */
173 : :
174 [ + + ]: 372708 : if (bytes_left < to_read)
175 : 1312 : to_read = bytes_left;
176 : :
177 : 372708 : n_read = fread(buff, 1, to_read, json_file);
178 [ - + ]: 372708 : if (n_read < to_read)
27 jchampion@postgresql 179 :UBC 0 : pg_fatal("error reading input file: %d", ferror(json_file));
180 : :
27 jchampion@postgresql 181 :CBC 372708 : 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 : 372708 : appendStringInfoString(&json, "1+23 trailing junk");
189 : 372708 : bytes_left -= n_read;
190 [ + + ]: 372708 : if (bytes_left > 0)
191 : : {
192 : 370920 : result = pg_parse_json_incremental(lex, testsem,
193 : 370920 : json.data, n_read,
194 : : false);
195 [ + + ]: 370920 : if (result != JSON_INCOMPLETE)
196 : : {
197 : 172 : fprintf(stderr, "%s\n", json_errdetail(result, lex));
198 : 172 : ret = 1;
199 : 172 : goto cleanup;
200 : : }
201 : 370748 : resetStringInfo(&json);
202 : : }
203 : : else
204 : : {
205 : 1788 : result = pg_parse_json_incremental(lex, testsem,
206 : 1788 : json.data, n_read,
207 : : true);
208 [ + + ]: 1788 : if (result != JSON_SUCCESS)
209 : : {
210 : 780 : fprintf(stderr, "%s\n", json_errdetail(result, lex));
211 : 780 : ret = 1;
212 : 780 : goto cleanup;
213 : : }
214 [ + + ]: 1008 : if (!need_strings)
215 : 1004 : printf("SUCCESS!\n");
216 : 1008 : break;
217 : : }
218 : : }
219 : :
335 andrew@dunslane.net 220 : 1960 : cleanup:
27 jchampion@postgresql 221 : 1960 : 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 [ + + ]: 1960 : if (run_chunk_ranges)
229 : : {
230 : 1700 : fputc('\0', stdout);
231 : 1700 : fputc('\0', stderr);
232 : : }
233 [ + + + + ]: 1960 : } while (run_chunk_ranges && (--chunk_size > 0));
234 : :
597 andrew@dunslane.net 235 : 416 : fclose(json_file);
335 236 : 416 : free(json.data);
188 dgustafsson@postgres 237 : 416 : free(lex);
238 : :
335 andrew@dunslane.net 239 : 416 : 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
597 250 : 160 : do_object_start(void *state)
251 : : {
252 : 160 : DoState *_state = (DoState *) state;
253 : :
254 : 160 : printf("{\n");
255 : 160 : _state->elem_is_first = true;
256 : :
257 : 160 : return JSON_SUCCESS;
258 : : }
259 : :
260 : : static JsonParseErrorType
261 : 160 : do_object_end(void *state)
262 : : {
263 : 160 : DoState *_state = (DoState *) state;
264 : :
265 : 160 : printf("\n}\n");
266 : 160 : _state->elem_is_first = false;
267 : :
268 : 160 : return JSON_SUCCESS;
269 : : }
270 : :
271 : : static JsonParseErrorType
272 : 624 : do_object_field_start(void *state, char *fname, bool isnull)
273 : : {
274 : 624 : DoState *_state = (DoState *) state;
275 : :
276 [ + + ]: 624 : if (!_state->elem_is_first)
277 : 484 : printf(",\n");
278 : 624 : resetStringInfo(_state->buf);
279 : 624 : escape_json(_state->buf, fname);
280 : 624 : printf("%s: ", _state->buf->data);
281 : 624 : _state->elem_is_first = false;
282 : :
283 : 624 : return JSON_SUCCESS;
284 : : }
285 : :
286 : : static JsonParseErrorType
287 : 624 : do_object_field_end(void *state, char *fname, bool isnull)
288 : : {
335 289 [ + + ]: 624 : if (!lex_owns_tokens)
290 : 312 : free(fname);
291 : :
597 292 : 624 : return JSON_SUCCESS;
293 : : }
294 : :
295 : : static JsonParseErrorType
296 : 44 : do_array_start(void *state)
297 : : {
298 : 44 : DoState *_state = (DoState *) state;
299 : :
300 : 44 : printf("[\n");
301 : 44 : _state->elem_is_first = true;
302 : :
303 : 44 : return JSON_SUCCESS;
304 : : }
305 : :
306 : : static JsonParseErrorType
307 : 44 : do_array_end(void *state)
308 : : {
309 : 44 : DoState *_state = (DoState *) state;
310 : :
311 : 44 : printf("\n]\n");
312 : 44 : _state->elem_is_first = false;
313 : :
314 : 44 : return JSON_SUCCESS;
315 : : }
316 : :
317 : : static JsonParseErrorType
318 : 120 : do_array_element_start(void *state, bool isnull)
319 : : {
320 : 120 : DoState *_state = (DoState *) state;
321 : :
322 [ + + ]: 120 : if (!_state->elem_is_first)
323 : 76 : printf(",\n");
324 : 120 : _state->elem_is_first = false;
325 : :
326 : 120 : return JSON_SUCCESS;
327 : : }
328 : :
329 : : static JsonParseErrorType
330 : 120 : do_array_element_end(void *state, bool isnull)
331 : : {
332 : : /* nothing to do */
333 : :
334 : 120 : return JSON_SUCCESS;
335 : : }
336 : :
337 : : static JsonParseErrorType
338 : 544 : do_scalar(void *state, char *token, JsonTokenType tokentype)
339 : : {
340 : 544 : DoState *_state = (DoState *) state;
341 : :
342 [ + + ]: 544 : if (tokentype == JSON_TOKEN_STRING)
343 : : {
344 : 424 : resetStringInfo(_state->buf);
345 : 424 : escape_json(_state->buf, token);
346 : 424 : printf("%s", _state->buf->data);
347 : : }
348 : : else
349 : 120 : printf("%s", token);
350 : :
335 351 [ + + ]: 544 : if (!lex_owns_tokens)
352 : 272 : free(token);
353 : :
597 354 : 544 : return JSON_SUCCESS;
355 : : }
356 : :
357 : :
358 : : /* copied from backend code */
359 : : static void
360 : 1048 : escape_json(StringInfo buf, const char *str)
361 : : {
362 : : const char *p;
363 : :
364 [ - + ]: 1048 : appendStringInfoCharMacro(buf, '"');
365 [ + + ]: 16472 : for (p = str; *p; p++)
366 : : {
367 [ + + + + : 15424 : switch (*p)
+ - + + ]
368 : : {
369 : 4 : case '\b':
370 : 4 : appendStringInfoString(buf, "\\b");
371 : 4 : break;
372 : 4 : case '\f':
373 : 4 : appendStringInfoString(buf, "\\f");
374 : 4 : break;
375 : 4 : case '\n':
376 : 4 : appendStringInfoString(buf, "\\n");
377 : 4 : break;
378 : 4 : case '\r':
379 : 4 : appendStringInfoString(buf, "\\r");
380 : 4 : break;
381 : 4 : case '\t':
382 : 4 : appendStringInfoString(buf, "\\t");
383 : 4 : break;
597 andrew@dunslane.net 384 :UBC 0 : case '"':
385 : 0 : appendStringInfoString(buf, "\\\"");
386 : 0 : break;
597 andrew@dunslane.net 387 :CBC 4 : case '\\':
388 : 4 : appendStringInfoString(buf, "\\\\");
389 : 4 : break;
390 : 15400 : default:
391 [ + + ]: 15400 : if ((unsigned char) *p < ' ')
392 : 4 : appendStringInfo(buf, "\\u%04x", (int) *p);
393 : : else
394 [ - + ]: 15396 : appendStringInfoCharMacro(buf, *p);
395 : 15400 : break;
396 : : }
397 : : }
398 [ - + ]: 1048 : appendStringInfoCharMacro(buf, '"');
399 : 1048 : }
400 : :
401 : : static void
402 : 4 : usage(const char *progname)
403 : : {
404 : 4 : fprintf(stderr, "Usage: %s [OPTION ...] testfile\n", progname);
405 : 4 : fprintf(stderr, "Options:\n");
335 406 : 4 : fprintf(stderr, " -c chunksize size of piece fed to parser (default 64)\n");
407 : 4 : fprintf(stderr, " -o set JSONLEX_CTX_OWNS_TOKENS for leak checking\n");
597 408 : 4 : fprintf(stderr, " -s do semantic processing\n");
409 : :
410 : 4 : }
|