/* Lex JSON files. * See RFC 8259 (which obsoletes RFC 7159) * * The goal here is to parse valid JSON correctly; * we don't try terribly hard to deal with pathologically invalid input, * and support no extensions. * * We assume the input is UTF-8: this is required by RFC 8259 (but wasn't before). * * This is a very simple lexer. The only intricate bit is the parsing of \uXXXX escapes. * * All of the lvalues are s7 objects, not protected from garbage-collection. * * See also the notes at the top of parse-json.y * * This file is part of Beastie * SPDX-FileCopyrightText: 2025 Norman Gray * SPDX-License-Identifier: BSD-2-Clause */ %top{ #if __GNUC__ // for fileno #define _XOPEN_SOURCE 600 #endif } %{ #include #include #include "beastie.h" #include "util.h" #include "parse-json.h" #include "parse-json.tab.h" #include "unicode.h" #include "unicode-scm.h" #ifndef WITH_MAIN #define WITH_MAIN 0 #endif /* We can also define this as #define ERROR "sprintf" * to get the error returned as the bad_lexeme lvalue, * but not printed */ #define ERROR "print-warning" %} %option prefix="json" reentrant noyywrap nounput noinput yylineno bison-bridge bison-locations %option extra-type="json_extra_t" /* RFC 8259, Sect.6: number = [ minus ] int [ frac ] [ exp ] decimal-point = %x2E ; . digit1-9 = %x31-39 ; 1-9 e = %x65 / %x45 ; e E exp = e [ minus / plus ] 1*DIGIT frac = decimal-point 1*DIGIT int = zero / ( digit1-9 *DIGIT ) minus = %x2D ; - plus = %x2B ; + zero = %x30 ; 0 */ NUMBER [-]?(0|[1-9][0-9]*)(\.[0-9]+)?([eE][+-]?[0-9]+)? /* OWS isn't [[:space:]]* because the RFC (Sect.2) says that whitespace is _only_ these four characters */ OWS [ \t\n\r]* /* this pattern matches and includes \" escapes inside the string */ STRINGCONTENT [^"]*(\\\"[^"]*)* %% {OWS}"\""{STRINGCONTENT}"\""? { ustring_t us = make_ustring(NULL); const unsigned char* p = (const unsigned char*)yytext; // skip leading whitespace while (*p != '"') p++; p++; // step over opening quote uint16_t surrogate1 = 0; // holds the first of a pair of (escaped) surrogates int good_scan_p = 1; int scanning_p = 1; while (scanning_p) { if (*p == '\0') { // unterminated string *yylval = scheme_eval(ERROR, s7_make_string(S7, "unterminated string in input: ~s"), s7_make_string(S7, yytext), NULL); good_scan_p = scanning_p = 0; } else if (*p == '"') { scanning_p = 0; } else if (*p < 0x20) { // "control characters" aren't allowed in JSON strings unescaped *yylval = scheme_eval(ERROR, s7_make_string(S7, "control character 0x~x in string at ~a:~a"), s7_make_integer(S7, *p), s7_make_string(S7, (yyextra->path == NULL ? ".json" : yyextra->path)), s7_make_integer(S7, yyget_lineno(yyscanner)), NULL); good_scan_p = scanning_p = 0; } else if ((unsigned char)*p >= 0x80) { // We're assuming this is UTF-8 unsigned char nused; const char* errmsg; int ofs = p -(const unsigned char*)yytext; codepoint_t cp = decode_utf8(p, yyleng - ofs, &nused, &errmsg); if (cp == UNICODE_BAD_DECODE) { *yylval = scheme_eval(ERROR, s7_make_string(S7, "invalid UTF-8 at ~a:~a: ~a"), s7_make_string(S7, (yyextra->path == NULL ? ".json" : yyextra->path)), s7_make_integer(S7, yyget_lineno(yyscanner)), s7_make_string(S7, errmsg), NULL); good_scan_p = scanning_p = 0; } else { ustring_append_cp(us, cp, NULL); p += nused-1; // -1 because we p++ below } } else if (*p == '\\') { p++; switch (*p) { case '"': ustring_append_cp(us, '"', NULL); break; case '\\': ustring_append_cp(us, '\\', NULL); break; case '/': ustring_append_cp(us, '/', NULL); break; case 'b': ustring_append_cp(us, '\b', NULL); break; case 'f': ustring_append_cp(us, '\f', NULL); break; case 'n': ustring_append_cp(us, '\n', NULL); break; case 'r': ustring_append_cp(us, '\r', NULL); break; case 't': ustring_append_cp(us, '\t', NULL); break; case 'u': { // Unicode escape: \uXXXX, containing exactly four hex digits. // We do not tolerate fewer, // and we're careful to examine at most four. // The hex-digits can be in either case (which // matches the behaviour of isxdigit(3)). char buf[5]; int i; for (i=0; i<4; i++) { int c0 = *++p; if (c0 == 0) { // unexpected end of string, // but let this be reported as a short escape scanning_p = 0; break; } else if (isxdigit(c0)) { buf[i] = c0; } else { break; } } buf[i] = '\0'; if (i < 4) { // it would not unreasonable to make this a warning, // rather than an error *yylval = scheme_eval(ERROR, s7_make_string(S7, "short Unicode escape \\u~a in ~a:~a"), s7_make_string(S7, buf), s7_make_string(S7, (yyextra->path == NULL ? ".json" : yyextra->path)), s7_make_integer(S7, yyget_lineno(yyscanner)), NULL); good_scan_p = scanning_p = 0; break; // out of switch, and so soon out of while(scanning_p) } // this number can't be bigger than 0xffff, // since it's formed from four hex digits, // so it's safe to assign it to codepoint_t codepoint_t cp = (codepoint_t)strtol(buf, NULL, 16); if (is_surrogate(cp)) { // RFC 8259: a non-BMP character is // represented by two escapes forming a surrogate pair. // Example: // // So, for example, a string containing only the G clef // character (U+1D11E) may be represented as "\uD834\uDD1E". if (surrogate1 != 0) { // this should be the second surrogate pair -- good uint16_t surrogates[2] = {surrogate1, cp}; cp = from_surrogate(&surrogates[0], NULL); if (cp == UNICODE_BAD_DECODE) { *yylval = scheme_eval(ERROR, s7_make_string(S7, "bad surrogate pair in escape \\u~a, at ~a:~a"), s7_make_string(S7, buf), s7_make_string(S7, (yyextra->path == NULL ? ".json" : yyextra->path)), s7_make_integer(S7, yyget_lineno(yyscanner)), NULL); good_scan_p = scanning_p = 0; } else { ustring_append_cp(us, cp, NULL); } surrogate1 = 0; } else { // this is the first surrogate pair -- another is to follow surrogate1 = cp; } } else if (surrogate1) { *yylval = scheme_eval(ERROR, s7_make_string(S7, "bad surrogates in \\u~a: second missing, at ~a:~a"), s7_make_string(S7, buf), s7_make_string(S7, (yyextra->path == NULL ? ".json" : yyextra->path)), s7_make_integer(S7, yyget_lineno(yyscanner)), NULL); good_scan_p = scanning_p = 0; surrogate1 = 0; } else { ustring_append_cp(us, cp, NULL); } } break; default: *yylval = scheme_eval(ERROR, s7_make_string(S7, "unexpected escape \\~a at ~a:~a"), s7_make_character(S7, *p), s7_make_string(S7, (yyextra->path == NULL ? ".json" : yyextra->path)), s7_make_integer(S7, yyget_lineno(yyscanner)), NULL); good_scan_p = scanning_p = 0; break; } } else { // an ordinary character if (surrogate1 != 0) { // this is a non-surrogate following a surrogate *yylval = scheme_eval(ERROR, s7_make_string(S7, "bad surrogate pair in ~a:~a: surrogate U+~x followed by non-surrogate '~a'"), s7_make_string(S7, (yyextra->path == NULL ? ".json" : yyextra->path)), s7_make_integer(S7, yyget_lineno(yyscanner)), s7_make_integer(S7, surrogate1), s7_make_character(S7, *p), NULL); good_scan_p = scanning_p = 0; surrogate1 = 0; // discard the most recent character, // rather than accepting only one of a putative pair } else { ustring_append_cp(us, *p, NULL); } } p++; } if (good_scan_p) { *yylval = make_ustring_obj(S7, us); return STRING; } else { // yylval has been assigned return BAD_LEXEME; } } {OWS}{NUMBER}{OWS} { s7_double n = strtod(yytext, NULL); *yylval = s7_make_real(S7, n); return NUMBER; } {OWS}"{"{OWS} return '{'; {OWS}"}"{OWS} return '}'; {OWS}"["{OWS} return '['; {OWS}"]"{OWS} return ']'; {OWS}","{OWS} return ','; {OWS}":"{OWS} return ':'; /* RFC, Sect.3: ‘The literal names [false/true/null] MUST be lowercase. No other literal names are allowed.’ */ {OWS}"false"{OWS} *yylval = s7_f(S7); return JSON_FALSE; {OWS}"true"{OWS} *yylval = s7_t(S7); return JSON_TRUE; {OWS}"null"{OWS} *yylval = s7_nil(S7); return JSON_NULL; <> { yyterminate(); } . { #define OUTBUFLEN 256 char buf[OUTBUFLEN]; size_t stringsize = snprintf(buf, OUTBUFLEN, "Unexpected character '%c' at line %d of %s", yytext[0], yyget_lineno(yyscanner), (yyextra->path == NULL ? "" : yyextra->path)); if (stringsize > OUTBUFLEN) { // unlikely, but... memcpy(&buf[OUTBUFLEN-4], "...", 3); } *yylval = scheme_eval(ERROR, s7_make_string(S7, "error: ~a~%"), s7_make_string(S7, buf), NULL); return BAD_LEXEME; #undef OUTBUFLEN } %% /* For debugging/test purposes, * we also want to be able to parse .json files read from a string. * PATH==NULL means stdin; return NULL on error. */ yyscan_t parse_json_setup_file(json_extra_t extra, const char* path) { FILE* f = NULL; if (path != NULL) { f = fopen(path, "r"); if (f == NULL) return NULL; // JUMP OUT } yyscan_t scanner; yylex_init_extra(extra, &scanner); extra->yyscanbuf = 0; if (path == NULL) { extra->path = NULL; } else { extra->path = strdup(path); } extra->f = f; if (f != NULL) { yyset_in(f, scanner); } return scanner; } yyscan_t parse_json_setup_string(json_extra_t extra, const char* s) { yyscan_t scanner; yylex_init_extra(extra, &scanner); extra->yyscanbuf = yy_scan_string(s, scanner); const int maxchars = 20; // maximum number of chars shown from s const char* fmt; if (strlen(s) < maxchars) { fmt = ""; } else { fmt = ""; } //const char fmt[] = ""; const size_t totlen = maxchars + strlen(fmt); char* source_legend = malloc(totlen+1); snprintf(source_legend, totlen, fmt, maxchars, s); extra->path = source_legend; extra->f = NULL; yyset_debug(1, scanner); yyset_lineno(1, scanner); return scanner; } void parse_json_finish(json_extra_t extra, yyscan_t scanner) { if (extra->yyscanbuf) { yy_delete_buffer((YY_BUFFER_STATE)extra->yyscanbuf, scanner); extra->yyscanbuf = NULL; } if (extra->path) free(extra->path); if (extra->f) fclose(extra->f); yylex_destroy(scanner); } #if WITH_MAIN #include #include #include "s7.h" #include "util.h" YYSTYPE one_value; YYLTYPE locp; s7_scheme* S7; static void display_lexemes(yyscan_t scanner) { int l; int eof_p = 0; while (!eof_p && (l = jsonlex(&one_value, &locp, scanner)) != 0) { printf("l=%d\n", l); switch (l) { case '{': case '}': case '[': case ']': case ':': case ',': printf("%c\n", l); break; case JSON_FALSE: printf("false\n"); break; case JSON_NULL: printf("null\n"); break; case JSON_TRUE: printf("true\n"); break; case STRING: s7w("string(", one_value, ")\n"); break; case NUMBER: s7w("number(", one_value, ")\n"); break; case BAD_LEXEME: printf("** bad lexeme\n"); break; default: printf("Unexpected lexeme: %d\n", l); } } } const char* progname; void Usage(int exitstatus) { fprintf(stderr, "Usage: %s [-s] [foo.json]\n", progname); exit(exitstatus); } int main(int argc, char** argv) { const char* jsonin = NULL; char input_is_file_p = 1; progname = argv[0]; char optchar; while ((optchar = getopt(argc, argv, "hs")) >= 0) { switch (optchar) { case 's': input_is_file_p = 0; break; case 'h': Usage(0); default: Usage(1); } } argc -= optind; argv += optind; if (argc == 0) { if (! input_is_file_p) { fprintf(stderr, "With -s, an argument must be provided\n"); exit(1); } } else { jsonin = argv[0]; } S7 = s7_init(); s7_eval_c_string(S7, "(define (beastie-error fmt . args)\n" " (apply format\n" " (cons (current-error-port) (cons (string-append \"error: \" fmt \"~%\") args))))"); s7_eval_c_string(S7, "(define (print-warning fmt . args)\n" " (apply format\n" " (cons (current-error-port) (cons (string-append \"warning: \" fmt \"~%\") args))))"); unicode_load_hook(S7, s7_nil(S7)); yyscan_t scanner; struct json_extra_s S; if (input_is_file_p) { scanner = parse_json_setup_file(&S, jsonin); if (scanner == NULL) { fprintf(stderr, "%s: can't open file %s to read\n", progname, (jsonin == NULL ? "" : jsonin)); exit(1); } } else { scanner = parse_json_setup_string(&S, jsonin); if (scanner == NULL) { fprintf(stderr, "%s: can't open input string!\n", progname); exit(1); } } display_lexemes(scanner); parse_json_finish(&S, scanner); } #endif