Compare commits

...

15 Commits

Author SHA1 Message Date
Ed Hennis
13719d4eaa Merge branch 'develop' into ximinez/parse-guard 2026-07-08 17:23:42 -04:00
Sergey Kuznetsov
c7adb215ed chore: Add .envrc for automatic devshell switch by direnv (#7756) 2026-07-08 17:28:48 +00:00
Ayaz Salikhov
71ee0f400b chore: Use same compiler in Nix devshell as in CI (#7751) 2026-07-08 14:51:39 +00:00
Ayaz Salikhov
58af1e6f18 release: Bump version to 3.3.0-b1 (#7755) 2026-07-08 12:40:24 +00:00
Ed Hennis
a0a99b0cc7 Merge branch 'develop' into ximinez/parse-guard 2026-07-07 19:32:52 -04:00
Ed Hennis
cc179f1ebc Merge branch 'develop' into ximinez/parse-guard 2026-07-07 17:35:04 -04:00
Ayaz Salikhov
e372c45836 chore: Enable most performance checks (#7727) 2026-07-07 21:03:56 +00:00
Ayaz Salikhov
2ebc96a4a6 perf: Use std::from/to_chars for JSON double parsing/formating (#7735) 2026-07-07 21:02:39 +00:00
Ed Hennis
bb9d1323dd Merge branch 'develop' into ximinez/parse-guard 2026-07-02 11:18:45 -04:00
Ed Hennis
4a3365eb36 Merge branch 'develop' into ximinez/parse-guard 2026-07-01 16:03:10 -04:00
Ed Hennis
bd7c711ac8 Merge branch 'develop' into ximinez/parse-guard 2026-07-01 12:18:52 -04:00
Ed Hennis
e3ad106797 Merge branch 'develop' into ximinez/parse-guard 2026-06-30 16:28:16 -04:00
Ed Hennis
f6ab0b16bb fixup! Review feedback: ParseFailureGuard 2026-06-30 14:34:06 -04:00
Ed Hennis
a8ead867ba Review feedback: ParseFailureGuard
- Rename oldExpected to oldExpected_.
- Prevent copying, and moving.
- Make getParseFailureGuard [[nodiscard]].
2026-06-30 13:44:38 -04:00
Ed Hennis
84589262c1 test: Add an RAII class to manage the env.parseFailureExpected flag 2026-06-29 18:26:31 -04:00
20 changed files with 256 additions and 203 deletions

View File

@@ -50,13 +50,7 @@ Checks: "-*,
performance-*,
-performance-avoid-endl,
-performance-enum-size,
-performance-inefficient-algorithm,
-performance-inefficient-string-concatenation,
-performance-no-int-to-ptr,
-performance-noexcept-destructor,
-performance-noexcept-move-constructor,
-performance-noexcept-swap,
-performance-type-promotion-in-math-fn,
-performance-unnecessary-copy-initialization,
-performance-unnecessary-value-param,

1
.envrc Normal file
View File

@@ -0,0 +1 @@
use flake

View File

@@ -33,9 +33,10 @@ with a single command and without installing anything system-wide:
nix --experimental-features 'nix-command flakes' develop
```
On **Linux**, Nix also provides the compiler (GCC). On **macOS**, the shell uses
your **system-wide Apple Clang** as the compiler, so you still need to manage
its version (see below).
On **Linux**, Nix also provides the compiler (GCC); on **macOS**, it provides
Clang. If you instead opt to use your system-wide Apple Clang (via
`nix develop .#apple-clang`), you need to manage its version yourself (see
below).
See [Using the Nix development shell](./nix.md) for installation and usage
details, including how to select a different compiler.
@@ -48,10 +49,10 @@ details, including how to select a different compiler.
### macOS: managing the Apple Clang version
Because the Nix shell uses the system-wide Apple Clang on macOS, the compiler
version is whatever your installed Xcode (or Command Line Tools) provides. The
following command should return a version greater than or equal to the
[minimum required](#tested-compiler-versions):
If you use your system-wide Apple Clang on macOS (via `nix develop .#apple-clang`),
the compiler version is whatever your installed Xcode (or Command Line Tools)
provides. The following command should return a version greater than or equal to
the [minimum required](#tested-compiler-versions):
```bash
clang --version

40
docs/build/nix.md vendored
View File

@@ -9,7 +9,7 @@ This guide explains how to use Nix to set up a reproducible development environm
- **Reproducible environment**: Everyone gets the same versions of tools and compilers
- **Matches CI**: The Linux CI runs in Docker images built from this exact Nix environment
- **No system pollution**: Dependencies are isolated and don't affect your system packages
- **Multiple compiler versions**: Easily switch between different GCC and Clang versions
- **Consistent compilers**: The GCC and Clang shells use the same versions as CI
- **Quick setup**: Get started with a single command
- **Works on Linux and macOS**: Consistent experience across platforms
@@ -31,8 +31,8 @@ This will:
- Download and set up all required development tools (CMake, Ninja, Conan, etc.)
- Configure the appropriate compiler for your platform:
- **Linux**: GCC 15.2 (provided by Nix)
- **macOS**: Apple Clang (your system compiler)
- **Linux**: GCC (provided by Nix)
- **macOS**: Clang (provided by Nix)
The first time you run this command, it will take a few minutes to download and build the environment. Subsequent runs will be much faster.
@@ -40,12 +40,12 @@ The first time you run this command, it will take a few minutes to download and
- **Linux**: `nix develop` gives you a shell with all the tooling necessary to
develop xrpld and with GCC 15.2 (also provided by Nix). There are no caveats.
- **macOS**: `nix develop` gives you a full environment too. The compiler is
your system-wide Apple Clang, while every other tool including Conan — is
provided by Nix. Conan has no binary in the Nix cache for macOS, so it is
built from source the first time you enter the shell, which makes the initial
setup slower (this is handled automatically; see
[`nix/devshell.nix`](../../nix/devshell.nix)).
- **macOS**: `nix develop` gives you a full environment too, with Clang (and
every other tool, including Conan) provided by Nix. To use your system-wide
Apple Clang instead, enter `nix develop .#apple-clang`. Conan has no binary in
the Nix cache for macOS, so it is built from source the first time you enter
the shell, which makes the initial setup slower (this is handled
automatically; see [`nix/devshell.nix`](../../nix/devshell.nix)).
> [!TIP]
> To avoid typing `--experimental-features 'nix-command flakes'` every time, you can permanently enable flakes by creating `~/.config/nix/nix.conf`:
@@ -62,7 +62,9 @@ The first time you run this command, it will take a few minutes to download and
### Choosing a different compiler
A compiler can be chosen by providing its name with the `.#` prefix, e.g. `nix develop .#gcc15`.
A compiler can be chosen by providing its name with the `.#` prefix, e.g. `nix develop .#clang`.
The `.#gcc` and `.#clang` shells provide the same GCC and Clang versions used in CI
(pinned in [`nix/packages.nix`](../../nix/packages.nix)).
Use `nix flake show` to see all the available development shells.
Use `nix develop .#no-compiler` to use the compiler from your system.
@@ -70,11 +72,11 @@ Use `nix develop .#no-compiler` to use the compiler from your system.
### Example Usage
```bash
# Use GCC 14
nix develop .#gcc14
# Use GCC (same version as CI)
nix develop .#gcc
# Use Clang 19
nix develop .#clang19
# Use Clang (same version as CI)
nix develop .#clang
# Use default for your platform
nix develop
@@ -112,7 +114,15 @@ Once inside the Nix development shell, follow the standard [build instructions](
[direnv](https://direnv.net/) or [nix-direnv](https://github.com/nix-community/nix-direnv) can automatically activate the Nix development shell when you enter the repository directory.
This is also the most robust way to use the environment from **any shell** (bash, zsh, fish, …): direnv stays in your current shell and loads the environment _after_ your shell's startup files have run, so the Nix-provided tools take precedence over anything your shell configuration adds to `$PATH`. To use it, install direnv for your shell, then add an `.envrc` containing `use flake` at the repository root and run `direnv allow`.
This is also the most robust way to use the environment from **any shell** (bash, zsh, fish, …): direnv stays in your current shell and loads the environment _after_ your shell's startup files have run, so the Nix-provided tools take precedence over anything your shell configuration adds to `$PATH`.
The repository already ships an `.envrc` at its root that activates the Nix flake development shell, so you don't need to create one. To use it:
1. [Install direnv](https://direnv.net/docs/installation.html) and [hook it into your shell](https://direnv.net/docs/hook.html) (bash, zsh, fish, …). Installing [nix-direnv](https://github.com/nix-community/nix-direnv) as well is recommended: it caches the shell so that activation is near-instant after the first run.
2. Run `direnv allow` once in the repository root. direnv will then load (and reload) the Nix development shell automatically whenever you enter the directory.
> [!NOTE]
> direnv only caches the `.direnv` directory (already listed in `.gitignore`); no other repository files are affected.
## Conan and Prebuilt Packages

View File

@@ -641,6 +641,9 @@ template <class T>
T*
SharedWeakUnion<T>::unsafeGetRawPtr() const
{
// tp_ packs a raw pointer together with a strength bit; recovering the
// pointer inherently requires an integer-to-pointer cast.
// NOLINTNEXTLINE(performance-no-int-to-ptr)
return reinterpret_cast<T*>(tp_ & kPtrMask);
}

View File

@@ -507,6 +507,9 @@ TaggedPointer::operator=(TaggedPointer&& other)
[[nodiscard]] inline std::pair<std::uint8_t, void*>
TaggedPointer::decode() const
{
// tp_ packs a raw pointer together with the tag bits; recovering the
// pointer inherently requires an integer-to-pointer cast.
// NOLINTNEXTLINE(performance-no-int-to-ptr)
return {tp_ & kTagMask, reinterpret_cast<void*>(tp_ & kPtrMask)};
}
@@ -535,6 +538,9 @@ TaggedPointer::getHashesAndChildren() const
[[nodiscard]] inline SHAMapHash*
TaggedPointer::getHashes() const
{
// tp_ packs a raw pointer together with the tag bits; recovering the
// pointer inherently requires an integer-to-pointer cast.
// NOLINTNEXTLINE(performance-no-int-to-ptr)
return reinterpret_cast<SHAMapHash*>(tp_ & kPtrMask);
};

View File

@@ -4,14 +4,16 @@
...
}:
let
inherit (import ./packages.nix { inherit pkgs; }) commonPackages;
inherit (pkgs) lib;
inherit (import ./packages.nix { inherit pkgs; })
commonPackages
gccPackage
llvmPackages
llvmVersion
;
# Underlying compiler toolchains to wrap. Bump these in one place to
# roll the whole environment forward.
customGccPackage = pkgs.gcc15;
customLlvmPackages = pkgs.llvmPackages_22;
customClangMajor = lib.versions.major (lib.getVersion customLlvmPackages.clang-unwrapped);
# Underlying compiler toolchains to wrap (versions pinned in packages.nix).
customGccPackage = gccPackage;
customLlvmPackages = llvmPackages;
# binutils wrapped to emit binaries that reference the custom glibc
# (dynamic linker path, library search path, RPATH).
@@ -90,7 +92,7 @@ let
extraBuildCommands = ''
rsrc="$out/resource-root"
mkdir "$rsrc"
ln -s "${customLlvmPackages.clang-unwrapped.lib}/lib/clang/${customClangMajor}/include" "$rsrc/include"
ln -s "${customLlvmPackages.clang-unwrapped.lib}/lib/clang/${toString llvmVersion}/include" "$rsrc/include"
ln -s "${customCompilerRt.out}/lib" "$rsrc/lib"
ln -s "${customCompilerRt.out}/share" "$rsrc/share" || true
echo "-resource-dir=$rsrc" >> $out/nix-support/cc-cflags

View File

@@ -1,127 +1,57 @@
{ pkgs, ... }:
let
inherit (import ./packages.nix { inherit pkgs; }) commonPackages;
inherit (import ./packages.nix { inherit pkgs; })
commonPackages
gccVersion
llvmPackages
;
# Supported compiler versions
gccVersion = pkgs.lib.range 13 15;
clangVersions = pkgs.lib.range 18 21;
# Plain nixpkgs stdenvs — no custom glibc, unlike ci-env.nix.
gccStdenv = pkgs."gcc${toString gccVersion}Stdenv";
clangStdenv = llvmPackages.stdenv;
defaultCompiler = if pkgs.stdenv.isDarwin then "apple-clang" else "gcc";
defaultGccVersion = pkgs.lib.last gccVersion;
defaultClangVersion = pkgs.lib.last clangVersions;
strToCompilerEnv =
compiler: version:
(
if compiler == "gcc" then
let
gccPkg = pkgs."gcc${toString version}Stdenv" or null;
in
if gccPkg != null && builtins.elem version gccVersion then
gccPkg
else
throw "Invalid GCC version: ${toString version}. Must be one of: ${toString gccVersion}"
else if compiler == "clang" then
let
clangPkg = pkgs."llvmPackages_${toString version}".stdenv or null;
in
if clangPkg != null && builtins.elem version clangVersions then
clangPkg
else
throw "Invalid Clang version: ${toString version}. Must be one of: ${toString clangVersions}"
else if compiler == "apple-clang" || compiler == "none" then
pkgs.stdenvNoCC
else
throw "Invalid compiler: ${compiler}. Must be one of: gcc, clang, apple-clang, none"
);
# Helper function to create a shell with a specific compiler
# compilerName is the command used to print the version, or null for none.
makeShell =
{
compiler ? defaultCompiler,
version ? (
if compiler == "gcc" then
defaultGccVersion
else if compiler == "clang" then
defaultClangVersion
else
null
),
stdenv,
compilerName,
}:
let
compilerStdEnv = strToCompilerEnv compiler version;
compilerName =
if compiler == "apple-clang" then
"clang"
else if compiler == "none" then
null
else
compiler;
gccOnMacWarning =
if pkgs.stdenv.isDarwin && compiler == "gcc" then
''
echo "WARNING: Using GCC on macOS with Conan may not work."
echo " Consider using 'nix develop .#clang' or the default shell instead."
echo ""
''
else
"";
compilerVersion =
if compilerName != null then
if compilerName == null then
''echo "No compiler specified - using system compiler"''
else
''
echo "Compiler: "
${compilerName} --version
''
else
''
echo "No compiler specified - using system compiler"
'';
shellAttrs = {
packages = commonPackages;
shellHook = ''
echo "Welcome to xrpld development shell";
${gccOnMacWarning}${compilerVersion}
'';
};
in
pkgs.mkShell.override { stdenv = compilerStdEnv; } shellAttrs;
# Generate shells for each compiler version
gccShells = builtins.listToAttrs (
map (version: {
name = "gcc${toString version}";
value = makeShell {
compiler = "gcc";
version = version;
};
}) gccVersion
);
clangShells = builtins.listToAttrs (
map (version: {
name = "clang${toString version}";
value = makeShell {
compiler = "clang";
version = version;
};
}) clangVersions
);
(pkgs.mkShell.override { inherit stdenv; }) {
packages = commonPackages;
shellHook = ''
echo "Welcome to xrpld development shell";
${compilerVersion}
'';
};
in
gccShells
// clangShells
// {
# Default shells
default = makeShell { };
gcc = makeShell { compiler = "gcc"; };
clang = makeShell { compiler = "clang"; };
rec {
# macOS: Nix Clang. Linux: Nix GCC.
default = if pkgs.stdenv.isDarwin then clang else gcc;
# No compiler
no-compiler = makeShell { compiler = "none"; };
apple-clang = makeShell { compiler = "apple-clang"; };
gcc = makeShell {
stdenv = gccStdenv;
compilerName = "gcc";
};
clang = makeShell {
stdenv = clangStdenv;
compilerName = "clang";
};
# Nix provides no compiler; use the one from your system (e.g. Apple Clang).
no-compiler = makeShell {
stdenv = pkgs.stdenvNoCC;
compilerName = null;
};
apple-clang = no-compiler;
}

View File

@@ -1,15 +1,33 @@
{ pkgs }:
let
# Compiler versions used across the dev shell and the CI environment.
gccVersion = 15;
llvmVersion = 22;
gccPackage = pkgs."gcc${toString gccVersion}";
llvmPackages = pkgs."llvmPackages_${toString llvmVersion}";
# Bound explicitly so it tracks llvmPackages above, not the `with pkgs` default.
clangTools = llvmPackages.clang-tools;
# In LLVM 22, run-clang-tidy.py moved from share/clang/ to bin/, so nixpkgs
# clang-tools no longer links it. Wrap it manually.
runClangTidy = pkgs.writeShellScriptBin "run-clang-tidy" ''
exec ${pkgs.python3}/bin/python3 ${pkgs.llvmPackages_22.clang-unwrapped}/bin/run-clang-tidy "$@"
exec ${pkgs.python3}/bin/python3 ${llvmPackages.clang-unwrapped}/bin/run-clang-tidy "$@"
'';
in
{
inherit
gccVersion
llvmVersion
gccPackage
llvmPackages
;
commonPackages = with pkgs; [
ccache
clangbuildanalyzer
clangTools
cmake
conan
curlMinimal # needed for codecov/codecov-action
@@ -23,7 +41,6 @@ in
gnumake
gnupg # needed for signing commits & codecov/codecov-action
graphviz
llvmPackages_22.clang-tools
less # needed for git diff
mold
nettools # provides netstat, used to debug failures in CI

View File

@@ -5,12 +5,13 @@
#include <algorithm>
#include <cctype>
#include <charconv>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <istream>
#include <stdexcept>
#include <string>
#include <system_error>
namespace json {
// Implementation of class Reader
@@ -605,36 +606,17 @@ bool
Reader::decodeDouble(Token& token)
{
double value = 0;
int const bufferSize = 32;
int count = 0;
int const length = int(token.end - token.start);
// Sanity check to avoid buffer overflow exploits.
if (length < 0)
{
return addError("Unable to parse token length", token);
}
// Avoid using a string constant for the format control string given to
// sscanf, as this can cause hard to debug crashes on OS X. See here for
// more info:
//
// http://developer.apple.com/library/mac/#DOCUMENTATION/DeveloperTools/gcc-4.0.1/gcc/Incompatibilities.html
char format[] = "%lf";
if (length <= bufferSize)
{
Char buffer[bufferSize + 1];
memcpy(buffer, token.start, length);
buffer[length] = 0;
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg)
count = sscanf(buffer, format, &value);
}
else
{
std::string const buffer(token.start, token.end);
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg)
count = sscanf(buffer.c_str(), format, &value);
}
if (count != 1)
auto const [ptr, ec] = std::from_chars(token.start, token.end, value);
// Reject anything from_chars could not turn into a finite double:
// - ec != std::errc{}: no valid conversion, or an out-of-range magnitude
// (e.g. 1e400).
// - ptr != token.end: readNumber() is permissive about which characters
// it collects into a token (it will, for example, keep a '+' mid-token),
// but from_chars() will stop at the first character it cannot parse.
if (ec != std::errc{} || ptr != token.end)
return addError("'" + std::string(token.start, token.end) + "' is not a number.", token);
currentValue() = value;
return true;
}

View File

@@ -4,13 +4,14 @@
#include <xrpl/json/json_forwards.h>
#include <xrpl/json/json_value.h>
#include <cstdio>
#include <charconv>
#include <cstring>
#include <iomanip>
#include <ios>
#include <ostream>
#include <sstream>
#include <string>
#include <system_error>
#include <utility>
namespace json {
@@ -76,20 +77,18 @@ valueToString(UInt value)
std::string
valueToString(double value)
{
// Allocate a buffer that is more than large enough to store the 16 digits
// of precision requested below.
// Format with 16 significant digits.
// We need not request the alternative representation that always has a
// decimal point because JSON doesn't distinguish the concepts of reals and integers.
// A double never needs more than 32 characters in this form,
// so to_chars cannot actually run out of room here.
char buffer[32];
// Print into the buffer. We need not request the alternative representation
// that always has a decimal point because JSON doesn't distinguish the
// concepts of reals and integers.
#if defined(_MSC_VER) && defined(__STDC_SECURE_LIB__) // Use secure version with visual studio 2005
// to avoid warning.
sprintf_s(buffer, sizeof(buffer), "%.16g", value);
#else
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-vararg)
snprintf(buffer, sizeof(buffer), "%.16g", value);
#endif
return buffer;
auto const [ptr, ec] =
std::to_chars(buffer, buffer + sizeof(buffer), value, std::chars_format::general, 16);
XRPL_ASSERT(ec == std::errc{}, "json::valueToString(double) : conversion fits buffer");
if (ec != std::errc{})
return {};
return std::string(buffer, ptr);
}
std::string

View File

@@ -23,7 +23,7 @@ namespace {
//------------------------------------------------------------------------------
// clang-format off
// NOLINTNEXTLINE(readability-identifier-naming)
char const* const versionString = "3.3.0-b0"
char const* const versionString = "3.3.0-b1"
// clang-format on
;

View File

@@ -69,6 +69,17 @@ toUnsigned(U2 value)
return static_cast<U1>(value);
}
static std::string
joinName(std::string const& jsonName, std::string const& fieldName)
{
std::string result;
result.reserve(jsonName.size() + 1 + fieldName.size());
result += jsonName;
result += '.';
result += fieldName;
return result;
}
// LCOV_EXCL_START
static inline std::string
makeName(std::string const& object, std::string const& field)
@@ -76,7 +87,7 @@ makeName(std::string const& object, std::string const& field)
if (field.empty())
return object;
return object + "." + field;
return joinName(object, field);
}
static inline json::Value
@@ -1036,8 +1047,8 @@ parseObject(
try
{
auto ret =
parseObject(jsonName + "." + fieldName, value, field, depth + 1, error);
auto ret = parseObject(
joinName(jsonName, fieldName), value, field, depth + 1, error);
if (!ret)
return std::nullopt;
data.emplaceBack(std::move(*ret));
@@ -1054,8 +1065,8 @@ parseObject(
case STI_ARRAY:
try
{
auto array =
parseArray(jsonName + "." + fieldName, value, field, depth + 1, error);
auto array = parseArray(
joinName(jsonName, fieldName), value, field, depth + 1, error);
if (!array.has_value())
return std::nullopt;
data.emplaceBack(std::move(*array));

View File

@@ -213,6 +213,12 @@ public:
if (auto [conn, keepAlive] = getConnection(); conn)
{
(void)keepAlive;
// The checkpointer is identified to the C callback by an integer id
// (resolved via checkpointerFromId) rather than a raw `this`, so it
// cannot dangle if the checkpointer is destroyed. Passing the id
// through sqlite's void* user-data requires an integer-to-pointer
// cast.
// NOLINTNEXTLINE(performance-no-int-to-ptr)
sqlite_api::sqlite3_wal_hook(conn, &sqliteWALHook, reinterpret_cast<void*>(id_));
}
}

View File

@@ -498,7 +498,7 @@ class Batch_test : public beast::unit_test::Suite
auto const batchFee = batch::calcBatchFee(env, 0, 2);
auto tx1 = batch::Inner(pay(alice, bob, XRP(1)), seq + 1);
tx1[jss::Fee] = "1.5";
env.setParseFailureExpected(true);
auto const g = env.getParseFailureGuard(true);
try
{
env(batch::outer(alice, seq, batchFee, tfAllOrNothing),
@@ -510,7 +510,6 @@ class Batch_test : public beast::unit_test::Suite
{
BEAST_EXPECT(true);
}
env.setParseFailureExpected(false);
}
// temSEQ_AND_TICKET: Batch: inner txn cannot have both Sequence

View File

@@ -5451,9 +5451,9 @@ class Vault_test : public beast::unit_test::Suite
env.close();
// 2. Mantissa larger than uint64 max
env.setParseFailureExpected(true);
try
{
auto const g = env.getParseFailureGuard(true);
tx[sfAssetsMaximum] = "18446744073709551617e5"; // uint64 max + 1
env(tx);
BEAST_EXPECTS(false, "Expected parse_error for mantissa larger than uint64 max");
@@ -5464,7 +5464,6 @@ class Vault_test : public beast::unit_test::Suite
BEAST_EXPECT(
e.what() == "invalidParamsField 'tx_json.AssetsMaximum' has invalid data."s);
}
env.setParseFailureExpected(false);
}
}

View File

@@ -1475,6 +1475,7 @@ r.ripple.com:51235
std::string toLoad(R"xrpldConfig(
[amendment_majority_time]
)xrpldConfig");
// NOLINTNEXTLINE(performance-inefficient-string-concatenation)
toLoad += std::to_string(val) + space + unit;
space = space.empty() ? " " : "";

View File

@@ -495,6 +495,46 @@ public:
parseFailureExpected_ = b;
}
/** RAII class to set and restore the parse failure flag (setParseFailureExpected).
*
* Can be created directly, or through the `getParseFailureGuard(bool)` function.
*/
class ParseFailureGuard final
{
Env& self_;
bool oldExpected_ = self_.parseFailureExpected_;
public:
ParseFailureGuard(Env& self, bool b) : self_(self)
{
self_.setParseFailureExpected(b);
}
~ParseFailureGuard()
{
self_.setParseFailureExpected(oldExpected_);
}
// No copy, no move
ParseFailureGuard(ParseFailureGuard const&) = delete;
ParseFailureGuard&
operator=(ParseFailureGuard const&) = delete;
ParseFailureGuard(ParseFailureGuard&& other) = delete;
ParseFailureGuard&
operator=(ParseFailureGuard&&) = delete;
};
/** Gets an RAII guard to set and restore the parse failure flag
*
* Usage:
* auto const guard = env.getParseFailureGuard(true/false);
*/
[[nodiscard]] ParseFailureGuard
getParseFailureGuard(bool b)
{
return ParseFailureGuard{*this, b};
}
/** Turn off signature checks. */
void
disableSigs()

View File

@@ -602,7 +602,7 @@ Env::autofill(JTx& jt)
catch (ParseError const&)
{
if (!parseFailureExpected_)
test.log << "parse failed:\n" << pretty(jv) << std::endl;
test.log << "parse failure:\n" << pretty(jv) << std::endl;
rethrow();
}
}

View File

@@ -13,6 +13,7 @@
#include <exception>
#include <limits>
#include <numbers>
#include <optional>
#include <regex>
#include <sstream>
#include <string>
@@ -592,6 +593,57 @@ TEST(json_value, bad_json)
EXPECT_TRUE(r.parse(s, j));
}
namespace {
std::optional<json::Value>
parseValue(std::string const& doc)
{
json::Value j;
json::Reader r;
if (!r.parse("{\"v\":" + doc + "}", j))
return std::nullopt;
return j["v"];
}
} // namespace
TEST(json_value, parse_double_valid)
{
// 1e300 is large but still representable, so it parses (unlike the out-of-range cases below).
for (auto const& [text, expected] :
{std::pair{"2.5", 2.5},
std::pair{"-3.25e2", -325.0},
std::pair{"0.0", 0.0},
std::pair{"1E3", 1000.0},
std::pair{"-0.5e-1", -0.05},
std::pair{"1e300", 1e300}})
{
auto const v = parseValue(text);
ASSERT_TRUE(v.has_value()) << text;
// NOLINTBEGIN(bugprone-unchecked-optional-access)
EXPECT_TRUE(v->isDouble()) << text;
EXPECT_EQ(v->asDouble(), expected) << text;
// NOLINTEND(bugprone-unchecked-optional-access)
}
}
TEST(json_value, parse_double_out_of_range)
{
// Magnitudes with no finite double representation are rejected.
for (char const* oor : {"1e400", "-1e400", "0.001e500", "1e-400", "-1e-400", "123e-500"})
EXPECT_FALSE(parseValue(oor).has_value()) << oor;
}
TEST(json_value, parse_double_malformed)
{
// readNumber() collects any run of digits and '.eE+-' into a single Double
// token, so these malformed tokens reach decodeDouble. Each has a valid
// leading prefix that from_chars would accept on its own; requiring the
// entire token be consumed rejects them instead of silently truncating.
for (char const* bad : {"1+2", "1-2", "1.2.3", "1e5e6", "1..2", "++5", "1e", "1e+", ".", "-"})
EXPECT_FALSE(parseValue(bad).has_value()) << bad;
}
TEST(json_value, edge_cases)
{
std::uint32_t const maxUInt = std::numeric_limits<std::uint32_t>::max();