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 : : * If the "-c SIZE" option is provided, that chunk size is used instead
16 : : * of the default of 60.
17 : : *
18 : : * If the -s flag is given, the program does semantic processing. This should
19 : : * just mirror back the json, albeit with white space changes.
20 : : *
21 : : * If the -o flag is given, the JSONLEX_CTX_OWNS_TOKENS flag is set. (This can
22 : : * be used in combination with a leak sanitizer; without the option, the parser
23 : : * may leak memory with invalid JSON.)
24 : : *
25 : : * The argument specifies the file containing the JSON input.
26 : : *
27 : : *-------------------------------------------------------------------------
28 : : */
29 : :
30 : : #include "postgres_fe.h"
31 : :
32 : : #include <stdio.h>
33 : : #include <sys/types.h>
34 : : #include <sys/stat.h>
35 : : #include <unistd.h>
36 : :
37 : : #include "common/jsonapi.h"
38 : : #include "common/logging.h"
39 : : #include "lib/stringinfo.h"
40 : : #include "mb/pg_wchar.h"
41 : : #include "pg_getopt.h"
42 : :
43 : : #define BUFSIZE 6000
44 : : #define DEFAULT_CHUNK_SIZE 60
45 : :
46 : : typedef struct DoState
47 : : {
48 : : JsonLexContext *lex;
49 : : bool elem_is_first;
50 : : StringInfo buf;
51 : : } DoState;
52 : :
53 : : static void usage(const char *progname);
54 : : static void escape_json(StringInfo buf, const char *str);
55 : :
56 : : /* semantic action functions for parser */
57 : : static JsonParseErrorType do_object_start(void *state);
58 : : static JsonParseErrorType do_object_end(void *state);
59 : : static JsonParseErrorType do_object_field_start(void *state, char *fname, bool isnull);
60 : : static JsonParseErrorType do_object_field_end(void *state, char *fname, bool isnull);
61 : : static JsonParseErrorType do_array_start(void *state);
62 : : static JsonParseErrorType do_array_end(void *state);
63 : : static JsonParseErrorType do_array_element_start(void *state, bool isnull);
64 : : static JsonParseErrorType do_array_element_end(void *state, bool isnull);
65 : : static JsonParseErrorType do_scalar(void *state, char *token, JsonTokenType tokentype);
66 : :
67 : : static JsonSemAction sem = {
68 : : .object_start = do_object_start,
69 : : .object_end = do_object_end,
70 : : .object_field_start = do_object_field_start,
71 : : .object_field_end = do_object_field_end,
72 : : .array_start = do_array_start,
73 : : .array_end = do_array_end,
74 : : .array_element_start = do_array_element_start,
75 : : .array_element_end = do_array_element_end,
76 : : .scalar = do_scalar
77 : : };
78 : :
79 : : static bool lex_owns_tokens = false;
80 : :
81 : : int
545 andrew@dunslane.net 82 :CBC 1964 : main(int argc, char **argv)
83 : : {
84 : : char buff[BUFSIZE];
85 : : FILE *json_file;
86 : : JsonParseErrorType result;
87 : : JsonLexContext *lex;
88 : : StringInfoData json;
89 : : int n_read;
515 90 : 1964 : size_t chunk_size = DEFAULT_CHUNK_SIZE;
91 : : struct stat statbuf;
92 : : off_t bytes_left;
396 heikki.linnakangas@i 93 : 1964 : const JsonSemAction *testsem = &nullSemAction;
94 : : char *testfile;
95 : : int c;
545 andrew@dunslane.net 96 : 1964 : bool need_strings = false;
283 97 : 1964 : int ret = 0;
98 : :
500 99 : 1964 : pg_logging_init(argv[0]);
100 : :
136 dgustafsson@postgres 101 : 1964 : lex = calloc(1, sizeof(JsonLexContext));
102 [ - + ]: 1964 : if (!lex)
136 dgustafsson@postgres 103 :UBC 0 : pg_fatal("out of memory");
104 : :
283 andrew@dunslane.net 105 [ + + ]:CBC 6874 : while ((c = getopt(argc, argv, "c:os")) != -1)
106 : : {
545 107 [ - + + + ]: 2946 : switch (c)
108 : : {
109 : 1960 : case 'c': /* chunksize */
447 noah@leadboat.com 110 : 1960 : chunk_size = strtou64(optarg, NULL, 10);
515 andrew@dunslane.net 111 [ - + ]: 1960 : if (chunk_size > BUFSIZE)
501 andrew@dunslane.net 112 :UBC 0 : pg_fatal("chunk size cannot exceed %d", BUFSIZE);
545 andrew@dunslane.net 113 :CBC 1960 : break;
283 114 : 982 : case 'o': /* switch token ownership */
115 : 982 : lex_owns_tokens = true;
116 : 982 : break;
545 117 : 4 : case 's': /* do semantic processing */
118 : 4 : testsem = &sem;
119 : 4 : sem.semstate = palloc(sizeof(struct DoState));
136 dgustafsson@postgres 120 : 4 : ((struct DoState *) sem.semstate)->lex = lex;
545 andrew@dunslane.net 121 : 4 : ((struct DoState *) sem.semstate)->buf = makeStringInfo();
122 : 4 : need_strings = true;
123 : 4 : break;
124 : : }
125 : : }
126 : :
127 [ + + ]: 1964 : if (optind < argc)
128 : : {
283 129 : 1960 : testfile = argv[optind];
545 130 : 1960 : optind++;
131 : : }
132 : : else
133 : : {
134 : 4 : usage(argv[0]);
135 : 4 : exit(1);
136 : : }
137 : :
136 dgustafsson@postgres 138 : 1960 : makeJsonLexContextIncremental(lex, PG_UTF8, need_strings);
139 : 1960 : setJsonLexContextOwnsTokens(lex, lex_owns_tokens);
545 andrew@dunslane.net 140 : 1960 : initStringInfo(&json);
141 : :
424 142 [ - + ]: 1960 : if ((json_file = fopen(testfile, PG_BINARY_R)) == NULL)
501 andrew@dunslane.net 143 :UBC 0 : pg_fatal("error opening input: %m");
144 : :
501 andrew@dunslane.net 145 [ - + ]:CBC 1960 : if (fstat(fileno(json_file), &statbuf) != 0)
501 andrew@dunslane.net 146 :UBC 0 : pg_fatal("error statting input: %m");
147 : :
545 andrew@dunslane.net 148 :CBC 1960 : bytes_left = statbuf.st_size;
149 : :
150 : : for (;;)
151 : : {
152 : : /* We will break when there's nothing left to read */
153 : :
501 154 [ + + ]: 372708 : if (bytes_left < chunk_size)
155 : 1312 : chunk_size = bytes_left;
156 : :
545 157 : 372708 : n_read = fread(buff, 1, chunk_size, json_file);
501 158 [ - + ]: 372708 : if (n_read < chunk_size)
501 andrew@dunslane.net 159 :UBC 0 : pg_fatal("error reading input file: %d", ferror(json_file));
160 : :
545 andrew@dunslane.net 161 :CBC 372708 : appendBinaryStringInfo(&json, buff, n_read);
162 : :
163 : : /*
164 : : * Append some trailing junk to the buffer passed to the parser. This
165 : : * helps us ensure that the parser does the right thing even if the
166 : : * chunk isn't terminated with a '\0'.
167 : : */
168 : 372708 : appendStringInfoString(&json, "1+23 trailing junk");
169 : 372708 : bytes_left -= n_read;
170 [ + + ]: 372708 : if (bytes_left > 0)
171 : : {
136 dgustafsson@postgres 172 : 370920 : result = pg_parse_json_incremental(lex, testsem,
545 andrew@dunslane.net 173 : 370920 : json.data, n_read,
174 : : false);
175 [ + + ]: 370920 : if (result != JSON_INCOMPLETE)
176 : : {
136 dgustafsson@postgres 177 : 172 : fprintf(stderr, "%s\n", json_errdetail(result, lex));
283 andrew@dunslane.net 178 : 172 : ret = 1;
179 : 172 : goto cleanup;
180 : : }
545 181 : 370748 : resetStringInfo(&json);
182 : : }
183 : : else
184 : : {
136 dgustafsson@postgres 185 : 1788 : result = pg_parse_json_incremental(lex, testsem,
545 andrew@dunslane.net 186 : 1788 : json.data, n_read,
187 : : true);
188 [ + + ]: 1788 : if (result != JSON_SUCCESS)
189 : : {
136 dgustafsson@postgres 190 : 780 : fprintf(stderr, "%s\n", json_errdetail(result, lex));
283 andrew@dunslane.net 191 : 780 : ret = 1;
192 : 780 : goto cleanup;
193 : : }
545 194 [ + + ]: 1008 : if (!need_strings)
195 : 1004 : printf("SUCCESS!\n");
196 : 1008 : break;
197 : : }
198 : : }
199 : :
283 200 : 1960 : cleanup:
545 201 : 1960 : fclose(json_file);
136 dgustafsson@postgres 202 : 1960 : freeJsonLexContext(lex);
283 andrew@dunslane.net 203 : 1960 : free(json.data);
136 dgustafsson@postgres 204 : 1960 : free(lex);
205 : :
283 andrew@dunslane.net 206 : 1960 : return ret;
207 : : }
208 : :
209 : : /*
210 : : * The semantic routines here essentially just output the same json, except
211 : : * for white space. We could pretty print it but there's no need for our
212 : : * purposes. The result should be able to be fed to any JSON processor
213 : : * such as jq for validation.
214 : : */
215 : :
216 : : static JsonParseErrorType
545 217 : 160 : do_object_start(void *state)
218 : : {
219 : 160 : DoState *_state = (DoState *) state;
220 : :
221 : 160 : printf("{\n");
222 : 160 : _state->elem_is_first = true;
223 : :
224 : 160 : return JSON_SUCCESS;
225 : : }
226 : :
227 : : static JsonParseErrorType
228 : 160 : do_object_end(void *state)
229 : : {
230 : 160 : DoState *_state = (DoState *) state;
231 : :
232 : 160 : printf("\n}\n");
233 : 160 : _state->elem_is_first = false;
234 : :
235 : 160 : return JSON_SUCCESS;
236 : : }
237 : :
238 : : static JsonParseErrorType
239 : 624 : do_object_field_start(void *state, char *fname, bool isnull)
240 : : {
241 : 624 : DoState *_state = (DoState *) state;
242 : :
243 [ + + ]: 624 : if (!_state->elem_is_first)
244 : 484 : printf(",\n");
245 : 624 : resetStringInfo(_state->buf);
246 : 624 : escape_json(_state->buf, fname);
247 : 624 : printf("%s: ", _state->buf->data);
248 : 624 : _state->elem_is_first = false;
249 : :
250 : 624 : return JSON_SUCCESS;
251 : : }
252 : :
253 : : static JsonParseErrorType
254 : 624 : do_object_field_end(void *state, char *fname, bool isnull)
255 : : {
283 256 [ + + ]: 624 : if (!lex_owns_tokens)
257 : 312 : free(fname);
258 : :
545 259 : 624 : return JSON_SUCCESS;
260 : : }
261 : :
262 : : static JsonParseErrorType
263 : 44 : do_array_start(void *state)
264 : : {
265 : 44 : DoState *_state = (DoState *) state;
266 : :
267 : 44 : printf("[\n");
268 : 44 : _state->elem_is_first = true;
269 : :
270 : 44 : return JSON_SUCCESS;
271 : : }
272 : :
273 : : static JsonParseErrorType
274 : 44 : do_array_end(void *state)
275 : : {
276 : 44 : DoState *_state = (DoState *) state;
277 : :
278 : 44 : printf("\n]\n");
279 : 44 : _state->elem_is_first = false;
280 : :
281 : 44 : return JSON_SUCCESS;
282 : : }
283 : :
284 : : static JsonParseErrorType
285 : 120 : do_array_element_start(void *state, bool isnull)
286 : : {
287 : 120 : DoState *_state = (DoState *) state;
288 : :
289 [ + + ]: 120 : if (!_state->elem_is_first)
290 : 76 : printf(",\n");
291 : 120 : _state->elem_is_first = false;
292 : :
293 : 120 : return JSON_SUCCESS;
294 : : }
295 : :
296 : : static JsonParseErrorType
297 : 120 : do_array_element_end(void *state, bool isnull)
298 : : {
299 : : /* nothing to do */
300 : :
301 : 120 : return JSON_SUCCESS;
302 : : }
303 : :
304 : : static JsonParseErrorType
305 : 544 : do_scalar(void *state, char *token, JsonTokenType tokentype)
306 : : {
307 : 544 : DoState *_state = (DoState *) state;
308 : :
309 [ + + ]: 544 : if (tokentype == JSON_TOKEN_STRING)
310 : : {
311 : 424 : resetStringInfo(_state->buf);
312 : 424 : escape_json(_state->buf, token);
313 : 424 : printf("%s", _state->buf->data);
314 : : }
315 : : else
316 : 120 : printf("%s", token);
317 : :
283 318 [ + + ]: 544 : if (!lex_owns_tokens)
319 : 272 : free(token);
320 : :
545 321 : 544 : return JSON_SUCCESS;
322 : : }
323 : :
324 : :
325 : : /* copied from backend code */
326 : : static void
327 : 1048 : escape_json(StringInfo buf, const char *str)
328 : : {
329 : : const char *p;
330 : :
331 [ - + ]: 1048 : appendStringInfoCharMacro(buf, '"');
332 [ + + ]: 16472 : for (p = str; *p; p++)
333 : : {
334 [ + + + + : 15424 : switch (*p)
+ - + + ]
335 : : {
336 : 4 : case '\b':
337 : 4 : appendStringInfoString(buf, "\\b");
338 : 4 : break;
339 : 4 : case '\f':
340 : 4 : appendStringInfoString(buf, "\\f");
341 : 4 : break;
342 : 4 : case '\n':
343 : 4 : appendStringInfoString(buf, "\\n");
344 : 4 : break;
345 : 4 : case '\r':
346 : 4 : appendStringInfoString(buf, "\\r");
347 : 4 : break;
348 : 4 : case '\t':
349 : 4 : appendStringInfoString(buf, "\\t");
350 : 4 : break;
545 andrew@dunslane.net 351 :UBC 0 : case '"':
352 : 0 : appendStringInfoString(buf, "\\\"");
353 : 0 : break;
545 andrew@dunslane.net 354 :CBC 4 : case '\\':
355 : 4 : appendStringInfoString(buf, "\\\\");
356 : 4 : break;
357 : 15400 : default:
358 [ + + ]: 15400 : if ((unsigned char) *p < ' ')
359 : 4 : appendStringInfo(buf, "\\u%04x", (int) *p);
360 : : else
361 [ - + ]: 15396 : appendStringInfoCharMacro(buf, *p);
362 : 15400 : break;
363 : : }
364 : : }
365 [ - + ]: 1048 : appendStringInfoCharMacro(buf, '"');
366 : 1048 : }
367 : :
368 : : static void
369 : 4 : usage(const char *progname)
370 : : {
371 : 4 : fprintf(stderr, "Usage: %s [OPTION ...] testfile\n", progname);
372 : 4 : fprintf(stderr, "Options:\n");
283 373 : 4 : fprintf(stderr, " -c chunksize size of piece fed to parser (default 64)\n");
374 : 4 : fprintf(stderr, " -o set JSONLEX_CTX_OWNS_TOKENS for leak checking\n");
545 375 : 4 : fprintf(stderr, " -s do semantic processing\n");
376 : :
377 : 4 : }
|