25 comments

[ 4.1 ms ] story [ 51.7 ms ] thread
What’s the usecase for something like this? There are lots of excellent libraries for json available. Is this a teaching tool?
What I love about this author's work is that they're usually single-file libraries in ANSI C or Lua with focused scope, easy-to-use interface, and good documentation. And free software license. Aside from the posted project, some I like are:

- log.c - A simple logging library implemented in C99

- microui - A tiny immediate-mode UI library

- fe - A tiny, embeddable language implemented in ANSI C

- microtar - A lightweight tar library written in ANSI C

- cembed - A small utility for embedding files in a C header

- ini - A tiny ANSI C library for loading .ini config files

- json.lua - A lightweight JSON library for Lua

- lite - A lightweight text editor written in Lua

- cmixer - Portable ANSI C audio mixer for games

- uuid4 - A tiny C library for generating uuid4 strings

JSON parser libraries in general is a black hole of suffering imo.

They're either written with a different use case in mind, or a complex mess of abstractions; often both.

It's not a very difficult problem to solve if you only write exactly what you need for your specific use case.

I can see one bug just glancing at the code - feeding a stray '}' at the top level can result in depth becoming negative
I wouldn’t rate those as very serious issues for this project. They’ll only be triggered if there are over MAX_INT lines or depth levels in the input. Yes, an attacker might be able to do that, but you’d have to put that input in a memory buffer to call this code. On many smaller systems, that will OOM.

Skimming the code, they also are loose in parsing incorrect json, it seems:

    static bool sj__is_number_cont(char c) {
        return (c >= '0' && c <= '9')
            ||  c == 'e' || c == 'E' || c == '.' || c == '-' || c == '+';
    }

    case '-': case '0': case '1': case '2': case '3': case '4':
    case '5': case '6': case '7': case '8': case '9':
        res.type = SJ_NUMBER;
        while (r->cur != r->end && sj__is_number_cont(*r->cur)) { r->cur++; }
        break;
that seems to imply it treats “00.-E.e-8..7-E7E12” as a valid json number.

    case '}': case ']':
        res.type = SJ_END;
        if (--r->depth < 0) {
            r->error = (*r->cur == '}') ? "stray '}'" : "stray ']'";
            goto top;
        }
        r->cur++;
        break;
I think that means the code finds [1,2} a valid array and {"foo": 42] a valid struct (maybe, it even is happy with [1,2,"foo":42})

Those, to me, seem a more likely attack vector. The example code, for example, calls atoi on something parsed by the first piece of code.

⇒ I only would use this for parsing json config files.

Being tiny is one thing, but the json grammar isn’t that complex. They could easily do a better job at this without adding zillions of lines of code.

Easy enough to fix:

  -sj_Reader sj_reader(char *data, size_t len) {
  +sj_Reader sj_reader(char *data, int len) {
Not everyone needs to waste cycles on supporting JSON files larger than 2^31-1.
this is really nice. i also _must_ use it because my initials are S.J H.. :').

on the more code side, love this, been looking to implement a simple json parser for some projects but this is small enough i can study it and either learn what i need or even use it. lovely!

It feels like a stretch to call this a parser. It’s looks like a typical lexer?
It looks like it's kinda half a parser in that it doesn't turn numbers into floats or ints, so you'd have to build that on top of it.

Im still impressed and might use it, but just noting this.

Ah a Tsoding special, I'll have to find the livestream where he implemented it, and with his funny laugh of "ohohohoho".
I really enjoy these simple libraries, even though they are too flawed to be used for anything serious. There's great freedom in just drilling down to the basics, ignoring all the complexities, and just writing code that'll probably work most of the time.
This is rather lenient. There's not anything wrong with that (although perhaps it should be noted for people that will use it without looking at the code), but it's the main reason this can be so small. Using their demo in the readme:

    {"x",10eee"y"22:5,{[:::,,}]"w"7"h"33
    rect: { 10, 22, 7, 33 }
A recommendation: we use rxi's json.lua at Planimeter, but upon benchmarking JSON implementations across all major languages, it was found to be the slowest by an order of magnitude when reading very large numbers of JSON files.

So if you can, try and at least use LuaJIT, which when using json.lua seems to bring it back down into range with other performant languages, or jump down into LuaJIT and use Sj.h there, through the C FFI or just simdjson.

json.lua is great for when you're restricted in some ways to use a pure Lua implementation, though. It's the de facto solution.

I see this being useful for embedded/micro-controller use, very nice!
I believe json requires unicode...
As far as I can see, the library is encoding agnostic (eg ASCII, Extended ASCII and UTF-8 should all work fine - and UTF-8 is the defacto standard UNICODE encoding since the late 1990s.
What is the purpose of declaring an sj_Reader object with an sj_read method if you don't support lazy parsing? Seems like you could tighten up the code by combining the declaration and parsing into one subroutine.
With JSON being so simple, how do you even manage to create a library which accepts so many invalid documents