fix: Read GH release JSON as stream in OTA updater (#1810)
## Summary The GitHub recent release API returns lots of data, including the full release notes. For 1.2.0, that data is 30,530 bytes. The existing OtaUpdater HTTP handler and single-buffer JSON parsing can't reliably handle that much data in the constrained ESP32 environment. This change does a few things: 1. Adds a very simple lib/JsonParser/StreamingJsonParser.cpp with SAX-style callbacks to read JSON data incrementally. 2. Adds a very simple lib/JsonParser/ReleaseJsonParser.cpp building on StreamingJsonParser, which specifically parses the GitHub release JSON for the release version, URL, and size. 3. Updates OtaUpdater.cpp to use an instance of ReleaseJsonParser to incrementally parse the large response it may receive from GitHub. Building from this commit while overriding my local version to 1.1.9, I was able to run the OTA update process ~5 times in a row successfully. Fixes #1561 (second part, after #1805). --- ### AI Usage While CrossPoint doesn't have restrictions on AI tools in contributing, please be transparent about their usage as it helps set the right context for reviewers. Did you use AI tools to help write this code? _**YES**_
This commit is contained in:
@@ -0,0 +1,208 @@
|
||||
#include "ReleaseJsonParser.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
namespace {
|
||||
|
||||
void safeCopy(char* dst, size_t dstSize, const char* src, size_t srcLen) {
|
||||
size_t n = srcLen < dstSize - 1 ? srcLen : dstSize - 1;
|
||||
memcpy(dst, src, n);
|
||||
dst[n] = '\0';
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ReleaseJsonParser::ReleaseJsonParser()
|
||||
: parser(JsonCallbacks{this, sOnKey, sOnString, sOnNumber, sOnBool, sOnNull, sOnObjectStart, sOnObjectEnd,
|
||||
sOnArrayStart, sOnArrayEnd}) {
|
||||
reset();
|
||||
}
|
||||
|
||||
void ReleaseJsonParser::reset() {
|
||||
parser.reset();
|
||||
position = Position::TOP_LEVEL;
|
||||
lastKey = LastKey::NONE;
|
||||
depth = 0;
|
||||
assetDepth = 0;
|
||||
tagName[0] = '\0';
|
||||
firmwareUrl[0] = '\0';
|
||||
firmwareSize = 0;
|
||||
tagFound = false;
|
||||
firmwareFound = false;
|
||||
currentAssetName[0] = '\0';
|
||||
currentAssetUrl[0] = '\0';
|
||||
currentAssetSize = 0;
|
||||
}
|
||||
|
||||
void ReleaseJsonParser::feed(const char* data, size_t len) { parser.feed(data, len); }
|
||||
|
||||
bool ReleaseJsonParser::foundTag() const { return tagFound; }
|
||||
bool ReleaseJsonParser::foundFirmware() const { return firmwareFound; }
|
||||
const char* ReleaseJsonParser::getTagName() const { return tagName; }
|
||||
const char* ReleaseJsonParser::getFirmwareUrl() const { return firmwareUrl; }
|
||||
size_t ReleaseJsonParser::getFirmwareSize() const { return firmwareSize; }
|
||||
|
||||
void ReleaseJsonParser::commitAsset() {
|
||||
if (strcmp(currentAssetName, "firmware.bin") == 0) {
|
||||
memcpy(firmwareUrl, currentAssetUrl, sizeof(firmwareUrl));
|
||||
firmwareSize = currentAssetSize;
|
||||
firmwareFound = true;
|
||||
}
|
||||
currentAssetName[0] = '\0';
|
||||
currentAssetUrl[0] = '\0';
|
||||
currentAssetSize = 0;
|
||||
}
|
||||
|
||||
// -- SAX callbacks (static trampolines) -------------------------------------
|
||||
|
||||
void ReleaseJsonParser::sOnKey(void* ctx, const char* key, size_t len) {
|
||||
auto* self = static_cast<ReleaseJsonParser*>(ctx);
|
||||
|
||||
switch (self->position) {
|
||||
case Position::TOP_LEVEL:
|
||||
if (self->depth == 1) {
|
||||
if (len == 8 && memcmp(key, "tag_name", 8) == 0)
|
||||
self->lastKey = LastKey::TAG_NAME;
|
||||
else if (len == 6 && memcmp(key, "assets", 6) == 0)
|
||||
self->lastKey = LastKey::ASSETS;
|
||||
else
|
||||
self->lastKey = LastKey::NONE;
|
||||
}
|
||||
break;
|
||||
case Position::IN_ASSET_OBJECT:
|
||||
if (self->assetDepth == 1) {
|
||||
if (len == 4 && memcmp(key, "name", 4) == 0)
|
||||
self->lastKey = LastKey::ASSET_NAME;
|
||||
else if (len == 20 && memcmp(key, "browser_download_url", 20) == 0)
|
||||
self->lastKey = LastKey::ASSET_URL;
|
||||
else if (len == 4 && memcmp(key, "size", 4) == 0)
|
||||
self->lastKey = LastKey::ASSET_SIZE;
|
||||
else
|
||||
self->lastKey = LastKey::NONE;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void ReleaseJsonParser::sOnString(void* ctx, const char* value, size_t len) {
|
||||
auto* self = static_cast<ReleaseJsonParser*>(ctx);
|
||||
|
||||
switch (self->lastKey) {
|
||||
case LastKey::TAG_NAME:
|
||||
if (self->position == Position::TOP_LEVEL && self->depth == 1) {
|
||||
safeCopy(self->tagName, sizeof(self->tagName), value, len);
|
||||
self->tagFound = true;
|
||||
}
|
||||
break;
|
||||
case LastKey::ASSET_NAME:
|
||||
if (self->position == Position::IN_ASSET_OBJECT && self->assetDepth == 1)
|
||||
safeCopy(self->currentAssetName, sizeof(self->currentAssetName), value, len);
|
||||
break;
|
||||
case LastKey::ASSET_URL:
|
||||
if (self->position == Position::IN_ASSET_OBJECT && self->assetDepth == 1)
|
||||
safeCopy(self->currentAssetUrl, sizeof(self->currentAssetUrl), value, len);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
self->lastKey = LastKey::NONE;
|
||||
}
|
||||
|
||||
void ReleaseJsonParser::sOnNumber(void* ctx, const char* value, size_t /*len*/) {
|
||||
auto* self = static_cast<ReleaseJsonParser*>(ctx);
|
||||
|
||||
if (self->lastKey == LastKey::ASSET_SIZE && self->position == Position::IN_ASSET_OBJECT && self->assetDepth == 1) {
|
||||
self->currentAssetSize = static_cast<size_t>(strtoul(value, nullptr, 10));
|
||||
}
|
||||
self->lastKey = LastKey::NONE;
|
||||
}
|
||||
|
||||
void ReleaseJsonParser::sOnBool(void* ctx, bool /*value*/) {
|
||||
static_cast<ReleaseJsonParser*>(ctx)->lastKey = LastKey::NONE;
|
||||
}
|
||||
|
||||
void ReleaseJsonParser::sOnNull(void* ctx) { static_cast<ReleaseJsonParser*>(ctx)->lastKey = LastKey::NONE; }
|
||||
|
||||
void ReleaseJsonParser::sOnObjectStart(void* ctx) {
|
||||
auto* self = static_cast<ReleaseJsonParser*>(ctx);
|
||||
|
||||
switch (self->position) {
|
||||
case Position::TOP_LEVEL:
|
||||
self->depth++;
|
||||
self->lastKey = LastKey::NONE;
|
||||
break;
|
||||
case Position::IN_ASSETS_ARRAY:
|
||||
self->position = Position::IN_ASSET_OBJECT;
|
||||
self->assetDepth = 1;
|
||||
self->currentAssetName[0] = '\0';
|
||||
self->currentAssetUrl[0] = '\0';
|
||||
self->currentAssetSize = 0;
|
||||
self->lastKey = LastKey::NONE;
|
||||
break;
|
||||
case Position::IN_ASSET_OBJECT:
|
||||
self->assetDepth++;
|
||||
self->lastKey = LastKey::NONE;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void ReleaseJsonParser::sOnObjectEnd(void* ctx) {
|
||||
auto* self = static_cast<ReleaseJsonParser*>(ctx);
|
||||
|
||||
switch (self->position) {
|
||||
case Position::TOP_LEVEL:
|
||||
if (self->depth > 0) self->depth--;
|
||||
break;
|
||||
case Position::IN_ASSET_OBJECT:
|
||||
self->assetDepth--;
|
||||
if (self->assetDepth == 0) {
|
||||
self->commitAsset();
|
||||
self->position = Position::IN_ASSETS_ARRAY;
|
||||
}
|
||||
self->lastKey = LastKey::NONE;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void ReleaseJsonParser::sOnArrayStart(void* ctx) {
|
||||
auto* self = static_cast<ReleaseJsonParser*>(ctx);
|
||||
|
||||
switch (self->position) {
|
||||
case Position::TOP_LEVEL:
|
||||
if (self->lastKey == LastKey::ASSETS && self->depth == 1) {
|
||||
self->position = Position::IN_ASSETS_ARRAY;
|
||||
} else {
|
||||
self->depth++;
|
||||
}
|
||||
self->lastKey = LastKey::NONE;
|
||||
break;
|
||||
case Position::IN_ASSET_OBJECT:
|
||||
self->assetDepth++;
|
||||
self->lastKey = LastKey::NONE;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void ReleaseJsonParser::sOnArrayEnd(void* ctx) {
|
||||
auto* self = static_cast<ReleaseJsonParser*>(ctx);
|
||||
|
||||
switch (self->position) {
|
||||
case Position::TOP_LEVEL:
|
||||
if (self->depth > 0) self->depth--;
|
||||
break;
|
||||
case Position::IN_ASSETS_ARRAY:
|
||||
self->position = Position::TOP_LEVEL;
|
||||
break;
|
||||
case Position::IN_ASSET_OBJECT:
|
||||
self->assetDepth--;
|
||||
self->lastKey = LastKey::NONE;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
#include "StreamingJsonParser.h"
|
||||
|
||||
class ReleaseJsonParser {
|
||||
public:
|
||||
ReleaseJsonParser();
|
||||
|
||||
ReleaseJsonParser(const ReleaseJsonParser&) = delete;
|
||||
ReleaseJsonParser& operator=(const ReleaseJsonParser&) = delete;
|
||||
|
||||
void reset();
|
||||
void feed(const char* data, size_t len);
|
||||
|
||||
bool foundTag() const;
|
||||
bool foundFirmware() const;
|
||||
const char* getTagName() const;
|
||||
const char* getFirmwareUrl() const;
|
||||
size_t getFirmwareSize() const;
|
||||
|
||||
private:
|
||||
enum class Position : uint8_t {
|
||||
TOP_LEVEL,
|
||||
IN_ASSETS_ARRAY,
|
||||
IN_ASSET_OBJECT,
|
||||
};
|
||||
|
||||
enum class LastKey : uint8_t {
|
||||
NONE,
|
||||
TAG_NAME,
|
||||
ASSETS,
|
||||
ASSET_NAME,
|
||||
ASSET_URL,
|
||||
ASSET_SIZE,
|
||||
};
|
||||
|
||||
static void sOnKey(void* ctx, const char* key, size_t len);
|
||||
static void sOnString(void* ctx, const char* value, size_t len);
|
||||
static void sOnNumber(void* ctx, const char* value, size_t len);
|
||||
static void sOnBool(void* ctx, bool value);
|
||||
static void sOnNull(void* ctx);
|
||||
static void sOnObjectStart(void* ctx);
|
||||
static void sOnObjectEnd(void* ctx);
|
||||
static void sOnArrayStart(void* ctx);
|
||||
static void sOnArrayEnd(void* ctx);
|
||||
|
||||
void commitAsset();
|
||||
|
||||
StreamingJsonParser parser;
|
||||
|
||||
Position position;
|
||||
LastKey lastKey;
|
||||
uint8_t depth;
|
||||
uint8_t assetDepth;
|
||||
|
||||
char tagName[32];
|
||||
char firmwareUrl[512];
|
||||
size_t firmwareSize;
|
||||
bool tagFound;
|
||||
bool firmwareFound;
|
||||
|
||||
char currentAssetName[32];
|
||||
char currentAssetUrl[512];
|
||||
size_t currentAssetSize;
|
||||
};
|
||||
@@ -0,0 +1,249 @@
|
||||
#include "StreamingJsonParser.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
StreamingJsonParser::StreamingJsonParser(const JsonCallbacks& callbacks) : cb(callbacks) { reset(); }
|
||||
|
||||
void StreamingJsonParser::reset() {
|
||||
tokenLen = 0;
|
||||
state = State::SCANNING;
|
||||
expectingValue = false;
|
||||
escaped = false;
|
||||
tokenOverflow = false;
|
||||
error = false;
|
||||
nestingDepth = 0;
|
||||
literalLen = 0;
|
||||
literalPos = 0;
|
||||
}
|
||||
|
||||
void StreamingJsonParser::feed(const char* data, size_t len) {
|
||||
for (size_t i = 0; i < len && !error; ++i) {
|
||||
char c = data[i];
|
||||
switch (state) {
|
||||
case State::SCANNING:
|
||||
handleScanning(c);
|
||||
break;
|
||||
case State::IN_STRING_KEY:
|
||||
case State::IN_STRING_VALUE:
|
||||
handleStringChar(c);
|
||||
break;
|
||||
case State::IN_NUMBER:
|
||||
handleNumber(c);
|
||||
break;
|
||||
case State::IN_LITERAL:
|
||||
handleLiteral(c);
|
||||
break;
|
||||
case State::SKIP_STRING:
|
||||
handleSkipString(c);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void StreamingJsonParser::handleScanning(char c) {
|
||||
switch (c) {
|
||||
case '"':
|
||||
tokenLen = 0;
|
||||
tokenOverflow = false;
|
||||
if (expectingValue || inArray()) {
|
||||
state = State::IN_STRING_VALUE;
|
||||
} else {
|
||||
state = State::IN_STRING_KEY;
|
||||
}
|
||||
break;
|
||||
case '{':
|
||||
if (nestingDepth < MAX_NESTING) {
|
||||
nestingStack[nestingDepth++] = Container::OBJECT;
|
||||
} else {
|
||||
error = true;
|
||||
return;
|
||||
}
|
||||
if (cb.onObjectStart) cb.onObjectStart(cb.ctx);
|
||||
expectingValue = false;
|
||||
break;
|
||||
case '}':
|
||||
if (cb.onObjectEnd) cb.onObjectEnd(cb.ctx);
|
||||
if (nestingDepth > 0) --nestingDepth;
|
||||
expectingValue = false;
|
||||
break;
|
||||
case '[':
|
||||
if (nestingDepth < MAX_NESTING) {
|
||||
nestingStack[nestingDepth++] = Container::ARRAY;
|
||||
} else {
|
||||
error = true;
|
||||
return;
|
||||
}
|
||||
if (cb.onArrayStart) cb.onArrayStart(cb.ctx);
|
||||
expectingValue = false;
|
||||
break;
|
||||
case ']':
|
||||
if (cb.onArrayEnd) cb.onArrayEnd(cb.ctx);
|
||||
if (nestingDepth > 0) --nestingDepth;
|
||||
expectingValue = false;
|
||||
break;
|
||||
case ':':
|
||||
expectingValue = true;
|
||||
break;
|
||||
case ',':
|
||||
expectingValue = false;
|
||||
break;
|
||||
case 't':
|
||||
if (expectingValue || inArray()) {
|
||||
memcpy(literalExpected, "true", 4);
|
||||
literalLen = 4;
|
||||
literalPos = 1;
|
||||
state = State::IN_LITERAL;
|
||||
}
|
||||
break;
|
||||
case 'f':
|
||||
if (expectingValue || inArray()) {
|
||||
memcpy(literalExpected, "false", 5);
|
||||
literalLen = 5;
|
||||
literalPos = 1;
|
||||
state = State::IN_LITERAL;
|
||||
}
|
||||
break;
|
||||
case 'n':
|
||||
if (expectingValue || inArray()) {
|
||||
memcpy(literalExpected, "null", 4);
|
||||
literalLen = 4;
|
||||
literalPos = 1;
|
||||
state = State::IN_LITERAL;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if ((expectingValue || inArray()) && (c == '-' || (c >= '0' && c <= '9'))) {
|
||||
tokenLen = 0;
|
||||
tokenOverflow = false;
|
||||
appendToken(c);
|
||||
state = State::IN_NUMBER;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void StreamingJsonParser::handleStringChar(char c) {
|
||||
if (escaped) {
|
||||
escaped = false;
|
||||
switch (c) {
|
||||
case '"':
|
||||
case '\\':
|
||||
case '/':
|
||||
appendToken(c);
|
||||
break;
|
||||
case 'b':
|
||||
appendToken('\b');
|
||||
break;
|
||||
case 'f':
|
||||
appendToken('\f');
|
||||
break;
|
||||
case 'n':
|
||||
appendToken('\n');
|
||||
break;
|
||||
case 'r':
|
||||
appendToken('\r');
|
||||
break;
|
||||
case 't':
|
||||
appendToken('\t');
|
||||
break;
|
||||
case 'u':
|
||||
// Pass \uXXXX through as literal characters -- we don't decode
|
||||
// Unicode escapes since our use case only needs ASCII field matching.
|
||||
appendToken('\\');
|
||||
appendToken('u');
|
||||
break;
|
||||
default:
|
||||
appendToken('\\');
|
||||
appendToken(c);
|
||||
break;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (c == '\\') {
|
||||
escaped = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (c == '"') {
|
||||
emitToken();
|
||||
return;
|
||||
}
|
||||
|
||||
appendToken(c);
|
||||
}
|
||||
|
||||
void StreamingJsonParser::handleNumber(char c) {
|
||||
if ((c >= '0' && c <= '9') || c == '.' || c == '-' || c == '+' || c == 'e' || c == 'E') {
|
||||
appendToken(c);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!tokenOverflow && cb.onNumber) {
|
||||
tokenBuf[tokenLen] = '\0';
|
||||
cb.onNumber(cb.ctx, tokenBuf, tokenLen);
|
||||
}
|
||||
state = State::SCANNING;
|
||||
expectingValue = false;
|
||||
|
||||
handleScanning(c);
|
||||
}
|
||||
|
||||
void StreamingJsonParser::handleLiteral(char c) {
|
||||
if (c == literalExpected[literalPos]) {
|
||||
++literalPos;
|
||||
if (literalPos == literalLen) {
|
||||
if (literalExpected[0] == 't') {
|
||||
if (cb.onBool) cb.onBool(cb.ctx, true);
|
||||
} else if (literalExpected[0] == 'f') {
|
||||
if (cb.onBool) cb.onBool(cb.ctx, false);
|
||||
} else {
|
||||
if (cb.onNull) cb.onNull(cb.ctx);
|
||||
}
|
||||
state = State::SCANNING;
|
||||
expectingValue = false;
|
||||
}
|
||||
} else {
|
||||
error = true;
|
||||
}
|
||||
}
|
||||
|
||||
void StreamingJsonParser::handleSkipString(char c) {
|
||||
if (escaped) {
|
||||
escaped = false;
|
||||
return;
|
||||
}
|
||||
if (c == '\\') {
|
||||
escaped = true;
|
||||
return;
|
||||
}
|
||||
if (c == '"') {
|
||||
state = State::SCANNING;
|
||||
expectingValue = false;
|
||||
}
|
||||
}
|
||||
|
||||
void StreamingJsonParser::appendToken(char c) {
|
||||
if (tokenLen < TOKEN_BUF_SIZE - 1) {
|
||||
tokenBuf[tokenLen++] = c;
|
||||
} else {
|
||||
tokenOverflow = true;
|
||||
}
|
||||
}
|
||||
|
||||
void StreamingJsonParser::emitToken() {
|
||||
if (state == State::IN_STRING_KEY) {
|
||||
if (!tokenOverflow && cb.onKey) {
|
||||
tokenBuf[tokenLen] = '\0';
|
||||
cb.onKey(cb.ctx, tokenBuf, tokenLen);
|
||||
}
|
||||
state = State::SCANNING;
|
||||
} else {
|
||||
if (!tokenOverflow && cb.onString) {
|
||||
tokenBuf[tokenLen] = '\0';
|
||||
cb.onString(cb.ctx, tokenBuf, tokenLen);
|
||||
}
|
||||
state = State::SCANNING;
|
||||
expectingValue = false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
struct JsonCallbacks {
|
||||
void* ctx;
|
||||
void (*onKey)(void* ctx, const char* key, size_t len);
|
||||
void (*onString)(void* ctx, const char* value, size_t len);
|
||||
void (*onNumber)(void* ctx, const char* value, size_t len);
|
||||
void (*onBool)(void* ctx, bool value);
|
||||
void (*onNull)(void* ctx);
|
||||
void (*onObjectStart)(void* ctx);
|
||||
void (*onObjectEnd)(void* ctx);
|
||||
void (*onArrayStart)(void* ctx);
|
||||
void (*onArrayEnd)(void* ctx);
|
||||
};
|
||||
|
||||
class StreamingJsonParser {
|
||||
public:
|
||||
static constexpr size_t TOKEN_BUF_SIZE = 512;
|
||||
static constexpr size_t MAX_NESTING = 32;
|
||||
|
||||
explicit StreamingJsonParser(const JsonCallbacks& callbacks);
|
||||
|
||||
void reset();
|
||||
void feed(const char* data, size_t len);
|
||||
|
||||
bool hasError() const { return error; }
|
||||
|
||||
private:
|
||||
enum class State : uint8_t {
|
||||
SCANNING,
|
||||
IN_STRING_KEY,
|
||||
IN_STRING_VALUE,
|
||||
IN_NUMBER,
|
||||
IN_LITERAL,
|
||||
SKIP_STRING,
|
||||
};
|
||||
|
||||
enum class Container : uint8_t {
|
||||
NONE,
|
||||
OBJECT,
|
||||
ARRAY,
|
||||
};
|
||||
|
||||
void handleScanning(char c);
|
||||
void handleStringChar(char c);
|
||||
void handleNumber(char c);
|
||||
void handleLiteral(char c);
|
||||
void handleSkipString(char c);
|
||||
|
||||
void appendToken(char c);
|
||||
void emitToken();
|
||||
|
||||
bool inArray() const { return nestingDepth > 0 && nestingStack[nestingDepth - 1] == Container::ARRAY; }
|
||||
|
||||
JsonCallbacks cb;
|
||||
char tokenBuf[TOKEN_BUF_SIZE];
|
||||
size_t tokenLen;
|
||||
State state;
|
||||
bool expectingValue;
|
||||
bool escaped;
|
||||
bool tokenOverflow;
|
||||
bool error;
|
||||
|
||||
Container nestingStack[MAX_NESTING];
|
||||
uint8_t nestingDepth;
|
||||
|
||||
char literalExpected[6];
|
||||
uint8_t literalLen;
|
||||
uint8_t literalPos;
|
||||
};
|
||||
+32
-92
@@ -1,91 +1,48 @@
|
||||
#include "OtaUpdater.h"
|
||||
|
||||
#include <ArduinoJson.h>
|
||||
#include <Logging.h>
|
||||
|
||||
#include "esp_http_client.h"
|
||||
#include "esp_https_ota.h"
|
||||
#include "esp_wifi.h"
|
||||
#include <ReleaseJsonParser.h>
|
||||
#include <esp_crt_bundle.h>
|
||||
#include <esp_http_client.h>
|
||||
#include <esp_https_ota.h>
|
||||
#include <esp_wifi.h>
|
||||
|
||||
namespace {
|
||||
constexpr char latestReleaseUrl[] = "https://api.github.com/repos/crosspoint-reader/crosspoint-reader/releases/latest";
|
||||
|
||||
/* This is buffer and size holder to keep upcoming data from latestReleaseUrl */
|
||||
char* local_buf;
|
||||
int output_len;
|
||||
|
||||
/*
|
||||
* When esp_crt_bundle.h included, it is pointing wrong header file
|
||||
* which is something under WifiClientSecure because of our framework based on arduno platform.
|
||||
* To manage this obstacle, don't include anything, just extern and it will point correct one.
|
||||
*/
|
||||
extern "C" {
|
||||
extern esp_err_t esp_crt_bundle_attach(void* conf);
|
||||
}
|
||||
|
||||
esp_err_t http_client_set_header_cb(esp_http_client_handle_t http_client) {
|
||||
return esp_http_client_set_header(http_client, "User-Agent", "CrossPoint-ESP32-" CROSSPOINT_VERSION);
|
||||
}
|
||||
|
||||
size_t totalBytesReceived = 0;
|
||||
|
||||
esp_err_t event_handler(esp_http_client_event_t* event) {
|
||||
/* We do interested in only HTTP_EVENT_ON_DATA event only */
|
||||
if (event->event_id != HTTP_EVENT_ON_DATA) return ESP_OK;
|
||||
|
||||
if (!esp_http_client_is_chunked_response(event->client)) {
|
||||
int content_len = esp_http_client_get_content_length(event->client);
|
||||
int copy_len = 0;
|
||||
|
||||
if (local_buf == NULL) {
|
||||
/* local_buf life span is tracked by caller checkForUpdate */
|
||||
local_buf = static_cast<char*>(calloc(content_len + 1, sizeof(char)));
|
||||
output_len = 0;
|
||||
if (local_buf == NULL) {
|
||||
LOG_ERR("OTA", "HTTP Client Out of Memory Failed, Allocation %d", content_len);
|
||||
return ESP_ERR_NO_MEM;
|
||||
}
|
||||
}
|
||||
copy_len = min(event->data_len, (content_len - output_len));
|
||||
if (copy_len) {
|
||||
memcpy(local_buf + output_len, event->data, copy_len);
|
||||
}
|
||||
output_len += copy_len;
|
||||
} else {
|
||||
/* Code might be hits here, It happened once (for version checking) but I need more logs to handle that */
|
||||
int chunked_len;
|
||||
esp_http_client_get_chunk_length(event->client, &chunked_len);
|
||||
LOG_DBG("OTA", "esp_http_client_is_chunked_response failed, chunked_len: %d", chunked_len);
|
||||
}
|
||||
|
||||
totalBytesReceived += event->data_len;
|
||||
LOG_DBG("OTA", "HTTP chunk: %d bytes (total: %zu)", event->data_len, totalBytesReceived);
|
||||
auto* parser = static_cast<ReleaseJsonParser*>(event->user_data);
|
||||
parser->feed(static_cast<const char*>(event->data), event->data_len);
|
||||
return ESP_OK;
|
||||
} /* event_handler */
|
||||
} /* namespace */
|
||||
}
|
||||
} // namespace
|
||||
|
||||
OtaUpdater::OtaUpdaterError OtaUpdater::checkForUpdate() {
|
||||
JsonDocument filter;
|
||||
esp_err_t esp_err;
|
||||
JsonDocument doc;
|
||||
ReleaseJsonParser releaseParser;
|
||||
|
||||
esp_http_client_config_t client_config = {
|
||||
.url = latestReleaseUrl,
|
||||
.event_handler = event_handler,
|
||||
/* Default HTTP client buffer size 512 byte only */
|
||||
.buffer_size = 8192,
|
||||
.buffer_size_tx = 8192,
|
||||
.user_data = &releaseParser,
|
||||
.skip_cert_common_name_check = true,
|
||||
.crt_bundle_attach = esp_crt_bundle_attach,
|
||||
.keep_alive_enable = true,
|
||||
};
|
||||
|
||||
/* To track life time of local_buf, dtor will be called on exit from that function */
|
||||
struct localBufCleaner {
|
||||
char** bufPtr;
|
||||
~localBufCleaner() {
|
||||
if (*bufPtr) {
|
||||
free(*bufPtr);
|
||||
*bufPtr = NULL;
|
||||
}
|
||||
}
|
||||
} localBufCleaner = {&local_buf};
|
||||
totalBytesReceived = 0;
|
||||
LOG_DBG("OTA", "Checking for update (current: %s)", CROSSPOINT_VERSION);
|
||||
|
||||
esp_http_client_handle_t client_handle = esp_http_client_init(&client_config);
|
||||
if (!client_handle) {
|
||||
@@ -107,51 +64,34 @@ OtaUpdater::OtaUpdaterError OtaUpdater::checkForUpdate() {
|
||||
return HTTP_ERROR;
|
||||
}
|
||||
|
||||
/* esp_http_client_close will be called inside cleanup as well*/
|
||||
esp_err = esp_http_client_cleanup(client_handle);
|
||||
if (esp_err != ESP_OK) {
|
||||
LOG_ERR("OTA", "esp_http_client_cleanup Failed : %s", esp_err_to_name(esp_err));
|
||||
return INTERNAL_UPDATE_ERROR;
|
||||
}
|
||||
|
||||
filter["tag_name"] = true;
|
||||
filter["assets"][0]["name"] = true;
|
||||
filter["assets"][0]["browser_download_url"] = true;
|
||||
filter["assets"][0]["size"] = true;
|
||||
const DeserializationError error = deserializeJson(doc, local_buf, DeserializationOption::Filter(filter));
|
||||
if (error) {
|
||||
LOG_ERR("OTA", "JSON parse failed: %s", error.c_str());
|
||||
LOG_DBG("OTA", "Response received: %zu bytes total", totalBytesReceived);
|
||||
LOG_DBG("OTA", "Parser results: tag=%s firmware=%s", releaseParser.foundTag() ? "yes" : "no",
|
||||
releaseParser.foundFirmware() ? "yes" : "no");
|
||||
|
||||
if (!releaseParser.foundTag()) {
|
||||
LOG_ERR("OTA", "No tag_name in release JSON");
|
||||
return JSON_PARSE_ERROR;
|
||||
}
|
||||
|
||||
if (!doc["tag_name"].is<std::string>()) {
|
||||
LOG_ERR("OTA", "No tag_name found");
|
||||
return JSON_PARSE_ERROR;
|
||||
}
|
||||
|
||||
if (!doc["assets"].is<JsonArray>()) {
|
||||
LOG_ERR("OTA", "No assets found");
|
||||
return JSON_PARSE_ERROR;
|
||||
}
|
||||
|
||||
latestVersion = doc["tag_name"].as<std::string>();
|
||||
|
||||
for (int i = 0; i < doc["assets"].size(); i++) {
|
||||
if (doc["assets"][i]["name"] == "firmware.bin") {
|
||||
otaUrl = doc["assets"][i]["browser_download_url"].as<std::string>();
|
||||
otaSize = doc["assets"][i]["size"].as<size_t>();
|
||||
totalSize = otaSize;
|
||||
updateAvailable = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!updateAvailable) {
|
||||
if (!releaseParser.foundFirmware()) {
|
||||
LOG_ERR("OTA", "No firmware.bin asset found");
|
||||
return NO_UPDATE;
|
||||
}
|
||||
|
||||
LOG_DBG("OTA", "Found update: %s", latestVersion.c_str());
|
||||
latestVersion = releaseParser.getTagName();
|
||||
otaUrl = releaseParser.getFirmwareUrl();
|
||||
otaSize = releaseParser.getFirmwareSize();
|
||||
totalSize = otaSize;
|
||||
updateAvailable = true;
|
||||
|
||||
LOG_DBG("OTA", "Found update: tag=%s size=%zu", latestVersion.c_str(), otaSize);
|
||||
LOG_DBG("OTA", "Firmware URL: %s", otaUrl.c_str());
|
||||
return OK;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,833 @@
|
||||
#include <cassert>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
#include "lib/JsonParser/ReleaseJsonParser.h"
|
||||
|
||||
static int testsPassed = 0;
|
||||
static int testsFailed = 0;
|
||||
|
||||
#define ASSERT_EQ(a, b) \
|
||||
do { \
|
||||
auto _a = (a); \
|
||||
auto _b = (b); \
|
||||
if (_a != _b) { \
|
||||
fprintf(stderr, " FAIL: %s:%d: %s != expected\n", __FILE__, __LINE__, #a); \
|
||||
testsFailed++; \
|
||||
return; \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
#define ASSERT_STREQ(a, b) \
|
||||
do { \
|
||||
const char* _a = (a); \
|
||||
const char* _b = (b); \
|
||||
if (strcmp(_a, _b) != 0) { \
|
||||
fprintf(stderr, " FAIL: %s:%d: \"%s\" != \"%s\"\n", __FILE__, __LINE__, _a, _b); \
|
||||
testsFailed++; \
|
||||
return; \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
#define ASSERT_TRUE(cond) \
|
||||
do { \
|
||||
if (!(cond)) { \
|
||||
fprintf(stderr, " FAIL: %s:%d: %s\n", __FILE__, __LINE__, #cond); \
|
||||
testsFailed++; \
|
||||
return; \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
#define PASS() testsPassed++
|
||||
|
||||
// ============================================================================
|
||||
// Realistic GitHub release JSON payloads
|
||||
// ============================================================================
|
||||
|
||||
static const char* kRealisticPretty = R"({
|
||||
"url": "https://api.github.com/repos/crosspoint-reader/crosspoint-reader/releases/12345",
|
||||
"assets_url": "https://api.github.com/repos/crosspoint-reader/crosspoint-reader/releases/12345/assets",
|
||||
"upload_url": "https://uploads.github.com/repos/crosspoint-reader/crosspoint-reader/releases/12345/assets{?name,label}",
|
||||
"html_url": "https://github.com/crosspoint-reader/crosspoint-reader/releases/tag/v2.4.1",
|
||||
"id": 12345,
|
||||
"author": {
|
||||
"login": "releasebot",
|
||||
"id": 99887766,
|
||||
"node_id": "MDQ6VXNlcjk5ODg3NzY2",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/99887766?v=4",
|
||||
"url": "https://api.github.com/users/releasebot",
|
||||
"type": "User",
|
||||
"site_admin": false
|
||||
},
|
||||
"node_id": "RE_kwDOAbCdEf4AADBN",
|
||||
"tag_name": "v2.4.1",
|
||||
"target_commitish": "main",
|
||||
"name": "CrossPoint Reader v2.4.1",
|
||||
"draft": false,
|
||||
"prerelease": false,
|
||||
"created_at": "2026-04-28T10:00:00Z",
|
||||
"published_at": "2026-04-28T10:30:00Z",
|
||||
"assets": [
|
||||
{
|
||||
"url": "https://api.github.com/repos/crosspoint-reader/crosspoint-reader/releases/assets/100001",
|
||||
"id": 100001,
|
||||
"node_id": "RA_kwDOAbCdEf4AAGHR",
|
||||
"name": "crosspoint-reader-v2.4.1-source.zip",
|
||||
"label": null,
|
||||
"uploader": {
|
||||
"login": "releasebot",
|
||||
"id": 99887766,
|
||||
"node_id": "MDQ6VXNlcjk5ODg3NzY2",
|
||||
"type": "User"
|
||||
},
|
||||
"content_type": "application/zip",
|
||||
"state": "uploaded",
|
||||
"size": 2048576,
|
||||
"download_count": 42,
|
||||
"created_at": "2026-04-28T10:15:00Z",
|
||||
"updated_at": "2026-04-28T10:15:30Z",
|
||||
"browser_download_url": "https://github.com/crosspoint-reader/crosspoint-reader/releases/download/v2.4.1/crosspoint-reader-v2.4.1-source.zip"
|
||||
},
|
||||
{
|
||||
"url": "https://api.github.com/repos/crosspoint-reader/crosspoint-reader/releases/assets/100002",
|
||||
"id": 100002,
|
||||
"node_id": "RA_kwDOAbCdEf4AAGHS",
|
||||
"name": "firmware.bin",
|
||||
"label": "ESP32-C3 Firmware",
|
||||
"uploader": {
|
||||
"login": "releasebot",
|
||||
"id": 99887766,
|
||||
"node_id": "MDQ6VXNlcjk5ODg3NzY2",
|
||||
"type": "User"
|
||||
},
|
||||
"content_type": "application/octet-stream",
|
||||
"state": "uploaded",
|
||||
"size": 1572864,
|
||||
"download_count": 187,
|
||||
"created_at": "2026-04-28T10:16:00Z",
|
||||
"updated_at": "2026-04-28T10:16:45Z",
|
||||
"browser_download_url": "https://github.com/crosspoint-reader/crosspoint-reader/releases/download/v2.4.1/firmware.bin"
|
||||
},
|
||||
{
|
||||
"url": "https://api.github.com/repos/crosspoint-reader/crosspoint-reader/releases/assets/100003",
|
||||
"id": 100003,
|
||||
"node_id": "RA_kwDOAbCdEf4AAGHR",
|
||||
"name": "checksums.sha256",
|
||||
"label": null,
|
||||
"uploader": {
|
||||
"login": "releasebot",
|
||||
"id": 99887766,
|
||||
"node_id": "MDQ6VXNlcjk5ODg3NzY2",
|
||||
"type": "User"
|
||||
},
|
||||
"content_type": "text/plain",
|
||||
"state": "uploaded",
|
||||
"size": 192,
|
||||
"download_count": 15,
|
||||
"created_at": "2026-04-28T10:17:00Z",
|
||||
"updated_at": "2026-04-28T10:17:10Z",
|
||||
"browser_download_url": "https://github.com/crosspoint-reader/crosspoint-reader/releases/download/v2.4.1/checksums.sha256"
|
||||
}
|
||||
],
|
||||
"tarball_url": "https://api.github.com/repos/crosspoint-reader/crosspoint-reader/tarball/v2.4.1",
|
||||
"zipball_url": "https://api.github.com/repos/crosspoint-reader/crosspoint-reader/zipball/v2.4.1",
|
||||
"body": "## What's Changed\n\n* Fixed orientation crash (#123)\n* Improved EPUB rendering performance\n* Added Serbian translation\n\n**Full Changelog**: https://github.com/crosspoint-reader/crosspoint-reader/compare/v2.4.0...v2.4.1",
|
||||
"reactions": {
|
||||
"url": "https://api.github.com/repos/crosspoint-reader/crosspoint-reader/releases/12345/reactions",
|
||||
"total_count": 5,
|
||||
"+1": 3,
|
||||
"-1": 0,
|
||||
"laugh": 1,
|
||||
"hooray": 1,
|
||||
"confused": 0,
|
||||
"heart": 0,
|
||||
"rocket": 0,
|
||||
"eyes": 0
|
||||
}
|
||||
})";
|
||||
|
||||
static const char* kRealisticMinified =
|
||||
R"({"url":"https://api.github.com/repos/crosspoint-reader/crosspoint-reader/releases/12345","assets_url":"https://api.github.com/repos/crosspoint-reader/crosspoint-reader/releases/12345/assets","id":12345,"author":{"login":"releasebot","id":99887766,"node_id":"MDQ6VXNlcjk5ODg3NzY2","type":"User","site_admin":false},"tag_name":"v2.4.1","target_commitish":"main","name":"CrossPoint Reader v2.4.1","draft":false,"prerelease":false,"assets":[{"url":"https://api.github.com/repos/crosspoint-reader/crosspoint-reader/releases/assets/100001","id":100001,"name":"crosspoint-reader-v2.4.1-source.zip","uploader":{"login":"releasebot","id":99887766},"content_type":"application/zip","state":"uploaded","size":2048576,"download_count":42,"browser_download_url":"https://github.com/crosspoint-reader/crosspoint-reader/releases/download/v2.4.1/crosspoint-reader-v2.4.1-source.zip"},{"url":"https://api.github.com/repos/crosspoint-reader/crosspoint-reader/releases/assets/100002","id":100002,"name":"firmware.bin","uploader":{"login":"releasebot","id":99887766},"content_type":"application/octet-stream","state":"uploaded","size":1572864,"download_count":187,"browser_download_url":"https://github.com/crosspoint-reader/crosspoint-reader/releases/download/v2.4.1/firmware.bin"},{"url":"https://api.github.com/repos/crosspoint-reader/crosspoint-reader/releases/assets/100003","id":100003,"name":"checksums.sha256","uploader":{"login":"releasebot","id":99887766},"content_type":"text/plain","state":"uploaded","size":192,"download_count":15,"browser_download_url":"https://github.com/crosspoint-reader/crosspoint-reader/releases/download/v2.4.1/checksums.sha256"}],"body":"## What's Changed\n\n* Fixed orientation crash","reactions":{"url":"https://api.github.com/repos/crosspoint-reader/crosspoint-reader/releases/12345/reactions","total_count":5,"+1":3}})";
|
||||
|
||||
// Helper: feed JSON in fixed-size chunks
|
||||
static void feedChunked(ReleaseJsonParser& p, const char* json, size_t chunkSize) {
|
||||
size_t len = strlen(json);
|
||||
for (size_t off = 0; off < len; off += chunkSize) {
|
||||
size_t n = len - off < chunkSize ? len - off : chunkSize;
|
||||
p.feed(json + off, n);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Tests
|
||||
// ============================================================================
|
||||
|
||||
void testRealisticPrettyPrinted() {
|
||||
printf("testRealisticPrettyPrinted...\n");
|
||||
|
||||
ReleaseJsonParser p;
|
||||
p.feed(kRealisticPretty, strlen(kRealisticPretty));
|
||||
|
||||
ASSERT_TRUE(p.foundTag());
|
||||
ASSERT_TRUE(p.foundFirmware());
|
||||
ASSERT_STREQ(p.getTagName(), "v2.4.1");
|
||||
ASSERT_STREQ(p.getFirmwareUrl(),
|
||||
"https://github.com/crosspoint-reader/crosspoint-reader/releases/download/v2.4.1/firmware.bin");
|
||||
ASSERT_EQ(p.getFirmwareSize(), 1572864u);
|
||||
|
||||
printf(" passed\n");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void testRealisticMinified() {
|
||||
printf("testRealisticMinified...\n");
|
||||
|
||||
ReleaseJsonParser p;
|
||||
p.feed(kRealisticMinified, strlen(kRealisticMinified));
|
||||
|
||||
ASSERT_TRUE(p.foundTag());
|
||||
ASSERT_TRUE(p.foundFirmware());
|
||||
ASSERT_STREQ(p.getTagName(), "v2.4.1");
|
||||
ASSERT_STREQ(p.getFirmwareUrl(),
|
||||
"https://github.com/crosspoint-reader/crosspoint-reader/releases/download/v2.4.1/firmware.bin");
|
||||
ASSERT_EQ(p.getFirmwareSize(), 1572864u);
|
||||
|
||||
printf(" passed\n");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void testPrettyAndMinifiedAgree() {
|
||||
printf("testPrettyAndMinifiedAgree...\n");
|
||||
|
||||
ReleaseJsonParser pretty;
|
||||
pretty.feed(kRealisticPretty, strlen(kRealisticPretty));
|
||||
|
||||
ReleaseJsonParser minified;
|
||||
minified.feed(kRealisticMinified, strlen(kRealisticMinified));
|
||||
|
||||
ASSERT_STREQ(pretty.getTagName(), minified.getTagName());
|
||||
ASSERT_STREQ(pretty.getFirmwareUrl(), minified.getFirmwareUrl());
|
||||
ASSERT_EQ(pretty.getFirmwareSize(), minified.getFirmwareSize());
|
||||
|
||||
printf(" passed\n");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void testFirmwareNotFirstAsset() {
|
||||
printf("testFirmwareNotFirstAsset...\n");
|
||||
|
||||
// firmware.bin is the third of four assets
|
||||
const char* json = R"({
|
||||
"tag_name": "v1.0.0",
|
||||
"assets": [
|
||||
{"name": "source.tar.gz", "browser_download_url": "https://example.com/src.tar.gz", "size": 500000},
|
||||
{"name": "docs.pdf", "browser_download_url": "https://example.com/docs.pdf", "size": 120000},
|
||||
{"name": "firmware.bin", "browser_download_url": "https://example.com/firmware.bin", "size": 987654},
|
||||
{"name": "checksums.txt", "browser_download_url": "https://example.com/checksums.txt", "size": 256}
|
||||
]
|
||||
})";
|
||||
|
||||
ReleaseJsonParser p;
|
||||
p.feed(json, strlen(json));
|
||||
|
||||
ASSERT_TRUE(p.foundTag());
|
||||
ASSERT_TRUE(p.foundFirmware());
|
||||
ASSERT_STREQ(p.getTagName(), "v1.0.0");
|
||||
ASSERT_STREQ(p.getFirmwareUrl(), "https://example.com/firmware.bin");
|
||||
ASSERT_EQ(p.getFirmwareSize(), 987654u);
|
||||
|
||||
printf(" passed\n");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void testFieldOrderUrlBeforeName() {
|
||||
printf("testFieldOrderUrlBeforeName...\n");
|
||||
|
||||
const char* json = R"({
|
||||
"tag_name": "v3.0",
|
||||
"assets": [{
|
||||
"browser_download_url": "https://example.com/fw.bin",
|
||||
"name": "firmware.bin",
|
||||
"size": 2222
|
||||
}]
|
||||
})";
|
||||
|
||||
ReleaseJsonParser p;
|
||||
p.feed(json, strlen(json));
|
||||
|
||||
ASSERT_TRUE(p.foundFirmware());
|
||||
ASSERT_STREQ(p.getFirmwareUrl(), "https://example.com/fw.bin");
|
||||
ASSERT_EQ(p.getFirmwareSize(), 2222u);
|
||||
|
||||
printf(" passed\n");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void testFieldOrderSizeBeforeUrl() {
|
||||
printf("testFieldOrderSizeBeforeUrl...\n");
|
||||
|
||||
const char* json = R"({
|
||||
"tag_name": "v3.1",
|
||||
"assets": [{
|
||||
"size": 3333,
|
||||
"browser_download_url": "https://example.com/fw2.bin",
|
||||
"name": "firmware.bin"
|
||||
}]
|
||||
})";
|
||||
|
||||
ReleaseJsonParser p;
|
||||
p.feed(json, strlen(json));
|
||||
|
||||
ASSERT_TRUE(p.foundFirmware());
|
||||
ASSERT_STREQ(p.getFirmwareUrl(), "https://example.com/fw2.bin");
|
||||
ASSERT_EQ(p.getFirmwareSize(), 3333u);
|
||||
|
||||
printf(" passed\n");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void testFieldOrderNameFirst() {
|
||||
printf("testFieldOrderNameFirst...\n");
|
||||
|
||||
const char* json = R"({
|
||||
"tag_name": "v3.2",
|
||||
"assets": [{
|
||||
"name": "firmware.bin",
|
||||
"size": 4444,
|
||||
"browser_download_url": "https://example.com/fw3.bin"
|
||||
}]
|
||||
})";
|
||||
|
||||
ReleaseJsonParser p;
|
||||
p.feed(json, strlen(json));
|
||||
|
||||
ASSERT_TRUE(p.foundFirmware());
|
||||
ASSERT_STREQ(p.getFirmwareUrl(), "https://example.com/fw3.bin");
|
||||
ASSERT_EQ(p.getFirmwareSize(), 4444u);
|
||||
|
||||
printf(" passed\n");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void testAssetsBeforeTagName() {
|
||||
printf("testAssetsBeforeTagName...\n");
|
||||
|
||||
// tag_name appears after assets in the JSON
|
||||
const char* json = R"({
|
||||
"name": "Release",
|
||||
"assets": [{
|
||||
"name": "firmware.bin",
|
||||
"browser_download_url": "https://example.com/fw.bin",
|
||||
"size": 5555
|
||||
}],
|
||||
"tag_name": "v4.0"
|
||||
})";
|
||||
|
||||
ReleaseJsonParser p;
|
||||
p.feed(json, strlen(json));
|
||||
|
||||
ASSERT_TRUE(p.foundTag());
|
||||
ASSERT_TRUE(p.foundFirmware());
|
||||
ASSERT_STREQ(p.getTagName(), "v4.0");
|
||||
ASSERT_STREQ(p.getFirmwareUrl(), "https://example.com/fw.bin");
|
||||
ASSERT_EQ(p.getFirmwareSize(), 5555u);
|
||||
|
||||
printf(" passed\n");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void testChunkedFeedingRealisticSmallChunks() {
|
||||
printf("testChunkedFeedingRealisticSmallChunks...\n");
|
||||
|
||||
// Simulate HTTP chunked transfer with small chunks (64 bytes)
|
||||
ReleaseJsonParser p;
|
||||
feedChunked(p, kRealisticPretty, 64);
|
||||
|
||||
ASSERT_TRUE(p.foundTag());
|
||||
ASSERT_TRUE(p.foundFirmware());
|
||||
ASSERT_STREQ(p.getTagName(), "v2.4.1");
|
||||
ASSERT_STREQ(p.getFirmwareUrl(),
|
||||
"https://github.com/crosspoint-reader/crosspoint-reader/releases/download/v2.4.1/firmware.bin");
|
||||
ASSERT_EQ(p.getFirmwareSize(), 1572864u);
|
||||
|
||||
printf(" passed\n");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void testChunkedFeedingByteByByte() {
|
||||
printf("testChunkedFeedingByteByByte...\n");
|
||||
|
||||
ReleaseJsonParser p;
|
||||
feedChunked(p, kRealisticMinified, 1);
|
||||
|
||||
ASSERT_TRUE(p.foundTag());
|
||||
ASSERT_TRUE(p.foundFirmware());
|
||||
ASSERT_STREQ(p.getTagName(), "v2.4.1");
|
||||
ASSERT_EQ(p.getFirmwareSize(), 1572864u);
|
||||
|
||||
printf(" passed\n");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void testChunkedFeedingVariousChunkSizes() {
|
||||
printf("testChunkedFeedingVariousChunkSizes...\n");
|
||||
|
||||
for (size_t chunkSize : {3, 7, 13, 31, 97, 128, 256, 512, 1024}) {
|
||||
ReleaseJsonParser p;
|
||||
feedChunked(p, kRealisticPretty, chunkSize);
|
||||
|
||||
ASSERT_TRUE(p.foundTag());
|
||||
ASSERT_TRUE(p.foundFirmware());
|
||||
ASSERT_STREQ(p.getTagName(), "v2.4.1");
|
||||
ASSERT_EQ(p.getFirmwareSize(), 1572864u);
|
||||
}
|
||||
|
||||
printf(" passed (9 chunk sizes)\n");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void testMissingTagName() {
|
||||
printf("testMissingTagName...\n");
|
||||
|
||||
const char* json = R"({
|
||||
"name": "Some Release",
|
||||
"draft": false,
|
||||
"assets": [{
|
||||
"name": "firmware.bin",
|
||||
"browser_download_url": "https://example.com/fw.bin",
|
||||
"size": 1000
|
||||
}]
|
||||
})";
|
||||
|
||||
ReleaseJsonParser p;
|
||||
p.feed(json, strlen(json));
|
||||
|
||||
ASSERT_TRUE(!p.foundTag());
|
||||
ASSERT_TRUE(p.foundFirmware());
|
||||
ASSERT_STREQ(p.getTagName(), "");
|
||||
|
||||
printf(" passed\n");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void testMissingFirmwareBinAsset() {
|
||||
printf("testMissingFirmwareBinAsset...\n");
|
||||
|
||||
const char* json = R"({
|
||||
"tag_name": "v1.0.0",
|
||||
"assets": [
|
||||
{"name": "source.zip", "browser_download_url": "https://example.com/src.zip", "size": 1000},
|
||||
{"name": "docs.tar.gz", "browser_download_url": "https://example.com/docs.tar.gz", "size": 2000}
|
||||
]
|
||||
})";
|
||||
|
||||
ReleaseJsonParser p;
|
||||
p.feed(json, strlen(json));
|
||||
|
||||
ASSERT_TRUE(p.foundTag());
|
||||
ASSERT_TRUE(!p.foundFirmware());
|
||||
ASSERT_STREQ(p.getTagName(), "v1.0.0");
|
||||
ASSERT_STREQ(p.getFirmwareUrl(), "");
|
||||
ASSERT_EQ(p.getFirmwareSize(), 0u);
|
||||
|
||||
printf(" passed\n");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void testEmptyAssetsArray() {
|
||||
printf("testEmptyAssetsArray...\n");
|
||||
|
||||
const char* json = R"({"tag_name": "v1.0.0", "assets": []})";
|
||||
|
||||
ReleaseJsonParser p;
|
||||
p.feed(json, strlen(json));
|
||||
|
||||
ASSERT_TRUE(p.foundTag());
|
||||
ASSERT_TRUE(!p.foundFirmware());
|
||||
|
||||
printf(" passed\n");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void testNoAssetsKey() {
|
||||
printf("testNoAssetsKey...\n");
|
||||
|
||||
const char* json = R"({"tag_name": "v1.0.0", "name": "Release"})";
|
||||
|
||||
ReleaseJsonParser p;
|
||||
p.feed(json, strlen(json));
|
||||
|
||||
ASSERT_TRUE(p.foundTag());
|
||||
ASSERT_TRUE(!p.foundFirmware());
|
||||
|
||||
printf(" passed\n");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void testTruncatedBeforeTagValue() {
|
||||
printf("testTruncatedBeforeTagValue...\n");
|
||||
|
||||
const char* json = R"({"tag_name": )";
|
||||
|
||||
ReleaseJsonParser p;
|
||||
p.feed(json, strlen(json));
|
||||
|
||||
ASSERT_TRUE(!p.foundTag());
|
||||
ASSERT_TRUE(!p.foundFirmware());
|
||||
|
||||
printf(" passed\n");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void testTruncatedInsideTagValue() {
|
||||
printf("testTruncatedInsideTagValue...\n");
|
||||
|
||||
const char* json = R"({"tag_name": "v2.4)";
|
||||
|
||||
ReleaseJsonParser p;
|
||||
p.feed(json, strlen(json));
|
||||
|
||||
ASSERT_TRUE(!p.foundTag());
|
||||
|
||||
printf(" passed\n");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void testTruncatedInsideAssetsArray() {
|
||||
printf("testTruncatedInsideAssetsArray...\n");
|
||||
|
||||
const char* json = R"({"tag_name": "v2.4.1", "assets": [{"name": "firm)";
|
||||
|
||||
ReleaseJsonParser p;
|
||||
p.feed(json, strlen(json));
|
||||
|
||||
ASSERT_TRUE(p.foundTag());
|
||||
ASSERT_STREQ(p.getTagName(), "v2.4.1");
|
||||
ASSERT_TRUE(!p.foundFirmware());
|
||||
|
||||
printf(" passed\n");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void testTruncatedAfterFirmwareName() {
|
||||
printf("testTruncatedAfterFirmwareName...\n");
|
||||
|
||||
// Found the name but connection dropped before URL/size
|
||||
const char* json = R"({"tag_name":"v1.0","assets":[{"name":"firmware.bin","browser_dow)";
|
||||
|
||||
ReleaseJsonParser p;
|
||||
p.feed(json, strlen(json));
|
||||
|
||||
ASSERT_TRUE(p.foundTag());
|
||||
ASSERT_TRUE(!p.foundFirmware());
|
||||
|
||||
printf(" passed\n");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void testTruncatedRealisticJson() {
|
||||
printf("testTruncatedRealisticJson...\n");
|
||||
|
||||
// Truncate the realistic JSON at various points; none should crash
|
||||
std::string full(kRealisticPretty);
|
||||
for (size_t cutPoint : {10u, 50u, 100u, 200u, 500u, 1000u, 1500u, 2000u}) {
|
||||
if (cutPoint >= full.size()) continue;
|
||||
|
||||
ReleaseJsonParser p;
|
||||
p.feed(full.c_str(), cutPoint);
|
||||
// Just verify no crash; results depend on where we cut
|
||||
(void)p.foundTag();
|
||||
(void)p.foundFirmware();
|
||||
}
|
||||
|
||||
printf(" passed (no crashes on truncated realistic JSON)\n");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void testNestedObjectsInAsset() {
|
||||
printf("testNestedObjectsInAsset...\n");
|
||||
|
||||
// Asset with deeply nested "uploader" object -- should not confuse depth tracking
|
||||
const char* json = R"({
|
||||
"tag_name": "v5.0",
|
||||
"assets": [{
|
||||
"name": "firmware.bin",
|
||||
"uploader": {
|
||||
"login": "bot",
|
||||
"id": 42,
|
||||
"permissions": {"admin": false, "push": true, "pull": true}
|
||||
},
|
||||
"browser_download_url": "https://example.com/fw5.bin",
|
||||
"size": 8888
|
||||
}]
|
||||
})";
|
||||
|
||||
ReleaseJsonParser p;
|
||||
p.feed(json, strlen(json));
|
||||
|
||||
ASSERT_TRUE(p.foundFirmware());
|
||||
ASSERT_STREQ(p.getFirmwareUrl(), "https://example.com/fw5.bin");
|
||||
ASSERT_EQ(p.getFirmwareSize(), 8888u);
|
||||
|
||||
printf(" passed\n");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void testNestedObjectsAtTopLevel() {
|
||||
printf("testNestedObjectsAtTopLevel...\n");
|
||||
|
||||
// Multiple nested objects at the top level before/after tag_name and assets
|
||||
const char* json = R"({
|
||||
"author": {"login": "dev", "id": 1, "nested": {"deep": true}},
|
||||
"tag_name": "v6.0",
|
||||
"reactions": {"url": "https://reactions", "total_count": 0, "+1": 0},
|
||||
"assets": [{"name": "firmware.bin", "browser_download_url": "https://fw6", "size": 1111}],
|
||||
"mentions_count": 3
|
||||
})";
|
||||
|
||||
ReleaseJsonParser p;
|
||||
p.feed(json, strlen(json));
|
||||
|
||||
ASSERT_TRUE(p.foundTag());
|
||||
ASSERT_TRUE(p.foundFirmware());
|
||||
ASSERT_STREQ(p.getTagName(), "v6.0");
|
||||
ASSERT_EQ(p.getFirmwareSize(), 1111u);
|
||||
|
||||
printf(" passed\n");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void testArraysAtTopLevel() {
|
||||
printf("testArraysAtTopLevel...\n");
|
||||
|
||||
// A non-assets array at the top level should not interfere
|
||||
const char* json = R"({
|
||||
"tag_name": "v7.0",
|
||||
"labels": ["release", "stable"],
|
||||
"assets": [{"name": "firmware.bin", "browser_download_url": "https://fw7", "size": 7070}]
|
||||
})";
|
||||
|
||||
ReleaseJsonParser p;
|
||||
p.feed(json, strlen(json));
|
||||
|
||||
ASSERT_TRUE(p.foundTag());
|
||||
ASSERT_TRUE(p.foundFirmware());
|
||||
ASSERT_STREQ(p.getTagName(), "v7.0");
|
||||
ASSERT_EQ(p.getFirmwareSize(), 7070u);
|
||||
|
||||
printf(" passed\n");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void testResetAndReuse() {
|
||||
printf("testResetAndReuse...\n");
|
||||
|
||||
ReleaseJsonParser p;
|
||||
|
||||
const char* json1 =
|
||||
R"({"tag_name":"v1.0","assets":[{"name":"firmware.bin","browser_download_url":"https://a","size":1}]})";
|
||||
p.feed(json1, strlen(json1));
|
||||
ASSERT_TRUE(p.foundTag());
|
||||
ASSERT_STREQ(p.getTagName(), "v1.0");
|
||||
ASSERT_STREQ(p.getFirmwareUrl(), "https://a");
|
||||
ASSERT_EQ(p.getFirmwareSize(), 1u);
|
||||
|
||||
p.reset();
|
||||
|
||||
// Second document with different values
|
||||
const char* json2 =
|
||||
R"({"tag_name":"v2.0","assets":[{"name":"firmware.bin","browser_download_url":"https://b","size":2}]})";
|
||||
p.feed(json2, strlen(json2));
|
||||
ASSERT_TRUE(p.foundTag());
|
||||
ASSERT_STREQ(p.getTagName(), "v2.0");
|
||||
ASSERT_STREQ(p.getFirmwareUrl(), "https://b");
|
||||
ASSERT_EQ(p.getFirmwareSize(), 2u);
|
||||
|
||||
printf(" passed\n");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void testResetClearsState() {
|
||||
printf("testResetClearsState...\n");
|
||||
|
||||
ReleaseJsonParser p;
|
||||
|
||||
const char* json =
|
||||
R"({"tag_name":"v1.0","assets":[{"name":"firmware.bin","browser_download_url":"https://a","size":100}]})";
|
||||
p.feed(json, strlen(json));
|
||||
ASSERT_TRUE(p.foundTag());
|
||||
ASSERT_TRUE(p.foundFirmware());
|
||||
|
||||
p.reset();
|
||||
|
||||
ASSERT_TRUE(!p.foundTag());
|
||||
ASSERT_TRUE(!p.foundFirmware());
|
||||
ASSERT_STREQ(p.getTagName(), "");
|
||||
ASSERT_STREQ(p.getFirmwareUrl(), "");
|
||||
ASSERT_EQ(p.getFirmwareSize(), 0u);
|
||||
|
||||
printf(" passed\n");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void testPartialAssetNameMatch() {
|
||||
printf("testPartialAssetNameMatch...\n");
|
||||
|
||||
// "firmware.bin.bak" should NOT match "firmware.bin"
|
||||
const char* json = R"({
|
||||
"tag_name": "v1.0",
|
||||
"assets": [
|
||||
{"name": "firmware.bin.bak", "browser_download_url": "https://bak", "size": 100},
|
||||
{"name": "firmware.bin.old", "browser_download_url": "https://old", "size": 200}
|
||||
]
|
||||
})";
|
||||
|
||||
ReleaseJsonParser p;
|
||||
p.feed(json, strlen(json));
|
||||
|
||||
ASSERT_TRUE(p.foundTag());
|
||||
ASSERT_TRUE(!p.foundFirmware());
|
||||
|
||||
printf(" passed\n");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void testFirmwareBinExactMatch() {
|
||||
printf("testFirmwareBinExactMatch...\n");
|
||||
|
||||
// Only exact "firmware.bin" matches, not similar names
|
||||
const char* json = R"({
|
||||
"tag_name": "v1.0",
|
||||
"assets": [
|
||||
{"name": "FIRMWARE.BIN", "browser_download_url": "https://upper", "size": 100},
|
||||
{"name": "firmware.bin", "browser_download_url": "https://exact", "size": 200},
|
||||
{"name": "firmware.bin2", "browser_download_url": "https://suffix", "size": 300}
|
||||
]
|
||||
})";
|
||||
|
||||
ReleaseJsonParser p;
|
||||
p.feed(json, strlen(json));
|
||||
|
||||
ASSERT_TRUE(p.foundFirmware());
|
||||
ASSERT_STREQ(p.getFirmwareUrl(), "https://exact");
|
||||
ASSERT_EQ(p.getFirmwareSize(), 200u);
|
||||
|
||||
printf(" passed\n");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void testLargeSize() {
|
||||
printf("testLargeSize...\n");
|
||||
|
||||
// 16MB firmware (maximum flash size)
|
||||
const char* json =
|
||||
R"({"tag_name":"v1.0","assets":[{"name":"firmware.bin","browser_download_url":"https://fw","size":16777216}]})";
|
||||
|
||||
ReleaseJsonParser p;
|
||||
p.feed(json, strlen(json));
|
||||
|
||||
ASSERT_EQ(p.getFirmwareSize(), 16777216u);
|
||||
|
||||
printf(" passed\n");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void testSizeZero() {
|
||||
printf("testSizeZero...\n");
|
||||
|
||||
const char* json =
|
||||
R"({"tag_name":"v1.0","assets":[{"name":"firmware.bin","browser_download_url":"https://fw","size":0}]})";
|
||||
|
||||
ReleaseJsonParser p;
|
||||
p.feed(json, strlen(json));
|
||||
|
||||
ASSERT_TRUE(p.foundFirmware());
|
||||
ASSERT_EQ(p.getFirmwareSize(), 0u);
|
||||
|
||||
printf(" passed\n");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void testMinimalValidJson() {
|
||||
printf("testMinimalValidJson...\n");
|
||||
|
||||
const char* json = R"({"tag_name":"v0","assets":[{"name":"firmware.bin","browser_download_url":"u","size":1}]})";
|
||||
|
||||
ReleaseJsonParser p;
|
||||
p.feed(json, strlen(json));
|
||||
|
||||
ASSERT_TRUE(p.foundTag());
|
||||
ASSERT_TRUE(p.foundFirmware());
|
||||
ASSERT_STREQ(p.getTagName(), "v0");
|
||||
ASSERT_STREQ(p.getFirmwareUrl(), "u");
|
||||
ASSERT_EQ(p.getFirmwareSize(), 1u);
|
||||
|
||||
printf(" passed\n");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void testChunkedRealisticEveryBoundary() {
|
||||
printf("testChunkedRealisticEveryBoundary...\n");
|
||||
|
||||
// Two-chunk split at every byte boundary on a compact JSON
|
||||
const char* json =
|
||||
R"({"tag_name":"v2.0","assets":[{"name":"firmware.bin","browser_download_url":"https://example.com/fw","size":9999}]})";
|
||||
size_t len = strlen(json);
|
||||
|
||||
for (size_t split = 0; split <= len; ++split) {
|
||||
ReleaseJsonParser p;
|
||||
if (split > 0) p.feed(json, split);
|
||||
if (split < len) p.feed(json + split, len - split);
|
||||
|
||||
ASSERT_TRUE(p.foundTag());
|
||||
ASSERT_TRUE(p.foundFirmware());
|
||||
ASSERT_STREQ(p.getTagName(), "v2.0");
|
||||
ASSERT_STREQ(p.getFirmwareUrl(), "https://example.com/fw");
|
||||
ASSERT_EQ(p.getFirmwareSize(), 9999u);
|
||||
}
|
||||
|
||||
printf(" passed (all %zu split points)\n", len + 1);
|
||||
PASS();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
int main() {
|
||||
printf("=== ReleaseJsonParser Tests ===\n\n");
|
||||
|
||||
testRealisticPrettyPrinted();
|
||||
testRealisticMinified();
|
||||
testPrettyAndMinifiedAgree();
|
||||
testFirmwareNotFirstAsset();
|
||||
testFieldOrderUrlBeforeName();
|
||||
testFieldOrderSizeBeforeUrl();
|
||||
testFieldOrderNameFirst();
|
||||
testAssetsBeforeTagName();
|
||||
testChunkedFeedingRealisticSmallChunks();
|
||||
testChunkedFeedingByteByByte();
|
||||
testChunkedFeedingVariousChunkSizes();
|
||||
testMissingTagName();
|
||||
testMissingFirmwareBinAsset();
|
||||
testEmptyAssetsArray();
|
||||
testNoAssetsKey();
|
||||
testTruncatedBeforeTagValue();
|
||||
testTruncatedInsideTagValue();
|
||||
testTruncatedInsideAssetsArray();
|
||||
testTruncatedAfterFirmwareName();
|
||||
testTruncatedRealisticJson();
|
||||
testNestedObjectsInAsset();
|
||||
testNestedObjectsAtTopLevel();
|
||||
testArraysAtTopLevel();
|
||||
testResetAndReuse();
|
||||
testResetClearsState();
|
||||
testPartialAssetNameMatch();
|
||||
testFirmwareBinExactMatch();
|
||||
testLargeSize();
|
||||
testSizeZero();
|
||||
testMinimalValidJson();
|
||||
testChunkedRealisticEveryBoundary();
|
||||
|
||||
printf("\n=== Results: %d passed, %d failed ===\n", testsPassed, testsFailed);
|
||||
return testsFailed > 0 ? 1 : 0;
|
||||
}
|
||||
Executable
+29
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
BUILD_DIR="$ROOT_DIR/build/release_json_parser"
|
||||
BINARY="$BUILD_DIR/ReleaseJsonParserTest"
|
||||
|
||||
mkdir -p "$BUILD_DIR"
|
||||
|
||||
SOURCES=(
|
||||
"$ROOT_DIR/test/release_json_parser/ReleaseJsonParserTest.cpp"
|
||||
"$ROOT_DIR/lib/JsonParser/ReleaseJsonParser.cpp"
|
||||
"$ROOT_DIR/lib/JsonParser/StreamingJsonParser.cpp"
|
||||
)
|
||||
|
||||
CXXFLAGS=(
|
||||
-std=c++20
|
||||
-O2
|
||||
-Wall
|
||||
-Wextra
|
||||
-pedantic
|
||||
-I"$ROOT_DIR"
|
||||
-I"$ROOT_DIR/lib"
|
||||
-I"$ROOT_DIR/lib/JsonParser"
|
||||
)
|
||||
|
||||
c++ "${CXXFLAGS[@]}" "${SOURCES[@]}" -o "$BINARY"
|
||||
|
||||
"$BINARY" "$@"
|
||||
Executable
+28
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
BUILD_DIR="$ROOT_DIR/build/streaming_json_parser"
|
||||
BINARY="$BUILD_DIR/StreamingJsonParserTest"
|
||||
|
||||
mkdir -p "$BUILD_DIR"
|
||||
|
||||
SOURCES=(
|
||||
"$ROOT_DIR/test/streaming_json_parser/StreamingJsonParserTest.cpp"
|
||||
"$ROOT_DIR/lib/JsonParser/StreamingJsonParser.cpp"
|
||||
)
|
||||
|
||||
CXXFLAGS=(
|
||||
-std=c++20
|
||||
-O2
|
||||
-Wall
|
||||
-Wextra
|
||||
-pedantic
|
||||
-I"$ROOT_DIR"
|
||||
-I"$ROOT_DIR/lib"
|
||||
-I"$ROOT_DIR/lib/JsonParser"
|
||||
)
|
||||
|
||||
c++ "${CXXFLAGS[@]}" "${SOURCES[@]}" -o "$BINARY"
|
||||
|
||||
"$BINARY" "$@"
|
||||
@@ -0,0 +1,737 @@
|
||||
#include <cassert>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "lib/JsonParser/StreamingJsonParser.h"
|
||||
|
||||
static int testsPassed = 0;
|
||||
static int testsFailed = 0;
|
||||
|
||||
#define ASSERT_EQ(a, b) \
|
||||
do { \
|
||||
auto _a = (a); \
|
||||
auto _b = (b); \
|
||||
if (_a != _b) { \
|
||||
fprintf(stderr, " FAIL: %s:%d: %s != expected\n", __FILE__, __LINE__, #a); \
|
||||
testsFailed++; \
|
||||
return; \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
#define ASSERT_TRUE(cond) \
|
||||
do { \
|
||||
if (!(cond)) { \
|
||||
fprintf(stderr, " FAIL: %s:%d: %s\n", __FILE__, __LINE__, #cond); \
|
||||
testsFailed++; \
|
||||
return; \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
#define PASS() testsPassed++
|
||||
|
||||
// Event types for recording callback sequences
|
||||
enum class EventType {
|
||||
KEY,
|
||||
STRING,
|
||||
NUMBER,
|
||||
BOOL_TRUE,
|
||||
BOOL_FALSE,
|
||||
NULL_VAL,
|
||||
OBJECT_START,
|
||||
OBJECT_END,
|
||||
ARRAY_START,
|
||||
ARRAY_END,
|
||||
};
|
||||
|
||||
struct Event {
|
||||
EventType type;
|
||||
std::string value;
|
||||
};
|
||||
|
||||
struct TestContext {
|
||||
std::vector<Event> events;
|
||||
};
|
||||
|
||||
static void onKey(void* ctx, const char* key, size_t len) {
|
||||
static_cast<TestContext*>(ctx)->events.push_back({EventType::KEY, std::string(key, len)});
|
||||
}
|
||||
static void onString(void* ctx, const char* value, size_t len) {
|
||||
static_cast<TestContext*>(ctx)->events.push_back({EventType::STRING, std::string(value, len)});
|
||||
}
|
||||
static void onNumber(void* ctx, const char* value, size_t len) {
|
||||
static_cast<TestContext*>(ctx)->events.push_back({EventType::NUMBER, std::string(value, len)});
|
||||
}
|
||||
static void onBool(void* ctx, bool value) {
|
||||
static_cast<TestContext*>(ctx)->events.push_back({value ? EventType::BOOL_TRUE : EventType::BOOL_FALSE, {}});
|
||||
}
|
||||
static void onNull(void* ctx) { static_cast<TestContext*>(ctx)->events.push_back({EventType::NULL_VAL, {}}); }
|
||||
static void onObjectStart(void* ctx) {
|
||||
static_cast<TestContext*>(ctx)->events.push_back({EventType::OBJECT_START, {}});
|
||||
}
|
||||
static void onObjectEnd(void* ctx) { static_cast<TestContext*>(ctx)->events.push_back({EventType::OBJECT_END, {}}); }
|
||||
static void onArrayStart(void* ctx) { static_cast<TestContext*>(ctx)->events.push_back({EventType::ARRAY_START, {}}); }
|
||||
static void onArrayEnd(void* ctx) { static_cast<TestContext*>(ctx)->events.push_back({EventType::ARRAY_END, {}}); }
|
||||
|
||||
static JsonCallbacks makeCallbacks(TestContext* ctx) {
|
||||
return {ctx, onKey, onString, onNumber, onBool, onNull, onObjectStart, onObjectEnd, onArrayStart, onArrayEnd};
|
||||
}
|
||||
|
||||
// Feed entire input at once
|
||||
static std::vector<Event> parse(const char* json) {
|
||||
TestContext ctx;
|
||||
StreamingJsonParser parser(makeCallbacks(&ctx));
|
||||
parser.feed(json, strlen(json));
|
||||
return ctx.events;
|
||||
}
|
||||
|
||||
// Feed input one byte at a time
|
||||
static std::vector<Event> parseBytewise(const char* json) {
|
||||
TestContext ctx;
|
||||
StreamingJsonParser parser(makeCallbacks(&ctx));
|
||||
size_t len = strlen(json);
|
||||
for (size_t i = 0; i < len; ++i) {
|
||||
parser.feed(json + i, 1);
|
||||
}
|
||||
return ctx.events;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Tests
|
||||
// ============================================================================
|
||||
|
||||
void testSimpleObject() {
|
||||
printf("testSimpleObject...\n");
|
||||
|
||||
auto events = parse(R"({"key": "value", "num": 42})");
|
||||
|
||||
ASSERT_EQ(events.size(), 6u);
|
||||
ASSERT_EQ(events[0].type, EventType::OBJECT_START);
|
||||
ASSERT_EQ(events[1].type, EventType::KEY);
|
||||
ASSERT_EQ(events[1].value, "key");
|
||||
ASSERT_EQ(events[2].type, EventType::STRING);
|
||||
ASSERT_EQ(events[2].value, "value");
|
||||
ASSERT_EQ(events[3].type, EventType::KEY);
|
||||
ASSERT_EQ(events[3].value, "num");
|
||||
ASSERT_EQ(events[4].type, EventType::NUMBER);
|
||||
ASSERT_EQ(events[4].value, "42");
|
||||
ASSERT_EQ(events[5].type, EventType::OBJECT_END);
|
||||
|
||||
printf(" passed\n");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void testNestedObjects() {
|
||||
printf("testNestedObjects...\n");
|
||||
|
||||
auto events = parse(R"({"a": {"b": "c"}})");
|
||||
|
||||
ASSERT_EQ(events.size(), 7u);
|
||||
ASSERT_EQ(events[0].type, EventType::OBJECT_START);
|
||||
ASSERT_EQ(events[1].type, EventType::KEY);
|
||||
ASSERT_EQ(events[1].value, "a");
|
||||
ASSERT_EQ(events[2].type, EventType::OBJECT_START);
|
||||
ASSERT_EQ(events[3].type, EventType::KEY);
|
||||
ASSERT_EQ(events[3].value, "b");
|
||||
ASSERT_EQ(events[4].type, EventType::STRING);
|
||||
ASSERT_EQ(events[4].value, "c");
|
||||
ASSERT_EQ(events[5].type, EventType::OBJECT_END);
|
||||
ASSERT_EQ(events[6].type, EventType::OBJECT_END);
|
||||
|
||||
printf(" passed\n");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void testArrayOfValues() {
|
||||
printf("testArrayOfValues...\n");
|
||||
|
||||
auto events = parse(R"({"items": [1, "two", true, false, null]})");
|
||||
|
||||
ASSERT_EQ(events.size(), 10u);
|
||||
ASSERT_EQ(events[0].type, EventType::OBJECT_START);
|
||||
ASSERT_EQ(events[1].type, EventType::KEY);
|
||||
ASSERT_EQ(events[1].value, "items");
|
||||
ASSERT_EQ(events[2].type, EventType::ARRAY_START);
|
||||
ASSERT_EQ(events[3].type, EventType::NUMBER);
|
||||
ASSERT_EQ(events[3].value, "1");
|
||||
ASSERT_EQ(events[4].type, EventType::STRING);
|
||||
ASSERT_EQ(events[4].value, "two");
|
||||
ASSERT_EQ(events[5].type, EventType::BOOL_TRUE);
|
||||
ASSERT_EQ(events[6].type, EventType::BOOL_FALSE);
|
||||
ASSERT_EQ(events[7].type, EventType::NULL_VAL);
|
||||
ASSERT_EQ(events[8].type, EventType::ARRAY_END);
|
||||
ASSERT_EQ(events[9].type, EventType::OBJECT_END);
|
||||
|
||||
printf(" passed\n");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void testArrayOfObjects() {
|
||||
printf("testArrayOfObjects...\n");
|
||||
|
||||
auto events = parse(R"([{"a": 1}, {"b": 2}])");
|
||||
|
||||
ASSERT_EQ(events.size(), 10u);
|
||||
ASSERT_EQ(events[0].type, EventType::ARRAY_START);
|
||||
ASSERT_EQ(events[1].type, EventType::OBJECT_START);
|
||||
ASSERT_EQ(events[2].type, EventType::KEY);
|
||||
ASSERT_EQ(events[2].value, "a");
|
||||
ASSERT_EQ(events[3].type, EventType::NUMBER);
|
||||
ASSERT_EQ(events[3].value, "1");
|
||||
ASSERT_EQ(events[4].type, EventType::OBJECT_END);
|
||||
ASSERT_EQ(events[5].type, EventType::OBJECT_START);
|
||||
ASSERT_EQ(events[6].type, EventType::KEY);
|
||||
ASSERT_EQ(events[6].value, "b");
|
||||
ASSERT_EQ(events[7].type, EventType::NUMBER);
|
||||
ASSERT_EQ(events[7].value, "2");
|
||||
ASSERT_EQ(events[8].type, EventType::OBJECT_END);
|
||||
ASSERT_EQ(events[9].type, EventType::ARRAY_END);
|
||||
|
||||
printf(" passed\n");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void testStringEscapes() {
|
||||
printf("testStringEscapes...\n");
|
||||
|
||||
auto events = parse(R"({"esc": "a\"b\\c\/d\ne\tf"})");
|
||||
|
||||
ASSERT_EQ(events.size(), 4u);
|
||||
ASSERT_EQ(events[2].type, EventType::STRING);
|
||||
ASSERT_EQ(events[2].value, std::string("a\"b\\c/d\ne\tf"));
|
||||
|
||||
printf(" passed\n");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void testUnicodeEscapePassthrough() {
|
||||
printf("testUnicodeEscapePassthrough...\n");
|
||||
|
||||
auto events = parse(R"({"u": "\u0041\u0042"})");
|
||||
|
||||
ASSERT_EQ(events[2].type, EventType::STRING);
|
||||
// \uXXXX passed through as literal \u followed by the hex digits
|
||||
ASSERT_EQ(events[2].value, "\\u0041\\u0042");
|
||||
|
||||
printf(" passed\n");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void testNumbers() {
|
||||
printf("testNumbers...\n");
|
||||
|
||||
auto events = parse(R"({"int": 42, "neg": -7, "flt": 3.14, "exp": 1e10, "nexp": -2.5E-3})");
|
||||
|
||||
ASSERT_EQ(events[2].type, EventType::NUMBER);
|
||||
ASSERT_EQ(events[2].value, "42");
|
||||
ASSERT_EQ(events[4].type, EventType::NUMBER);
|
||||
ASSERT_EQ(events[4].value, "-7");
|
||||
ASSERT_EQ(events[6].type, EventType::NUMBER);
|
||||
ASSERT_EQ(events[6].value, "3.14");
|
||||
ASSERT_EQ(events[8].type, EventType::NUMBER);
|
||||
ASSERT_EQ(events[8].value, "1e10");
|
||||
ASSERT_EQ(events[10].type, EventType::NUMBER);
|
||||
ASSERT_EQ(events[10].value, "-2.5E-3");
|
||||
|
||||
printf(" passed\n");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void testBooleansAndNull() {
|
||||
printf("testBooleansAndNull...\n");
|
||||
|
||||
auto events = parse(R"({"t": true, "f": false, "n": null})");
|
||||
|
||||
ASSERT_EQ(events[2].type, EventType::BOOL_TRUE);
|
||||
ASSERT_EQ(events[4].type, EventType::BOOL_FALSE);
|
||||
ASSERT_EQ(events[6].type, EventType::NULL_VAL);
|
||||
|
||||
printf(" passed\n");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void testChunkedFeeding() {
|
||||
printf("testChunkedFeeding...\n");
|
||||
|
||||
const char* json = R"({"key": "value", "num": 42, "arr": [1, 2]})";
|
||||
auto reference = parse(json);
|
||||
|
||||
// Feed byte-by-byte and verify identical event sequence
|
||||
auto bytewise = parseBytewise(json);
|
||||
|
||||
ASSERT_EQ(bytewise.size(), reference.size());
|
||||
for (size_t i = 0; i < reference.size(); ++i) {
|
||||
ASSERT_EQ(bytewise[i].type, reference[i].type);
|
||||
ASSERT_EQ(bytewise[i].value, reference[i].value);
|
||||
}
|
||||
|
||||
// Feed in chunks of varying size
|
||||
for (size_t chunkSize = 2; chunkSize <= 7; ++chunkSize) {
|
||||
TestContext ctx;
|
||||
StreamingJsonParser parser(makeCallbacks(&ctx));
|
||||
size_t len = strlen(json);
|
||||
for (size_t offset = 0; offset < len; offset += chunkSize) {
|
||||
size_t remaining = len - offset;
|
||||
size_t feedLen = remaining < chunkSize ? remaining : chunkSize;
|
||||
parser.feed(json + offset, feedLen);
|
||||
}
|
||||
|
||||
ASSERT_EQ(ctx.events.size(), reference.size());
|
||||
for (size_t i = 0; i < reference.size(); ++i) {
|
||||
ASSERT_EQ(ctx.events[i].type, reference[i].type);
|
||||
ASSERT_EQ(ctx.events[i].value, reference[i].value);
|
||||
}
|
||||
}
|
||||
|
||||
printf(" passed (byte-by-byte + chunk sizes 2-7)\n");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void testEveryByteBoundary() {
|
||||
printf("testEveryByteBoundary...\n");
|
||||
|
||||
const char* json = R"({"tag_name":"v1.2.3","assets":[{"name":"firmware.bin","size":12345}]})";
|
||||
auto reference = parse(json);
|
||||
size_t len = strlen(json);
|
||||
|
||||
for (size_t split = 0; split <= len; ++split) {
|
||||
TestContext ctx;
|
||||
StreamingJsonParser parser(makeCallbacks(&ctx));
|
||||
if (split > 0) parser.feed(json, split);
|
||||
if (split < len) parser.feed(json + split, len - split);
|
||||
|
||||
ASSERT_EQ(ctx.events.size(), reference.size());
|
||||
for (size_t i = 0; i < reference.size(); ++i) {
|
||||
if (ctx.events[i].type != reference[i].type || ctx.events[i].value != reference[i].value) {
|
||||
fprintf(stderr, " FAIL at split=%zu, event %zu\n", split, i);
|
||||
testsFailed++;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
printf(" passed (all %zu split points)\n", len + 1);
|
||||
PASS();
|
||||
}
|
||||
|
||||
void testLargeTokenTruncation() {
|
||||
printf("testLargeTokenTruncation...\n");
|
||||
|
||||
// Build a string value that exceeds TOKEN_BUF_SIZE
|
||||
std::string longVal(StreamingJsonParser::TOKEN_BUF_SIZE + 100, 'x');
|
||||
std::string json = R"({"short": "ok", "long": ")" + longVal + R"("})";
|
||||
|
||||
auto events = parse(json.c_str());
|
||||
|
||||
// "short" key + "ok" value should still fire
|
||||
ASSERT_TRUE(events.size() >= 3);
|
||||
ASSERT_EQ(events[1].type, EventType::KEY);
|
||||
ASSERT_EQ(events[1].value, "short");
|
||||
ASSERT_EQ(events[2].type, EventType::STRING);
|
||||
ASSERT_EQ(events[2].value, "ok");
|
||||
|
||||
// The "long" key fires, but the oversized value is silently dropped
|
||||
bool foundLongKey = false;
|
||||
bool foundLongValue = false;
|
||||
for (auto& e : events) {
|
||||
if (e.type == EventType::KEY && e.value == "long") foundLongKey = true;
|
||||
if (e.type == EventType::STRING && e.value.size() > 500) foundLongValue = true;
|
||||
}
|
||||
ASSERT_TRUE(foundLongKey);
|
||||
ASSERT_TRUE(!foundLongValue);
|
||||
|
||||
printf(" passed\n");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void testEmptyObject() {
|
||||
printf("testEmptyObject...\n");
|
||||
|
||||
auto events = parse("{}");
|
||||
ASSERT_EQ(events.size(), 2u);
|
||||
ASSERT_EQ(events[0].type, EventType::OBJECT_START);
|
||||
ASSERT_EQ(events[1].type, EventType::OBJECT_END);
|
||||
|
||||
printf(" passed\n");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void testEmptyArray() {
|
||||
printf("testEmptyArray...\n");
|
||||
|
||||
auto events = parse("[]");
|
||||
ASSERT_EQ(events.size(), 2u);
|
||||
ASSERT_EQ(events[0].type, EventType::ARRAY_START);
|
||||
ASSERT_EQ(events[1].type, EventType::ARRAY_END);
|
||||
|
||||
printf(" passed\n");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void testNestedArrays() {
|
||||
printf("testNestedArrays...\n");
|
||||
|
||||
auto events = parse("[[1, 2], [3]]");
|
||||
ASSERT_EQ(events.size(), 9u);
|
||||
ASSERT_EQ(events[0].type, EventType::ARRAY_START);
|
||||
ASSERT_EQ(events[1].type, EventType::ARRAY_START);
|
||||
ASSERT_EQ(events[2].type, EventType::NUMBER);
|
||||
ASSERT_EQ(events[2].value, "1");
|
||||
ASSERT_EQ(events[3].type, EventType::NUMBER);
|
||||
ASSERT_EQ(events[3].value, "2");
|
||||
ASSERT_EQ(events[4].type, EventType::ARRAY_END);
|
||||
ASSERT_EQ(events[5].type, EventType::ARRAY_START);
|
||||
ASSERT_EQ(events[6].type, EventType::NUMBER);
|
||||
ASSERT_EQ(events[6].value, "3");
|
||||
ASSERT_EQ(events[7].type, EventType::ARRAY_END);
|
||||
ASSERT_EQ(events[8].type, EventType::ARRAY_END);
|
||||
|
||||
printf(" passed\n");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void testTopLevelArray() {
|
||||
printf("testTopLevelArray...\n");
|
||||
|
||||
auto events = parse(R"(["hello", 42, true, null])");
|
||||
ASSERT_EQ(events.size(), 6u);
|
||||
ASSERT_EQ(events[0].type, EventType::ARRAY_START);
|
||||
ASSERT_EQ(events[1].type, EventType::STRING);
|
||||
ASSERT_EQ(events[1].value, "hello");
|
||||
ASSERT_EQ(events[2].type, EventType::NUMBER);
|
||||
ASSERT_EQ(events[2].value, "42");
|
||||
ASSERT_EQ(events[3].type, EventType::BOOL_TRUE);
|
||||
ASSERT_EQ(events[4].type, EventType::NULL_VAL);
|
||||
ASSERT_EQ(events[5].type, EventType::ARRAY_END);
|
||||
|
||||
printf(" passed\n");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void testWhitespaceVariants() {
|
||||
printf("testWhitespaceVariants...\n");
|
||||
|
||||
// Minified
|
||||
auto minified = parse(R"({"a":1,"b":"x"})");
|
||||
|
||||
// Pretty-printed
|
||||
const char* pretty = "{\n \"a\": 1,\n \"b\": \"x\"\n}";
|
||||
auto prettyEvents = parse(pretty);
|
||||
|
||||
ASSERT_EQ(minified.size(), prettyEvents.size());
|
||||
for (size_t i = 0; i < minified.size(); ++i) {
|
||||
ASSERT_EQ(minified[i].type, prettyEvents[i].type);
|
||||
ASSERT_EQ(minified[i].value, prettyEvents[i].value);
|
||||
}
|
||||
|
||||
printf(" passed\n");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void testResetBetweenDocuments() {
|
||||
printf("testResetBetweenDocuments...\n");
|
||||
|
||||
TestContext ctx;
|
||||
StreamingJsonParser parser(makeCallbacks(&ctx));
|
||||
|
||||
const char* json1 = R"({"a": 1})";
|
||||
parser.feed(json1, strlen(json1));
|
||||
ASSERT_EQ(ctx.events.size(), 4u);
|
||||
|
||||
ctx.events.clear();
|
||||
parser.reset();
|
||||
|
||||
const char* json2 = R"({"b": 2})";
|
||||
parser.feed(json2, strlen(json2));
|
||||
ASSERT_EQ(ctx.events.size(), 4u);
|
||||
ASSERT_EQ(ctx.events[1].value, "b");
|
||||
ASSERT_EQ(ctx.events[2].value, "2");
|
||||
|
||||
printf(" passed\n");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void testNumberAtEndOfInput() {
|
||||
printf("testNumberAtEndOfInput...\n");
|
||||
|
||||
// Number terminated by end of input (no trailing whitespace or structural char).
|
||||
// The parser must emit the number when feed() ends (after a closing brace).
|
||||
auto events = parse(R"({"n": 99})");
|
||||
bool found = false;
|
||||
for (auto& e : events) {
|
||||
if (e.type == EventType::NUMBER && e.value == "99") found = true;
|
||||
}
|
||||
ASSERT_TRUE(found);
|
||||
|
||||
printf(" passed\n");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void testArrayOfStrings() {
|
||||
printf("testArrayOfStrings...\n");
|
||||
|
||||
auto events = parse(R"(["a", "b", "c"])");
|
||||
|
||||
ASSERT_EQ(events.size(), 5u);
|
||||
ASSERT_EQ(events[0].type, EventType::ARRAY_START);
|
||||
ASSERT_EQ(events[1].type, EventType::STRING);
|
||||
ASSERT_EQ(events[1].value, "a");
|
||||
ASSERT_EQ(events[2].type, EventType::STRING);
|
||||
ASSERT_EQ(events[2].value, "b");
|
||||
ASSERT_EQ(events[3].type, EventType::STRING);
|
||||
ASSERT_EQ(events[3].value, "c");
|
||||
ASSERT_EQ(events[4].type, EventType::ARRAY_END);
|
||||
|
||||
printf(" passed\n");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void testTruncatedInputNoCrash() {
|
||||
printf("testTruncatedInputNoCrash...\n");
|
||||
|
||||
// Simulates a connection drop mid-JSON. Parser must not crash.
|
||||
const char* truncated[] = {
|
||||
R"({"key": "val)", R"({"key": )", R"({"key)", R"([1, 2, )",
|
||||
R"({"a": tru)", R"({"a": fal)", R"({"a": nul)", R"({"a": "hello\)",
|
||||
};
|
||||
|
||||
for (auto* json : truncated) {
|
||||
TestContext ctx;
|
||||
StreamingJsonParser parser(makeCallbacks(&ctx));
|
||||
parser.feed(json, strlen(json));
|
||||
// Just verify no crash; partial results are acceptable
|
||||
}
|
||||
|
||||
printf(" passed (no crashes on %d truncated inputs)\n", 8);
|
||||
PASS();
|
||||
}
|
||||
|
||||
void testAllEscapeSequences() {
|
||||
printf("testAllEscapeSequences...\n");
|
||||
|
||||
auto events = parse(R"({"e": "\b\f\n\r\t\"\\\/"})");
|
||||
ASSERT_EQ(events[2].type, EventType::STRING);
|
||||
ASSERT_EQ(events[2].value, std::string("\b\f\n\r\t\"\\/"));
|
||||
|
||||
printf(" passed\n");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void testObjectInArray() {
|
||||
printf("testObjectInArray...\n");
|
||||
|
||||
// After an object closes inside an array, the next string after comma
|
||||
// should be correctly identified as a key (inside the next object) or
|
||||
// a string value (if directly in the array).
|
||||
auto events = parse(R"([{"k":"v"}, "bare"])");
|
||||
|
||||
ASSERT_EQ(events.size(), 7u);
|
||||
ASSERT_EQ(events[0].type, EventType::ARRAY_START);
|
||||
ASSERT_EQ(events[1].type, EventType::OBJECT_START);
|
||||
ASSERT_EQ(events[2].type, EventType::KEY);
|
||||
ASSERT_EQ(events[2].value, "k");
|
||||
ASSERT_EQ(events[3].type, EventType::STRING);
|
||||
ASSERT_EQ(events[3].value, "v");
|
||||
ASSERT_EQ(events[4].type, EventType::OBJECT_END);
|
||||
ASSERT_EQ(events[5].type, EventType::STRING);
|
||||
ASSERT_EQ(events[5].value, "bare");
|
||||
ASSERT_EQ(events[6].type, EventType::ARRAY_END);
|
||||
|
||||
printf(" passed\n");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void testDeeplyNested() {
|
||||
printf("testDeeplyNested...\n");
|
||||
|
||||
// 20 levels of nesting (well within MAX_NESTING=32)
|
||||
std::string json;
|
||||
for (int i = 0; i < 20; ++i) json += R"({"d":)";
|
||||
json += "0";
|
||||
for (int i = 0; i < 20; ++i) json += "}";
|
||||
|
||||
auto events = parse(json.c_str());
|
||||
|
||||
// 20 OBJECT_START + 20 KEY + 1 NUMBER + 20 OBJECT_END = 61
|
||||
ASSERT_EQ(events.size(), 61u);
|
||||
ASSERT_EQ(events[0].type, EventType::OBJECT_START);
|
||||
ASSERT_EQ(events[40].type, EventType::NUMBER);
|
||||
ASSERT_EQ(events[40].value, "0");
|
||||
ASSERT_EQ(events[60].type, EventType::OBJECT_END);
|
||||
|
||||
printf(" passed\n");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void testNestingOverflow() {
|
||||
printf("testNestingOverflow...\n");
|
||||
|
||||
// Exceed MAX_NESTING -- parser should set error flag, not crash
|
||||
std::string json;
|
||||
for (size_t i = 0; i < StreamingJsonParser::MAX_NESTING + 5; ++i) json += "[";
|
||||
|
||||
TestContext ctx;
|
||||
StreamingJsonParser parser(makeCallbacks(&ctx));
|
||||
parser.feed(json.c_str(), json.size());
|
||||
|
||||
ASSERT_TRUE(parser.hasError());
|
||||
|
||||
printf(" passed\n");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void testNumberZero() {
|
||||
printf("testNumberZero...\n");
|
||||
|
||||
auto events = parse(R"({"z": 0})");
|
||||
ASSERT_EQ(events[2].type, EventType::NUMBER);
|
||||
ASSERT_EQ(events[2].value, "0");
|
||||
|
||||
printf(" passed\n");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void testMultipleValuesInObject() {
|
||||
printf("testMultipleValuesInObject...\n");
|
||||
|
||||
auto events = parse(R"({"a": "x", "b": "y", "c": "z"})");
|
||||
|
||||
ASSERT_EQ(events.size(), 8u);
|
||||
ASSERT_EQ(events[1].value, "a");
|
||||
ASSERT_EQ(events[2].value, "x");
|
||||
ASSERT_EQ(events[3].value, "b");
|
||||
ASSERT_EQ(events[4].value, "y");
|
||||
ASSERT_EQ(events[5].value, "c");
|
||||
ASSERT_EQ(events[6].value, "z");
|
||||
|
||||
printf(" passed\n");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void testChunkedSplitInsideString() {
|
||||
printf("testChunkedSplitInsideString...\n");
|
||||
|
||||
const char* json = R"({"key": "hello world"})";
|
||||
auto reference = parse(json);
|
||||
|
||||
// Split right in the middle of "hello world"
|
||||
size_t splitAt = 14; // inside the string value
|
||||
TestContext ctx;
|
||||
StreamingJsonParser parser(makeCallbacks(&ctx));
|
||||
parser.feed(json, splitAt);
|
||||
parser.feed(json + splitAt, strlen(json) - splitAt);
|
||||
|
||||
ASSERT_EQ(ctx.events.size(), reference.size());
|
||||
for (size_t i = 0; i < reference.size(); ++i) {
|
||||
ASSERT_EQ(ctx.events[i].type, reference[i].type);
|
||||
ASSERT_EQ(ctx.events[i].value, reference[i].value);
|
||||
}
|
||||
|
||||
printf(" passed\n");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void testChunkedSplitInsideEscape() {
|
||||
printf("testChunkedSplitInsideEscape...\n");
|
||||
|
||||
const char* json = R"({"k": "a\"b"})";
|
||||
auto reference = parse(json);
|
||||
|
||||
// Find the backslash position and split right after it
|
||||
const char* bs = strchr(json + 7, '\\');
|
||||
ASSERT_TRUE(bs != nullptr);
|
||||
size_t splitAt = static_cast<size_t>(bs - json) + 1; // after the backslash
|
||||
|
||||
TestContext ctx;
|
||||
StreamingJsonParser parser(makeCallbacks(&ctx));
|
||||
parser.feed(json, splitAt);
|
||||
parser.feed(json + splitAt, strlen(json) - splitAt);
|
||||
|
||||
ASSERT_EQ(ctx.events.size(), reference.size());
|
||||
for (size_t i = 0; i < reference.size(); ++i) {
|
||||
ASSERT_EQ(ctx.events[i].type, reference[i].type);
|
||||
ASSERT_EQ(ctx.events[i].value, reference[i].value);
|
||||
}
|
||||
|
||||
printf(" passed\n");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void testChunkedSplitInsideLiteral() {
|
||||
printf("testChunkedSplitInsideLiteral...\n");
|
||||
|
||||
const char* json = R"({"a": true, "b": false, "c": null})";
|
||||
auto reference = parse(json);
|
||||
|
||||
// Split inside "true" (at "tr|ue")
|
||||
size_t splitAt = 7;
|
||||
TestContext ctx;
|
||||
StreamingJsonParser parser(makeCallbacks(&ctx));
|
||||
parser.feed(json, splitAt);
|
||||
parser.feed(json + splitAt, strlen(json) - splitAt);
|
||||
|
||||
ASSERT_EQ(ctx.events.size(), reference.size());
|
||||
for (size_t i = 0; i < reference.size(); ++i) {
|
||||
ASSERT_EQ(ctx.events[i].type, reference[i].type);
|
||||
ASSERT_EQ(ctx.events[i].value, reference[i].value);
|
||||
}
|
||||
|
||||
printf(" passed\n");
|
||||
PASS();
|
||||
}
|
||||
|
||||
void testNullCallbacksNoCrash() {
|
||||
printf("testNullCallbacksNoCrash...\n");
|
||||
|
||||
JsonCallbacks nullCbs = {};
|
||||
nullCbs.ctx = nullptr;
|
||||
StreamingJsonParser parser(nullCbs);
|
||||
|
||||
const char* json = R"({"key": "value", "num": 42, "b": true, "n": null, "a": [1]})";
|
||||
parser.feed(json, strlen(json));
|
||||
ASSERT_TRUE(!parser.hasError());
|
||||
|
||||
printf(" passed\n");
|
||||
PASS();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
int main() {
|
||||
printf("=== StreamingJsonParser Tests ===\n\n");
|
||||
|
||||
testSimpleObject();
|
||||
testNestedObjects();
|
||||
testArrayOfValues();
|
||||
testArrayOfObjects();
|
||||
testStringEscapes();
|
||||
testUnicodeEscapePassthrough();
|
||||
testNumbers();
|
||||
testBooleansAndNull();
|
||||
testChunkedFeeding();
|
||||
testEveryByteBoundary();
|
||||
testLargeTokenTruncation();
|
||||
testEmptyObject();
|
||||
testEmptyArray();
|
||||
testNestedArrays();
|
||||
testTopLevelArray();
|
||||
testWhitespaceVariants();
|
||||
testResetBetweenDocuments();
|
||||
testNumberAtEndOfInput();
|
||||
testArrayOfStrings();
|
||||
testTruncatedInputNoCrash();
|
||||
testAllEscapeSequences();
|
||||
testObjectInArray();
|
||||
testDeeplyNested();
|
||||
testNestingOverflow();
|
||||
testNumberZero();
|
||||
testMultipleValuesInObject();
|
||||
testChunkedSplitInsideString();
|
||||
testChunkedSplitInsideEscape();
|
||||
testChunkedSplitInsideLiteral();
|
||||
testNullCallbacksNoCrash();
|
||||
|
||||
printf("\n=== Results: %d passed, %d failed ===\n", testsPassed, testsFailed);
|
||||
return testsFailed > 0 ? 1 : 0;
|
||||
}
|
||||
Reference in New Issue
Block a user