diff --git a/CHANGELOG.md b/CHANGELOG.md index c950a4a..2377fb1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,15 @@ 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.3] - 2026-05-19 + +### Added +- Atomic file saving: Implemented a safe save mechanism using temporary `-journal` files to prevent data loss or corruption during writes. +- Explicit `flush()` and `close()` checks for improved reliability during file operations. + +### Changed +- Incremented version to 0.2.3. + ## [0.2.2] - 2026-05-15 ### Added diff --git a/README.md b/README.md index 6f218cc..ddba4b9 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,12 @@ [![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.2-blue.svg)](src/TinyJSON.h) +[![Version](https://img.shields.io/badge/version-0.2.3-blue.svg)](src/TinyJSON.h) A lightweight and lightning-fast C++ JSON & JSON5 parser designed for high performance and minimal footprint. * Zero-dependency: Easy to integrate into any C++ project. +* Atomic file saving: Guarantees data integrity by using a temporary file before overwriting. * Minimal memory overhead: Ideal for resource-constrained environments. * High-performance parsing: Benchmarked for speed against other leading parsers. * Simple API: Intuitive and easy to use. @@ -129,8 +130,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 = 0; -static const char TJ_VERSION_STRING[] = "0.2.0"; +static const short TJ_VERSION_PATCH = 3; +static const char TJ_VERSION_STRING[] = "0.2.3"; ``` ## Options diff --git a/src/TinyJSON.cpp b/src/TinyJSON.cpp index ea1900a..678d8fd 100644 --- a/src/TinyJSON.cpp +++ b/src/TinyJSON.cpp @@ -28,6 +28,7 @@ #include #include #include +#include static constexpr short TJ_MAX_NUMBER_OF_DIGGITS = 19; static constexpr short TJ_DEFAULT_STRING_READ_SIZE = 10; @@ -4188,11 +4189,21 @@ namespace TinyJSON return false; } - // try and optn the file... - std::ofstream outFile(file_path, std::ios::out | std::ios::binary); + // Allocate temporary file path + auto file_path_length = TJHelper::string_length(file_path); + auto journal_suffix = TJCHARPREFIX("-journal"); + auto journal_length = TJHelper::string_length(journal_suffix); + TJCHAR* tmp_file_path = new TJCHAR[file_path_length + journal_length + 1]; + TJHelper::copy_string(file_path, tmp_file_path, file_path_length); + TJHelper::copy_string(journal_suffix, tmp_file_path + file_path_length, journal_length); + tmp_file_path[file_path_length + journal_length] = TJ_NULL_TERMINATOR; + + // try and open the temporary file... + std::ofstream outFile((const char*)tmp_file_path, std::ios::out | std::ios::binary); if (!outFile) { write_result.assign_exception_message("Unable to open file for writing."); + delete[] tmp_file_path; write_result.throw_if_exception(); return false; } @@ -4204,6 +4215,7 @@ namespace TinyJSON if (!outFile) { write_result.assign_exception_message("Unable to write UTF-8 BOM."); + delete[] tmp_file_path; write_result.throw_if_exception(); return false; } @@ -4215,6 +4227,16 @@ namespace TinyJSON if (!outFile) { write_result.assign_exception_message("Unable to write to file."); + delete[] tmp_file_path; + write_result.throw_if_exception(); + return false; + } + + outFile.flush(); + if (!outFile) + { + write_result.assign_exception_message("Unable to flush the file to disk."); + delete[] tmp_file_path; write_result.throw_if_exception(); return false; } @@ -4224,9 +4246,22 @@ namespace TinyJSON if (!outFile) { write_result.assign_exception_message("Unable to close the file."); + delete[] tmp_file_path; write_result.throw_if_exception(); return false; } + + // Atomic replace + std::remove((const char*)file_path); + if (std::rename((const char*)tmp_file_path, (const char*)file_path) != 0) + { + write_result.assign_exception_message("Unable to atomically rename the temporary file to the target file path."); + delete[] tmp_file_path; + write_result.throw_if_exception(); + return false; + } + + delete[] tmp_file_path; return true; } diff --git a/src/TinyJSON.h b/src/TinyJSON.h index db4fe75..eb2e977 100644 --- a/src/TinyJSON.h +++ b/src/TinyJSON.h @@ -44,10 +44,11 @@ // v0.2.0 - Breaking change: get_* methods no longer take throw parameters, use parse_options::strict instead. // v0.2.1 - added remove_at to TJValueArray. // v0.2.2 - added support for Json5 https://github.com/json5/ +// v0.2.3 - added atomic file saving static const short TJ_VERSION_MAJOR = 0; static const short TJ_VERSION_MINOR = 2; -static const short TJ_VERSION_PATCH = 2; -static const char TJ_VERSION_STRING[] = "0.2.2"; +static const short TJ_VERSION_PATCH = 3; +static const char TJ_VERSION_STRING[] = "0.2.3"; #ifndef TJ_USE_CHAR # define TJ_USE_CHAR 1 diff --git a/tests/testtinyjsonversion.cpp b/tests/testtinyjsonversion.cpp index bca9369..49b0971 100644 --- a/tests/testtinyjsonversion.cpp +++ b/tests/testtinyjsonversion.cpp @@ -14,9 +14,9 @@ TEST(TestVersion, CheckVersionMinor) { } TEST(TestVersion, CheckVersionPatch) { - ASSERT_EQ(2, TJ_VERSION_PATCH); + ASSERT_EQ(3, TJ_VERSION_PATCH); } TEST(TestVersion, CheckVersionString) { - ASSERT_STREQ("0.2.2", TJ_VERSION_STRING); + ASSERT_STREQ("0.2.3", TJ_VERSION_STRING); }