Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,26 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.2.10] - 2026-11-04

### Added
- Warning messages via callback when precision is lost due to truncated whole number or fraction digits (see https://github.com/FFMG/tinyjson/issues/1).

### Changed
- Incremented version to 0.2.10.
- **Breaking Change**: `get_number()` now clamps whole numbers that exceed `long long` range to `LLONG_MAX` or `LLONG_MIN` instead of wrapping around (e.g. `18446744073709551615` used to return `-1`). Use `get_float()` or `dump()` to retrieve the full value (see https://github.com/FFMG/tinyjson/issues/55).

### Fixed
- Fixed a stack buffer overflow when dumping numbers with more than 254 fraction digits by dynamically sizing the buffer (see https://github.com/FFMG/tinyjson/issues/53).
- Fixed silent integer overflows and parsing errors for large numbers and exponents (see https://github.com/FFMG/tinyjson/issues/1, https://github.com/FFMG/tinyjson/issues/54):
- Whole numbers exceeding `ULLONG_MAX` are converted to exponent notation instead of wrapping around.
- Fractions are capped at 19 significant digits without wrapping around.
- Exponents exceeding 32 bits or outside `[-100000, 100000]` are rejected with an out-of-range parse error.
- Fixed exponent normalisation and shifting for fractions with leading zeros or large offsets (e.g. `0.12e20`, `0.12e30`, `0.1234e-1`).
- Fixed `fast_power_of_10()` overflow for exponents >= 64.
- Fixed `get_float()` for whole numbers between 2^63 and 2^64-1 returning a negative value, and for fractions with more than 19 digits (see https://github.com/FFMG/tinyjson/issues/54, https://github.com/FFMG/tinyjson/issues/55).
- Fixed undefined behaviour in `get_number()` when negating `LLONG_MIN` (`-9223372036854775808`) and when casting out-of-range floats or exponents to `long long` (see https://github.com/FFMG/tinyjson/issues/55).

## [0.2.9] - 2026-10-29

### Added
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

[![CI](https://github.com/FFMG/TinyJSON/actions/workflows/c-cpp.yml/badge.svg)](https://github.com/FFMG/TinyJSON/actions/workflows/c-cpp.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![Version](https://img.shields.io/badge/version-0.2.9-blue.svg)](src/TinyJSON.h)
[![Version](https://img.shields.io/badge/version-0.2.10-blue.svg)](src/TinyJSON.h)

A lightweight and lightning-fast C++ JSON & JSON5 parser designed for high performance and minimal footprint.

Expand Down Expand Up @@ -131,8 +131,8 @@ The version is set in the `TinyJSON.h` file.
```cpp
static const short TJ_VERSION_MAJOR = 0;
static const short TJ_VERSION_MINOR = 2;
static const short TJ_VERSION_PATCH = 9;
static const char TJ_VERSION_STRING[] = "0.2.9";
static const short TJ_VERSION_PATCH = 10;
static const char TJ_VERSION_STRING[] = "0.2.10";
```

### Simple Value Access
Expand Down Expand Up @@ -785,7 +785,7 @@ include(FetchContent)
FetchContent_Declare(
TinyJSON
GIT_REPOSITORY https://github.com/FFMG/TinyJSON.git
GIT_TAG v0.2.9 # or main
GIT_TAG v0.2.10 # or main
)
FetchContent_MakeAvailable(TinyJSON)
target_link_libraries(your_target PUBLIC TinyJSON)
Expand Down
454 changes: 295 additions & 159 deletions src/TinyJSON.cpp

Large diffs are not rendered by default.

6 changes: 4 additions & 2 deletions src/TinyJSON.h
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,11 @@
// v0.2.7 - improved parse_file error reporting with specific system errors and file name.
// v0.2.8 - conformed empty input parsing to RFC specifications (returns nullptr / throws instead of empty string).
// v0.2.9 - added line and column numbers to parse messages and TJParseException.
// v0.2.10 - fixed number overflows when parsing, dumping and getting numbers, (issues #53, #54 and #55).
static const short TJ_VERSION_MAJOR = 0;
static const short TJ_VERSION_MINOR = 2;
static const short TJ_VERSION_PATCH = 9;
static const char TJ_VERSION_STRING[] = "0.2.9";
static const short TJ_VERSION_PATCH = 10;
static const char TJ_VERSION_STRING[] = "0.2.10";

#ifndef TJ_USE_CHAR
# define TJ_USE_CHAR 1
Expand Down Expand Up @@ -1819,6 +1820,7 @@ class TJDictionary;
virtual ~TJValueNumberInt() = default;

long long get_number() const;
long double get_float() const;

protected:
/// <summary>
Expand Down
98 changes: 97 additions & 1 deletion tests/testtinyjsondump.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
#define TJ_USE_CHAR 1
#include "../src/TinyJSON.h"

#include <string>
#include <vector>

TEST(TestDump, EmptyArray) {
auto json = TinyJSON::TJ::parse("[]");
ASSERT_NE(nullptr, json);
Expand Down Expand Up @@ -411,4 +414,97 @@ TEST(TestDump, DumpOfAStringWillExcapeTheNewLine) {

ASSERT_STREQ(R"("This is a string.\nAnd this is a new line")", text);
delete json;
}
}

// https://github.com/FFMG/tinyjson/issues/53
// Build a number with a very long run of zeros, for example "0." + "000...000" + "1"
static std::string make_number_with_zeros(const std::string& prefix, const unsigned int number_of_zeros, const std::string& suffix)
{
return prefix + std::string(number_of_zeros, '0') + suffix;
}

// https://github.com/FFMG/tinyjson/issues/53
// The fraction is written into a 255 char buffer, anything longer overflows the stack.
TEST(TestDump, Issue53_FloatWithMoreThan254FractionDigitsCanBeDumped)
{
const auto value = make_number_with_zeros("0.", 300, "1");
auto json = TinyJSON::TJ::parse(value.c_str());
ASSERT_NE(nullptr, json);

const auto& text = json->dump(TinyJSON::formatting::minify);
ASSERT_NE(nullptr, text);

ASSERT_STREQ(value.c_str(), text);
delete json;
}

// https://github.com/FFMG/tinyjson/issues/53
TEST(TestDump, Issue53_NegativeFloatWithMoreThan254FractionDigitsCanBeDumped)
{
const auto value = make_number_with_zeros("-0.", 300, "1");
auto json = TinyJSON::TJ::parse(value.c_str());
ASSERT_NE(nullptr, json);

const auto& text = json->dump(TinyJSON::formatting::minify);
ASSERT_NE(nullptr, text);

ASSERT_STREQ(value.c_str(), text);
delete json;
}

// https://github.com/FFMG/tinyjson/issues/53
// Walk across the 255 char boundary, (fraction lengths 250 to 300).
TEST(TestDump, Issue53_FloatFractionLengthAroundTheBufferBoundaryCanBeDumped)
{
for (unsigned int number_of_zeros = 249; number_of_zeros < 300; ++number_of_zeros)
{
const auto value = make_number_with_zeros("0.", number_of_zeros, "1");
auto json = TinyJSON::TJ::parse(value.c_str());
ASSERT_NE(nullptr, json) << "Number of zeros: " << number_of_zeros;

const auto& text = json->dump(TinyJSON::formatting::minify);
ASSERT_NE(nullptr, text) << "Number of zeros: " << number_of_zeros;

ASSERT_STREQ(value.c_str(), text) << "Number of zeros: " << number_of_zeros;
delete json;
}
}

// https://github.com/FFMG/tinyjson/issues/53
// Same overflow but via fast_number_fraction_and_exponent_to_string(...)
TEST(TestDump, Issue53_ExponentWithMoreThan254FractionDigitsCanBeDumped)
{
std::vector<std::string> values;
values.push_back(make_number_with_zeros("1.", 300, "1e-20"));
values.push_back(make_number_with_zeros("1.", 300, "1e+20"));
values.push_back(make_number_with_zeros("-1.", 300, "1e-20"));
values.push_back(make_number_with_zeros("-1.", 300, "1e+20"));
for (auto& value : values)
{
auto json = TinyJSON::TJ::parse(value.c_str());
ASSERT_NE(nullptr, json);

const auto& text = json->dump(TinyJSON::formatting::minify);
ASSERT_NE(nullptr, text);

ASSERT_STREQ(value.c_str(), text);
delete json;
}
}

// https://github.com/FFMG/tinyjson/issues/53
// The example given in the issue, "1.000...0001e-1" cannot be moved to "0.1000...0001"
// without overflowing, so it stays as an exponent.
TEST(TestDump, Issue53_SmallNegativeExponentWithMoreThan254FractionDigitsCanBeDumped)
{
const auto value = make_number_with_zeros("1.", 300, "1e-1");
auto json = TinyJSON::TJ::parse(value.c_str());
ASSERT_NE(nullptr, json);

const auto& text = json->dump(TinyJSON::formatting::minify);
ASSERT_NE(nullptr, text);

ASSERT_STREQ(value.c_str(), text);
ASSERT_NEAR(0.1, static_cast<double>(json->get_float()), 1e-15);
delete json;
}
48 changes: 46 additions & 2 deletions tests/testtinyjsonexponents.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#define TJ_USE_CHAR 1
#include "../src/TinyJSON.h"
#include <cmath>
#include <limits>

constexpr long double EPSILON = 1e-10;

Expand Down Expand Up @@ -350,7 +351,11 @@ TEST(TestExponents, ShortNumberWithLongPositiveExponentShiftsEnoughToBecomeANumb

auto valuea = dynamic_cast<const TinyJSON::TJValueNumberInt*>(jobject->try_get_value("a"));
ASSERT_NE(nullptr, valuea);
ASSERT_EQ(10000000000000000000ULL, (unsigned long long)valuea->get_number());
// 10^19 does not fit in a long long so get_number() is clamped, (see https://github.com/FFMG/tinyjson/issues/55)
// but the number itself is still kept.
ASSERT_EQ(std::numeric_limits<long long>::max(), valuea->get_number());
ASSERT_DOUBLE_EQ(1e19, static_cast<double>(valuea->get_float()));
ASSERT_STREQ("10000000000000000000", valuea->dump(TinyJSON::formatting::minify));

delete json;
}
Expand Down Expand Up @@ -706,4 +711,43 @@ TEST(TestExponents, PositiveExponentToString)
auto json = TinyJSON::TJ::parse("2.5e+2");
EXPECT_STREQ("250", json->dump(TinyJSON::formatting::minify));
delete json;
}
}

// https://github.com/FFMG/tinyjson/issues/54
// The whole fraction becomes the whole number, the fraction has more than one digit.
TEST(TestExponents, Issue54_LongFractionWithLargePositiveExponentBecomesAWholeNumber)
{
auto json = TinyJSON::TJ::parse("0.12e20");
ASSERT_NE(nullptr, json);

auto value = dynamic_cast<const TinyJSON::TJValueNumberInt*>(json);
ASSERT_NE(nullptr, value);
ASSERT_STREQ("12000000000000000000", json->dump(TinyJSON::formatting::minify));
ASSERT_DOUBLE_EQ(1.2e19, static_cast<double>(json->get_float()));
delete json;
}

// https://github.com/FFMG/tinyjson/issues/54
// The number is too big to be a whole number so it remains an exponent.
TEST(TestExponents, Issue54_LongFractionWithVeryLargePositiveExponentRemainsAnExponent)
{
auto json = TinyJSON::TJ::parse("0.12e30");
ASSERT_NE(nullptr, json);

auto value = dynamic_cast<const TinyJSON::TJValueNumberExponent*>(json);
ASSERT_NE(nullptr, value);
ASSERT_STREQ("1.2e+29", json->dump(TinyJSON::formatting::minify));
ASSERT_DOUBLE_EQ(1.2e29, static_cast<double>(json->get_float()));
delete json;
}

// https://github.com/FFMG/tinyjson/issues/54
// The fraction has TJ_MAX_NUMBER_OF_DIGGITS digits and a small negative exponent.
TEST(TestExponents, Issue54_LongFractionWithSmallNegativeExponent)
{
auto json = TinyJSON::TJ::parse("0.1234567890123456789e-1");
ASSERT_NE(nullptr, json);

ASSERT_DOUBLE_EQ(0.01234567890123456789, static_cast<double>(json->get_float()));
delete json;
}
Loading
Loading