mirror of
https://github.com/itsPLK/ps5-payload-manager.git
synced 2026-10-19 02:19:53 -05:00
fix: bound JSON extraction and decode Unicode escapes
This commit is contained in:
1 parent
0d39bd5cf4
commit
3c42e3cf0e
13 files changed
+690
-83
No files matched your search
@@ -15,10 +15,8 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Build host-test environment
|
||||
run: docker build -f tests/Dockerfile -t pldmgr-tests .
|
||||
- name: Test HTTP responses with sanitizers and real sockets
|
||||
run: docker run --rm --network=none -v "$GITHUB_WORKSPACE:/src:ro" pldmgr-tests
|
||||
- name: Test JSON encoding with sanitizers and an independent decoder
|
||||
run: docker run --rm --network=none -v "$GITHUB_WORKSPACE:/src:ro" pldmgr-tests python3 tests/run_json_escape_tests.py
|
||||
- name: Test backend JSON and HTTP with sanitizers and real sockets
|
||||
run: docker run --rm --network=none -v "$GITHUB_WORKSPACE:/src:ro" pldmgr-tests make test-host
|
||||
|
||||
frontend:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
+16
-7
@@ -49,7 +49,7 @@ are enabled by default. Test files, storage paths, and binaries are temporary;
|
||||
the suite does not connect to a console or launch payloads.
|
||||
|
||||
On a host with Clang, its sanitizer runtime, Python 3, pkg-config, and
|
||||
libmicrohttpd development files installed:
|
||||
libmicrohttpd/libcurl development files installed:
|
||||
|
||||
```bash
|
||||
make test-host
|
||||
@@ -60,9 +60,7 @@ The container option builds libmicrohttpd **1.0.1**, matching `build_deps.sh`:
|
||||
|
||||
```bash
|
||||
docker build -f tests/Dockerfile -t ps5-payload-manager-tests .
|
||||
docker run --rm --network=none -v "$(pwd):/src:ro" ps5-payload-manager-tests
|
||||
docker run --rm --network=none -v "$(pwd):/src:ro" ps5-payload-manager-tests \
|
||||
python3 tests/run_json_escape_tests.py
|
||||
docker run --rm --network=none -v "$(pwd):/src:ro" ps5-payload-manager-tests make test-host
|
||||
```
|
||||
|
||||
The build downloads test dependencies; the test container itself has external
|
||||
@@ -74,10 +72,21 @@ The JSON encoder suite compiles actual `src/utils.c` and checks its output with
|
||||
Python's independent JSON and UTF-8 decoders. It covers Unicode, all non-NUL
|
||||
control bytes, invalid UTF-8 replacement, every small escape/scalar boundary,
|
||||
truncation status, and guarded destination bounds, with ASan/UBSan enabled.
|
||||
C strings cannot represent embedded NUL; escaped-NUL decoding belongs to the
|
||||
separate decoder contract. `make test-host` runs both host suites.
|
||||
C strings cannot represent embedded NUL. The decoder rejects decoded NUL in the
|
||||
requested value, while accepting valid escaped NUL in unrelated fields.
|
||||
|
||||
`.github/workflows/verify.yml` runs both host suites, existing frontend tests,
|
||||
The decoder suite passes exact, non-NUL-terminated buffers to actual
|
||||
`src/json_helpers.c`; it checks bounds, direct-member selection, all escapes,
|
||||
UTF-8, surrogate pairs, malformed input, and strict/prefix overflow contracts.
|
||||
Objects are validated completely with a nesting limit of 64. Strict extraction
|
||||
clears the result on overflow; descriptions/release notes explicitly opt into
|
||||
UTF-8-safe prefixes. The backend suite uses real repository, source save/load,
|
||||
and slot metadata read/write code inside temporary storage. It parses their
|
||||
results independently with Python. Unused network/PS5 code is discarded at
|
||||
link time, and no download or payload launch occurs. `make test-host` runs all
|
||||
four host suites with sanitizers.
|
||||
|
||||
`.github/workflows/verify.yml` runs all host suites, existing frontend tests,
|
||||
and frontend build without publishing or deploying. Frontend lint still reports existing errors. The host suite tests response
|
||||
ownership/status handling, request cleanup, and autoload-list bounds. Storage
|
||||
transactions, autoload worker concurrency, and PS5 firmware behavior remain
|
||||
|
||||
@@ -51,6 +51,8 @@ all: $(ELF)
|
||||
.PHONY: test-host test-frontend test
|
||||
test-host:
|
||||
HOST_CC="$(HOST_CC)" $(PYTHON) tests/run_json_escape_tests.py
|
||||
HOST_CC="$(HOST_CC)" $(PYTHON) tests/run_json_decode_tests.py
|
||||
HOST_CC="$(HOST_CC)" $(PYTHON) tests/run_json_backend_tests.py
|
||||
HOST_CC="$(HOST_CC)" $(PYTHON) tests/run_http_tests.py
|
||||
|
||||
test-frontend:
|
||||
|
||||
+22
-2
@@ -14,11 +14,31 @@ typedef struct JsonListBuilder {
|
||||
/* Append formatted text to the builder. Returns 0 on success, -1 on overflow. */
|
||||
int json_append(JsonListBuilder *jb, const char *fmt, ...);
|
||||
|
||||
/* Extract a quoted string value for a given key from a JSON object bounded by
|
||||
* [obj_start, obj_end). Returns 0 on success, -1 if key not found. */
|
||||
enum {
|
||||
JSON_STRING_OK = 0,
|
||||
JSON_STRING_TRUNCATED = 1,
|
||||
JSON_STRING_MISSING = -1,
|
||||
JSON_STRING_INVALID = -2,
|
||||
JSON_STRING_TOO_LONG = -3
|
||||
};
|
||||
|
||||
/* Extract a direct member of one complete object in [obj_start, obj_end).
|
||||
* Decode all JSON escapes/Unicode, validating the entire object (max depth 64).
|
||||
* Duplicate requested keys, non-string values, invalid UTF-8, and decoded NUL
|
||||
* in the requested value are invalid. Errors clear out when capacity is nonzero.
|
||||
* Strict extraction never returns a partial identifier/URL/checksum. */
|
||||
int json_extract_string(const char *obj_start, const char *obj_end,
|
||||
const char *key, char *out, size_t out_size);
|
||||
|
||||
/* Display-text variant: overflow returns TRUNCATED with a terminated prefix of
|
||||
* complete UTF-8 scalars. Other results have the same contract as above. */
|
||||
int json_extract_string_prefix(const char *obj_start, const char *obj_end,
|
||||
const char *key, char *out, size_t out_size);
|
||||
|
||||
/* Validate one object starting at '{', returning its exclusive end. Never reads
|
||||
* outside [start, limit); allows following array members after the object. */
|
||||
int json_find_object_end(const char *start, const char *limit, const char **end);
|
||||
|
||||
/* Read an entire file into a malloc'd buffer. Caller must free *out_buf.
|
||||
* Returns 0 on success, -1 on failure. */
|
||||
int read_file_text(const char *path, char **out_buf, size_t *out_size);
|
||||
|
||||
+236
-40
@@ -3,6 +3,7 @@
|
||||
#include <string.h>
|
||||
#include <stdarg.h>
|
||||
#include <ctype.h>
|
||||
#include <stdint.h>
|
||||
#include <sys/stat.h>
|
||||
#include "json_helpers.h"
|
||||
|
||||
@@ -32,52 +33,247 @@ int json_append(JsonListBuilder *jb, const char *fmt, ...) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int json_extract_string(const char *obj_start, const char *obj_end,
|
||||
const char *key, char *out, size_t out_size) {
|
||||
char key_pattern[96];
|
||||
const char *p;
|
||||
const char *colon;
|
||||
const char *q;
|
||||
size_t pos = 0;
|
||||
typedef struct {
|
||||
const char *p, *end;
|
||||
} JsonReader;
|
||||
|
||||
if (out_size == 0) {
|
||||
return -1;
|
||||
}
|
||||
out[0] = '\0';
|
||||
typedef struct {
|
||||
char *out;
|
||||
size_t capacity, written;
|
||||
int truncated;
|
||||
const char *expected;
|
||||
size_t expected_size, matched;
|
||||
int equal;
|
||||
} JsonStringSink;
|
||||
|
||||
snprintf(key_pattern, sizeof(key_pattern), "\"%s\"", key);
|
||||
p = strstr(obj_start, key_pattern);
|
||||
if (!p || p >= obj_end) {
|
||||
return -1;
|
||||
}
|
||||
static void json_space(JsonReader *r) {
|
||||
while (r->p < r->end && (*r->p == ' ' || *r->p == '\t' ||
|
||||
*r->p == '\r' || *r->p == '\n')) r->p++;
|
||||
}
|
||||
|
||||
colon = strchr(p + strlen(key_pattern), ':');
|
||||
if (!colon || colon >= obj_end) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
q = colon + 1;
|
||||
while (q < obj_end && isspace((unsigned char)*q)) {
|
||||
q++;
|
||||
}
|
||||
if (q >= obj_end || *q != '"') {
|
||||
return -1;
|
||||
}
|
||||
q++;
|
||||
|
||||
while (q < obj_end && *q != '"') {
|
||||
if (*q == '\\' && (q + 1) < obj_end) {
|
||||
q++;
|
||||
}
|
||||
if (pos + 1 < out_size) {
|
||||
out[pos++] = *q;
|
||||
}
|
||||
q++;
|
||||
}
|
||||
out[pos] = '\0';
|
||||
static int json_take(JsonReader *r, char c) {
|
||||
if (r->p == r->end || *r->p != c) return -1;
|
||||
r->p++;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int json_hex4(JsonReader *r, uint32_t *value) {
|
||||
if ((size_t)(r->end - r->p) < 4) return -1;
|
||||
*value = 0;
|
||||
for (int i = 0; i < 4; i++) {
|
||||
unsigned char c = (unsigned char)*r->p++;
|
||||
unsigned digit;
|
||||
if (c >= '0' && c <= '9') digit = c - '0';
|
||||
else if (c >= 'a' && c <= 'f') digit = c - 'a' + 10;
|
||||
else if (c >= 'A' && c <= 'F') digit = c - 'A' + 10;
|
||||
else return -1;
|
||||
*value = (*value << 4) | digit;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int json_emit(JsonStringSink *sink, const char *bytes, size_t size) {
|
||||
if (!sink) return 0;
|
||||
if (sink->expected && sink->equal) {
|
||||
if (size > sink->expected_size - sink->matched ||
|
||||
memcmp(bytes, sink->expected + sink->matched, size) != 0) sink->equal = 0;
|
||||
else sink->matched += size;
|
||||
}
|
||||
if (sink->out) {
|
||||
if (memchr(bytes, 0, size)) return -1; /* C-string API cannot represent NUL. */
|
||||
if (!sink->truncated) {
|
||||
if (size >= sink->capacity - sink->written) sink->truncated = 1;
|
||||
else {
|
||||
memcpy(sink->out + sink->written, bytes, size);
|
||||
sink->written += size;
|
||||
sink->out[sink->written] = '\0';
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int json_string(JsonReader *r, JsonStringSink *sink) {
|
||||
if (json_take(r, '"') != 0) return -1;
|
||||
while (r->p < r->end) {
|
||||
unsigned char c = (unsigned char)*r->p++;
|
||||
char bytes[4];
|
||||
const char *part = bytes;
|
||||
size_t size = 1;
|
||||
if (c == '"') return 0;
|
||||
if (c < 0x20) return -1;
|
||||
if (c == '\\') {
|
||||
if (r->p == r->end) return -1;
|
||||
switch (*r->p++) {
|
||||
case '"': bytes[0] = '"'; break;
|
||||
case '\\': bytes[0] = '\\'; break;
|
||||
case '/': bytes[0] = '/'; break;
|
||||
case 'b': bytes[0] = '\b'; break;
|
||||
case 'f': bytes[0] = '\f'; break;
|
||||
case 'n': bytes[0] = '\n'; break;
|
||||
case 'r': bytes[0] = '\r'; break;
|
||||
case 't': bytes[0] = '\t'; break;
|
||||
case 'u': {
|
||||
uint32_t cp, low;
|
||||
if (json_hex4(r, &cp) != 0) return -1;
|
||||
if (cp >= 0xd800 && cp <= 0xdbff) {
|
||||
if (json_take(r, '\\') != 0 || json_take(r, 'u') != 0 ||
|
||||
json_hex4(r, &low) != 0 || low < 0xdc00 || low > 0xdfff) return -1;
|
||||
cp = 0x10000 + ((cp - 0xd800) << 10) + (low - 0xdc00);
|
||||
} else if (cp >= 0xdc00 && cp <= 0xdfff) return -1;
|
||||
if (cp < 0x80) bytes[0] = (char)cp;
|
||||
else if (cp < 0x800) {
|
||||
size = 2;
|
||||
bytes[0] = (char)(0xc0 | (cp >> 6));
|
||||
bytes[1] = (char)(0x80 | (cp & 63));
|
||||
} else if (cp < 0x10000) {
|
||||
size = 3;
|
||||
bytes[0] = (char)(0xe0 | (cp >> 12));
|
||||
bytes[1] = (char)(0x80 | ((cp >> 6) & 63));
|
||||
bytes[2] = (char)(0x80 | (cp & 63));
|
||||
} else {
|
||||
size = 4;
|
||||
bytes[0] = (char)(0xf0 | (cp >> 18));
|
||||
bytes[1] = (char)(0x80 | ((cp >> 12) & 63));
|
||||
bytes[2] = (char)(0x80 | ((cp >> 6) & 63));
|
||||
bytes[3] = (char)(0x80 | (cp & 63));
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: return -1;
|
||||
}
|
||||
} else if (c < 0x80) bytes[0] = (char)c;
|
||||
else {
|
||||
part = r->p - 1;
|
||||
if (c >= 0xc2 && c <= 0xdf) size = 2;
|
||||
else if (c >= 0xe0 && c <= 0xef) size = 3;
|
||||
else if (c >= 0xf0 && c <= 0xf4) size = 4;
|
||||
else return -1;
|
||||
if ((size_t)(r->end - part) < size) return -1;
|
||||
for (size_t i = 1; i < size; i++)
|
||||
if ((unsigned char)part[i] < 0x80 || (unsigned char)part[i] > 0xbf) return -1;
|
||||
unsigned char second = (unsigned char)part[1];
|
||||
if ((c == 0xe0 && second < 0xa0) || (c == 0xed && second >= 0xa0) ||
|
||||
(c == 0xf0 && second < 0x90) || (c == 0xf4 && second > 0x8f)) return -1;
|
||||
r->p = part + size;
|
||||
}
|
||||
if (json_emit(sink, part, size) != 0) return -1;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int json_digit(JsonReader *r) {
|
||||
return r->p < r->end && *r->p >= '0' && *r->p <= '9';
|
||||
}
|
||||
|
||||
static int json_value(JsonReader *r, unsigned depth) {
|
||||
json_space(r);
|
||||
if (r->p == r->end) return -1;
|
||||
char c = *r->p;
|
||||
if (c == '"') return json_string(r, NULL);
|
||||
if (c == '{' || c == '[') {
|
||||
if (depth >= 64) return -1;
|
||||
r->p++;
|
||||
char close = c == '{' ? '}' : ']';
|
||||
json_space(r);
|
||||
if (r->p < r->end && *r->p == close) { r->p++; return 0; }
|
||||
for (;;) {
|
||||
if (c == '{') {
|
||||
if (json_string(r, NULL) != 0) return -1;
|
||||
json_space(r);
|
||||
if (json_take(r, ':') != 0) return -1;
|
||||
}
|
||||
if (json_value(r, depth + 1) != 0) return -1;
|
||||
json_space(r);
|
||||
if (r->p < r->end && *r->p == close) { r->p++; return 0; }
|
||||
if (json_take(r, ',') != 0) return -1;
|
||||
json_space(r);
|
||||
}
|
||||
}
|
||||
if (c == 't' || c == 'f' || c == 'n') {
|
||||
const char *literal = c == 't' ? "true" : c == 'f' ? "false" : "null";
|
||||
size_t size = strlen(literal);
|
||||
if ((size_t)(r->end - r->p) < size || memcmp(r->p, literal, size) != 0) return -1;
|
||||
r->p += size;
|
||||
return 0;
|
||||
}
|
||||
if (c == '-') r->p++;
|
||||
if (!json_digit(r)) return -1;
|
||||
if (*r->p == '0') r->p++;
|
||||
else while (json_digit(r)) r->p++;
|
||||
if (r->p < r->end && *r->p == '.') {
|
||||
r->p++;
|
||||
if (!json_digit(r)) return -1;
|
||||
while (json_digit(r)) r->p++;
|
||||
}
|
||||
if (r->p < r->end && (*r->p == 'e' || *r->p == 'E')) {
|
||||
r->p++;
|
||||
if (r->p < r->end && (*r->p == '+' || *r->p == '-')) r->p++;
|
||||
if (!json_digit(r)) return -1;
|
||||
while (json_digit(r)) r->p++;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int json_find_object_end(const char *start, const char *limit, const char **end) {
|
||||
if (!end) return -1;
|
||||
*end = NULL;
|
||||
if (!start || !limit || start >= limit || *start != '{') return -1;
|
||||
JsonReader r = { start, limit };
|
||||
if (json_value(&r, 0) != 0) return -1;
|
||||
*end = r.p;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int json_extract(const char *start, const char *end, const char *key,
|
||||
char *out, size_t capacity, int prefix) {
|
||||
if (!out || !capacity) return JSON_STRING_INVALID;
|
||||
out[0] = '\0';
|
||||
if (!start || !end || start > end || !key) return JSON_STRING_INVALID;
|
||||
JsonReader r = { start, end };
|
||||
JsonStringSink value = { .out = out, .capacity = capacity };
|
||||
int found = 0;
|
||||
json_space(&r);
|
||||
if (json_take(&r, '{') != 0) goto invalid;
|
||||
json_space(&r);
|
||||
if (r.p < r.end && *r.p == '}') r.p++;
|
||||
else for (;;) {
|
||||
JsonStringSink member = { .expected = key, .expected_size = strlen(key), .equal = 1 };
|
||||
if (json_string(&r, &member) != 0) goto invalid;
|
||||
json_space(&r);
|
||||
if (json_take(&r, ':') != 0) goto invalid;
|
||||
json_space(&r);
|
||||
if (member.equal && member.matched == member.expected_size) {
|
||||
if (found || json_string(&r, &value) != 0) goto invalid;
|
||||
found = 1;
|
||||
} else if (json_value(&r, 1) != 0) goto invalid;
|
||||
json_space(&r);
|
||||
if (r.p < r.end && *r.p == '}') { r.p++; break; }
|
||||
if (json_take(&r, ',') != 0) goto invalid;
|
||||
json_space(&r);
|
||||
}
|
||||
json_space(&r);
|
||||
if (r.p != r.end) goto invalid;
|
||||
if (!found) return JSON_STRING_MISSING;
|
||||
if (!value.truncated) return JSON_STRING_OK;
|
||||
if (prefix) return JSON_STRING_TRUNCATED;
|
||||
out[0] = '\0';
|
||||
return JSON_STRING_TOO_LONG;
|
||||
invalid:
|
||||
out[0] = '\0';
|
||||
return JSON_STRING_INVALID;
|
||||
}
|
||||
|
||||
int json_extract_string(const char *start, const char *end, const char *key,
|
||||
char *out, size_t capacity) {
|
||||
return json_extract(start, end, key, out, capacity, 0);
|
||||
}
|
||||
|
||||
int json_extract_string_prefix(const char *start, const char *end, const char *key,
|
||||
char *out, size_t capacity) {
|
||||
return json_extract(start, end, key, out, capacity, 1);
|
||||
}
|
||||
|
||||
int read_file_text(const char *path, char **out_buf, size_t *out_size) {
|
||||
FILE *f;
|
||||
long fsize;
|
||||
|
||||
+28
-17
@@ -194,41 +194,52 @@ int parse_repository_payloads(const char *json, RepoPayload **out_items, size_t
|
||||
}
|
||||
|
||||
memset(item, 0, sizeof(*item));
|
||||
if (json_extract_string(p, end, "name", item->name, sizeof(item->name)) != 0 ||
|
||||
json_extract_string(p, end, "filename", item->filename, sizeof(item->filename)) != 0 ||
|
||||
json_extract_string(p, end, "url", item->url, sizeof(item->url)) != 0) {
|
||||
if (json_extract_string(p, end + 1, "name", item->name, sizeof(item->name)) != 0 ||
|
||||
json_extract_string(p, end + 1, "filename", item->filename, sizeof(item->filename)) != 0 ||
|
||||
json_extract_string(p, end + 1, "url", item->url, sizeof(item->url)) != 0) {
|
||||
free(item);
|
||||
p = end + 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Extract id or fallback to slug(name) */
|
||||
json_extract_string(p, end, "id", item->id, sizeof(item->id));
|
||||
json_extract_string(p, end + 1, "id", item->id, sizeof(item->id));
|
||||
if (item->id[0] == '\0') {
|
||||
slot_slug(item->name, item->id, sizeof(item->id));
|
||||
}
|
||||
|
||||
json_extract_string(p, end, "source", item->source, sizeof(item->source));
|
||||
json_extract_string(p, end, "source_direct", item->source_direct, sizeof(item->source_direct));
|
||||
json_extract_string(p, end, "description", item->description, sizeof(item->description));
|
||||
json_extract_string(p, end, "last_update", item->last_update, sizeof(item->last_update));
|
||||
json_extract_string(p, end, "version", item->version, sizeof(item->version));
|
||||
json_extract_string(p, end, "checksum", item->checksum, sizeof(item->checksum));
|
||||
json_extract_string(p, end, "author", item->author, sizeof(item->author));
|
||||
json_extract_string(p, end, "details_url", item->details_url, sizeof(item->details_url));
|
||||
json_extract_string(p, end + 1, "source", item->source, sizeof(item->source));
|
||||
json_extract_string(p, end + 1, "source_direct", item->source_direct, sizeof(item->source_direct));
|
||||
json_extract_string_prefix(p, end + 1, "description", item->description, sizeof(item->description));
|
||||
json_extract_string(p, end + 1, "last_update", item->last_update, sizeof(item->last_update));
|
||||
json_extract_string(p, end + 1, "version", item->version, sizeof(item->version));
|
||||
int checksum_result = json_extract_string(p, end + 1, "checksum", item->checksum, sizeof(item->checksum));
|
||||
/* A supplied oversized/non-string hash must not turn into an absent,
|
||||
* optional checksum after extraction fails. Hex validation is separate. */
|
||||
if (checksum_result != JSON_STRING_OK && checksum_result != JSON_STRING_MISSING) {
|
||||
free(item);
|
||||
p = end + 1;
|
||||
continue;
|
||||
}
|
||||
json_extract_string(p, end + 1, "author", item->author, sizeof(item->author));
|
||||
json_extract_string(p, end + 1, "details_url", item->details_url, sizeof(item->details_url));
|
||||
|
||||
json_extract_string(p, end, "release_notes", item->release_notes, sizeof(item->release_notes));
|
||||
if (strlen(item->release_notes) == sizeof(item->release_notes) - 1) {
|
||||
strcpy(item->release_notes + sizeof(item->release_notes) - 4, "...");
|
||||
int notes_result = json_extract_string_prefix(p, end + 1, "release_notes",
|
||||
item->release_notes, sizeof(item->release_notes));
|
||||
if (notes_result == JSON_STRING_TRUNCATED) {
|
||||
size_t length = strlen(item->release_notes);
|
||||
if (length > sizeof(item->release_notes) - 4) length = sizeof(item->release_notes) - 4;
|
||||
while (length && ((unsigned char)item->release_notes[length] & 0xc0) == 0x80) length--;
|
||||
strcpy(item->release_notes + length, "...");
|
||||
}
|
||||
|
||||
json_extract_string(p, end, "icon", item->icon, sizeof(item->icon));
|
||||
json_extract_string(p, end + 1, "icon", item->icon, sizeof(item->icon));
|
||||
/* Only allow inline data URIs (e.g. data:image/svg+xml;base64,... or data:image/png;base64,...); reject external URLs */
|
||||
if (strncmp(item->icon, "data:image/", 11) != 0 || strlen(item->icon) > 12288) {
|
||||
item->icon[0] = '\0';
|
||||
}
|
||||
|
||||
if (json_extract_string(p, end, "category", item->category, sizeof(item->category)) != 0 ||
|
||||
if (json_extract_string(p, end + 1, "category", item->category, sizeof(item->category)) != 0 ||
|
||||
strlen(item->category) == 0) {
|
||||
strncpy(item->category, "Uncategorized", sizeof(item->category) - 1);
|
||||
}
|
||||
|
||||
+1
-1
@@ -231,7 +231,7 @@ int slot_read_meta(const char *slot_dir, SlotMeta *out_meta) {
|
||||
json_extract_string(json, end, "source", out_meta->source, sizeof(out_meta->source));
|
||||
json_extract_string(json, end, "source_direct", out_meta->source_direct, sizeof(out_meta->source_direct));
|
||||
json_extract_string(json, end, "category", out_meta->category, sizeof(out_meta->category));
|
||||
json_extract_string(json, end, "description", out_meta->description, sizeof(out_meta->description));
|
||||
json_extract_string_prefix(json, end, "description", out_meta->description, sizeof(out_meta->description));
|
||||
json_extract_string(json, end, "author", out_meta->author, sizeof(out_meta->author));
|
||||
json_extract_string(json, end, "details_url", out_meta->details_url, sizeof(out_meta->details_url));
|
||||
|
||||
|
||||
+13
-11
@@ -55,9 +55,10 @@ static int load_sources(SourceEntry *out, int *out_count) {
|
||||
if (!arr_start) { free(json); *out_count = count; return 0; }
|
||||
|
||||
const char *p = arr_start + 1;
|
||||
while (count < MAX_SOURCES && (p = strchr(p, '{')) != NULL) {
|
||||
const char *end = strchr(p, '}');
|
||||
if (!end) break;
|
||||
while (count < MAX_SOURCES && p < json + size &&
|
||||
(p = memchr(p, '{', (size_t)(json + size - p))) != NULL) {
|
||||
const char *end;
|
||||
if (json_find_object_end(p, json + size, &end) != 0) break;
|
||||
|
||||
char id[64] = "", name[256] = "", url[1024] = "";
|
||||
char removable_str[8] = "";
|
||||
@@ -67,7 +68,7 @@ static int load_sources(SourceEntry *out, int *out_count) {
|
||||
json_extract_string(p, end, "url", url, sizeof(url));
|
||||
json_extract_string(p, end, "removable", removable_str, sizeof(removable_str));
|
||||
|
||||
if (strcmp(id, "default") == 0) { p = end + 1; continue; }
|
||||
if (strcmp(id, "default") == 0) { p = end; continue; }
|
||||
|
||||
if (name[0] && url[0] && id[0]) {
|
||||
strncpy(out[count].id, id, sizeof(out[count].id) - 1);
|
||||
@@ -76,7 +77,7 @@ static int load_sources(SourceEntry *out, int *out_count) {
|
||||
out[count].removable = (strcmp(removable_str, "false") != 0);
|
||||
count++;
|
||||
}
|
||||
p = end + 1;
|
||||
p = end;
|
||||
}
|
||||
|
||||
free(json);
|
||||
@@ -160,24 +161,25 @@ int sources_save(const char *json, size_t len) {
|
||||
if (!arr_start) { free(sources); return -1; }
|
||||
|
||||
const char *p = arr_start + 1;
|
||||
while (count < MAX_SOURCES && (p = strchr(p, '{')) != NULL) {
|
||||
const char *end = strchr(p, '}');
|
||||
if (!end) break;
|
||||
while (count < MAX_SOURCES && p < json + len &&
|
||||
(p = memchr(p, '{', (size_t)(json + len - p))) != NULL) {
|
||||
const char *end;
|
||||
if (json_find_object_end(p, json + len, &end) != 0) break;
|
||||
|
||||
char id[64] = "", name[256] = "", url[1024] = "";
|
||||
json_extract_string(p, end, "id", id, sizeof(id));
|
||||
json_extract_string(p, end, "name", name, sizeof(name));
|
||||
json_extract_string(p, end, "url", url, sizeof(url));
|
||||
|
||||
if (strcmp(id, "default") == 0) { p = end + 1; continue; }
|
||||
if (!name[0] || !url[0] || !id[0]) { p = end + 1; continue; }
|
||||
if (strcmp(id, "default") == 0) { p = end; continue; }
|
||||
if (!name[0] || !url[0] || !id[0]) { p = end; continue; }
|
||||
|
||||
strncpy(sources[count].id, id, sizeof(sources[count].id) - 1);
|
||||
strncpy(sources[count].name, name, sizeof(sources[count].name) - 1);
|
||||
strncpy(sources[count].url, url, sizeof(sources[count].url) - 1);
|
||||
sources[count].removable = 1;
|
||||
count++;
|
||||
p = end + 1;
|
||||
p = end;
|
||||
}
|
||||
|
||||
int ret = save_sources(sources, count);
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
FROM ubuntu:24.04
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
clang libclang-rt-18-dev make libc6-dev curl pkg-config python3 ca-certificates \
|
||||
clang libclang-rt-18-dev make libc6-dev curl libcurl4-openssl-dev pkg-config python3 ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Match the library shipped by build_deps.sh rather than Ubuntu's older MHD.
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include "repository.h"
|
||||
#include "slot.h"
|
||||
#include "sources.h"
|
||||
#include "json_helpers.h"
|
||||
#include "utils.h"
|
||||
|
||||
static void string(const char *value) {
|
||||
size_t size = 6 * strlen(value) + 1;
|
||||
char *escaped = malloc(size);
|
||||
if (!escaped || pldmgr_json_escape(value, escaped, size) != 0) exit(2);
|
||||
printf("\"%s\"", escaped);
|
||||
free(escaped);
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
if (argc != 3) return 2;
|
||||
char *input = malloc(65536);
|
||||
if (!input) return 2;
|
||||
size_t size = fread(input, 1, 65535, stdin);
|
||||
input[size] = 0;
|
||||
if (argv[1][0] == 'R') {
|
||||
RepoPayload *items;
|
||||
size_t count;
|
||||
int result = parse_repository_payloads(input, &items, &count);
|
||||
printf("{\"status\":%d,\"count\":%zu,\"items\":[", result, count);
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
if (i) putchar(',');
|
||||
printf("{\"name\":"); string(items[i].name);
|
||||
printf(",\"filename\":"); string(items[i].filename);
|
||||
printf(",\"url\":"); string(items[i].url);
|
||||
printf(",\"description\":"); string(items[i].description);
|
||||
printf(",\"release_notes\":"); string(items[i].release_notes);
|
||||
printf(",\"checksum\":"); string(items[i].checksum);
|
||||
putchar('}');
|
||||
}
|
||||
puts("]}");
|
||||
free(items);
|
||||
} else if (argv[1][0] == 'S') {
|
||||
if (sources_save(input, size) != 0) return 3;
|
||||
char *output = malloc(65536);
|
||||
if (!output) return 2;
|
||||
int result = sources_list_json(output, 65536);
|
||||
if (result != 0) { free(output); free(input); return 4; }
|
||||
fwrite(output, 1, strlen(output), stdout);
|
||||
free(output);
|
||||
} else if (argv[1][0] == 'M') {
|
||||
if (mkdir(argv[2], 0700) != 0) return 3;
|
||||
char path[1024];
|
||||
snprintf(path, sizeof(path), "%s/meta.json", argv[2]);
|
||||
if (write_file_text(path, input, size) != 0) return 3;
|
||||
SlotMeta meta;
|
||||
if (slot_read_meta(argv[2], &meta) != 0 || slot_write_meta(argv[2], &meta) != 0) return 4;
|
||||
char *output;
|
||||
size_t written;
|
||||
if (read_file_text(path, &output, &written) != 0) return 5;
|
||||
fwrite(output, 1, written, stdout);
|
||||
free(output);
|
||||
} else return 2;
|
||||
free(input);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include "json_helpers.h"
|
||||
|
||||
static char *unhex(const char *hex, size_t *size, int terminated) {
|
||||
*size = strcmp(hex, "-") == 0 ? 0 : strlen(hex) / 2;
|
||||
char *out = malloc(*size + (terminated || !*size));
|
||||
if (!out) exit(2);
|
||||
for (size_t i = 0; i < *size; i++) {
|
||||
unsigned byte;
|
||||
if (sscanf(hex + 2 * i, "%2x", &byte) != 1) exit(2);
|
||||
out[i] = (char)byte;
|
||||
}
|
||||
if (terminated) out[*size] = 0;
|
||||
return out;
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
char mode, input_hex[32769], key_hex[8193];
|
||||
size_t capacity, bound;
|
||||
while (scanf(" %c %zu %zu %32768s %8192s", &mode, &capacity, &bound,
|
||||
input_hex, key_hex) == 5) {
|
||||
size_t input_size, key_size;
|
||||
#ifdef LEGACY_DECODE
|
||||
char *input = unhex(input_hex, &input_size, 1);
|
||||
#else
|
||||
/* Exact allocation: bounded input deliberately has no trailing NUL. */
|
||||
char *input = unhex(input_hex, &input_size, 0);
|
||||
#endif
|
||||
char *key = unhex(key_hex, &key_size, 1);
|
||||
unsigned char output[4098];
|
||||
if (capacity > 4096 || bound > input_size) return 2;
|
||||
memset(output, 0xa5, sizeof(output));
|
||||
int result;
|
||||
#ifndef LEGACY_DECODE
|
||||
if (mode == 'P') result = json_extract_string_prefix(input, input + bound, key,
|
||||
(char *)output + 1, capacity);
|
||||
else if (mode == 'E') {
|
||||
const char *end;
|
||||
result = json_find_object_end(input, input + bound, &end);
|
||||
printf("%d %zu\n", result, end ? (size_t)(end - input) : 0);
|
||||
free(input); free(key);
|
||||
continue;
|
||||
} else
|
||||
#endif
|
||||
result = json_extract_string(input, input + bound, key, (char *)output + 1, capacity);
|
||||
if (output[0] != 0xa5) return 3;
|
||||
for (size_t i = capacity + 1; i < sizeof(output); i++)
|
||||
if (output[i] != 0xa5) return 3;
|
||||
if (capacity && !memchr(output + 1, 0, capacity)) return 4;
|
||||
printf("%d ", result);
|
||||
if (capacity) {
|
||||
size_t size = strlen((char *)output + 1);
|
||||
for (size_t i = 0; i < size; i++) printf("%02x", output[i + 1]);
|
||||
}
|
||||
puts("");
|
||||
free(input); free(key);
|
||||
}
|
||||
return ferror(stdin) ? 5 : 0;
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Actual repository parsing, source save/load, and slot metadata round trips."""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--no-sanitize", action="store_true")
|
||||
args = parser.parse_args()
|
||||
os.environ.setdefault("ASAN_OPTIONS", "detect_leaks=1:halt_on_error=1")
|
||||
os.environ.setdefault("UBSAN_OPTIONS", "halt_on_error=1:print_stacktrace=1")
|
||||
compiler = shlex.split(os.environ.get("HOST_CC", "clang"))
|
||||
with tempfile.TemporaryDirectory(prefix="pldmgr-json-backend-") as temp:
|
||||
build = Path(temp)
|
||||
storage = build / "data"
|
||||
storage.mkdir()
|
||||
(build / "pldmgr.h").write_text((ROOT / "include/pldmgr.h").read_text().replace("/data/pldmgr", str(storage)))
|
||||
(build / "assets_cacert_pem.h").write_text("static const unsigned char assets_cacert_pem[] = {0};\n"
|
||||
"static const unsigned int assets_cacert_pem_len = 0;\n")
|
||||
binary = build / "json_backend_tests"
|
||||
command = compiler + ["-std=c11", "-D_DEFAULT_SOURCE", "-g", "-O1", "-Wall", "-Wextra",
|
||||
"-ffunction-sections", "-fdata-sections",
|
||||
"-Wl,-dead_strip" if sys.platform == "darwin" else "-Wl,--gc-sections",
|
||||
"-I" + str(build), "-Iinclude", "-pthread"]
|
||||
if not args.no_sanitize:
|
||||
command += ["-fsanitize=address,undefined", "-fno-omit-frame-pointer"]
|
||||
subprocess.run(command + ["src/json_helpers.c", "src/utils.c", "src/repository.c",
|
||||
"src/sources.c", "src/slot.c", "tests/json_backend_driver.c",
|
||||
"-o", str(binary)], cwd=ROOT, check=True)
|
||||
checks = 0
|
||||
|
||||
def run(mode, document):
|
||||
nonlocal checks
|
||||
checks += 1
|
||||
slot = build / f"slot-{checks}"
|
||||
result = subprocess.run([str(binary), mode, str(slot)], input=json.dumps(document).encode(),
|
||||
capture_output=True)
|
||||
if result.returncode:
|
||||
raise AssertionError(result.stderr.decode() or f"Backend driver failed: {result.returncode}")
|
||||
return json.loads(result.stdout)
|
||||
|
||||
value = 'Zażółć Україна 日本語 😀 } { "quote" \\path\nnext\tline\x01'
|
||||
item = {"id": "test", "name": value, "filename": "safe.elf",
|
||||
"url": "https://example.invalid/safe.elf", "description": value,
|
||||
"release_notes": value, "checksum": "a" * 64}
|
||||
for manifest in [[item], {"payloads": [item]}, item]:
|
||||
actual = run("R", manifest)
|
||||
assert actual["status"] == 0 and actual["count"] == 1, actual
|
||||
for field in ["name", "filename", "url", "description", "release_notes", "checksum"]:
|
||||
assert actual["items"][0][field] == item[field], (field, actual)
|
||||
exact = dict(item, release_notes="a" * 2047)
|
||||
assert run("R", [exact])["items"][0]["release_notes"] == exact["release_notes"]
|
||||
for notes in ["a" * 3000, "日" * 1000, "a" * 2043 + "😀" * 50]:
|
||||
actual = run("R", [dict(item, release_notes=notes)])["items"][0]["release_notes"]
|
||||
assert actual.endswith("...") and len(actual.encode()) <= 2047, actual
|
||||
assert notes.startswith(actual[:-3]), actual
|
||||
actual = run("R", [dict(item, description="😀" * 1000)])["items"][0]["description"]
|
||||
assert actual == "😀" * 255
|
||||
# Strict fields must not become valid-looking shortened identifiers/hashes.
|
||||
for field, content in [("name", "a" * 128), ("filename", "a" * 256),
|
||||
("url", "a" * 1024), ("checksum", "a" * 65), ("checksum", None)]:
|
||||
assert run("R", [dict(item, **{field: content})])["count"] == 0
|
||||
assert run("R", [dict(item, checksum="")])["count"] == 1
|
||||
nested = {"nested": item, "filename": "safe.elf", "url": item["url"]}
|
||||
assert run("R", [nested])["count"] == 0
|
||||
source = {"id": "custom", "name": value, "url": "https://example.invalid/source.json", "removable": True}
|
||||
actual = run("S", {"sources": [source]})
|
||||
assert actual["sources"][1] == source, actual
|
||||
meta = {"schema": 2, "kind": "repo", "filename": "safe.elf", "installed_at": "2026-10-12",
|
||||
"id": "test", "source_url": source["url"], "source_name": value,
|
||||
"name": value, "version": "1.0", "checksum": "a" * 64,
|
||||
"url": item["url"], "description": value, "author": value}
|
||||
actual = run("M", meta)
|
||||
for field, content in meta.items():
|
||||
assert actual[field] == content, (field, actual)
|
||||
local = {"schema": 2, "kind": "local", "filename": "safe.elf", "origin": "web_upload",
|
||||
"origin_detail": value, "installed_at": "2026-10-12"}
|
||||
actual = run("M", local)
|
||||
for field, content in local.items():
|
||||
assert actual[field] == content, (field, actual)
|
||||
print(f"JSON backend: {checks} real repository/source/metadata scenarios passed")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,149 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Bounded production JSON decoding, with Python supplying expected UTF-8."""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import random
|
||||
import shlex
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--source", default="src/json_helpers.c")
|
||||
parser.add_argument("--legacy-api", action="store_true")
|
||||
parser.add_argument("--no-sanitize", action="store_true")
|
||||
args = parser.parse_args()
|
||||
os.environ.setdefault("ASAN_OPTIONS", "detect_leaks=1:halt_on_error=1")
|
||||
os.environ.setdefault("UBSAN_OPTIONS", "halt_on_error=1:print_stacktrace=1")
|
||||
cases = []
|
||||
|
||||
def add(document, key="value", value=None, error=None, capacities=(4096,), bound=None):
|
||||
if isinstance(document, str):
|
||||
document = document.encode()
|
||||
if bound is None:
|
||||
bound = len(document)
|
||||
for capacity in capacities:
|
||||
for mode in ("S", "P"):
|
||||
if error is not None:
|
||||
expected_status, expected = error, b""
|
||||
else:
|
||||
data = value.encode()
|
||||
prefix = b""
|
||||
for char in value:
|
||||
scalar = char.encode()
|
||||
if len(prefix) + len(scalar) >= capacity:
|
||||
break
|
||||
prefix += scalar
|
||||
if not capacity:
|
||||
expected_status, expected = -2, b""
|
||||
elif prefix == data:
|
||||
expected_status, expected = 0, data
|
||||
elif mode == "S":
|
||||
expected_status, expected = -3, b""
|
||||
else:
|
||||
expected_status, expected = 1, prefix
|
||||
cases.append((mode, capacity, bound, document, key, expected_status, expected))
|
||||
|
||||
values = ["", 'quote" backslash\\ slash/', "\b\f\n\r\t", "".join(map(chr, range(1, 32))),
|
||||
"Zażółć Україна 日本語 😀", "¢€😀", "\u0080\u07ff\u0800\ud7ff\ue000\uffff\U0010ffff"]
|
||||
for value in values:
|
||||
for ascii_only in (True, False):
|
||||
add(json.dumps({"value": value}, ensure_ascii=ascii_only), value=value)
|
||||
document = json.dumps({"nested": {"value": "wrong"}, "value": value,
|
||||
"tail": [True, False, None, -1.25e3, {"x": ":{}[]"}]},
|
||||
ensure_ascii=ascii_only)
|
||||
add(document, value=value, capacities=range(len(value.encode()) + 3))
|
||||
add('{"\\u0076alue":"escaped key"}', value="escaped key")
|
||||
for key in ['quo"te\\日本語', 'k' * 150, '']:
|
||||
add(json.dumps({key: "correct"}), key=key, value="correct")
|
||||
add('{"x":"value","value":"correct"}', value="correct")
|
||||
add('{"nested":{"value":"wrong"}}', error=-1)
|
||||
add('{"value2":"wrong"}', error=-1)
|
||||
add('{}', error=-1)
|
||||
add('{"value":"first","value":"second"}', error=-2)
|
||||
add('{"value":123}', error=-2)
|
||||
add('{"value":null}', error=-2)
|
||||
add('{"value":"ok","unrelated":"\\u0000"}', value="ok")
|
||||
add('{"value":"\\u0000"}', error=-2)
|
||||
malformed = [b'{"value":"unterminated}', b'{"value":"x\\"}',
|
||||
b'{"value":"\\q"}', b'{"value":"\\u123"}', b'{"value":"\\uxxxx"}',
|
||||
b'{"value":"\\ud800"}', b'{"value":"\\udc00"}',
|
||||
b'{"value":"\\ud800\\u0041"}', b'{"value":"\x00"}', b'{"value":"\x01"}',
|
||||
b'{"value":"\xc0\x80"}', b'{"value":"\xed\xa0\x80"}',
|
||||
b'{"value":"\xf4\x90\x80\x80"}', b'{"value":"\xe2\x82"}',
|
||||
b'{"value":"ok",}', b'{"value":"ok"}junk', b'{"value":"ok","x":01}',
|
||||
b'{"value":"ok","x":1.}', b'{"value":"ok","x":1e}',
|
||||
b'{"value":"ok","x":tru}', b'{"value":"ok","x":[1,]}',
|
||||
b'{"value":"ok","x":{"k":"\\q"}}', b'[]', b'', b'\v{"value":"ok"}']
|
||||
for document in malformed:
|
||||
add(document, error=-2, capacities=(1, 4096))
|
||||
# End can cut at every byte, including inside Unicode escapes/scalars.
|
||||
document = '{"value":"😀\\n\\ud83d\\ude00"}'.encode()
|
||||
for bound in range(len(document)):
|
||||
add(document, error=-2, bound=bound)
|
||||
add(b'{"value":"ok"} trailing bytes', value="ok", bound=len(b'{"value":"ok"}'))
|
||||
# A missing key must not be found in data after the supplied bound.
|
||||
add(b'{} {"value":"outside"}', error=-1, bound=2)
|
||||
add('{"value":"ok","x":' + '[' * 63 + '0' + ']' * 63 + '}', value="ok")
|
||||
add('{"value":"ok","x":' + '[' * 64 + '0' + ']' * 64 + '}', error=-2)
|
||||
rng = random.Random(20261012)
|
||||
alphabet = ['a', '"', '\\', '\n', '\t', '\x01', 'ż', 'Я', '日', '😀']
|
||||
for _ in range(500):
|
||||
value = "".join(rng.choice(alphabet) for _ in range(rng.randrange(60)))
|
||||
document = json.dumps({"value": value, "nested": ["value", {"value": "wrong"}]},
|
||||
ensure_ascii=rng.choice([True, False]))
|
||||
add(document, value=json.loads(document)["value"], capacities=(rng.randrange(200),))
|
||||
# Validate object framing independently of subsequent array members.
|
||||
if not args.legacy_api:
|
||||
documents = [(b'{"value":"} { \\\"", "nested":{"x":[]}}, {"next":true}', 0),
|
||||
(b'{"x":[1,{"z":"}"}]}]', 0), (b'{"x":[1,]}', -1)]
|
||||
for document, status in documents:
|
||||
if status == 0:
|
||||
_, character_end = json.JSONDecoder().raw_decode(document.decode())
|
||||
end = len(document.decode()[:character_end].encode())
|
||||
else:
|
||||
end = 0
|
||||
cases.append(("E", 4096, len(document), document, "", status, str(end).encode()))
|
||||
if status == 0:
|
||||
for bound in range(end):
|
||||
cases.append(("E", 4096, bound, document, "", -1, b"0"))
|
||||
compiler = shlex.split(os.environ.get("HOST_CC", "clang"))
|
||||
with tempfile.TemporaryDirectory(prefix="pldmgr-json-decode-") as temp:
|
||||
binary = Path(temp) / "json_decode_tests"
|
||||
command = compiler + ["-std=c11", "-D_DEFAULT_SOURCE", "-g", "-O1", "-Wall", "-Wextra", "-Iinclude"]
|
||||
if not args.no_sanitize:
|
||||
command += ["-fsanitize=address,undefined", "-fno-omit-frame-pointer"]
|
||||
if args.legacy_api:
|
||||
command += ["-DLEGACY_DECODE"]
|
||||
subprocess.run(command + [args.source, "tests/json_decode_driver.c", "-o", str(binary)],
|
||||
cwd=ROOT, check=True)
|
||||
protocol = "".join(f"{mode} {capacity} {bound} {doc.hex() or '-'} {key.encode().hex() or '-'}\n"
|
||||
for mode, capacity, bound, doc, key, _, _ in cases)
|
||||
process = subprocess.run([str(binary)], input=protocol, text=True, capture_output=True)
|
||||
if process.returncode:
|
||||
raise SystemExit(process.stderr or f"Decoder driver failed: {process.returncode}")
|
||||
output = process.stdout.splitlines()
|
||||
assert len(output) == len(cases)
|
||||
failures = []
|
||||
for case, line in zip(cases, output):
|
||||
mode, capacity, bound, doc, key, expected_status, expected = case
|
||||
status, result = line.split(" ", 1)
|
||||
actual = result.encode() if mode == "E" else bytes.fromhex(result)
|
||||
# Old status codes differ; compare content and success/error semantics.
|
||||
status_ok = int(status) == expected_status if not args.legacy_api else ((int(status) == 0) == (expected_status >= 0))
|
||||
if not status_ok or actual != expected:
|
||||
failures.append((doc[:120], mode, capacity, bound, int(status), actual, expected_status, expected))
|
||||
if failures:
|
||||
for failure in failures[:8]:
|
||||
print("FAIL document/mode/capacity/bound/status/output/expected:", failure)
|
||||
raise SystemExit(f"JSON decoder: {len(failures)} failures / {len(cases)} cases")
|
||||
print(f"JSON decoder: {len(cases)} bounded decoding/selection checks passed")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in new issue
Block a user