Compare commits

..

15 Commits

Author SHA1 Message Date
733bc4ee82 Set version to 7.4.2 2025-06-20 09:37:14 +02:00
9aa77994b4 Remove sponsor Programming Electronics Academy 2025-06-20 08:43:48 +02:00
f051f8328d Undef true and false macros
Replacing `true` with `1` changes `JsonString("ABC", true)` into `JsonString("ABC", 1)`, so all static strings had a length of 1.

This is a workaround for arduino/ArduinoCore-sam#50
Fixes #2181
2025-06-19 13:59:15 +02:00
411424b74e CI: upgrade clang-tidy 2025-05-20 20:55:01 +02:00
5e5c287978 CI: upgrade Clang versions 2025-05-20 18:24:46 +02:00
377cf63075 CI: upgrade runner to ubuntu-22.04 2025-05-20 14:48:42 +02:00
3252013509 Set version to 7.4.1 2025-04-11 15:43:42 +02:00
deab127a2f Fix crash with tiny Flash strings (issue #2170) 2025-04-11 10:23:43 +02:00
96281de682 Set version to 7.4.0 2025-04-09 14:49:13 +02:00
f0e84e4933 Fix support for const char[]
Fixes #2166
2025-04-09 08:55:09 +02:00
91397f9f06 Optimize storage of tiny strings (up to 3 characters) 2025-04-09 08:55:08 +02:00
7f75985e47 Change StringBuffer::save() to take a VariantData* 2025-02-28 10:05:50 +01:00
05b68fc7cc Change StringBuilder::save() to take a VariantData* 2025-02-28 09:59:46 +01:00
b06cee8f4d Remove the overload of setString() for StringNode 2025-02-28 09:23:56 +01:00
e03d8ae885 Set version to 7.3.1 2025-02-27 19:35:14 +01:00
61 changed files with 567 additions and 439 deletions

View File

@ -93,44 +93,40 @@ jobs:
fail-fast: false
matrix:
include:
- clang: "3.9"
runner: ubuntu-20.04
archive: bionic
- clang: "4.0"
runner: ubuntu-20.04
archive: bionic
- clang: "5.0"
runner: ubuntu-20.04
archive: bionic
- clang: "6.0"
runner: ubuntu-20.04
archive: bionic
- clang: "7"
runner: ubuntu-20.04
runner: ubuntu-22.04
archive: focal
- clang: "8"
cxxflags: -fsanitize=leak -fno-sanitize-recover=all
runner: ubuntu-20.04
runner: ubuntu-22.04
archive: focal
- clang: "9"
cxxflags: -fsanitize=undefined -fno-sanitize-recover=all
runner: ubuntu-20.04
runner: ubuntu-22.04
archive: focal
- clang: "10"
cxxflags: -fsanitize=address -fno-sanitize-recover=all
runner: ubuntu-20.04
runner: ubuntu-22.04
archive: focal
- clang: "11"
runner: ubuntu-22.04
- clang: "12"
runner: ubuntu-22.04
- clang: "13"
runner: ubuntu-22.04
- clang: "14"
runner: ubuntu-22.04
- clang: "15"
runner: ubuntu-22.04
runs-on: ${{ matrix.runner }}
- clang: 14
- clang: 15
- clang: 16
- clang: 17
- clang: 18
- clang: 19
runs-on: ${{ matrix.runner || 'ubuntu-latest' }}
steps:
- name: Add archive repositories
if: matrix.archive
run: |
sudo gpg --keyserver keyserver.ubuntu.com --recv-keys 3B4FE6ACC0B21F32
sudo gpg --export 3B4FE6ACC0B21F32 | sudo tee /etc/apt/trusted.gpg.d/ubuntu-keyring.gpg > /dev/null
sudo add-apt-repository -yn 'deb http://archive.ubuntu.com/ubuntu/ ${{ matrix.archive }} main'
sudo add-apt-repository -yn 'deb http://archive.ubuntu.com/ubuntu/ ${{ matrix.archive }} universe'
- name: Install Clang ${{ matrix.clang }}
@ -138,10 +134,9 @@ jobs:
sudo apt-get update
sudo apt-get install -y clang-${{ matrix.clang }}
- name: Install libc++ ${{ matrix.clang }}
if: matrix.clang >= 11
run: sudo apt-get install -y libc++-${{ matrix.clang }}-dev libc++abi-${{ matrix.clang }}-dev
- name: Install libunwind ${{ matrix.clang }}
if: matrix.clang == 12 # dependency is missing in Ubuntu 22.04
if: matrix.clang == 12 # dependency is missing in Ubuntu 22.04
run: sudo apt-get install -y libunwind-${{ matrix.clang }}-dev
- name: Checkout
uses: actions/checkout@v4
@ -163,7 +158,7 @@ jobs:
conf_test:
name: Test configuration on Linux
needs: [gcc, clang]
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
steps:
- name: Install
run: |
@ -257,7 +252,7 @@ jobs:
board: arduino:avr:uno
- core: arduino:samd
board: arduino:samd:mkr1000
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v4
@ -412,7 +407,7 @@ jobs:
arm:
name: GCC for ARM processor
needs: gcc
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
steps:
- name: Install
run: |
@ -431,7 +426,7 @@ jobs:
coverage:
needs: gcc
name: Coverage
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
steps:
- name: Install
run: sudo apt-get install -y lcov ninja-build
@ -463,7 +458,7 @@ jobs:
valgrind:
needs: gcc
name: Valgrind
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
steps:
- name: Install
run: |
@ -485,24 +480,24 @@ jobs:
clang-tidy:
needs: clang
name: Clang-Tidy
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
steps:
- name: Install
run: sudo apt-get install -y clang-tidy cmake ninja-build
run: sudo apt-get install -y clang-tidy libc++-dev libc++abi-dev
- name: Checkout
uses: actions/checkout@v4
- name: Configure
run: cmake -G Ninja -DCMAKE_CXX_CLANG_TIDY="clang-tidy-10;--warnings-as-errors=*" -DCMAKE_BUILD_TYPE=Debug .
run: cmake -G Ninja -DCMAKE_CXX_CLANG_TIDY="clang-tidy;--warnings-as-errors=*" -DCMAKE_BUILD_TYPE=Debug .
env:
CC: clang-10
CXX: clang++-10
CC: clang
CXX: clang++
- name: Check
run: cmake --build . -- -k 0
amalgamate:
needs: gcc
name: Amalgamate ArduinoJson.h
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v4
@ -578,7 +573,7 @@ jobs:
codeql:
name: CodeQL
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
needs: gcc
permissions:
@ -587,20 +582,20 @@ jobs:
security-events: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Checkout repository
uses: actions/checkout@v4
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: cpp
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: cpp
- name: Build
run: |
cmake -DCMAKE_BUILD_TYPE=Debug .
cmake --build .
- name: Build
run: |
cmake -DCMAKE_BUILD_TYPE=Debug .
cmake --build .
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: "/language:cpp"
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: "/language:cpp"

View File

@ -8,7 +8,7 @@ on:
jobs:
release:
name: Create release
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
steps:
- name: Set variables
id: init

View File

@ -1,44 +1,27 @@
ArduinoJson: change log
=======================
HEAD
----
v7.4.2 (2025-06-20)
------
* Fix truncated strings on Arduino Due (issue #2181)
v7.4.1 (2025-04-11)
------
* Fix crash with tiny Flash strings (issue #2170)
v7.4.0 (2025-04-09)
------
* Optimize storage of tiny strings (up to 3 characters)
* Fix support for `const char[]` (issue #2166)
v7.3.1 (2025-02-27)
------
* Fix conversion from static string to number
* Slightly reduce code size
* Optimize storage of static strings
> ### BREAKING CHANGES
>
> Static string cannot contain NUL characters anymore (they could since 7.3.0).
> This is an extremely rare case, so you probably won't be affected.
>
> For example, the following code produces different output in 7.3 and 7.4:
>
> ```cpp
> JsonDocument doc;
> doc["a\0b"] = "c\0d";
> serializeJson(doc, Serial);
> // With Arduino 7.3 -> {"a\u0000b":"c\u0000d"}
> // With Arduino 7.4 -> {"a":"c"}
> ```
>
> `JsonString` contructor now only accepts two arguments, not three.
> If your code uses `JsonString` to store a string as a pointer, you must remove the size argument.
>
> For example, if you have something like this:
>
> ```cpp
> doc["key"] = JsonString(str.c_str(), str.size(), true);
> ```
>
> You must replace with either:
>
> ```cpp
> doc["key"] = JsonString(str.c_str(), true); // store as pointer, cannot contain NUL characters
> doc["key"] = JsonString(str.c_str(), str.size()); // store by copy, NUL characters allowed
> doc["key"] = str; // same as previous line for supported string classes (`String`, `std::string`, etc.)
> ```
v7.3.0 (2024-12-29)
------

View File

@ -10,7 +10,7 @@ if(ESP_PLATFORM)
return()
endif()
project(ArduinoJson VERSION 7.3.0)
project(ArduinoJson VERSION 7.4.2)
if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME)
include(CTest)

View File

@ -80,7 +80,7 @@ ArduinoJson is a C++ JSON library for Arduino and IoT (Internet Of Things).
* Continuously tested on
* [Visual Studio 2017, 2019, 2022](https://ci.appveyor.com/project/bblanchon/arduinojson/branch/7.x)
* [GCC 4.8, 5, 6, 7, 8, 9, 10, 11, 12](https://github.com/bblanchon/ArduinoJson/actions?query=workflow%3A%22Continuous+Integration%22)
* [Clang 3.9, 4.0, 5.0, 6.0, 7, 8, 9, 10, 11, 12, 13, 14, 15](https://github.com/bblanchon/ArduinoJson/actions?query=workflow%3A%22Continuous+Integration%22)
* [Clang 7 to 19](https://github.com/bblanchon/ArduinoJson/actions?query=workflow%3A%22Continuous+Integration%22)
* [Continuously fuzzed with Google OSS Fuzz](https://bugs.chromium.org/p/oss-fuzz/issues/list?sort=-opened&can=1&q=proj:arduinojson)
* Passes all default checks of [clang-tidy](https://releases.llvm.org/10.0.0/tools/clang/tools/extra/docs/clang-tidy/)
* Well documented
@ -139,11 +139,6 @@ See the [tutorial on arduinojson.org](https://arduinojson.org/v7/doc/serializati
ArduinoJson is thankful to its sponsors. Please give them a visit; they deserve it!
<p>
<a href="https://www.programmingelectronics.com/" rel="sponsored">
<img src="https://arduinojson.org/images/2021/10/programmingeleactronicsacademy.png" alt="Programming Electronics Academy" width="200">
</a>
</p>
<p>
<a href="https://github.com/1technophile" rel="sponsored">
<img alt="1technophile" src="https://avatars.githubusercontent.com/u/12672732?s=40&v=4">

View File

@ -1,4 +1,4 @@
version: 7.3.0.{build}
version: 7.4.2.{build}
environment:
matrix:
- APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2022

View File

@ -46,7 +46,7 @@ TEST_CASE("BasicJsonDocument") {
deserializeJson(doc, "{\"hello\":\"world\"}");
REQUIRE(doc.as<std::string>() == "{\"hello\":\"world\"}");
doc.clear();
REQUIRE(allocatorLog == "ARAARDDD");
REQUIRE(allocatorLog == "AARARDDD");
}
SECTION("copy") {
@ -54,7 +54,7 @@ TEST_CASE("BasicJsonDocument") {
doc["hello"] = "world";
auto copy = doc;
REQUIRE(copy.as<std::string>() == "{\"hello\":\"world\"}");
REQUIRE(allocatorLog == "AAAA");
REQUIRE(allocatorLog == "AA");
}
SECTION("capacity") {

View File

@ -275,12 +275,6 @@ inline size_t sizeofPool(
return MemoryPool<VariantData>::slotsToBytes(n);
}
inline size_t sizeofStaticStringPool(
ArduinoJson::detail::SlotCount n = ARDUINOJSON_POOL_CAPACITY) {
using namespace ArduinoJson::detail;
return MemoryPool<const char*>::slotsToBytes(n);
}
inline size_t sizeofStringBuffer(size_t iteration = 1) {
// returns 31, 63, 127, 255, etc.
auto capacity = ArduinoJson::detail::StringBuilder::initialCapacity;

View File

@ -56,7 +56,6 @@ TEST_CASE("JsonArray::add(T)") {
REQUIRE(array[0].is<int>() == false);
REQUIRE(spy.log() == AllocatorLog{
Allocate(sizeofPool()),
Allocate(sizeofStaticStringPool()),
});
}

View File

@ -104,8 +104,6 @@ TEST_CASE("deserializeJson(MemberProxy)") {
REQUIRE(err == DeserializationError::Ok);
REQUIRE(doc.as<std::string>() == "{\"hello\":\"world\",\"value\":[42]}");
REQUIRE(spy.log() == AllocatorLog{
Allocate(sizeofStaticStringPool()),
});
REQUIRE(spy.log() == AllocatorLog{});
}
}

View File

@ -825,9 +825,7 @@ TEST_CASE("shrink filter") {
deserializeJson(doc, "{}", DeserializationOption::Filter(filter));
REQUIRE(spy.log() ==
AllocatorLog{
Reallocate(sizeofPool(), sizeofObject(1)),
Reallocate(sizeofStaticStringPool(), sizeofStaticStringPool(1)),
});
REQUIRE(spy.log() == AllocatorLog{
Reallocate(sizeofPool(), sizeofObject(1)),
});
}

View File

@ -26,8 +26,8 @@ TEST_CASE("deserializeJson(char*)") {
REQUIRE(spy.log() ==
AllocatorLog{
Allocate(sizeofStringBuffer()),
Reallocate(sizeofStringBuffer(), sizeofString("hello")),
Allocate(sizeofPool()),
Reallocate(sizeofStringBuffer(), sizeofString("hello")),
Allocate(sizeofStringBuffer()),
Reallocate(sizeofStringBuffer(), sizeofString("world")),
Reallocate(sizeofPool(), sizeofObject(1)),

View File

@ -292,22 +292,23 @@ TEST_CASE("deserialize JSON object") {
}
SECTION("Repeated key") {
DeserializationError err = deserializeJson(doc, "{a:{b:{c:1}},a:2}");
DeserializationError err =
deserializeJson(doc, "{alfa:{bravo:{charlie:1}},alfa:2}");
REQUIRE(err == DeserializationError::Ok);
REQUIRE(doc.as<std::string>() == "{\"a\":2}");
REQUIRE(doc.as<std::string>() == "{\"alfa\":2}");
REQUIRE(spy.log() ==
AllocatorLog{
Allocate(sizeofStringBuffer()),
Reallocate(sizeofStringBuffer(), sizeofString("a")),
Allocate(sizeofPool()),
Reallocate(sizeofStringBuffer(), sizeofString("alfa")),
Allocate(sizeofStringBuffer()),
Reallocate(sizeofStringBuffer(), sizeofString("b")),
Reallocate(sizeofStringBuffer(), sizeofString("bravo")),
Allocate(sizeofStringBuffer()),
Reallocate(sizeofStringBuffer(), sizeofString("c")),
Reallocate(sizeofStringBuffer(), sizeofString("charlie")),
Allocate(sizeofStringBuffer()),
Deallocate(sizeofString("b")),
Deallocate(sizeofString("c")),
Deallocate(sizeofString("bravo")),
Deallocate(sizeofString("charlie")),
Deallocate(sizeofStringBuffer()),
Reallocate(sizeofPool(), sizeofObject(2) + sizeofObject(1)),
});
@ -378,7 +379,7 @@ TEST_CASE("deserialize JSON object under memory constraints") {
}
SECTION("pool allocation fails") {
timebomb.setCountdown(2);
timebomb.setCountdown(1);
char input[] = "{\"a\":1}";
DeserializationError err = deserializeJson(doc, input);
@ -389,11 +390,11 @@ TEST_CASE("deserialize JSON object under memory constraints") {
SECTION("string allocation fails") {
timebomb.setCountdown(3);
char input[] = "{\"a\":\"b\"}";
char input[] = "{\"alfa\":\"bravo\"}";
DeserializationError err = deserializeJson(doc, input);
REQUIRE(err == DeserializationError::NoMemory);
REQUIRE(doc.as<std::string>() == "{\"a\":null}");
REQUIRE(doc.as<std::string>() == "{\"alfa\":null}");
}
}

View File

@ -133,8 +133,8 @@ TEST_CASE("Allocation of the key fails") {
REQUIRE(spy.log() ==
AllocatorLog{
Allocate(sizeofStringBuffer()),
Reallocate(sizeofStringBuffer(), sizeofString("hello")),
Allocate(sizeofPool()),
Reallocate(sizeofStringBuffer(), sizeofString("hello")),
AllocateFail(sizeofStringBuffer()),
ReallocateFail(sizeofPool(), sizeofObject(1)),
});
@ -155,8 +155,8 @@ TEST_CASE("Allocation of the key fails") {
REQUIRE(spy.log() ==
AllocatorLog{
Allocate(sizeofStringBuffer()),
Reallocate(sizeofStringBuffer(), sizeofString("hello")),
Allocate(sizeofPool()),
Reallocate(sizeofStringBuffer(), sizeofString("hello")),
AllocateFail(sizeofStringBuffer()),
ReallocateFail(sizeofPool(), sizeofObject(1)),
});

View File

@ -31,7 +31,6 @@ TEST_CASE("ElementProxy::add()") {
REQUIRE(doc.as<std::string>() == "[[\"world\"]]");
REQUIRE(spy.log() == AllocatorLog{
Allocate(sizeofPool()),
Allocate(sizeofStaticStringPool()),
});
}

View File

@ -25,7 +25,6 @@ TEST_CASE("MemberProxy::add()") {
REQUIRE(doc.as<std::string>() == "{\"hello\":[42]}");
REQUIRE(spy.log() == AllocatorLog{
Allocate(sizeofPool()),
Allocate(sizeofStaticStringPool()),
});
}
@ -35,7 +34,6 @@ TEST_CASE("MemberProxy::add()") {
REQUIRE(doc.as<std::string>() == "{\"hello\":[\"world\"]}");
REQUIRE(spy.log() == AllocatorLog{
Allocate(sizeofPool()),
Allocate(sizeofStaticStringPool()),
});
}
@ -46,7 +44,6 @@ TEST_CASE("MemberProxy::add()") {
REQUIRE(doc.as<std::string>() == "{\"hello\":[\"world\"]}");
REQUIRE(spy.log() == AllocatorLog{
Allocate(sizeofPool()),
Allocate(sizeofStaticStringPool()),
Allocate(sizeofString("world")),
});
}
@ -58,8 +55,8 @@ TEST_CASE("MemberProxy::add()") {
REQUIRE(doc.as<std::string>() == "{\"hello\":[\"world\"]}");
REQUIRE(spy.log() == AllocatorLog{
Allocate(sizeofPool()),
Allocate(sizeofStaticStringPool()),
Allocate(sizeofString("world")),
});
}
@ -74,7 +71,6 @@ TEST_CASE("MemberProxy::add()") {
REQUIRE(doc.as<std::string>() == "{\"hello\":[\"world\"]}");
REQUIRE(spy.log() == AllocatorLog{
Allocate(sizeofPool()),
Allocate(sizeofStaticStringPool()),
Allocate(sizeofString("world")),
});
}
@ -403,7 +399,7 @@ TEST_CASE("MemberProxy under memory constraints") {
}
SECTION("value slot allocation fails") {
timebomb.setCountdown(2);
timebomb.setCountdown(1);
// fill the pool entirely, but leave one slot for the key
doc["foo"][ARDUINOJSON_POOL_CAPACITY - 4] = 1;
@ -416,7 +412,6 @@ TEST_CASE("MemberProxy under memory constraints") {
REQUIRE(doc.overflowed() == true);
REQUIRE(spy.log() == AllocatorLog{
Allocate(sizeofPool()),
Allocate(sizeofStaticStringPool()),
AllocateFail(sizeofPool()),
});
}

View File

@ -32,7 +32,6 @@ TEST_CASE("JsonDocument::add(T)") {
REQUIRE(doc.as<std::string>() == "[\"hello\"]");
REQUIRE(spy.log() == AllocatorLog{
Allocate(sizeofPool()),
Allocate(sizeofStaticStringPool()),
});
}

View File

@ -69,6 +69,8 @@ TEST_CASE("JsonDocument assignment") {
doc2 = std::move(doc1);
REQUIRE(doc2.as<std::string>() == "{\"hello\":\"world\"}");
// NOLINTNEXTLINE(clang-analyzer-cplusplus.Move)
REQUIRE(doc1.as<std::string>() == "null");
}
REQUIRE(spyingAllocator.log() == AllocatorLog{

View File

@ -44,6 +44,8 @@ TEST_CASE("JsonDocument constructor") {
JsonDocument doc2(std::move(doc1));
REQUIRE(doc2.as<std::string>() == "The size of this string is 32!!");
// NOLINTNEXTLINE(clang-analyzer-cplusplus.Move)
REQUIRE(doc1.as<std::string>() == "null");
}
REQUIRE(spyingAllocator.log() == AllocatorLog{
@ -62,7 +64,6 @@ TEST_CASE("JsonDocument constructor") {
REQUIRE(doc2.as<std::string>() == "{\"hello\":\"world\"}");
REQUIRE(spyingAllocator.log() == AllocatorLog{
Allocate(sizeofPool()),
Allocate(sizeofStaticStringPool()),
});
}
@ -86,7 +87,6 @@ TEST_CASE("JsonDocument constructor") {
REQUIRE(doc2.as<std::string>() == "[\"hello\"]");
REQUIRE(spyingAllocator.log() == AllocatorLog{
Allocate(sizeofPool()),
Allocate(sizeofStaticStringPool()),
});
}

View File

@ -22,10 +22,10 @@ TEST_CASE("JsonDocument::remove()") {
SECTION("string literal") {
doc["a"] = 1;
doc["x"] = 2;
doc["a\0b"_s] = 2;
doc["b"] = 3;
doc.remove("x");
doc.remove("a\0b");
REQUIRE(doc.as<std::string>() == "{\"a\":1,\"b\":3}");
}

View File

@ -37,9 +37,7 @@ TEST_CASE("JsonDocument::set()") {
doc.set("example");
REQUIRE(doc.as<const char*>() == "example"_s);
REQUIRE(spy.log() == AllocatorLog{
Allocate(sizeofStaticStringPool()),
});
REQUIRE(spy.log() == AllocatorLog{});
}
SECTION("const char*") {
@ -89,6 +87,13 @@ TEST_CASE("JsonDocument::set()") {
});
}
SECTION("Flash tiny string") { // issue #2170
doc.set(F("abc"));
REQUIRE(doc.as<const char*>() == "abc"_s);
REQUIRE(spy.log() == AllocatorLog{});
}
#ifdef HAS_VARIABLE_LENGTH_ARRAY
SECTION("VLA") {
size_t i = 16;

View File

@ -75,11 +75,7 @@ TEST_CASE("JsonDocument::shrinkToFit()") {
doc.shrinkToFit();
REQUIRE(doc.as<std::string>() == "hello");
REQUIRE(spyingAllocator.log() ==
AllocatorLog{
Allocate(sizeofStaticStringPool()),
Reallocate(sizeofStaticStringPool(), sizeofStaticStringPool(1)),
});
REQUIRE(spyingAllocator.log() == AllocatorLog{});
}
SECTION("owned string") {
@ -114,9 +110,7 @@ TEST_CASE("JsonDocument::shrinkToFit()") {
REQUIRE(spyingAllocator.log() ==
AllocatorLog{
Allocate(sizeofPool()),
Allocate(sizeofStaticStringPool()),
Reallocate(sizeofPool(), sizeofObject(1)),
Reallocate(sizeofStaticStringPool(), sizeofStaticStringPool(1)),
});
}
@ -143,9 +137,7 @@ TEST_CASE("JsonDocument::shrinkToFit()") {
REQUIRE(spyingAllocator.log() ==
AllocatorLog{
Allocate(sizeofPool()),
Allocate(sizeofStaticStringPool()),
Reallocate(sizeofPool(), sizeofArray(1)),
Reallocate(sizeofStaticStringPool(), sizeofStaticStringPool(1)),
});
}
@ -172,23 +164,20 @@ TEST_CASE("JsonDocument::shrinkToFit()") {
REQUIRE(spyingAllocator.log() ==
AllocatorLog{
Allocate(sizeofPool()),
Allocate(sizeofStaticStringPool()),
Reallocate(sizeofPool(), sizeofObject(1)),
Reallocate(sizeofStaticStringPool(), sizeofStaticStringPool(2)),
});
}
SECTION("owned string in object") {
doc["key"_s] = "value"_s;
doc["key"] = "abcdefg"_s;
doc.shrinkToFit();
REQUIRE(doc.as<std::string>() == "{\"key\":\"value\"}");
REQUIRE(doc.as<std::string>() == "{\"key\":\"abcdefg\"}");
REQUIRE(spyingAllocator.log() ==
AllocatorLog{
Allocate(sizeofPool()),
Allocate(sizeofString("key")),
Allocate(sizeofString("value")),
Allocate(sizeofString("abcdefg")),
Reallocate(sizeofPool(), sizeofPool(2)),
});
}

View File

@ -25,6 +25,8 @@ TEST_CASE("JsonDocument::operator[]") {
SECTION("string literal") {
REQUIRE(doc["abc"] == "ABC");
REQUIRE(cdoc["abc"] == "ABC");
REQUIRE(doc["abc\0d"] == "ABCD");
REQUIRE(cdoc["abc\0d"] == "ABCD");
}
SECTION("std::string") {
@ -112,7 +114,6 @@ TEST_CASE("JsonDocument::operator[] key storage") {
REQUIRE(doc.as<std::string>() == "{\"hello\":0}");
REQUIRE(spy.log() == AllocatorLog{
Allocate(sizeofPool()),
Allocate(sizeofStaticStringPool()),
});
}

View File

@ -26,12 +26,25 @@ TEST_CASE("JsonObject::set()") {
REQUIRE(obj2["hello"] == "world"_s);
REQUIRE(spy.log() == AllocatorLog{
Allocate(sizeofPool()),
Allocate(sizeofStaticStringPool()),
});
}
SECTION("copy local string key and value") {
obj1["hello"_s] = "world"_s;
SECTION("copy local string value") {
obj1["hello"] = "world"_s;
spy.clearLog();
bool success = obj2.set(obj1);
REQUIRE(success == true);
REQUIRE(obj2["hello"] == "world"_s);
REQUIRE(spy.log() == AllocatorLog{
Allocate(sizeofPool()),
Allocate(sizeofString("world")),
});
}
SECTION("copy local key") {
obj1["hello"_s] = "world";
spy.clearLog();
bool success = obj2.set(obj1);
@ -41,7 +54,6 @@ TEST_CASE("JsonObject::set()") {
REQUIRE(spy.log() == AllocatorLog{
Allocate(sizeofPool()),
Allocate(sizeofString("hello")),
Allocate(sizeofString("world")),
});
}
@ -88,17 +100,17 @@ TEST_CASE("JsonObject::set()") {
JsonDocument doc3(&timebomb);
JsonObject obj3 = doc3.to<JsonObject>();
obj1["a"_s] = 1;
obj1["b"_s] = 2;
obj1["alpha"_s] = 1;
obj1["beta"_s] = 2;
bool success = obj3.set(obj1);
REQUIRE(success == false);
REQUIRE(doc3.as<std::string>() == "{\"a\":1}");
REQUIRE(doc3.as<std::string>() == "{\"alpha\":1}");
}
SECTION("copy fails in the middle of an array") {
TimebombAllocator timebomb(2);
TimebombAllocator timebomb(1);
JsonDocument doc3(&timebomb);
JsonObject obj3 = doc3.to<JsonObject>();

View File

@ -102,25 +102,21 @@ TEST_CASE("JsonObject::operator[]") {
REQUIRE(42 == obj[key]);
}
SECTION("string literals") {
SECTION("should not duplicate const char*") {
obj["hello"] = "world";
REQUIRE(spy.log() == AllocatorLog{
Allocate(sizeofPool()),
Allocate(sizeofStaticStringPool()),
});
REQUIRE(spy.log() == AllocatorLog{Allocate(sizeofPool())});
}
SECTION("should duplicate char* value") {
obj["hello"] = const_cast<char*>("world");
REQUIRE(spy.log() == AllocatorLog{
Allocate(sizeofPool()),
Allocate(sizeofStaticStringPool()),
Allocate(sizeofString("world")),
});
}
SECTION("should duplicate char* key") {
obj[const_cast<char*>("hello")] = 42;
obj[const_cast<char*>("hello")] = "world";
REQUIRE(spy.log() == AllocatorLog{
Allocate(sizeofPool()),
Allocate(sizeofString("hello")),
@ -140,13 +136,12 @@ TEST_CASE("JsonObject::operator[]") {
obj["hello"] = "world"_s;
REQUIRE(spy.log() == AllocatorLog{
Allocate(sizeofPool()),
Allocate(sizeofStaticStringPool()),
Allocate(sizeofString("world")),
});
}
SECTION("should duplicate std::string key") {
obj["hello"_s] = 42;
obj["hello"_s] = "world";
REQUIRE(spy.log() == AllocatorLog{
Allocate(sizeofPool()),
Allocate(sizeofString("hello")),
@ -163,7 +158,7 @@ TEST_CASE("JsonObject::operator[]") {
}
SECTION("should duplicate a non-static JsonString key") {
obj[JsonString("hello", false)] = 42;
obj[JsonString("hello", false)] = "world";
REQUIRE(spy.log() == AllocatorLog{
Allocate(sizeofPool()),
Allocate(sizeofString("hello")),
@ -171,10 +166,9 @@ TEST_CASE("JsonObject::operator[]") {
}
SECTION("should not duplicate a static JsonString key") {
obj[JsonString("hello", true)] = 42;
obj[JsonString("hello", true)] = "world";
REQUIRE(spy.log() == AllocatorLog{
Allocate(sizeofPool()),
Allocate(sizeofStaticStringPool()),
});
}

View File

@ -199,7 +199,7 @@ TEST_CASE("JsonVariant::as()") {
REQUIRE(variant.as<JsonString>() == "hello");
}
SECTION("set(std::string(\"4.2\"))") {
SECTION("set(std::string(\"4.2\")) (tiny string optimization)") {
variant.set("4.2"_s);
REQUIRE(variant.as<bool>() == true);
@ -211,6 +211,18 @@ TEST_CASE("JsonVariant::as()") {
REQUIRE(variant.as<JsonString>().isStatic() == false);
}
SECTION("set(std::string(\"123.45\"))") {
variant.set("123.45"_s);
REQUIRE(variant.as<bool>() == true);
REQUIRE(variant.as<long>() == 123L);
REQUIRE(variant.as<double>() == Approx(123.45));
REQUIRE(variant.as<const char*>() == "123.45"_s);
REQUIRE(variant.as<std::string>() == "123.45"_s);
REQUIRE(variant.as<JsonString>() == "123.45");
REQUIRE(variant.as<JsonString>().isStatic() == false);
}
SECTION("set(\"true\")") {
variant.set("true");

View File

@ -38,15 +38,13 @@ TEST_CASE("JsonVariant::set(JsonVariant)") {
REQUIRE(var1.as<std::string>() == "{\"value\":[42]}");
}
SECTION("stores string literals by pointer") {
SECTION("stores const char* by reference") {
var1.set("hello!!");
spyingAllocator.clearLog();
var2.set(var1);
REQUIRE(spyingAllocator.log() == AllocatorLog{
Allocate(sizeofStaticStringPool()),
});
REQUIRE(spyingAllocator.log() == AllocatorLog{});
}
SECTION("stores char* by copy") {

View File

@ -23,9 +23,7 @@ TEST_CASE("JsonVariant::set() when there is enough memory") {
REQUIRE(result == true);
CHECK(variant ==
"hello"_s); // linked string cannot contain '\0' at the moment
CHECK(spy.log() == AllocatorLog{
Allocate(sizeofStaticStringPool()),
});
CHECK(spy.log() == AllocatorLog{});
}
SECTION("const char*") {
@ -65,6 +63,18 @@ TEST_CASE("JsonVariant::set() when there is enough memory") {
});
}
SECTION("char* (tiny string optimization)") {
char str[16];
strcpy(str, "abc");
bool result = variant.set(str);
strcpy(str, "def");
REQUIRE(result == true);
REQUIRE(variant == "abc"); // stores by copy
REQUIRE(spy.log() == AllocatorLog{});
}
SECTION("(char*)0") {
bool result = variant.set(static_cast<char*>(0));
@ -139,9 +149,7 @@ TEST_CASE("JsonVariant::set() when there is enough memory") {
REQUIRE(result == true);
REQUIRE(variant == "world"); // stores by pointer
REQUIRE(spy.log() == AllocatorLog{
Allocate(sizeofStaticStringPool()),
});
REQUIRE(spy.log() == AllocatorLog{});
}
SECTION("non-static JsonString") {
@ -257,20 +265,6 @@ TEST_CASE("JsonVariant::set() with not enough memory") {
JsonVariant v = doc.to<JsonVariant>();
SECTION("string literal") {
bool result = v.set("hello world");
REQUIRE(result == false);
REQUIRE(v.isNull());
}
SECTION("static JsonString") {
bool result = v.set(JsonString("hello world", true));
REQUIRE(result == false);
REQUIRE(v.isNull());
}
SECTION("std::string") {
bool result = v.set("hello world!!"_s);

View File

@ -57,7 +57,7 @@ TEST_CASE("JsonVariantConst::operator[]") {
SECTION("string literal") {
REQUIRE(var["ab"] == "AB"_s);
REQUIRE(var["abc"] == "ABC"_s);
REQUIRE(var["abc\0d"] == "ABC"_s);
REQUIRE(var["abc\0d"] == "ABCD"_s);
REQUIRE(var["def"].isNull());
REQUIRE(var[0].isNull());
}

View File

@ -7,6 +7,7 @@ add_executable(MiscTests
conflicts.cpp
issue1967.cpp
issue2129.cpp
issue2166.cpp
JsonString.cpp
NoArduinoHeader.cpp
printable.cpp
@ -28,3 +29,23 @@ set_tests_properties(Misc
PROPERTIES
LABELS "Catch"
)
add_executable(Issue2181
issue2181.cpp # Cannot be linked with other tests
)
set_target_properties(Issue2181 PROPERTIES UNITY_BUILD OFF)
add_test(Issue2181 Issue2181)
set_tests_properties(Issue2181
PROPERTIES
LABELS "Catch"
)
if(CMAKE_CXX_COMPILER_ID MATCHES "Clang")
target_compile_options(Issue2181
PRIVATE
-Wno-keyword-macro # keyword is hidden by macro definition
)
endif()

View File

@ -21,7 +21,7 @@ TEST_CASE("adaptString()") {
auto s = adaptString("bravo\0alpha");
CHECK(s.isNull() == false);
CHECK(s.size() == 5);
CHECK(s.size() == 11);
CHECK(s.isStatic() == true);
}
@ -128,6 +128,7 @@ TEST_CASE("IsString<T>") {
CHECK(IsString<const __FlashStringHelper*>::value == true);
CHECK(IsString<const char*>::value == true);
CHECK(IsString<const char[8]>::value == true);
CHECK(IsString<const char[]>::value == true);
CHECK(IsString<::String>::value == true);
CHECK(IsString<::StringSumHelper>::value == true);
CHECK(IsString<const EmptyStruct*>::value == false);

View File

@ -0,0 +1,22 @@
// ArduinoJson - https://arduinojson.org
// Copyright © 2014-2025, Benoit BLANCHON
// MIT License
#include <ArduinoJson.h>
#include <catch.hpp>
struct CCLASS {
static const char mszKey[];
};
TEST_CASE("Issue #2166") {
JsonDocument doc;
doc[CCLASS::mszKey] = 12;
REQUIRE(doc.as<std::string>() == "{\"test3\":12}");
JsonObject obj = doc.to<JsonObject>();
obj[CCLASS::mszKey] = 12;
REQUIRE(doc.as<std::string>() == "{\"test3\":12}");
}
const char CCLASS::mszKey[] = "test3";

View File

@ -0,0 +1,15 @@
// ArduinoJson - https://arduinojson.org
// Copyright © 2014-2025, Benoit BLANCHON
// MIT License
#define true 0x1
#define false 0x0
#include <ArduinoJson.h>
#include <catch.hpp>
TEST_CASE("Issue #2181") {
JsonDocument doc;
doc["hello"] = "world";
REQUIRE(doc.as<std::string>() == "{\"hello\":\"world\"}");
}

View File

@ -5,8 +5,11 @@
#include <ArduinoJson.h>
#include <catch.hpp>
#include "Allocators.hpp"
TEST_CASE("deserialize MsgPack array") {
JsonDocument doc;
SpyingAllocator spy;
JsonDocument doc(&spy);
SECTION("fixarray") {
SECTION("empty") {
@ -30,6 +33,24 @@ TEST_CASE("deserialize MsgPack array") {
REQUIRE(array[0] == 1);
REQUIRE(array[1] == 2);
}
SECTION("tiny strings") {
DeserializationError error =
deserializeMsgPack(doc, "\x92\xA3xxx\xA3yyy");
REQUIRE(error == DeserializationError::Ok);
REQUIRE(doc.is<JsonArray>());
REQUIRE(doc.size() == 2);
REQUIRE(doc[0] == "xxx");
REQUIRE(doc[1] == "yyy");
REQUIRE(spy.log() == AllocatorLog{
Allocate(sizeofPool()),
Allocate(sizeofString("xxx")),
// Buffer is reused for the next string
Deallocate(sizeofString("xxx")),
Reallocate(sizeofPool(), sizeofPool(2)),
});
}
}
SECTION("array 16") {

View File

@ -348,13 +348,14 @@ TEST_CASE("deserializeMsgPack() under memory constaints") {
SECTION("{}") {
checkError(0, "\x80", DeserializationError::Ok);
}
SECTION("{H:1}") {
checkError(1, "\x81\xA1H\x01", DeserializationError::NoMemory);
checkError(2, "\x81\xA1H\x01", DeserializationError::Ok);
SECTION("{Hello:1}") {
checkError(1, "\x81\xA5Hello\x01", DeserializationError::NoMemory);
checkError(2, "\x81\xA5Hello\x01", DeserializationError::Ok);
}
SECTION("{H:1,W:2}") {
checkError(2, "\x82\xA1H\x01\xA1W\x02", DeserializationError::NoMemory);
checkError(3, "\x82\xA1H\x01\xA1W\x02", DeserializationError::Ok);
SECTION("{Hello:1,World:2}") {
checkError(2, "\x82\xA5Hello\x01\xA5World\x02",
DeserializationError::NoMemory);
checkError(3, "\x82\xA5Hello\x01\xA5World\x02", DeserializationError::Ok);
}
}
@ -362,14 +363,16 @@ TEST_CASE("deserializeMsgPack() under memory constaints") {
SECTION("{}") {
checkError(0, "\xDE\x00\x00", DeserializationError::Ok);
}
SECTION("{H:1}") {
checkError(1, "\xDE\x00\x01\xA1H\x01", DeserializationError::NoMemory);
checkError(2, "\xDE\x00\x01\xA1H\x01", DeserializationError::Ok);
}
SECTION("{H:1,W:2}") {
checkError(2, "\xDE\x00\x02\xA1H\x01\xA1W\x02",
SECTION("{Hello:1}") {
checkError(1, "\xDE\x00\x01\xA5Hello\x01",
DeserializationError::NoMemory);
checkError(3, "\xDE\x00\x02\xA1H\x01\xA1W\x02", DeserializationError::Ok);
checkError(2, "\xDE\x00\x01\xA5Hello\x01", DeserializationError::Ok);
}
SECTION("{Hello:1,World:2}") {
checkError(2, "\xDE\x00\x02\xA5Hello\x01\xA5World\x02",
DeserializationError::NoMemory);
checkError(3, "\xDE\x00\x02\xA5Hello\x01\xA5World\x02",
DeserializationError::Ok);
}
}
@ -382,8 +385,8 @@ TEST_CASE("deserializeMsgPack() under memory constaints") {
DeserializationError::NoMemory);
checkError(2, "\xDF\x00\x00\x00\x01\xA1H\x01", DeserializationError::Ok);
}
SECTION("{H:1,W:2}") {
checkError(2, "\xDF\x00\x00\x00\x02\xA1H\x01\xA1W\x02",
SECTION("{Hello:1,World:2}") {
checkError(2, "\xDF\x00\x00\x00\x02\xA5Hello\x01\xA5World\x02",
DeserializationError::NoMemory);
checkError(3, "\xDF\x00\x00\x00\x02\xA1H\x01\xA1W\x02",
DeserializationError::Ok);

View File

@ -104,8 +104,6 @@ TEST_CASE("deserializeMsgPack(MemberProxy)") {
REQUIRE(err == DeserializationError::Ok);
REQUIRE(doc.as<std::string>() == "{\"hello\":\"world\",\"value\":[42]}");
REQUIRE(spy.log() == AllocatorLog{
Allocate(sizeofStaticStringPool()),
});
REQUIRE(spy.log() == AllocatorLog{});
}
}

View File

@ -5,10 +5,10 @@
add_executable(ResourceManagerTests
allocVariant.cpp
clear.cpp
saveStaticString.cpp
saveString.cpp
shrinkToFit.cpp
size.cpp
StringBuffer.cpp
StringBuilder.cpp
swap.cpp
)

View File

@ -0,0 +1,50 @@
// ArduinoJson - https://arduinojson.org
// Copyright © 2014-2025, Benoit BLANCHON
// MIT License
#include <ArduinoJson/Memory/StringBuffer.hpp>
#include <catch.hpp>
#include "Allocators.hpp"
#include "Literals.hpp"
using namespace ArduinoJson::detail;
TEST_CASE("StringBuffer") {
SpyingAllocator spy;
ResourceManager resources(&spy);
StringBuffer sb(&resources);
VariantData variant;
SECTION("Tiny string") {
auto ptr = sb.reserve(3);
strcpy(ptr, "hi!");
sb.save(&variant);
REQUIRE(variant.type() == VariantType::TinyString);
REQUIRE(variant.asString() == "hi!");
}
SECTION("Tiny string can't contain NUL") {
auto ptr = sb.reserve(3);
memcpy(ptr, "a\0b", 3);
sb.save(&variant);
REQUIRE(variant.type() == VariantType::OwnedString);
auto str = variant.asString();
REQUIRE(str.size() == 3);
REQUIRE(str.c_str()[0] == 'a');
REQUIRE(str.c_str()[1] == 0);
REQUIRE(str.c_str()[2] == 'b');
}
SECTION("Tiny string can't have 4 characters") {
auto ptr = sb.reserve(4);
strcpy(ptr, "alfa");
sb.save(&variant);
REQUIRE(variant.type() == VariantType::OwnedString);
REQUIRE(variant.asString() == "alfa");
}
}

View File

@ -7,6 +7,7 @@
#include "Allocators.hpp"
using namespace ArduinoJson;
using namespace ArduinoJson::detail;
TEST_CASE("StringBuilder") {
@ -16,17 +17,36 @@ TEST_CASE("StringBuilder") {
SECTION("Empty string") {
StringBuilder str(&resources);
VariantData data;
str.startString();
str.save();
str.save(&data);
REQUIRE(resources.size() == sizeofString(""));
REQUIRE(resources.overflowed() == false);
REQUIRE(spyingAllocator.log() ==
AllocatorLog{
Allocate(sizeofStringBuffer()),
Reallocate(sizeofStringBuffer(), sizeofString("")),
});
REQUIRE(spyingAllocator.log() == AllocatorLog{
Allocate(sizeofStringBuffer()),
});
REQUIRE(data.type() == VariantType::TinyString);
}
SECTION("Tiny string") {
StringBuilder str(&resources);
str.startString();
str.append("url");
REQUIRE(str.isValid() == true);
REQUIRE(str.str() == "url");
REQUIRE(spyingAllocator.log() == AllocatorLog{
Allocate(sizeofStringBuffer()),
});
VariantData data;
str.save(&data);
REQUIRE(resources.overflowed() == false);
REQUIRE(data.type() == VariantType::TinyString);
REQUIRE(data.asString() == "url");
}
SECTION("Short string fits in first allocation") {
@ -96,48 +116,71 @@ TEST_CASE("StringBuilder") {
}
}
static StringNode* addStringToPool(ResourceManager& resources, const char* s) {
StringBuilder str(&resources);
str.startString();
str.append(s);
return str.save();
static VariantData saveString(StringBuilder& builder, const char* s) {
VariantData data;
builder.startString();
builder.append(s);
builder.save(&data);
return data;
}
TEST_CASE("StringBuilder::save() deduplicates strings") {
ResourceManager resources;
SpyingAllocator spy;
ResourceManager resources(&spy);
StringBuilder builder(&resources);
SECTION("Basic") {
auto s1 = addStringToPool(resources, "hello");
auto s2 = addStringToPool(resources, "world");
auto s3 = addStringToPool(resources, "hello");
auto s1 = saveString(builder, "hello");
auto s2 = saveString(builder, "world");
auto s3 = saveString(builder, "hello");
REQUIRE(s1 == s3);
REQUIRE(s2 != s3);
REQUIRE(s1->references == 2);
REQUIRE(s2->references == 1);
REQUIRE(s3->references == 2);
REQUIRE(resources.size() == sizeofString("hello") + sizeofString("world"));
REQUIRE(s1.asString() == "hello");
REQUIRE(s2.asString() == "world");
REQUIRE(+s1.asString().c_str() == +s3.asString().c_str()); // same address
REQUIRE(spy.log() ==
AllocatorLog{
Allocate(sizeofStringBuffer()),
Reallocate(sizeofStringBuffer(), sizeofString("hello")),
Allocate(sizeofStringBuffer()),
Reallocate(sizeofStringBuffer(), sizeofString("world")),
Allocate(sizeofStringBuffer()),
});
}
SECTION("Requires terminator") {
auto s1 = addStringToPool(resources, "hello world");
auto s2 = addStringToPool(resources, "hello");
auto s1 = saveString(builder, "hello world");
auto s2 = saveString(builder, "hello");
REQUIRE(s2 != s1);
REQUIRE(s1->references == 1);
REQUIRE(s2->references == 1);
REQUIRE(resources.size() ==
sizeofString("hello world") + sizeofString("hello"));
REQUIRE(s1.asString() == "hello world");
REQUIRE(s2.asString() == "hello");
REQUIRE(+s2.asString().c_str() !=
+s1.asString().c_str()); // different address
REQUIRE(spy.log() ==
AllocatorLog{
Allocate(sizeofStringBuffer()),
Reallocate(sizeofStringBuffer(), sizeofString("hello world")),
Allocate(sizeofStringBuffer()),
Reallocate(sizeofStringBuffer(), sizeofString("hello")),
});
}
SECTION("Don't overrun") {
auto s1 = addStringToPool(resources, "hello world");
auto s2 = addStringToPool(resources, "wor");
auto s1 = saveString(builder, "hello world");
auto s2 = saveString(builder, "worl");
REQUIRE(s2 != s1);
REQUIRE(s1->references == 1);
REQUIRE(s2->references == 1);
REQUIRE(resources.size() ==
sizeofString("hello world") + sizeofString("wor"));
REQUIRE(s1.asString() == "hello world");
REQUIRE(s2.asString() == "worl");
REQUIRE(s2.asString().c_str() !=
s1.asString().c_str()); // different address
REQUIRE(spy.log() ==
AllocatorLog{
Allocate(sizeofStringBuffer()),
Reallocate(sizeofStringBuffer(), sizeofString("hello world")),
Allocate(sizeofStringBuffer()),
Reallocate(sizeofStringBuffer(), sizeofString("worl")),
});
}
}

View File

@ -1,47 +0,0 @@
// ArduinoJson - https://arduinojson.org
// Copyright © 2014-2025, Benoit BLANCHON
// MIT License
#include <ArduinoJson/Memory/ResourceManager.hpp>
#include <ArduinoJson/Strings/StringAdapters.hpp>
#include <catch.hpp>
#include "Allocators.hpp"
using namespace ArduinoJson::detail;
TEST_CASE("ResourceManager::saveStaticString() deduplicates strings") {
SpyingAllocator spy;
ResourceManager resources(&spy);
auto str1 = "hello";
auto str2 = "world";
auto id1 = resources.saveStaticString(str1);
auto id2 = resources.saveStaticString(str2);
REQUIRE(id1 != id2);
auto id3 = resources.saveStaticString(str1);
REQUIRE(id1 == id3);
resources.shrinkToFit();
REQUIRE(spy.log() ==
AllocatorLog{
Allocate(sizeofStaticStringPool()),
Reallocate(sizeofStaticStringPool(), sizeofStaticStringPool(2)),
});
REQUIRE(resources.overflowed() == false);
}
TEST_CASE("ResourceManager::saveStaticString() when allocation fails") {
SpyingAllocator spy(FailingAllocator::instance());
ResourceManager resources(&spy);
auto slotId = resources.saveStaticString("hello");
REQUIRE(slotId == NULL_SLOT);
REQUIRE(resources.overflowed() == true);
REQUIRE(spy.log() == AllocatorLog{
AllocateFail(sizeofStaticStringPool()),
});
}

View File

@ -1,7 +1,7 @@
version: "7.3.0"
version: "7.4.2"
description: >-
A simple and efficient JSON library for embedded C++.
★ 6785 stars on GitHub!
★ 6953 stars on GitHub!
Supports serialization, deserialization, MessagePack, streams, filtering, and more.
Fully tested and documented.
url: https://arduinojson.org/

View File

@ -1,13 +1,13 @@
{
"name": "ArduinoJson",
"keywords": "json, rest, http, web",
"description": "A simple and efficient JSON library for embedded C++. ⭐ 6785 stars on GitHub! Supports serialization, deserialization, MessagePack, streams, filtering, and more. Fully tested and documented.",
"description": "A simple and efficient JSON library for embedded C++. ⭐ 6953 stars on GitHub! Supports serialization, deserialization, MessagePack, streams, filtering, and more. Fully tested and documented.",
"homepage": "https://arduinojson.org/?utm_source=meta&utm_medium=library.json",
"repository": {
"type": "git",
"url": "https://github.com/bblanchon/ArduinoJson.git"
},
"version": "7.3.0",
"version": "7.4.2",
"authors": {
"name": "Benoit Blanchon",
"url": "https://blog.benoitblanchon.fr"

View File

@ -1,9 +1,9 @@
name=ArduinoJson
version=7.3.0
version=7.4.2
author=Benoit Blanchon <blog.benoitblanchon.fr>
maintainer=Benoit Blanchon <blog.benoitblanchon.fr>
sentence=A simple and efficient JSON library for embedded C++.
paragraph=⭐ 6785 stars on GitHub! Supports serialization, deserialization, MessagePack, streams, filtering, and more. Fully tested and documented.
paragraph=⭐ 6953 stars on GitHub! Supports serialization, deserialization, MessagePack, streams, filtering, and more. Fully tested and documented.
category=Data Processing
url=https://arduinojson.org/?utm_source=meta&utm_medium=library.properties
architectures=*

View File

@ -26,6 +26,15 @@
# endif
#endif
// Remove true and false macros defined by some cores, such as Arduino Due's
// See issues #2181 and arduino/ArduinoCore-sam#50
#ifdef true
# undef true
#endif
#ifdef false
# undef false
#endif
#include "ArduinoJson/Array/JsonArray.hpp"
#include "ArduinoJson/Object/JsonObject.hpp"
#include "ArduinoJson/Variant/JsonVariantConst.hpp"

View File

@ -275,13 +275,11 @@ class JsonDeserializer {
if (memberFilter.allow()) {
auto member = object.getMember(adaptString(key), resources_);
if (!member) {
// Save key in memory pool.
auto savedKey = stringBuilder_.save();
// Allocate slot in object
member = object.addMember(savedKey, resources_);
if (!member)
auto keyVariant = object.addPair(&member, resources_);
if (!keyVariant)
return DeserializationError::NoMemory;
stringBuilder_.save(keyVariant);
} else {
member->clear(resources_);
}
@ -390,7 +388,7 @@ class JsonDeserializer {
if (err)
return err;
variant.setOwnedString(stringBuilder_.save());
stringBuilder_.save(&variant);
return DeserializationError::Ok;
}

View File

@ -34,11 +34,6 @@ class Slot {
return ptr_;
}
T& operator*() const {
ARDUINOJSON_ASSERT(ptr_ != nullptr);
return *ptr_;
}
T* operator->() const {
ARDUINOJSON_ASSERT(ptr_ != nullptr);
return ptr_;
@ -81,14 +76,6 @@ class MemoryPool {
return slots_ + id;
}
SlotId find(const T& value) const {
for (SlotId i = 0; i < usage_; i++) {
if (slots_[i] == value)
return i;
}
return NULL_SLOT;
}
void clear() {
usage_ = 0;
}

View File

@ -114,15 +114,6 @@ class MemoryPoolList {
return pools_[poolIndex].getSlot(indexInPool);
}
SlotId find(const T& value) const {
for (PoolCount i = 0; i < count_; i++) {
SlotId id = pools_[i].find(value);
if (id != NULL_SLOT)
return SlotId(i * ARDUINOJSON_POOL_CAPACITY + id);
}
return NULL_SLOT;
}
void clear(Allocator* allocator) {
for (PoolCount i = 0; i < count_; i++)
pools_[i].destroy(allocator);

View File

@ -34,7 +34,6 @@ class ResourceManager {
~ResourceManager() {
stringPool_.clear(allocator_);
variantPools_.clear(allocator_);
staticStringsPools_.clear(allocator_);
}
ResourceManager(const ResourceManager&) = delete;
@ -43,7 +42,6 @@ class ResourceManager {
friend void swap(ResourceManager& a, ResourceManager& b) {
swap(a.stringPool_, b.stringPool_);
swap(a.variantPools_, b.variantPools_);
swap(a.staticStringsPools_, b.staticStringsPools_);
swap_(a.allocator_, b.allocator_);
swap_(a.overflowed_, b.overflowed_);
}
@ -113,34 +111,14 @@ class ResourceManager {
stringPool_.dereference(s, allocator_);
}
SlotId saveStaticString(const char* s) {
auto existingSlotId = staticStringsPools_.find(s);
if (existingSlotId != NULL_SLOT)
return existingSlotId;
auto slot = staticStringsPools_.allocSlot(allocator_);
if (slot)
*slot = s;
else
overflowed_ = true;
return slot.id();
}
const char* getStaticString(SlotId id) const {
return *staticStringsPools_.getSlot(id);
}
void clear() {
overflowed_ = false;
variantPools_.clear(allocator_);
overflowed_ = false;
stringPool_.clear(allocator_);
staticStringsPools_.clear(allocator_);
}
void shrinkToFit() {
variantPools_.shrinkToFit(allocator_);
staticStringsPools_.shrinkToFit(allocator_);
}
private:
@ -148,7 +126,6 @@ class ResourceManager {
bool overflowed_;
StringPool stringPool_;
MemoryPoolList<SlotData> variantPools_;
MemoryPoolList<const char*> staticStringsPools_;
};
ARDUINOJSON_END_PRIVATE_NAMESPACE

View File

@ -32,7 +32,26 @@ class StringBuffer {
return node_->data;
}
StringNode* save() {
JsonString str() const {
ARDUINOJSON_ASSERT(node_ != nullptr);
return JsonString(node_->data, node_->length);
}
void save(VariantData* data) {
ARDUINOJSON_ASSERT(node_ != nullptr);
const char* s = node_->data;
if (isTinyString(s, size_))
data->setTinyString(adaptString(s, size_));
else
data->setOwnedString(commitStringNode());
}
void saveRaw(VariantData* data) {
data->setRawString(commitStringNode());
}
private:
StringNode* commitStringNode() {
ARDUINOJSON_ASSERT(node_ != nullptr);
node_->data[size_] = 0;
auto node = resources_->getString(adaptString(node_->data, size_));
@ -52,13 +71,6 @@ class StringBuffer {
return node;
}
JsonString str() const {
ARDUINOJSON_ASSERT(node_ != nullptr);
return JsonString(node_->data, node_->length);
}
private:
ResourceManager* resources_;
StringNode* node_ = nullptr;
size_t size_ = 0;

View File

@ -25,10 +25,18 @@ class StringBuilder {
node_ = resources_->createString(initialCapacity);
}
StringNode* save() {
void save(VariantData* variant) {
ARDUINOJSON_ASSERT(variant != nullptr);
ARDUINOJSON_ASSERT(node_ != nullptr);
node_->data[size_] = 0;
StringNode* node = resources_->getString(adaptString(node_->data, size_));
char* p = node_->data;
if (isTinyString(p, size_)) {
variant->setTinyString(adaptString(p, size_));
return;
}
p[size_] = 0;
StringNode* node = resources_->getString(adaptString(p, size_));
if (!node) {
node = resources_->resizeString(node_, size_);
ARDUINOJSON_ASSERT(node != nullptr); // realloc to smaller can't fail
@ -37,7 +45,7 @@ class StringBuilder {
} else {
node->references++;
}
return node;
variant->setOwnedString(node);
}
void append(const char* s) {

View File

@ -305,7 +305,7 @@ class MsgPackDeserializer {
if (err)
return err;
variant->setOwnedString(stringBuffer_.save());
stringBuffer_.save(variant);
return DeserializationError::Ok;
}
@ -334,7 +334,7 @@ class MsgPackDeserializer {
if (err)
return err;
variant->setRawString(stringBuffer_.save());
stringBuffer_.saveRaw(variant);
return DeserializationError::Ok;
}
@ -403,19 +403,16 @@ class MsgPackDeserializer {
JsonString key = stringBuffer_.str();
TFilter memberFilter = filter[key.c_str()];
VariantData* member;
VariantData* member = 0;
if (memberFilter.allow()) {
ARDUINOJSON_ASSERT(object != 0);
// Save key in memory pool.
auto savedKey = stringBuffer_.save();
member = object->addMember(savedKey, resources_);
if (!member)
auto keyVariant = object->addPair(&member, resources_);
if (!keyVariant)
return DeserializationError::NoMemory;
} else {
member = 0;
stringBuffer_.save(keyVariant);
}
err = parseVariant(member, memberFilter, nestingLimit.decrement());

View File

@ -18,7 +18,7 @@ class JsonPair {
JsonPair(detail::ObjectData::iterator iterator,
detail::ResourceManager* resources) {
if (!iterator.done()) {
key_ = iterator->asString(resources);
key_ = iterator->asString();
iterator.next(resources);
value_ = JsonVariant(iterator.data(), resources);
}
@ -46,7 +46,7 @@ class JsonPairConst {
JsonPairConst(detail::ObjectData::iterator iterator,
const detail::ResourceManager* resources) {
if (!iterator.done()) {
key_ = iterator->asString(resources);
key_ = iterator->asString();
iterator.next(resources);
value_ = JsonVariantConst(iterator.data(), resources);
}

View File

@ -10,9 +10,11 @@ ARDUINOJSON_BEGIN_PRIVATE_NAMESPACE
class ObjectData : public CollectionData {
public:
template <typename TAdaptedString> // also works with StringNode*
template <typename TAdaptedString>
VariantData* addMember(TAdaptedString key, ResourceManager* resources);
VariantData* addPair(VariantData** value, ResourceManager* resources);
template <typename TAdaptedString>
VariantData* getOrAddMember(TAdaptedString key, ResourceManager* resources);

View File

@ -36,7 +36,7 @@ inline ObjectData::iterator ObjectData::findKey(
return iterator();
bool isKey = true;
for (auto it = createIterator(resources); !it.done(); it.next(resources)) {
if (isKey && stringEquals(key, adaptString(it->asString(resources))))
if (isKey && stringEquals(key, adaptString(it->asString())))
return it;
isKey = !isKey;
}
@ -68,6 +68,22 @@ inline VariantData* ObjectData::addMember(TAdaptedString key,
return valueSlot.ptr();
}
inline VariantData* ObjectData::addPair(VariantData** value,
ResourceManager* resources) {
auto keySlot = resources->allocVariant();
if (!keySlot)
return nullptr;
auto valueSlot = resources->allocVariant();
if (!valueSlot)
return nullptr;
*value = valueSlot.ptr();
CollectionData::appendPair(keySlot, valueSlot, resources);
return keySlot.ptr();
}
// Returns the size (in bytes) of an object with n members.
constexpr size_t sizeofObject(size_t n) {
return 2 * n * ResourceManager::slotSize;

View File

@ -76,13 +76,22 @@ struct StringAdapter<TChar*, enable_if_t<IsChar<TChar>::value>> {
}
};
template <typename TChar>
struct StringAdapter<TChar[], enable_if_t<IsChar<TChar>::value>> {
using AdaptedString = RamString;
static AdaptedString adapt(const TChar* p) {
auto str = reinterpret_cast<const char*>(p);
return AdaptedString(str, str ? ::strlen(str) : 0);
}
};
template <size_t N>
struct StringAdapter<const char (&)[N]> {
using AdaptedString = RamString;
static AdaptedString adapt(const char* p) {
ARDUINOJSON_ASSERT(p);
return RamString(p, ::strlen(p), true);
static AdaptedString adapt(const char (&p)[N]) {
return RamString(p, N - 1, true);
}
};

View File

@ -28,7 +28,8 @@ class JsonString {
detail::enable_if_t<detail::is_integral<TSize>::value &&
!detail::is_same<TSize, bool>::value,
int> = 0>
JsonString(const char* data, TSize sz) : str_(data, size_t(sz), false) {}
JsonString(const char* data, TSize sz, bool isStatic = false)
: str_(data, size_t(sz), isStatic) {}
// Returns a pointer to the characters.
const char* c_str() const {

View File

@ -160,7 +160,7 @@ struct Converter<const char*> : private detail::VariantAttorney {
static const char* fromJson(JsonVariantConst src) {
auto data = getData(src);
return data ? data->asString(getResourceManager(src)).c_str() : 0;
return data ? data->asString().c_str() : 0;
}
static bool checkJson(JsonVariantConst src) {
@ -178,7 +178,7 @@ struct Converter<JsonString> : private detail::VariantAttorney {
static JsonString fromJson(JsonVariantConst src) {
auto data = getData(src);
return data ? data->asString(getResourceManager(src)) : JsonString();
return data ? data->asString() : JsonString();
}
static bool checkJson(JsonVariantConst src) {
@ -230,9 +230,9 @@ class StringBuilderPrint : public Print {
copier_.startString();
}
StringNode* save() {
void save(VariantData* data) {
ARDUINOJSON_ASSERT(!overflowed());
return copier_.save();
copier_.save(data);
}
size_t write(uint8_t c) {
@ -268,7 +268,7 @@ inline void convertToJson(const ::Printable& src, JsonVariant dst) {
src.printTo(print);
if (print.overflowed())
return;
data->setOwnedString(print.save());
print.save(data);
}
#endif

View File

@ -24,6 +24,7 @@ enum class VariantTypeBits : uint8_t {
enum class VariantType : uint8_t {
Null = 0, // 0000 0000
TinyString = 0x02, // 0000 0010
RawString = 0x03, // 0000 0011
LinkedString = 0x04, // 0000 0100
OwnedString = 0x05, // 0000 0101
@ -46,6 +47,8 @@ inline bool operator&(VariantType type, VariantTypeBits bit) {
return (uint8_t(type) & uint8_t(bit)) != 0;
}
const size_t tinyStringMaxLength = 3;
union VariantContent {
VariantContent() {}
@ -53,11 +56,15 @@ union VariantContent {
bool asBoolean;
uint32_t asUint32;
int32_t asInt32;
#if ARDUINOJSON_USE_EXTENSIONS
SlotId asSlotId;
#endif
ArrayData asArray;
ObjectData asObject;
CollectionData asCollection;
const char* asLinkedString;
struct StringNode* asOwnedString;
char asTinyString[tinyStringMaxLength + 1];
};
#if ARDUINOJSON_USE_EXTENSIONS

View File

@ -17,6 +17,16 @@ ARDUINOJSON_BEGIN_PRIVATE_NAMESPACE
template <typename T>
T parseNumber(const char* s);
template <typename T>
static bool isTinyString(const T& s, size_t n) {
if (n > tinyStringMaxLength)
return false;
bool containsNul = false;
for (uint8_t i = 0; i < uint8_t(n); i++)
containsNul |= !s[i];
return !containsNul;
}
class VariantData {
VariantContent content_; // must be first to allow cast from array to variant
VariantType type_;
@ -63,8 +73,11 @@ class VariantData {
case VariantType::Object:
return visit.visit(content_.asObject);
case VariantType::TinyString:
return visit.visit(JsonString(content_.asTinyString));
case VariantType::LinkedString:
return visit.visit(JsonString(asLinkedString(resources), true));
return visit.visit(JsonString(content_.asLinkedString, true));
case VariantType::OwnedString:
return visit.visit(JsonString(content_.asOwnedString->data,
@ -199,8 +212,11 @@ class VariantData {
case VariantType::Int64:
return static_cast<T>(extension->asInt64);
#endif
case VariantType::TinyString:
str = content_.asTinyString;
break;
case VariantType::LinkedString:
str = asLinkedString(resources);
str = content_.asLinkedString;
break;
case VariantType::OwnedString:
str = content_.asOwnedString->data;
@ -241,8 +257,11 @@ class VariantData {
case VariantType::Int64:
return convertNumber<T>(extension->asInt64);
#endif
case VariantType::TinyString:
str = content_.asTinyString;
break;
case VariantType::LinkedString:
str = asLinkedString(resources);
str = content_.asLinkedString;
break;
case VariantType::OwnedString:
str = content_.asOwnedString->data;
@ -279,12 +298,12 @@ class VariantData {
}
}
const char* asLinkedString(const ResourceManager* resources) const;
JsonString asString(const ResourceManager* resources) const {
JsonString asString() const {
switch (type_) {
case VariantType::TinyString:
return JsonString(content_.asTinyString);
case VariantType::LinkedString:
return JsonString(asLinkedString(resources), true);
return JsonString(content_.asLinkedString, true);
case VariantType::OwnedString:
return JsonString(content_.asOwnedString->data,
content_.asOwnedString->length);
@ -397,7 +416,8 @@ class VariantData {
bool isString() const {
return type_ == VariantType::LinkedString ||
type_ == VariantType::OwnedString;
type_ == VariantType::OwnedString ||
type_ == VariantType::TinyString;
}
size_t nesting(const ResourceManager* resources) const {
@ -490,11 +510,6 @@ class VariantData {
template <typename TAdaptedString>
bool setString(TAdaptedString value, ResourceManager* resources);
bool setString(StringNode* s, ResourceManager*) {
setOwnedString(s);
return true;
}
template <typename TAdaptedString>
static void setString(VariantData* var, TAdaptedString value,
ResourceManager* resources) {
@ -504,7 +519,29 @@ class VariantData {
var->setString(value, resources);
}
bool setLinkedString(const char* s, ResourceManager* resources);
void setLinkedString(const char* s) {
ARDUINOJSON_ASSERT(type_ == VariantType::Null); // must call clear() first
ARDUINOJSON_ASSERT(s);
type_ = VariantType::LinkedString;
content_.asLinkedString = s;
}
template <typename TAdaptedString>
void setTinyString(const TAdaptedString& s) {
ARDUINOJSON_ASSERT(type_ == VariantType::Null); // must call clear() first
ARDUINOJSON_ASSERT(s.size() <= tinyStringMaxLength);
type_ = VariantType::TinyString;
auto n = uint8_t(s.size());
for (uint8_t i = 0; i < n; i++) {
char c = s[i];
ARDUINOJSON_ASSERT(c != 0); // no NUL in tiny string
content_.asTinyString[i] = c;
}
content_.asTinyString[n] = 0;
}
void setOwnedString(StringNode* s) {
ARDUINOJSON_ASSERT(type_ == VariantType::Null); // must call clear() first

View File

@ -18,20 +18,6 @@ inline void VariantData::setRawString(SerializedValue<T> value,
setRawString(dup);
}
inline bool VariantData::setLinkedString(const char* s,
ResourceManager* resources) {
ARDUINOJSON_ASSERT(type_ == VariantType::Null); // must call clear() first
ARDUINOJSON_ASSERT(s);
auto slotId = resources->saveStaticString(s);
if (slotId == NULL_SLOT)
return false;
type_ = VariantType::LinkedString;
content_.asSlotId = slotId;
return true;
}
template <typename TAdaptedString>
inline bool VariantData::setString(TAdaptedString value,
ResourceManager* resources) {
@ -40,8 +26,15 @@ inline bool VariantData::setString(TAdaptedString value,
if (value.isNull())
return false;
if (value.isStatic())
return setLinkedString(value.data(), resources);
if (value.isStatic()) {
setLinkedString(value.data());
return true;
}
if (isTinyString(value, value.size())) {
setTinyString(value);
return true;
}
auto dup = resources->saveString(value);
if (dup) {
@ -77,12 +70,6 @@ inline const VariantExtension* VariantData::getExtension(
}
#endif
inline const char* VariantData::asLinkedString(
const ResourceManager* resources) const {
ARDUINOJSON_ASSERT(type_ == VariantType::LinkedString);
return resources->getStaticString(content_.asSlotId);
}
template <typename T>
enable_if_t<sizeof(T) == 8, bool> VariantData::setFloat(
T value, ResourceManager* resources) {

View File

@ -4,8 +4,8 @@
#pragma once
#define ARDUINOJSON_VERSION "7.3.0"
#define ARDUINOJSON_VERSION "7.4.2"
#define ARDUINOJSON_VERSION_MAJOR 7
#define ARDUINOJSON_VERSION_MINOR 3
#define ARDUINOJSON_VERSION_REVISION 0
#define ARDUINOJSON_VERSION_MACRO V730
#define ARDUINOJSON_VERSION_MINOR 4
#define ARDUINOJSON_VERSION_REVISION 2
#define ARDUINOJSON_VERSION_MACRO V742