LCOV - code coverage report
Current view: top level - src/test/modules/test_json_parser - test_json_parser_incremental.c (source / functions) Hit Total Coverage
Test: PostgreSQL 19devel Lines: 162 170 95.3 %
Date: 2025-10-10 18:17:38 Functions: 12 12 100.0 %
Legend: Lines: hit not hit

          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 : }

Generated by: LCOV version 1.16