diff --git a/.github/workflows/juce.yml b/.github/workflows/juce.yml new file mode 100644 index 0000000..e1d800f --- /dev/null +++ b/.github/workflows/juce.yml @@ -0,0 +1,98 @@ +name: JUCE module + +# Builds the moonbase_licensing JUCE module and runs its behavioral test suite +# (tests/juce) on every OS. This is the only CI that exercises the native-crypto +# backends (Security.framework / CNG / libcrypto) and the ActivationController +# state machine; the default ci.yml only covers the header-only SDK. +# +# The suite signs tokens with OpenSSL then verifies them with the platform's +# native backend, i.e. a real cross-backend RS256 round-trip on each OS. The +# module needs no curl, so the build turns it off (-DMOONBASE_USE_CURL=OFF). + +on: + push: + branches: [main] + pull_request: + +concurrency: + group: juce-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + controller-tests: + name: ${{ matrix.os }} + runs-on: ${{ matrix.os }} + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-14, windows-latest] + + steps: + - uses: actions/checkout@v4 + + - name: Install dependencies (Ubuntu) + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y \ + libssl-dev \ + libx11-dev libxext-dev libxinerama-dev libxrandr-dev libxcursor-dev \ + libxcomposite-dev libxrender-dev libfreetype6-dev libfontconfig1-dev \ + libasound2-dev libglu1-mesa-dev mesa-common-dev xvfb + + - name: Install dependencies (macOS) + if: runner.os == 'macOS' + run: brew install openssl@3 nlohmann-json + + - name: Install OpenSSL via vcpkg (Windows) + if: runner.os == 'Windows' + shell: bash + run: | + "$VCPKG_INSTALLATION_ROOT/vcpkg" install --triplet x64-windows openssl + + - name: Configure (Ubuntu) + if: runner.os == 'Linux' + run: | + cmake -S . -B build \ + -DCMAKE_BUILD_TYPE=Release \ + -DMOONBASE_USE_CURL=OFF \ + -DMOONBASE_BUILD_JUCE_TESTS=ON + + - name: Configure (macOS) + if: runner.os == 'macOS' + run: | + cmake -S . -B build \ + -DCMAKE_BUILD_TYPE=Release \ + -DMOONBASE_USE_CURL=OFF \ + -DMOONBASE_BUILD_JUCE_TESTS=ON \ + -DOPENSSL_ROOT_DIR="$(brew --prefix openssl@3)" + + - name: Configure (Windows) + if: runner.os == 'Windows' + shell: bash + run: | + cmake -S . -B build \ + -DMOONBASE_USE_CURL=OFF \ + -DMOONBASE_BUILD_JUCE_TESTS=ON \ + -DCMAKE_TOOLCHAIN_FILE="$VCPKG_INSTALLATION_ROOT/scripts/buildsystems/vcpkg.cmake" \ + -DVCPKG_TARGET_TRIPLET=x64-windows + + # On Linux the doctest discovery step runs the binary at build time, and the + # suite initialises the JUCE GUI subsystem, so both the build and the run + # need a virtual display. + - name: Build (Ubuntu) + if: runner.os == 'Linux' + run: xvfb-run -a cmake --build build --target MoonbaseJuceTests -j + + - name: Build + if: runner.os != 'Linux' + run: cmake --build build --target MoonbaseJuceTests --config Release -j + + - name: Test (Ubuntu) + if: runner.os == 'Linux' + run: xvfb-run -a ctest --test-dir build -R "Juce\." --output-on-failure + + - name: Test + if: runner.os != 'Linux' + run: ctest --test-dir build -R "Juce\." -C Release --output-on-failure diff --git a/.github/workflows/sanitizers.yml b/.github/workflows/sanitizers.yml new file mode 100644 index 0000000..52d91cf --- /dev/null +++ b/.github/workflows/sanitizers.yml @@ -0,0 +1,53 @@ +name: Sanitizers + +# Runs the test suite under AddressSanitizer + UndefinedBehaviorSanitizer and +# under ThreadSanitizer. The controller and the license store both spawn +# threads, so the thread run guards against data races / use-after-free that +# unit assertions can't see. Linux only: the sanitizer runtimes are reliable +# there (the repo's macOS toolchain trips ASan's process-startup checks). + +on: + push: + branches: [main] + pull_request: + +concurrency: + group: sanitizers-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + sdk: + name: SDK (${{ matrix.sanitizer }}) + runs-on: ubuntu-latest + timeout-minutes: 20 + strategy: + fail-fast: false + matrix: + sanitizer: ["address,undefined", "thread"] + + steps: + - uses: actions/checkout@v4 + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y libcurl4-openssl-dev libssl-dev nlohmann-json3-dev + + - name: Configure + run: | + cmake -S . -B build \ + -DCMAKE_BUILD_TYPE=RelWithDebInfo \ + -DMOONBASE_BUILD_TESTS=ON \ + -DMOONBASE_BUILD_EXAMPLES=OFF \ + -DMOONBASE_SANITIZER=${{ matrix.sanitizer }} + + - name: Build + run: cmake --build build --target moonbase_tests -j + + - name: Test + env: + # Surface the first finding with a full report and a non-zero exit. + ASAN_OPTIONS: halt_on_error=1:abort_on_error=1:detect_leaks=1 + UBSAN_OPTIONS: halt_on_error=1:print_stacktrace=1 + TSAN_OPTIONS: halt_on_error=1:second_deadlock_stack=1 + run: ctest --test-dir build --output-on-failure diff --git a/.github/workflows/visual.yml b/.github/workflows/visual.yml new file mode 100644 index 0000000..e1167b5 --- /dev/null +++ b/.github/workflows/visual.yml @@ -0,0 +1,44 @@ +name: Visual tests + +# Renders the activation UI in every state with the offscreen snapshot harness +# (tests/visual) and uploads the PNGs to Argos for visual diffing + baselines. +# +# Runs on macOS so the snapshots also exercise the Apple (Security.framework) +# crypto backend. Set the ARGOS_TOKEN repository secret to enable the upload; +# without it the job still builds, renders, and stores the PNGs as an artifact. + +on: + push: + branches: [main] + pull_request: + +concurrency: + group: visual-${{ github.ref }} + cancel-in-progress: true + +jobs: + ui-snapshots: + runs-on: macos-14 + env: + ARGOS_TOKEN: ${{ secrets.ARGOS_TOKEN }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # Argos uses git history to find the baseline build + + - uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Build harness + render snapshots + run: ./scripts/visual-snapshots.sh + + - name: Upload snapshots artifact + uses: actions/upload-artifact@v4 + with: + name: ui-snapshots + path: ui-snapshots/*.png + + - name: Upload to Argos + if: ${{ env.ARGOS_TOKEN != '' }} + run: npx --yes @argos-ci/cli upload ui-snapshots diff --git a/.gitignore b/.gitignore index 9939225..55e6ab8 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,7 @@ _deps/ license.mb node_modules/ CMakeLists.txt.bak +.DS_Store +ui-snapshots/ +node_modules +.argos diff --git a/CMakeLists.txt b/CMakeLists.txt index 6d2ff8d..e9c46f6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -21,8 +21,36 @@ option(MOONBASE_BUILD_TESTS "Build Moonbase C++ SDK tests" ${MOONBASE_IS_TOP_LEV option(MOONBASE_BUILD_EXAMPLES "Build Moonbase C++ SDK examples" ${MOONBASE_IS_TOP_LEVEL}) option(MOONBASE_BUILD_JUCE_EXAMPLE "Build the JUCE bridge example (fetches JUCE; off by default)" OFF) +option(MOONBASE_BUILD_JUCE_NATIVE_EXAMPLE + "Build the native JUCE module sample app (fetches JUCE; off by default)" OFF) +option(MOONBASE_BUILD_UI_SNAPSHOTS + "Build the UI snapshot harness for visual tests (fetches JUCE; off by default)" OFF) +option(MOONBASE_BUILD_JUCE_TESTS + "Build the JUCE module unit tests (fetches JUCE + doctest; off by default)" OFF) + +# Optional sanitizer for the test targets. One of: address | thread | undefined +# | address,undefined (ASan and TSan can't be combined). Empty = off, so normal +# builds are unaffected. Example: +# cmake -B build-tsan -DMOONBASE_BUILD_JUCE_TESTS=ON -DMOONBASE_SANITIZER=thread +set(MOONBASE_SANITIZER "" CACHE STRING + "Sanitizer for test targets: address | thread | undefined | address,undefined") + +function(moonbase_apply_sanitizer target) + if(MOONBASE_SANITIZER AND NOT MSVC) + target_compile_options(${target} PRIVATE + -fsanitize=${MOONBASE_SANITIZER} -fno-omit-frame-pointer -g) + target_link_options(${target} PRIVATE -fsanitize=${MOONBASE_SANITIZER}) + endif() +endfunction() + +# The libcurl HTTP transport is the SDK's default. The JUCE module ships its own +# JUCE-based transport and needs no curl, so JUCE-only builds (the module + its +# tests) can turn this off and avoid the dependency entirely. +option(MOONBASE_USE_CURL "Build the SDK target with the libcurl HTTP transport" ON) -find_package(CURL REQUIRED) +if(MOONBASE_USE_CURL) + find_package(CURL REQUIRED) +endif() find_package(OpenSSL REQUIRED) find_package(nlohmann_json QUIET) @@ -37,30 +65,34 @@ if(NOT nlohmann_json_FOUND) set(MOONBASE_NLOHMANN_FETCHED ON) endif() -add_library(moonbase_licensing INTERFACE) -add_library(moonbase::licensing ALIAS moonbase_licensing) -set_target_properties(moonbase_licensing PROPERTIES EXPORT_NAME licensing) +add_library(moonbase_cpp INTERFACE) +add_library(moonbase::licensing ALIAS moonbase_cpp) +set_target_properties(moonbase_cpp PROPERTIES EXPORT_NAME licensing) -target_compile_features(moonbase_licensing INTERFACE cxx_std_17) -target_include_directories(moonbase_licensing +target_compile_features(moonbase_cpp INTERFACE cxx_std_17) +target_include_directories(moonbase_cpp INTERFACE $ $) -target_link_libraries(moonbase_licensing +target_link_libraries(moonbase_cpp INTERFACE - CURL::libcurl OpenSSL::SSL OpenSSL::Crypto) +if(MOONBASE_USE_CURL) + target_link_libraries(moonbase_cpp INTERFACE CURL::libcurl) +else() + target_compile_definitions(moonbase_cpp INTERFACE MOONBASE_DISABLE_CURL_TRANSPORT=1) +endif() if(MOONBASE_NLOHMANN_FETCHED) - target_include_directories(moonbase_licensing + target_include_directories(moonbase_cpp INTERFACE $) else() - target_link_libraries(moonbase_licensing + target_link_libraries(moonbase_cpp INTERFACE nlohmann_json::nlohmann_json) endif() -target_compile_definitions(moonbase_licensing +target_compile_definitions(moonbase_cpp INTERFACE MOONBASE_CPP_VERSION="${PROJECT_VERSION}") @@ -77,6 +109,37 @@ if(MOONBASE_BUILD_JUCE_EXAMPLE) add_subdirectory(examples/juce) endif() +# The native sample app and the UI snapshot harness both consume the JUCE module. +# Fetch JUCE and add the module once here so the two targets can share them +# (juce_add_module on the same folder twice would collide). +if(MOONBASE_BUILD_JUCE_NATIVE_EXAMPLE OR MOONBASE_BUILD_UI_SNAPSHOTS OR MOONBASE_BUILD_JUCE_TESTS) + enable_language(C) + if(APPLE) + enable_language(OBJC OBJCXX) + endif() + + include(FetchContent) + set(MOONBASE_JUCE_VERSION "8.0.4" CACHE STRING "JUCE tag to fetch for the JUCE module targets") + FetchContent_Declare( + JUCE + GIT_REPOSITORY https://github.com/juce-framework/JUCE.git + GIT_TAG ${MOONBASE_JUCE_VERSION} + GIT_SHALLOW TRUE) + FetchContent_MakeAvailable(JUCE) + + if(NOT TARGET moonbase_licensing) + juce_add_module(${CMAKE_CURRENT_SOURCE_DIR}/modules/moonbase_licensing) + endif() +endif() + +if(MOONBASE_BUILD_JUCE_NATIVE_EXAMPLE) + add_subdirectory(examples/juce-native) +endif() + +if(MOONBASE_BUILD_UI_SNAPSHOTS) + add_subdirectory(tests/visual) +endif() + if(MOONBASE_BUILD_TESTS) enable_testing() @@ -103,11 +166,20 @@ if(MOONBASE_BUILD_TESTS) tests/validator_tests.cpp tests/live_tests.cpp) target_link_libraries(moonbase_tests PRIVATE moonbase::licensing doctest::doctest) + moonbase_apply_sanitizer(moonbase_tests) include(doctest) doctest_discover_tests(moonbase_tests) endif() -install(TARGETS moonbase_licensing +# JUCE module unit tests (doctest, links the moonbase_licensing JUCE module). +# Placed after the SDK-test block so doctest is already available when both +# MOONBASE_BUILD_TESTS and MOONBASE_BUILD_JUCE_TESTS are enabled together. +if(MOONBASE_BUILD_JUCE_TESTS) + enable_testing() + add_subdirectory(tests/juce) +endif() + +install(TARGETS moonbase_cpp EXPORT moonbase_cppTargets) install(DIRECTORY include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) install(FILES "${PROJECT_SOURCE_DIR}/LICENSE" diff --git a/docs/juce-module.md b/docs/juce-module.md new file mode 100644 index 0000000..04ac1d7 --- /dev/null +++ b/docs/juce-module.md @@ -0,0 +1,290 @@ +# JUCE module: `moonbase_licensing` + +A drop-in [JUCE module](https://github.com/juce-framework/JUCE/blob/master/docs/JUCE%20Module%20Format.md) +that adds Moonbase license activation — with a polished built-in UI — to any JUCE 8 +app or plugin. It integrates **natively** with the Moonbase licensing API; it does +not use `juce::OnlineUnlockStatus`. + +> Prefer the `juce::OnlineUnlockStatus` bridge? That older, copy-paste reference +> still lives in [`examples/juce/`](../examples/juce/) and is unchanged. This module +> is the native alternative. + +Lives at [`modules/moonbase_licensing/`](../modules/moonbase_licensing/). Requires +**JUCE 8** (8.0.4+) and **C++17**. + +## Why it's drop-in + +A normal JUCE module is self-contained and links only JUCE + system frameworks. +This one is too — it has **no third-party dependencies**: + +| Concern | How | +| --- | --- | +| HTTP | `juce::WebInputStream` (`juce_http_transport`) — no CURL | +| JSON | bundled `nlohmann/json` single header (in the module's `vendor/`) | +| RS256 JWT verification | OS-native: **Security.framework** (macOS/iOS), **CNG/bcrypt** (Windows), system **libcrypto** (Linux) | + +So a downstream project adds the module and builds — nothing to `brew` / `vcpkg` / +`apt` install. *(The only platform caveat: on Linux the module links the +always-present system `libcrypto`. macOS and Windows need nothing beyond the OS.)* + +The module sets `MOONBASE_CRYPTO_NATIVE` and `MOONBASE_DISABLE_CURL_TRANSPORT` +itself; the core SDK keeps its OpenSSL + CURL defaults for non-JUCE consumers, so +nothing about the existing `find_package(moonbase_cpp)` path changes. + +## Add it to your project + +Add this repository as a git submodule, then point your build at the module folder. + +**CMake** + +```cmake +juce_add_module(external/moonbase-cpp/modules/moonbase_licensing) + +target_link_libraries(MyPlugin PRIVATE moonbase_licensing) +target_compile_definitions(MyPlugin PRIVATE JUCE_USE_CURL=0) +``` + +**Projucer** — *Modules → Add a module from a specified folder…* → select +`modules/moonbase_licensing`. The bundled SDK headers and `nlohmann/json` resolve +from the module's own search paths. + +## Configure + show it + +```cpp +#include +using namespace moonbase::juce_integration; + +ActivationConfig config; +config.endpoint = "https://your-tenant.moonbase.sh"; +config.productId = "your-product"; +config.publicKey = embeddedPublicKeyPem; +config.productName = "Your Plugin"; +config.manufacturerName = "Your Manufacturer"; +config.accent = juce::Colour(0xff186cdc); +``` + +Embed in a plugin editor: + +```cpp +activation = std::make_unique(config); +addAndMakeVisible(*activation); +activation->onActivationChanged = [this](bool active) { /* enable/disable UI */ }; +``` + +…or pop it as a standalone window: + +```cpp +ActivationDialog::show(config, [](bool wasActivated) { /* … */ }); +``` + +## The flow + +`ActivationComponent` owns an `ActivationController` — a headless state machine over +`moonbase::licensing`. Network calls run on a controller-owned thread pool; all state +changes and repaints happen on the message thread, gated by a generation counter so a +slow request can never clobber a newer state. Destroying the controller cancels any +in-flight request and joins its workers, so nothing keeps running after teardown — you +can call `start()` straight from the editor constructor and destroy the editor at any +time (plugin scanning, pluginval, rapid open/close) without deferring or guarding it. +The screens: + +- **Welcome** — Activate online (browser flow) or Activate offline. +- **Activating** — opens the browser and polls `get_requested_activation()`; the + device chip shows the local fingerprint + platform; Cancel aborts. +- **Success** — animated confirmation with a mini license card. +- **Offline** — two-step machine-file flow: save the request (`generate_device_token`), + then load the response file (`read_offline_license`, validated locally). +- **Trial** — days-left badge, progress bar, included/excluded feature list, and an + Unlock action that routes into online activation. +- **License details** — issued-to / email / plan / activation / expiry, a seat counter, + and a Deactivate action (server-side `revoke_activation`, with a local-forget fallback + for offline or trial licenses). +- **Trial expired** — a locked "trial has ended" screen with Unlock / Activate offline. + +All transitions use JUCE 8's animation API (`juce::Animator` / `ValueAnimatorBuilder` +/ `Easings`, driven by a `VBlankAnimatorUpdater`): cross-fade + fade-up between +screens, the activating spinner, the success pop, and the breathing top-edge glow. + +## Gating + +For correctness in a plugin, give the **processor** the controller (so license state +survives the editor's lifetime) and have the **editor** share it — one license, no +hand-rolled re-sync: + +```cpp +// In your AudioProcessor: +ActivationController activation { makeConfig() }; // persistent +// activation.start(); // load any stored license + +// In createEditor(): share the processor's controller with the UI. +auto* editor = new ActivationComponent (processor.activation); // non-owning overload +``` + +Gate the audio thread off the lock-free flag — no `ChangeListener` needed: + +```cpp +void processBlock (juce::AudioBuffer& buffer, ...) override +{ + if (! activation.licensedFlag().load()) + buffer.clear(); +} +``` + +Or use `LicenseGate` for a click-free fade on activate/deactivate (you still own the +gating; the module never silences audio itself): + +```cpp +LicenseGate gate; // a member of your processor +void prepareToPlay (double sr, int) override { gate.prepare (sr); gate.reset (activation.licensedFlag().load()); } +void processBlock (juce::AudioBuffer& b, ...) override +{ + gate.process (b.getArrayOfWritePointers(), b.getNumChannels(), b.getNumSamples(), + activation.licensedFlag().load()); +} +``` + +`controller().license()` is the full `moonbase::license` — `trial`, `expires_at`, +`issued_to.email`, `owned_sub_product_ids`, custom `properties`, etc. — for richer +gating decisions (read it on the message thread). + +## Branding / theming + +Everything in `ActivationConfig` after the connection fields is brand/UI: product + +manufacturer name, `accent` colour, the Moonbase co-brand badge (`showMoonbaseBadge`), the +`trialLengthDays` + `trialFeatures` list (shown on the Trial / Expired screens), +`enableOffline`, and the `activationUrl`. For deeper re-skinning, mutate `ActivationLookAndFeel::palette` +(every colour is a token) or bundle real Inter / Space Mono typefaces and point the +`heading` / `body` / `mono` font helpers at them. + +## Fingerprinting + +By default the module identifies the device via +`juce::SystemStats::getUniqueDeviceID()` (so it never shells out to ioreg/dmidecode +inside a sandboxed host). Pick one fingerprint source when you ship and keep it — +changing it changes the device id Moonbase sees and invalidates existing activations. + +## Sample app + +[`examples/juce-native/`](../examples/juce-native/) is a runnable standalone app that +drops the component against the public Moonbase demo tenant: + +```bash +cmake -B build -DMOONBASE_BUILD_JUCE_NATIVE_EXAMPLE=ON +cmake --build build --target MoonbaseActivationNative +``` + +It fetches JUCE 8 on first configure and adds the module with `juce_add_module` — +exactly how a downstream project consumes it. + +## Tests + +Two automated suites cover the module: + +- **Behavioral** ([`tests/juce/`](../tests/juce/)) drives `ActivationController` against + injected fake store / transport / fingerprint and asserts the state machine: startup + routing (Welcome / Details / Trial), expired-offline-license removal, online revoke vs. + local forget vs. unreachable, and the offline activation round-trip. Tokens are signed + with OpenSSL and verified by the module's native crypto backend, so the suite also + exercises cross-backend RS256 parity. +- **Visual** ([`tests/visual/`](../tests/visual/)) renders every screen offscreen to PNGs + for Argos diffing. + +```bash +cmake -B build -DMOONBASE_BUILD_JUCE_TESTS=ON +cmake --build build --target MoonbaseJuceTests +ctest --test-dir build -R "Juce\." --output-on-failure +``` + +The controller is built for injection: alongside the production constructor there is an +`ActivationController(config, std::shared_ptr, deviceName)` overload +that takes ready-made dependencies, so tests run with no network and no real device id. + +Both suites can be built under a sanitizer (the controller and the license store both +spawn threads, so this guards against races and use-after-free): + +```bash +cmake -B build-tsan -DMOONBASE_BUILD_TESTS=ON -DMOONBASE_SANITIZER=thread +cmake -B build-asan -DMOONBASE_BUILD_TESTS=ON -DMOONBASE_SANITIZER=address,undefined +``` + +CI: `sanitizers.yml` runs the SDK suite under ASan/UBSan and TSan on Linux; `juce.yml` +builds the module and runs the behavioral suite on macOS. + +## Diagnostics + +The UI shows friendly, end-user-facing copy. To see the underlying reason behind a failure +(bad config, rejected token, unreachable server, persist failure), wire a diagnostic sink: + +```cpp +config.onDiagnostic = [] (const juce::String& message) { + juce::Logger::writeToLog ("[activation] " + message); +}; +``` + +It is invoked on the message thread. A missing or malformed `endpoint` / `productId` / +`publicKey` does not throw out of construction; the component shows an Error state and the +reason is reported through this sink (and `DBG` in debug builds). + +## Refreshing entitlements + +After a user buys something mid-session (a sub-product, an upgrade), re-validate the +license online so the new entitlements load without a restart: + +```cpp +activation->controller().refreshLicense (/*force*/ true, [] (bool refreshed) { + if (refreshed) reloadFeatures(); // read controller().license() again +}); +``` + +It runs async and silently (no screen change). On success the license is updated + +persisted and `onActivationChanged` fires; `controller().license()` then reflects the new +`owned_sub_product_ids`, `properties`, expiry, and seat counts. `force` bypasses the +SDK's `online_validation_min_interval` throttle (you want that right after a purchase); +pass `false` for a polite background re-check that respects it. A network failure is +non-fatal: the current license is kept and the reason goes to `onDiagnostic`. Offline +licenses are a no-op (they are permanent and not server-tracked). + +### Cadence and timeouts + +How often the app re-checks online, how long it tolerates being offline, and the request +timeouts are all configurable: + +```cpp +config.onlineCheckInterval = std::chrono::hours (1); // min spacing between online checks (default 5 min) +config.onlineGracePeriod = std::chrono::hours (24 * 30); // max time offline before locking (default 7 days) +config.httpConnectTimeout = std::chrono::seconds (5); +config.httpRequestTimeout = std::chrono::seconds (15); +``` + +The SDK never polls on a timer; it validates on launch (`start()`) and whenever you call +`refreshLicense()`, throttled to no more than once per `onlineCheckInterval`. A license +stays usable offline until `onlineGracePeriod` elapses since its last successful online +validation. + +## Telemetry / analytics + +Off by default. One flag attaches JUCE system + host metadata to every activation and +validation request (the same `juce.*` keys the reference bridge collects): + +```cpp +config.analytics.enabled = true; // OS, CPU, JUCE version, memory +config.analytics.includeHostInfo = true; // DAW host + plugin format (VST3/AU/AAX/...) +config.analytics.includeLocaleInfo = true; // language / region (opt in) +``` + +Host/plugin fields are captured only when `juce_audio_processors` is part of the build +(gated on JUCE's `JUCE_MODULE_AVAILABLE_juce_audio_processors`), so they light up +automatically in a plugin and are skipped in a plain app. Add your own fields, or rewrite +the assembled map last: + +```cpp +config.metadata["app.channel"] = "beta"; // sent as-is; wins on key collisions +config.onCollectMetadata = [] (std::map& m) { + m["cohort"] = abTestCohort(); +}; +``` + +The collected map flows into `moonbase::licensing_options::metadata` and is sent with the +SDK's requests. When you don't set `config.applicationVersion`, it auto-fills from +`JucePlugin_VersionString` in a plugin build (or the running app's version otherwise), so +telemetry reports a version without extra wiring. diff --git a/docs/juce.md b/docs/juce.md index d16e713..fee44b8 100644 --- a/docs/juce.md +++ b/docs/juce.md @@ -1,5 +1,11 @@ # JUCE Integration +> **Looking for a drop-in module with a built-in UI?** See +> [`docs/juce-module.md`](juce-module.md) for the `moonbase_licensing` JUCE module — +> native API integration plus a polished, themeable activation UI, with zero +> third-party dependencies. This page documents the older `juce::OnlineUnlockStatus` +> **bridge** (reference code you copy in), which remains available and unchanged. + A drop-in bridge for using the Moonbase C++ activation SDK from a JUCE-based plugin or application. The bridge lives under [`examples/juce/`](../examples/juce/) as reference code — copy the files into your project, since JUCE is not a diff --git a/examples/juce-native/CMakeLists.txt b/examples/juce-native/CMakeLists.txt new file mode 100644 index 0000000..15894bf --- /dev/null +++ b/examples/juce-native/CMakeLists.txt @@ -0,0 +1,29 @@ +# Standalone sample app that uses the moonbase_licensing JUCE module. +# +# JUCE is fetched and the module is added by the top-level CMakeLists (shared +# with the UI snapshot target). Opt in with: +# cmake -B build -DMOONBASE_BUILD_JUCE_NATIVE_EXAMPLE=ON +# cmake --build build --target MoonbaseActivationNative + +juce_add_gui_app(MoonbaseActivationNative + PRODUCT_NAME "Moonbase Activation" + COMPANY_NAME "Moonbase" + BUNDLE_ID "sh.moonbase.activation.native" + VERSION "1.0.0") + +target_sources(MoonbaseActivationNative PRIVATE Main.cpp) + +target_compile_features(MoonbaseActivationNative PRIVATE cxx_std_17) + +target_compile_definitions(MoonbaseActivationNative PRIVATE + JUCE_WEB_BROWSER=0 + JUCE_USE_CURL=0 + JUCE_APPLICATION_NAME_STRING="$" + JUCE_APPLICATION_VERSION_STRING="$") + +target_link_libraries(MoonbaseActivationNative PRIVATE + moonbase_licensing + juce::juce_gui_extra + juce::juce_recommended_config_flags + juce::juce_recommended_lto_flags + juce::juce_recommended_warning_flags) diff --git a/examples/juce-native/Main.cpp b/examples/juce-native/Main.cpp new file mode 100644 index 0000000..e93fb21 --- /dev/null +++ b/examples/juce-native/Main.cpp @@ -0,0 +1,193 @@ +// Standalone sample app for the moonbase_licensing JUCE module. +// +// It mimics a real plugin editor ("Solstice") with a License button, and shows +// the activation flow as a MODAL OVERLAY on top of it. "Open Solstice", the +// close button, and a successful activation all just dismiss the overlay to +// reveal the app underneath; the License button brings it back. The endpoint / +// product id / public key are the public Moonbase demo values. + +#include + +#include +#include + +using moonbase::juce_integration::ActivationComponent; +using moonbase::juce_integration::ActivationConfig; +using Screen = moonbase::juce_integration::ActivationController::Screen; + +static ActivationConfig makeConfig() +{ + ActivationConfig config; + config.endpoint = "https://demo.moonbase.sh"; + config.productId = "demo-app"; + config.publicKey = R"(-----BEGIN RSA PUBLIC KEY----- +MIIBCgKCAQEAutOqeUiPMgYjAwQ53CyKhJSqojr2bejce0CshQi9Hd8mNZbkoROx +oS56eIzehFSlX4YwHnF47AR1+fPOe7Q33Cgzd6d9xqksiMH7sWK2mADIlB66vZdW +uk3Me0UMB22Biy1RQbSRMivu79MxCofsympoL/5CFjJLd1u37kxjuRWVLjJS84Rr +3L2W7R7Exnno/giC+L/Dv711mjgstmtlAQm5ZINvFvoLA1eFTDs6nlCs3dpJSiq3 +fsBUMT9FtudzS5As54jeT/8MB66fJJ0A1LQ/v5CW8ACQYseFSIoOKErD3xU7QLIJ +ERUn++6CVMPvZo67jVbTY+GCXYfW4gGVZQIDAQAB +-----END RSA PUBLIC KEY-----)"; + + config.productName = "Solstice"; + config.manufacturerName = "Helio Audio"; + config.accent = juce::Colour(0xff186cdc); + config.overlayBackdrop = true; // render as a modal over the plugin editor + config.applicationVersion = JUCE_APPLICATION_VERSION_STRING; + + // Telemetry: attach JUCE system/host metadata to activation requests. + config.analytics.enabled = true; + config.metadata["app.channel"] = "sample"; // a custom field, sent alongside + config.onDiagnostic = [] (const juce::String& message) { + juce::Logger::writeToLog("[activation] " + message); + }; + return config; +} + +//============================================================================== +// A stand-in plugin editor. The real point of the sample is the activation +// modal it hosts; the knobs are just so there's an app to reveal. +class PluginEditor : public juce::Component +{ +public: + PluginEditor() + { + const char* knobNames[] = { "Drive", "Warmth", "Mix", "Output" }; + for (int i = 0; i < 4; ++i) + { + auto knob = std::make_unique(juce::Slider::RotaryHorizontalVerticalDrag, + juce::Slider::NoTextBox); + knob->setRange(0.0, 1.0); + knob->setValue(0.25 + 0.18 * i); + knob->setColour(juce::Slider::rotarySliderFillColourId, juce::Colour(0xff186cdc)); + knob->setColour(juce::Slider::rotarySliderOutlineColourId, juce::Colour(0x22ffffff)); + knob->setColour(juce::Slider::thumbColourId, juce::Colour(0xfff5f8fb)); + addAndMakeVisible(*knob); + + auto label = std::make_unique(juce::String(), knobNames[i]); + label->setJustificationType(juce::Justification::centred); + label->setColour(juce::Label::textColourId, juce::Colour(0xff90a0b8)); + label->setFont(juce::FontOptions(12.5f)); + addAndMakeVisible(*label); + + knobs.push_back(std::move(knob)); + labels.push_back(std::move(label)); + } + + licenseButton.setButtonText("License"); + licenseButton.setColour(juce::TextButton::buttonColourId, juce::Colour(0x18ffffff)); + licenseButton.setColour(juce::TextButton::textColourOffId, juce::Colour(0xffcdd8e6)); + licenseButton.onClick = [this] { showActivation(); }; + addAndMakeVisible(licenseButton); + + activation = std::make_unique(makeConfig()); + activation->onClose = [this] { hideActivation(); }; // "Open", close (X), success all dismiss + activation->onActivationChanged = [this](bool activated) + { + // On launch, lock behind the modal only if not already licensed. + // Once the initial check settles, the License button drives it. + if (! initialCheckSettled + && activation->controller().screen() != Screen::Loading) + { + initialCheckSettled = true; + if (! activated) + showActivation(); + } + }; + addChildComponent(*activation); + setSize(760, 520); + } + + void showActivation() { activation->appear(); } + void hideActivation() { activation->dismiss(); } + + void paint(juce::Graphics& g) override + { + auto b = getLocalBounds().toFloat(); + juce::ColourGradient bg(juce::Colour(0xff151c29), b.getCentreX(), 0.0f, + juce::Colour(0xff090d15), b.getCentreX(), b.getHeight(), false); + g.setGradientFill(bg); + g.fillRect(b); + + // top bar + auto header = getLocalBounds().removeFromTop(96).reduced(28, 0); + g.setColour(juce::Colour(0xfff5f8fb)); + g.setFont(juce::FontOptions(30.0f, juce::Font::bold)); + g.drawText("SOLSTICE", header.removeFromTop(64), juce::Justification::bottomLeft); + g.setColour(juce::Colour(0xff768aa4)); + g.setFont(juce::FontOptions(13.0f)); + g.drawText("Saturator by Helio Audio", header, juce::Justification::topLeft); + + g.setColour(juce::Colour(0x12ffffff)); + g.fillRect(getLocalBounds().withTrimmedTop(96).removeFromTop(1).reduced(28, 0)); + } + + void resized() override + { + activation->setBounds(getLocalBounds()); + licenseButton.setBounds(getWidth() - 28 - 96, 30, 96, 32); + + auto row = getLocalBounds().withTrimmedTop(150).reduced(40, 0).removeFromTop(170); + const int cell = row.getWidth() / juce::jmax(1, (int) knobs.size()); + for (size_t i = 0; i < knobs.size(); ++i) + { + auto c = row.removeFromLeft(cell); + knobs[i]->setBounds(c.removeFromTop(128).reduced(14)); + labels[i]->setBounds(c.removeFromTop(22)); + } + } + +private: + std::vector> knobs; + std::vector> labels; + juce::TextButton licenseButton; + std::unique_ptr activation; + bool initialCheckSettled = false; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(PluginEditor) +}; + +//============================================================================== +class MoonbaseActivationApplication : public juce::JUCEApplication +{ +public: + const juce::String getApplicationName() override { return "Moonbase Activation"; } + const juce::String getApplicationVersion() override { return JUCE_APPLICATION_VERSION_STRING; } + bool moreThanOneInstanceAllowed() override { return true; } + + void initialise(const juce::String&) override { mainWindow.reset(new MainWindow()); } + void shutdown() override { mainWindow = nullptr; } + void systemRequestedQuit() override { quit(); } + + class MainWindow : public juce::DocumentWindow + { + public: + MainWindow() + : juce::DocumentWindow("Solstice", juce::Colour(0xff090d15), + juce::DocumentWindow::allButtons) + { + auto* editor = new PluginEditor(); + setUsingNativeTitleBar(true); + setContentOwned(editor, true); + setResizable(true, false); + // Deliberately allow sizes below the modal's minimum so you can see + // the activation modal scale down to fit a small host window. + setResizeLimits(360, 320, 1400, 1000); + centreWithSize(editor->getWidth(), editor->getHeight()); + setVisible(true); + } + + void closeButtonPressed() override + { + juce::JUCEApplication::getInstance()->systemRequestedQuit(); + } + + private: + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(MainWindow) + }; + +private: + std::unique_ptr mainWindow; +}; + +START_JUCE_APPLICATION(MoonbaseActivationApplication) diff --git a/include/moonbase/client.hpp b/include/moonbase/client.hpp index 163dbd0..a7a656f 100644 --- a/include/moonbase/client.hpp +++ b/include/moonbase/client.hpp @@ -70,11 +70,16 @@ inline std::map client_query(const licensing_options& return query; } -inline std::map default_headers(const std::string& content_type = {}) +inline std::map default_headers(const licensing_options& options, + const std::string& content_type = {}) { + std::string user_agent = "moonbase-cpp/" + version_string(); + if (options.client_info && !options.client_info->empty()) { + user_agent += " " + *options.client_info; + } std::map headers{ {"Accept", "application/json, application/jwt, text/plain"}, - {"User-Agent", "moonbase-cpp/" + version_string()}, + {"User-Agent", user_agent}, {"x-mb-client", "moonbase-cpp"}, }; if (!content_type.empty()) { @@ -165,7 +170,7 @@ class license_client { http_request request; request.method = "POST"; request.url = url; - request.headers = detail::default_headers("application/json"); + request.headers = detail::default_headers(options_, "application/json"); request.connect_timeout = options_.http_connect_timeout; request.request_timeout = options_.http_request_timeout; request.body = payload.dump(); @@ -193,7 +198,7 @@ class license_client { http_request request; request.method = "POST"; request.url = url; - request.headers = detail::default_headers("text/plain"); + request.headers = detail::default_headers(options_, "text/plain"); request.connect_timeout = options_.http_connect_timeout; request.request_timeout = options_.http_request_timeout; request.body = std::string(token); @@ -214,7 +219,7 @@ class license_client { http_request request; request.method = "POST"; request.url = url; - request.headers = detail::default_headers("text/plain"); + request.headers = detail::default_headers(options_, "text/plain"); request.connect_timeout = options_.http_connect_timeout; request.request_timeout = options_.http_request_timeout; request.body = std::string(token); @@ -231,7 +236,7 @@ class license_client { http_request request; request.method = "GET"; request.url = activation.request_url; - request.headers = detail::default_headers(); + request.headers = detail::default_headers(options_); request.connect_timeout = options_.http_connect_timeout; request.request_timeout = options_.http_request_timeout; diff --git a/include/moonbase/default_fingerprint.hpp b/include/moonbase/default_fingerprint.hpp index 4f85640..470096d 100644 --- a/include/moonbase/default_fingerprint.hpp +++ b/include/moonbase/default_fingerprint.hpp @@ -26,8 +26,7 @@ #include #endif -#include - +#include "moonbase/detail/crypto/crypto.hpp" #include "moonbase/fingerprint.hpp" namespace moonbase { @@ -74,7 +73,7 @@ class default_fingerprint_provider : public fingerprint_provider { } } - return sha256_hex(material); + return detail::sha256_hex(material); } [[nodiscard]] static std::vector identity_parameters() @@ -153,22 +152,6 @@ class default_fingerprint_provider : public fingerprint_provider { } private: - [[nodiscard]] static std::string sha256_hex(std::string_view material) - { - unsigned char digest[SHA256_DIGEST_LENGTH]{}; - SHA256( - reinterpret_cast(material.data()), - material.size(), - digest); - - std::ostringstream output; - output << std::hex << std::setfill('0'); - for (const auto byte : digest) { - output << std::setw(2) << static_cast(byte); - } - return output.str(); - } - [[nodiscard]] static std::string read_file(const std::string& path) { std::ifstream file(path); diff --git a/include/moonbase/detail/crypto/apple_backend.hpp b/include/moonbase/detail/crypto/apple_backend.hpp new file mode 100644 index 0000000..53809fc --- /dev/null +++ b/include/moonbase/detail/crypto/apple_backend.hpp @@ -0,0 +1,129 @@ +#pragma once + +// Apple crypto backend — Security.framework + CommonCrypto. Used on macOS/iOS +// when MOONBASE_CRYPTO_NATIVE is set (the JUCE module), so the module needs no +// OpenSSL. RS256 verification uses SecKeyVerifySignature with the "Message" +// PKCS#1 v1.5 SHA-256 algorithm, which hashes the input internally. + +#include +#include +#include +#include +#include + +#include +#include + +#include "moonbase/detail/crypto/der.hpp" +#include "moonbase/errors.hpp" + +namespace moonbase::detail::crypto { + +inline std::array sha256_raw(const unsigned char* data, std::size_t length) +{ + std::array digest{}; + CC_SHA256(data, static_cast(length), digest.data()); + return digest; +} + +namespace apple_detail { + +// RAII for CoreFoundation handles (CFRelease on destruction). +template +class cf_ref { +public: + cf_ref() = default; + explicit cf_ref(T ref) : ref_(ref) {} + cf_ref(const cf_ref&) = delete; + cf_ref& operator=(const cf_ref&) = delete; + cf_ref(cf_ref&& other) noexcept : ref_(other.ref_) { other.ref_ = nullptr; } + cf_ref& operator=(cf_ref&& other) noexcept + { + if (this != &other) { + reset(); + ref_ = other.ref_; + other.ref_ = nullptr; + } + return *this; + } + ~cf_ref() { reset(); } + + [[nodiscard]] T get() const noexcept { return ref_; } + explicit operator bool() const noexcept { return ref_ != nullptr; } + +private: + void reset() + { + if (ref_ != nullptr) { + CFRelease(ref_); + ref_ = nullptr; + } + } + T ref_ = nullptr; +}; + +inline cf_ref make_data(const unsigned char* bytes, std::size_t length) +{ + return cf_ref( + CFDataCreate(kCFAllocatorDefault, bytes, static_cast(length))); +} + +} // namespace apple_detail + +class rsa_public_key { +public: + explicit rsa_public_key(const std::string& key_material) + { + const std::vector pkcs1 = der::normalize_to_pkcs1(key_material); + auto key_data = apple_detail::make_data(pkcs1.data(), pkcs1.size()); + if (!key_data) { + throw license_invalid_error("Public key is not a supported RSA public key"); + } + + const void* keys[] = {kSecAttrKeyType, kSecAttrKeyClass}; + const void* values[] = {kSecAttrKeyTypeRSA, kSecAttrKeyClassPublic}; + apple_detail::cf_ref attributes( + CFDictionaryCreate(kCFAllocatorDefault, keys, values, 2, + &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks)); + + CFErrorRef error = nullptr; + SecKeyRef key = SecKeyCreateWithData(key_data.get(), attributes.get(), &error); + if (key == nullptr) { + if (error != nullptr) { + CFRelease(error); + } + throw license_invalid_error("Public key is not a supported RSA public key"); + } + key_ = apple_detail::cf_ref(key); + } + + void verify_rs256(std::string_view signing_input, + const std::vector& signature) const + { + auto message = apple_detail::make_data( + reinterpret_cast(signing_input.data()), signing_input.size()); + auto sig = apple_detail::make_data(signature.data(), signature.size()); + if (!message || !sig) { + throw license_invalid_error("License token signature is not valid"); + } + + CFErrorRef error = nullptr; + const Boolean ok = SecKeyVerifySignature( + key_.get(), + kSecKeyAlgorithmRSASignatureMessagePKCS1v15SHA256, + message.get(), + sig.get(), + &error); + if (error != nullptr) { + CFRelease(error); + } + if (ok != true) { + throw license_invalid_error("License token signature is not valid"); + } + } + +private: + apple_detail::cf_ref key_; +}; + +} // namespace moonbase::detail::crypto diff --git a/include/moonbase/detail/crypto/crypto.hpp b/include/moonbase/detail/crypto/crypto.hpp new file mode 100644 index 0000000..34dd530 --- /dev/null +++ b/include/moonbase/detail/crypto/crypto.hpp @@ -0,0 +1,74 @@ +#pragma once + +// Crypto backend selector for Moonbase's RS256 JWT verification and the device +// fingerprint hash. The backend is chosen at compile time: +// +// * Default (no macro set): OpenSSL — keeps existing non-JUCE consumers and +// the test suite byte-for-byte unchanged. +// * Define MOONBASE_CRYPTO_NATIVE: use the OS-native backend — Security.framework +// on Apple, CNG/bcrypt on Windows, system libcrypto (OpenSSL) on Linux. This +// is what the JUCE module defines so it pulls in no third-party crypto. +// * Or force a specific backend by defining MOONBASE_CRYPTO_BACKEND to one of +// MOONBASE_CRYPTO_OPENSSL / MOONBASE_CRYPTO_APPLE / MOONBASE_CRYPTO_WINDOWS. +// +// Whichever backend is active provides, in namespace moonbase::detail::crypto: +// std::array sha256_raw(const unsigned char*, std::size_t); +// class rsa_public_key { rsa_public_key(const std::string&); +// void verify_rs256(std::string_view, const std::vector&) const; }; + +#include +#include +#include +#include + +#define MOONBASE_CRYPTO_OPENSSL 1 +#define MOONBASE_CRYPTO_APPLE 2 +#define MOONBASE_CRYPTO_WINDOWS 3 + +#if !defined(MOONBASE_CRYPTO_BACKEND) +#if defined(MOONBASE_CRYPTO_NATIVE) +#if defined(__APPLE__) +#define MOONBASE_CRYPTO_BACKEND MOONBASE_CRYPTO_APPLE +#elif defined(_WIN32) +#define MOONBASE_CRYPTO_BACKEND MOONBASE_CRYPTO_WINDOWS +#else +#define MOONBASE_CRYPTO_BACKEND MOONBASE_CRYPTO_OPENSSL +#endif +#else +#define MOONBASE_CRYPTO_BACKEND MOONBASE_CRYPTO_OPENSSL +#endif +#endif + +#if MOONBASE_CRYPTO_BACKEND == MOONBASE_CRYPTO_APPLE +#include "moonbase/detail/crypto/apple_backend.hpp" +#elif MOONBASE_CRYPTO_BACKEND == MOONBASE_CRYPTO_WINDOWS +#include "moonbase/detail/crypto/windows_backend.hpp" +#else +#include "moonbase/detail/crypto/openssl_backend.hpp" +#endif + +namespace moonbase::detail { + +// The RSA public key type the validator holds, resolved to the active backend. +using crypto::rsa_public_key; + +// Lowercase hex SHA-256 of the material. The hex formatting lives here (shared) +// so the device-fingerprint output is byte-identical across all backends — any +// change would invalidate already-issued licenses whose `sig` claim was derived +// from the previous hash. +inline std::string sha256_hex(std::string_view material) +{ + const std::array digest = crypto::sha256_raw( + reinterpret_cast(material.data()), material.size()); + + static constexpr char hex[] = "0123456789abcdef"; + std::string out; + out.reserve(digest.size() * 2); + for (const unsigned char byte : digest) { + out.push_back(hex[byte >> 4U]); + out.push_back(hex[byte & 0x0FU]); + } + return out; +} + +} // namespace moonbase::detail diff --git a/include/moonbase/detail/crypto/der.hpp b/include/moonbase/detail/crypto/der.hpp new file mode 100644 index 0000000..622f59e --- /dev/null +++ b/include/moonbase/detail/crypto/der.hpp @@ -0,0 +1,180 @@ +#pragma once + +// Minimal DER/TLV reader, just enough to normalize an RSA public key into +// PKCS#1 `RSAPublicKey` form and (for the Windows CNG backend) split it into +// its modulus and exponent. Shared by the Apple and Windows crypto backends so +// they accept exactly the same key inputs the OpenSSL backend does: PEM SPKI +// (`-----BEGIN PUBLIC KEY-----`), PEM PKCS#1 (`-----BEGIN RSA PUBLIC KEY-----`), +// and raw base64 of either DER encoding. +// +// The OpenSSL backend does not use this file — it lets OpenSSL parse the key. + +#include +#include +#include +#include +#include + +#include "moonbase/detail/base64.hpp" +#include "moonbase/errors.hpp" + +namespace moonbase::detail::crypto::der { + +struct cursor { + const unsigned char* p; + const unsigned char* end; +}; + +struct tlv { + unsigned char tag; + const unsigned char* content; + std::size_t length; +}; + +inline unsigned char read_byte(cursor& c) +{ + if (c.p >= c.end) { + throw license_invalid_error("Public key is not a supported RSA public key"); + } + return *c.p++; +} + +// Definite-length encoding, short and long form. RSA-2048 keys push the inner +// structures past 127 bytes, so long-form (0x82 hi lo) is the common case, not +// a corner case — handle it or real keys mis-parse. +inline std::size_t read_length(cursor& c) +{ + const unsigned char first = read_byte(c); + if ((first & 0x80U) == 0) { + return first; + } + const unsigned count = first & 0x7FU; + if (count == 0 || count > sizeof(std::size_t)) { + throw license_invalid_error("Public key is not a supported RSA public key"); + } + std::size_t length = 0; + for (unsigned i = 0; i < count; ++i) { + length = (length << 8U) | read_byte(c); + } + return length; +} + +inline tlv read_tlv(cursor& c) +{ + const unsigned char tag = read_byte(c); + const std::size_t length = read_length(c); + // Bounds-check before advancing: a short- or long-form length larger than + // the bytes that remain is a malformed key, not a licence to read past the + // decoded buffer. + if (static_cast(c.end - c.p) < length) { + throw license_invalid_error("Public key is not a supported RSA public key"); + } + const unsigned char* content = c.p; + c.p += length; + return tlv{tag, content, length}; +} + +// Strip the PEM armor (if any) and base64-decode to raw DER bytes. +inline std::vector decode_key_bytes(const std::string& key_material) +{ + if (key_material.find("-----BEGIN") != std::string::npos) { + std::string body; + std::istringstream stream(key_material); + std::string line; + bool inside = false; + while (std::getline(stream, line)) { + if (line.find("-----BEGIN") != std::string::npos) { + inside = true; + continue; + } + if (line.find("-----END") != std::string::npos) { + break; + } + if (inside) { + body += line; + } + } + return base64_decode(body); + } + return base64_decode(key_material); +} + +// Normalize any accepted key shape to PKCS#1 `RSAPublicKey` DER +// (`SEQUENCE { INTEGER n, INTEGER e }`). +inline std::vector normalize_to_pkcs1(const std::string& key_material) +{ + std::vector der = decode_key_bytes(key_material); + if (der.empty()) { + throw license_invalid_error("Public key is not a supported RSA public key"); + } + + const unsigned char* begin = der.data(); + cursor top{der.data(), der.data() + der.size()}; + const tlv outer = read_tlv(top); + if (outer.tag != 0x30) { + throw license_invalid_error("Public key is not a supported RSA public key"); + } + const std::size_t outer_total = static_cast(top.p - begin); + + // Peek the first element inside the outer SEQUENCE to discriminate. + cursor inner{outer.content, outer.content + outer.length}; + cursor peek = inner; + const tlv first = read_tlv(peek); + + if (first.tag == 0x02) { + // INTEGER first => already PKCS#1 RSAPublicKey. Return the outer TLV. + return std::vector(der.begin(), + der.begin() + static_cast(outer_total)); + } + + if (first.tag == 0x30) { + // SEQUENCE first => SPKI: SEQUENCE { AlgorithmIdentifier, BIT STRING }. + read_tlv(inner); // skip AlgorithmIdentifier + const tlv bitstring = read_tlv(inner); + if (bitstring.tag != 0x03 || bitstring.length < 1) { + throw license_invalid_error("Public key is not a supported RSA public key"); + } + // First content byte of a BIT STRING is the unused-bit count (0 here); + // the remainder is the embedded PKCS#1 RSAPublicKey DER. + const unsigned char* pk = bitstring.content + 1; + const std::size_t pk_len = bitstring.length - 1; + return std::vector(pk, pk + pk_len); + } + + throw license_invalid_error("Public key is not a supported RSA public key"); +} + +struct rsa_components { + std::vector modulus; // big-endian, sign byte stripped + std::vector exponent; // big-endian, sign byte stripped +}; + +// Split PKCS#1 `SEQUENCE { INTEGER n, INTEGER e }` into raw big-endian (n, e), +// stripping the leading 0x00 sign byte DER prepends when the high bit is set. +inline rsa_components parse_rsa_pkcs1(const std::vector& pkcs1) +{ + cursor top{pkcs1.data(), pkcs1.data() + pkcs1.size()}; + const tlv seq = read_tlv(top); + if (seq.tag != 0x30) { + throw license_invalid_error("Public key is not a supported RSA public key"); + } + cursor in{seq.content, seq.content + seq.length}; + const tlv modulus = read_tlv(in); + const tlv exponent = read_tlv(in); + if (modulus.tag != 0x02 || exponent.tag != 0x02) { + throw license_invalid_error("Public key is not a supported RSA public key"); + } + + const auto strip = [](const unsigned char* p, std::size_t len) { + while (len > 1 && p[0] == 0x00) { + ++p; + --len; + } + return std::vector(p, p + len); + }; + + return rsa_components{strip(modulus.content, modulus.length), + strip(exponent.content, exponent.length)}; +} + +} // namespace moonbase::detail::crypto::der diff --git a/include/moonbase/detail/crypto/openssl_backend.hpp b/include/moonbase/detail/crypto/openssl_backend.hpp new file mode 100644 index 0000000..9ab2d08 --- /dev/null +++ b/include/moonbase/detail/crypto/openssl_backend.hpp @@ -0,0 +1,165 @@ +#pragma once + +// OpenSSL crypto backend — the default. Selected unless a different +// MOONBASE_CRYPTO_BACKEND is configured. This is the path existing non-JUCE +// consumers (and the test suite) compile, so its behavior and error messages +// are kept identical to the original inline implementation in validator.hpp. + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "moonbase/detail/base64.hpp" +#include "moonbase/errors.hpp" + +namespace moonbase::detail::crypto { + +inline std::array sha256_raw(const unsigned char* data, std::size_t length) +{ + std::array digest{}; + SHA256(data, length, digest.data()); + return digest; +} + +namespace openssl_detail { + +#if defined(__clang__) || defined(__GNUC__) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" +#endif + +using evp_pkey_ptr = std::unique_ptr; +using bio_ptr = std::unique_ptr; +using evp_md_ctx_ptr = std::unique_ptr; + +inline evp_pkey_ptr make_empty_pkey() +{ + return evp_pkey_ptr(nullptr, EVP_PKEY_free); +} + +inline bio_ptr make_memory_bio(const std::string& value) +{ + return bio_ptr(BIO_new_mem_buf(value.data(), static_cast(value.size())), BIO_free); +} + +inline evp_pkey_ptr read_pem_public_key(const std::string& public_key) +{ + { + auto bio = make_memory_bio(public_key); + if (bio) { + if (auto* pkey = PEM_read_bio_PUBKEY(bio.get(), nullptr, nullptr, nullptr)) { + return evp_pkey_ptr(pkey, EVP_PKEY_free); + } + } + } + + { + auto bio = make_memory_bio(public_key); + if (bio) { + if (auto* rsa = PEM_read_bio_RSAPublicKey(bio.get(), nullptr, nullptr, nullptr)) { + auto* pkey = EVP_PKEY_new(); + if (!pkey) { + RSA_free(rsa); + throw license_invalid_error("Could not allocate RSA public key"); + } + if (EVP_PKEY_assign_RSA(pkey, rsa) != 1) { + RSA_free(rsa); + EVP_PKEY_free(pkey); + throw license_invalid_error("Could not assign RSA public key"); + } + return evp_pkey_ptr(pkey, EVP_PKEY_free); + } + } + } + + return make_empty_pkey(); +} + +inline evp_pkey_ptr read_der_public_key(const std::vector& der) +{ + const unsigned char* cursor = der.data(); + if (auto* pkey = d2i_PUBKEY(nullptr, &cursor, static_cast(der.size()))) { + return evp_pkey_ptr(pkey, EVP_PKEY_free); + } + + cursor = der.data(); + if (auto* rsa = d2i_RSAPublicKey(nullptr, &cursor, static_cast(der.size()))) { + auto* pkey = EVP_PKEY_new(); + if (!pkey) { + RSA_free(rsa); + throw license_invalid_error("Could not allocate RSA public key"); + } + if (EVP_PKEY_assign_RSA(pkey, rsa) != 1) { + RSA_free(rsa); + EVP_PKEY_free(pkey); + throw license_invalid_error("Could not assign RSA public key"); + } + return evp_pkey_ptr(pkey, EVP_PKEY_free); + } + + return make_empty_pkey(); +} + +#if defined(__clang__) || defined(__GNUC__) +#pragma GCC diagnostic pop +#endif + +inline evp_pkey_ptr load_public_key(const std::string& public_key) +{ + if (public_key.find("-----BEGIN") != std::string::npos) { + auto pkey = read_pem_public_key(public_key); + if (pkey) { + return pkey; + } + } + + try { + auto der = base64_decode(public_key); + auto pkey = read_der_public_key(der); + if (pkey) { + return pkey; + } + } catch (const std::exception&) { + } + + throw license_invalid_error("Public key is not a supported RSA public key"); +} + +} // namespace openssl_detail + +class rsa_public_key { +public: + explicit rsa_public_key(const std::string& key_material) + : key_(openssl_detail::load_public_key(key_material)) + { + } + + void verify_rs256(std::string_view signing_input, + const std::vector& signature) const + { + openssl_detail::evp_md_ctx_ptr ctx(EVP_MD_CTX_new(), EVP_MD_CTX_free); + if (!ctx) { + throw license_invalid_error("Could not initialize signature verifier"); + } + + if (EVP_DigestVerifyInit(ctx.get(), nullptr, EVP_sha256(), nullptr, key_.get()) != 1 || + EVP_DigestVerifyUpdate(ctx.get(), signing_input.data(), signing_input.size()) != 1 || + EVP_DigestVerifyFinal(ctx.get(), signature.data(), signature.size()) != 1) { + throw license_invalid_error("License token signature is not valid"); + } + } + +private: + openssl_detail::evp_pkey_ptr key_; +}; + +} // namespace moonbase::detail::crypto diff --git a/include/moonbase/detail/crypto/windows_backend.hpp b/include/moonbase/detail/crypto/windows_backend.hpp new file mode 100644 index 0000000..af4b57e --- /dev/null +++ b/include/moonbase/detail/crypto/windows_backend.hpp @@ -0,0 +1,135 @@ +#pragma once + +// Windows crypto backend — CNG (bcrypt). Used on Windows when +// MOONBASE_CRYPTO_NATIVE is set (the JUCE module), so the module needs no +// OpenSSL. Unlike the Apple "Message" algorithm, BCryptVerifySignature wants a +// pre-computed SHA-256 digest plus a PKCS#1 padding descriptor. + +#include +#include +#include +#include +#include +#include + +#ifndef NOMINMAX +#define NOMINMAX +#endif +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include + +#include + +#include "moonbase/detail/crypto/der.hpp" +#include "moonbase/errors.hpp" + +namespace moonbase::detail::crypto { + +inline std::array sha256_raw(const unsigned char* data, std::size_t length) +{ + std::array digest{}; + + BCRYPT_ALG_HANDLE algorithm = nullptr; + if (!BCRYPT_SUCCESS(BCryptOpenAlgorithmProvider(&algorithm, BCRYPT_SHA256_ALGORITHM, nullptr, 0))) { + throw license_invalid_error("Could not initialize signature verifier"); + } + + BCRYPT_HASH_HANDLE hash = nullptr; + NTSTATUS status = BCryptCreateHash(algorithm, &hash, nullptr, 0, nullptr, 0, 0); + if (BCRYPT_SUCCESS(status)) { + status = BCryptHashData(hash, const_cast(reinterpret_cast(data)), + static_cast(length), 0); + } + if (BCRYPT_SUCCESS(status)) { + status = BCryptFinishHash(hash, digest.data(), static_cast(digest.size()), 0); + } + if (hash != nullptr) { + BCryptDestroyHash(hash); + } + BCryptCloseAlgorithmProvider(algorithm, 0); + + if (!BCRYPT_SUCCESS(status)) { + throw license_invalid_error("Could not initialize signature verifier"); + } + return digest; +} + +class rsa_public_key { +public: + explicit rsa_public_key(const std::string& key_material) + { + const std::vector pkcs1 = der::normalize_to_pkcs1(key_material); + const der::rsa_components components = der::parse_rsa_pkcs1(pkcs1); + + if (!BCRYPT_SUCCESS(BCryptOpenAlgorithmProvider(&algorithm_, BCRYPT_RSA_ALGORITHM, nullptr, 0))) { + throw license_invalid_error("Public key is not a supported RSA public key"); + } + + BCRYPT_RSAKEY_BLOB header{}; + header.Magic = BCRYPT_RSAPUBLIC_MAGIC; + header.BitLength = static_cast(components.modulus.size() * 8U); + header.cbPublicExp = static_cast(components.exponent.size()); + header.cbModulus = static_cast(components.modulus.size()); + header.cbPrime1 = 0; + header.cbPrime2 = 0; + + std::vector blob(sizeof(header) + components.exponent.size() + + components.modulus.size()); + std::memcpy(blob.data(), &header, sizeof(header)); + std::memcpy(blob.data() + sizeof(header), components.exponent.data(), + components.exponent.size()); + std::memcpy(blob.data() + sizeof(header) + components.exponent.size(), + components.modulus.data(), components.modulus.size()); + + const NTSTATUS status = BCryptImportKeyPair( + algorithm_, nullptr, BCRYPT_RSAPUBLIC_BLOB, &key_, + blob.data(), static_cast(blob.size()), 0); + if (!BCRYPT_SUCCESS(status)) { + BCryptCloseAlgorithmProvider(algorithm_, 0); + algorithm_ = nullptr; + throw license_invalid_error("Public key is not a supported RSA public key"); + } + } + + rsa_public_key(const rsa_public_key&) = delete; + rsa_public_key& operator=(const rsa_public_key&) = delete; + rsa_public_key(rsa_public_key&&) = delete; + rsa_public_key& operator=(rsa_public_key&&) = delete; + + ~rsa_public_key() + { + if (key_ != nullptr) { + BCryptDestroyKey(key_); + } + if (algorithm_ != nullptr) { + BCryptCloseAlgorithmProvider(algorithm_, 0); + } + } + + void verify_rs256(std::string_view signing_input, + const std::vector& signature) const + { + const std::array digest = sha256_raw( + reinterpret_cast(signing_input.data()), signing_input.size()); + + BCRYPT_PKCS1_PADDING_INFO padding{}; + padding.pszAlgId = BCRYPT_SHA256_ALGORITHM; + + const NTSTATUS status = BCryptVerifySignature( + key_, &padding, + const_cast(digest.data()), static_cast(digest.size()), + const_cast(signature.data()), static_cast(signature.size()), + BCRYPT_PAD_PKCS1); + if (!BCRYPT_SUCCESS(status)) { + throw license_invalid_error("License token signature is not valid"); + } + } + +private: + BCRYPT_ALG_HANDLE algorithm_ = nullptr; + BCRYPT_KEY_HANDLE key_ = nullptr; +}; + +} // namespace moonbase::detail::crypto diff --git a/include/moonbase/detail/url.hpp b/include/moonbase/detail/url.hpp index 79f3fcf..577d6ad 100644 --- a/include/moonbase/detail/url.hpp +++ b/include/moonbase/detail/url.hpp @@ -21,7 +21,8 @@ inline std::string url_encode(std::string_view value) { std::ostringstream out; out << std::uppercase << std::hex; - for (const unsigned char c : value) { + for (const char ch : value) { + const auto c = static_cast(ch); if (std::isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~') { out << static_cast(c); } else { diff --git a/include/moonbase/licensing.hpp b/include/moonbase/licensing.hpp index c347d3d..37fc0a6 100644 --- a/include/moonbase/licensing.hpp +++ b/include/moonbase/licensing.hpp @@ -14,7 +14,9 @@ #include "moonbase/errors.hpp" #include "moonbase/fingerprint.hpp" #include "moonbase/http.hpp" +#ifndef MOONBASE_DISABLE_CURL_TRANSPORT #include "moonbase/http_curl.hpp" +#endif #include "moonbase/store.hpp" #include "moonbase/types.hpp" #include "moonbase/validator.hpp" @@ -40,7 +42,12 @@ class licensing { fingerprints_ = std::make_shared(); } if (!transport_) { +#ifdef MOONBASE_DISABLE_CURL_TRANSPORT + throw configuration_error( + "An HTTP transport is required (built without the bundled CURL transport)"); +#else transport_ = std::make_shared(); +#endif } validator_ = std::make_shared(options_, fingerprints_); client_ = std::make_shared(options_, fingerprints_, validator_, transport_); @@ -62,6 +69,14 @@ class licensing { return validator_->validate_token(token); } + // Same as validate_token_local but does not throw on a past `exp`. Lets a + // caller inspect an aged-out token (e.g. to learn its activation method and + // expiry) and decide what to do with it, rather than only seeing the throw. + [[nodiscard]] license validate_token_local_allow_expired(std::string_view token) const + { + return validator_->validate_token_allow_expired(token); + } + // Emits the device token ("machine file") for the offline activation flow. // The returned string is a base64-encoded JSON descriptor of this device + // product; write it to a file (conventionally with a ".dt" extension) that diff --git a/include/moonbase/moonbase.hpp b/include/moonbase/moonbase.hpp index 1cfc7f3..54999f7 100644 --- a/include/moonbase/moonbase.hpp +++ b/include/moonbase/moonbase.hpp @@ -5,7 +5,9 @@ #include "moonbase/errors.hpp" #include "moonbase/fingerprint.hpp" #include "moonbase/http.hpp" +#ifndef MOONBASE_DISABLE_CURL_TRANSPORT #include "moonbase/http_curl.hpp" +#endif #include "moonbase/licensing.hpp" #include "moonbase/store.hpp" #include "moonbase/types.hpp" diff --git a/include/moonbase/types.hpp b/include/moonbase/types.hpp index 8ddfaa1..f3cf3db 100644 --- a/include/moonbase/types.hpp +++ b/include/moonbase/types.hpp @@ -101,6 +101,8 @@ struct license { std::chrono::system_clock::time_point validated_at{}; std::vector owned_sub_product_ids; std::optional subscription_id; + std::optional seat_count; // total activation seats (from token claims), if provided + std::optional seats_used; // seats currently in use, if provided nlohmann::json properties = nlohmann::json::object(); std::string token; }; @@ -118,6 +120,10 @@ struct licensing_options { std::optional account_id; platform target_platform = current_platform(); std::optional application_version; + // Identifies a higher-level integration built on top of the SDK (e.g. the + // JUCE module). Appended to the User-Agent after "moonbase-cpp/" so + // the server can tell which client made the request. + std::optional client_info; std::map metadata; std::chrono::milliseconds http_connect_timeout{std::chrono::seconds{10}}; std::chrono::milliseconds http_request_timeout{std::chrono::seconds{30}}; @@ -188,6 +194,12 @@ inline void to_json(nlohmann::json& json, const license& value) if (value.subscription_id) { json["subscriptionId"] = *value.subscription_id; } + if (value.seat_count) { + json["seatCount"] = *value.seat_count; + } + if (value.seats_used) { + json["seatsUsed"] = *value.seats_used; + } } inline void from_json(const nlohmann::json& json, license& value) @@ -211,6 +223,16 @@ inline void from_json(const nlohmann::json& json, license& value) } else { value.subscription_id.reset(); } + if (json.contains("seatCount") && !json.at("seatCount").is_null()) { + value.seat_count = json.at("seatCount").get(); + } else { + value.seat_count.reset(); + } + if (json.contains("seatsUsed") && !json.at("seatsUsed").is_null()) { + value.seats_used = json.at("seatsUsed").get(); + } else { + value.seats_used.reset(); + } value.properties = json.value("properties", nlohmann::json::object()); json.at("token").get_to(value.token); } diff --git a/include/moonbase/validator.hpp b/include/moonbase/validator.hpp index d504fc4..6854581 100644 --- a/include/moonbase/validator.hpp +++ b/include/moonbase/validator.hpp @@ -11,14 +11,10 @@ #include #include -#include -#include -#include -#include - #include #include "moonbase/detail/base64.hpp" +#include "moonbase/detail/crypto/crypto.hpp" #include "moonbase/detail/time.hpp" #include "moonbase/errors.hpp" #include "moonbase/fingerprint.hpp" @@ -28,108 +24,6 @@ namespace moonbase { namespace detail { -#if defined(__clang__) || defined(__GNUC__) -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wdeprecated-declarations" -#endif - -using evp_pkey_ptr = std::unique_ptr; -using bio_ptr = std::unique_ptr; -using evp_md_ctx_ptr = std::unique_ptr; - -inline evp_pkey_ptr make_empty_pkey() -{ - return evp_pkey_ptr(nullptr, EVP_PKEY_free); -} - -inline bio_ptr make_memory_bio(const std::string& value) -{ - return bio_ptr(BIO_new_mem_buf(value.data(), static_cast(value.size())), BIO_free); -} - -inline evp_pkey_ptr read_pem_public_key(const std::string& public_key) -{ - { - auto bio = make_memory_bio(public_key); - if (bio) { - if (auto* pkey = PEM_read_bio_PUBKEY(bio.get(), nullptr, nullptr, nullptr)) { - return evp_pkey_ptr(pkey, EVP_PKEY_free); - } - } - } - - { - auto bio = make_memory_bio(public_key); - if (bio) { - if (auto* rsa = PEM_read_bio_RSAPublicKey(bio.get(), nullptr, nullptr, nullptr)) { - auto* pkey = EVP_PKEY_new(); - if (!pkey) { - RSA_free(rsa); - throw license_invalid_error("Could not allocate RSA public key"); - } - if (EVP_PKEY_assign_RSA(pkey, rsa) != 1) { - RSA_free(rsa); - EVP_PKEY_free(pkey); - throw license_invalid_error("Could not assign RSA public key"); - } - return evp_pkey_ptr(pkey, EVP_PKEY_free); - } - } - } - - return make_empty_pkey(); -} - -inline evp_pkey_ptr read_der_public_key(const std::vector& der) -{ - const unsigned char* cursor = der.data(); - if (auto* pkey = d2i_PUBKEY(nullptr, &cursor, static_cast(der.size()))) { - return evp_pkey_ptr(pkey, EVP_PKEY_free); - } - - cursor = der.data(); - if (auto* rsa = d2i_RSAPublicKey(nullptr, &cursor, static_cast(der.size()))) { - auto* pkey = EVP_PKEY_new(); - if (!pkey) { - RSA_free(rsa); - throw license_invalid_error("Could not allocate RSA public key"); - } - if (EVP_PKEY_assign_RSA(pkey, rsa) != 1) { - RSA_free(rsa); - EVP_PKEY_free(pkey); - throw license_invalid_error("Could not assign RSA public key"); - } - return evp_pkey_ptr(pkey, EVP_PKEY_free); - } - - return make_empty_pkey(); -} - -#if defined(__clang__) || defined(__GNUC__) -#pragma GCC diagnostic pop -#endif - -inline evp_pkey_ptr load_public_key(const std::string& public_key) -{ - if (public_key.find("-----BEGIN") != std::string::npos) { - auto pkey = read_pem_public_key(public_key); - if (pkey) { - return pkey; - } - } - - try { - auto der = base64_decode(public_key); - auto pkey = read_der_public_key(der); - if (pkey) { - return pkey; - } - } catch (const std::exception&) { - } - - throw license_invalid_error("Public key is not a supported RSA public key"); -} - inline std::vector split_jwt(std::string_view token) { std::vector parts; @@ -170,23 +64,6 @@ inline nlohmann::json decode_jwt_json(const std::string& encoded, const char* la } } -inline void verify_rs256( - EVP_PKEY* pkey, - const std::string& signing_input, - const std::vector& signature) -{ - evp_md_ctx_ptr ctx(EVP_MD_CTX_new(), EVP_MD_CTX_free); - if (!ctx) { - throw license_invalid_error("Could not initialize signature verifier"); - } - - if (EVP_DigestVerifyInit(ctx.get(), nullptr, EVP_sha256(), nullptr, pkey) != 1 || - EVP_DigestVerifyUpdate(ctx.get(), signing_input.data(), signing_input.size()) != 1 || - EVP_DigestVerifyFinal(ctx.get(), signature.data(), signature.size()) != 1) { - throw license_invalid_error("License token signature is not valid"); - } -} - inline bool has_audience(const nlohmann::json& payload, const std::string& expected) { if (!payload.contains("aud")) { @@ -313,7 +190,7 @@ class license_validator { license_validator(licensing_options options, std::shared_ptr fingerprints) : options_(std::move(options)), fingerprints_(std::move(fingerprints)), - key_(detail::load_public_key(options_.public_key)) + key_(options_.public_key) { if (!fingerprints_) { throw configuration_error("A fingerprint provider is required"); @@ -354,10 +231,7 @@ class license_validator { throw license_invalid_error( std::string("License token signature is malformed: ") + ex.what()); } - detail::verify_rs256( - key_.get(), - signing_input, - signature); + key_.verify_rs256(signing_input, signature); if (!detail::has_audience(payload, options_.product_id)) { throw license_invalid_error("License token audience does not match the configured product"); @@ -399,6 +273,30 @@ class license_validator { ? detail::object_claim_or_empty(payload, "t:properties") : detail::object_claim_or_empty(payload, "l:properties"); + // Seat counts. The backend issues these as the claims p:seats:total / + // p:seats:used (p:seats:remaining is derivable). Fall back to a couple of + // alternative spellings and to the license properties object. + result.seat_count = detail::optional_integer(payload, "p:seats:total"); + result.seats_used = detail::optional_integer(payload, "p:seats:used"); + if (!result.seat_count) { + result.seat_count = detail::optional_integer(payload, "seats"); + } + if (!result.seats_used) { + result.seats_used = detail::optional_integer(payload, "seatsUsed"); + } + const auto seat_from_properties = [&result](const char* key) -> std::optional { + if (result.properties.contains(key) && result.properties.at(key).is_number_integer()) { + return result.properties.at(key).get(); + } + return std::nullopt; + }; + if (!result.seat_count) { + result.seat_count = seat_from_properties("seats"); + } + if (!result.seats_used) { + result.seats_used = seat_from_properties("seatsUsed"); + } + if (!allow_expired && result.expires_at && *result.expires_at < std::chrono::system_clock::now()) { throw license_expired_error("License has expired"); @@ -415,7 +313,7 @@ class license_validator { licensing_options options_; std::shared_ptr fingerprints_; - detail::evp_pkey_ptr key_; + detail::rsa_public_key key_; }; } // namespace moonbase diff --git a/modules/moonbase_licensing/README.md b/modules/moonbase_licensing/README.md new file mode 100644 index 0000000..451098a --- /dev/null +++ b/modules/moonbase_licensing/README.md @@ -0,0 +1,105 @@ +# moonbase_licensing — JUCE module + +License activation for JUCE 8 apps and plugins, with a polished built-in UI, in one +drop-in [JUCE module](https://github.com/juce-framework/JUCE/blob/master/docs/JUCE%20Module%20Format.md). +Add the module, fill in three fields, show one component. + +## Features + +- **Native integration.** Talks to the Moonbase licensing API directly: online + (browser) activation, offline (machine-file) activation, validation with a grace + period, and server-side deactivation. Not a `juce::OnlineUnlockStatus` wrapper. (If + you want that bridge instead, see [`examples/juce/`](../../examples/juce/).) +- **Built-in UI.** A configurable, themeable `ActivationComponent` (and one-call + `ActivationDialog`) covering every state — welcome, activating, success, offline, + trial, trial expired, license details — with JUCE 8 animated transitions and drag-and-drop for + offline license files. Designed to sit as a modal over your plugin and lock it + until activated. +- **Zero third-party dependencies.** HTTP over `juce::WebInputStream` (no CURL), JSON + via a bundled `nlohmann/json`, and RS256 verification via OS-native crypto: + Security.framework (macOS/iOS), CNG/bcrypt (Windows), system libcrypto (Linux). + Nothing to `brew`/`vcpkg`/`apt` install. +- **Batteries included.** Persists the license to a per-user file, fails loud on + misconfiguration, exposes a diagnostics sink for field debugging, and can attach + JUCE system/host telemetry to requests. Brandable end to end. + +Requires **JUCE 8** (8.0.4+) and C++17. Supports macOS, Windows, and Linux. + +## Add it to your project + +Add this repository (as a git submodule, say) and point your build at the module folder. + +### CMake + +```cmake +juce_add_module(path/to/moonbase-cpp/modules/moonbase_licensing) + +target_link_libraries(MyPlugin PRIVATE moonbase_licensing) +target_compile_definitions(MyPlugin PRIVATE JUCE_USE_CURL=0) # keep the zero-dep HTTP path +``` + +### Projucer + +*Modules → Add a module → Add a module from a specified folder…* and select +`modules/moonbase_licensing`. The bundled SDK headers and `nlohmann/json` resolve from +the module's own search paths — nothing else to set up. + +## Configure and use + +Only three fields are required; in a plugin the product and manufacturer names default +from your `JucePlugin_Name` / `JucePlugin_Manufacturer` macros. + +```cpp +#include +using namespace moonbase::juce_integration; + +ActivationConfig config; +config.endpoint = "https://your-tenant.moonbase.sh"; +config.productId = "your-product"; +config.publicKey = embeddedPublicKeyPem; // your product's RSA public key +// Optional: config.productName / manufacturerName / accent / logo / strings ... +``` + +Show it as a modal over your editor (locks until activated), or pop it from a menu item: + +```cpp +// Embedded modal: +auto activation = std::make_unique(config); +activation->onClose = [this] { /* dismiss the modal */ }; +addAndMakeVisible(*activation); + +// …or a standalone window: +ActivationDialog::show(config, [](bool wasActivated) { /* update UI */ }); +``` + +Gate your DSP/features on the live license state: + +```cpp +if (! activation->controller().license().has_value()) + buffer.clear(); // not activated +``` + +`controller().license()` is the full `moonbase::license` (`trial`, `expires_at`, +`issued_to.email`, seat counts, sub-product ownership, custom `properties`, …) for +richer gating, and `onActivationChanged` fires whenever it changes. + +## Going further + +- **Branding** — everything in `ActivationConfig` after the connection fields is UI: + names, accent colour, logo `Drawable`, overridable copy (`config.strings`), trial + length + feature list, the removable Moonbase badge, and the activation URL. + Re-skin deeper via `ActivationLookAndFeel::palette`. +- **Refresh entitlements** — `controller().refreshLicense()` re-validates online so a + freshly purchased sub-product/upgrade loads without a restart (async, silent, with an + optional completion callback). +- **Persistence** — the validated license is stored at + `userApplicationDataDirectory///license.mb` by default + (override with `config.licenseFile`). +- **Diagnostics** — `config.onDiagnostic` receives the underlying reason behind any + friendly error (bad config, rejected token, unreachable server). +- **Telemetry** — `config.analytics.enabled = true` attaches JUCE system/host metadata + (OS, CPU, DAW host, plugin format, …) to activation requests; add your own via + `config.metadata` / `config.onCollectMetadata`. + +See [`docs/juce-module.md`](../../docs/juce-module.md) for the full guide and +[`examples/juce-native/`](../../examples/juce-native/) for a runnable sample app. diff --git a/modules/moonbase_licensing/juce/ActivationConfig.h b/modules/moonbase_licensing/juce/ActivationConfig.h new file mode 100644 index 0000000..bbd75c2 --- /dev/null +++ b/modules/moonbase_licensing/juce/ActivationConfig.h @@ -0,0 +1,261 @@ +#pragma once + +// Everything needed to wire up + brand an activation flow. The connection +// fields configure the Moonbase SDK; the branding fields drive the built-in UI +// (the Solstice design's product name, accent, trial copy, co-brand badge). + +#include +#include +#include +#include +#include +#include + +#include + +#include +#include + +#include "JuceMetadata.h" + +// Normally defined by the module umbrella header; fall back so this header is +// self-contained if included on its own. +#ifndef MOONBASE_LICENSING_VERSION + #define MOONBASE_LICENSING_VERSION "0.0.0" +#endif + +namespace moonbase::juce_integration { + +struct TrialFeature +{ + juce::String label; + bool included = true; // true -> green check, false -> dimmed cross +}; + +// Merchant-overridable UI copy. Leave any field empty to use the built-in +// default (several interpolate productName / manufacturerName). Override only what +// you want to change, e.g. config.strings.welcomeBody = "...". +struct ActivationStrings +{ + juce::String welcomeTitle; // default: "Activate {productName}" + juce::String welcomeBody; // default: "Unlock the full plugin through your {manufacturerName} account." + juce::String activateOnline; // default: "Activate online" + juce::String activateOffline; // default: "No internet? Activate offline" +}; + +struct ActivationConfig +{ + //== Connection (Moonbase SDK) ============================================= + juce::String endpoint; // e.g. "https://your-tenant.moonbase.sh" + juce::String productId; // e.g. "your-product" + juce::String publicKey; // RSA public key (PEM or base64 DER) + juce::String accountId; // optional issuer pin + juce::String applicationVersion; + + //== Validation / network tuning ========================================== + // How long a license stays valid offline since its last successful online + // validation before it is treated as stale (and the app locks). Default 7 days. + std::chrono::seconds onlineGracePeriod{std::chrono::hours(24 * 7)}; + + // Minimum time between online validation calls. Within this window a + // validation returns the cached license with no network round trip, so this + // is the effective "how often we re-check online" cadence. Default 5 minutes. + // (controller().refreshLicense(force=true) bypasses it.) + std::chrono::seconds onlineCheckInterval{std::chrono::minutes(5)}; + + // HTTP timeouts for activation / validation requests (they run on background + // threads, so these never block the UI). Defaults: 10s connect, 30s request. + std::chrono::milliseconds httpConnectTimeout{std::chrono::seconds(10)}; + std::chrono::milliseconds httpRequestTimeout{std::chrono::seconds(30)}; + + // Where the validated license is persisted. Defaults to a per-user app-data + // file under "//license.mb" when left empty + // (see resolvedLicenseFile()). + juce::File licenseFile; + + //== Branding / UI ========================================================= + juce::String productName; // defaults to JucePlugin_Name (see resolvedProductName()) + juce::String manufacturerName; // defaults to JucePlugin_Manufacturer; shown under the product name + juce::Colour accent = juce::Colour(0xff186cdc); // Moonbase blue + + // Where the customer exchanges their machine file for a license file during + // offline activation. Defaults to "{endpoint}/activate" when left unset. + juce::URL activationUrl; + + bool showMoonbaseBadge = true; + bool enableOffline = true; // show the offline activation flow + bool reduceMotion = false; // skip transition/spinner/pop animation (a11y + snapshot tests) + bool overlayBackdrop = false; // dim the host behind the panel (modal over a plugin) instead of a full opaque backdrop + int trialLengthDays = 14; // trial length shown on the Trial / Expired screens (trials are granted by the backend, not started from the UI) + + // Product / manufacturer brand mark shown in the header lockup. When unset, + // a generated sun mark in the accent colour is drawn. Provide a vector + // Drawable (e.g. juce::Drawable::createFromSVG(...)) for crispness: + // config.logo = std::shared_ptr(juce::Drawable::createFromSVG(*xml)); + std::shared_ptr logo; + + // Optional UI copy overrides (see ActivationStrings). + ActivationStrings strings; + + // Optional feature list shown on the trial screen (included vs. excluded). + std::vector trialFeatures; + + // Optional sink for diagnostic detail (the underlying error behind the + // friendly UI text). Wire it to juce::Logger, a file, or your telemetry to + // debug activation issues in the field. Invoked on the message thread. + std::function onDiagnostic; + + //== Telemetry / analytics ================================================= + // Off by default. Set analytics.enabled = true to attach JUCE system/host + // metadata (OS, CPU, JUCE version, DAW host, plugin format, ...) to every + // activation + validation request. See JuceMetadata.h / AnalyticsOptions. + AnalyticsOptions analytics; + + // Extra metadata sent with every request. Merged first, so these win over + // the auto-collected analytics keys on a collision. + std::map metadata; + + // Last-word hook to add or rewrite metadata programmatically (e.g. a build + // channel, an A/B cohort). Receives the assembled map; runs after the + // static metadata + analytics capture. + std::function&)> onCollectMetadata; + + //== Resolved display names ================================================ + // The product / manufacturer shown throughout the UI and used for the + // default license path. Prefer the explicit config field; otherwise fall + // back to the JUCE plugin macros (so a plugin target "just works" without + // duplicating its name here), then to a neutral default. Route every + // display-name read through these so branding stays consistent everywhere. + [[nodiscard]] juce::String resolvedProductName() const + { + if (productName.isNotEmpty()) + return productName; + #if defined (JucePlugin_Name) + return JucePlugin_Name; + #else + return "Your Plugin"; + #endif + } + [[nodiscard]] juce::String resolvedManufacturerName() const + { + if (manufacturerName.isNotEmpty()) + return manufacturerName; + #if defined (JucePlugin_Manufacturer) + return JucePlugin_Manufacturer; + #else + return {}; + #endif + } + + //== Resolved UI copy (override-or-default) ================================ + [[nodiscard]] juce::String brandAccountName() const + { + const auto manufacturer = resolvedManufacturerName(); + return manufacturer.isNotEmpty() ? manufacturer : juce::String("Moonbase"); + } + [[nodiscard]] juce::String welcomeTitleText() const + { + return strings.welcomeTitle.isNotEmpty() ? strings.welcomeTitle + : juce::String("Activate ") + resolvedProductName(); + } + [[nodiscard]] juce::String welcomeBodyText() const + { + if (strings.welcomeBody.isNotEmpty()) + return strings.welcomeBody; + return "Unlock the full plugin through your " + brandAccountName() + " account."; + } + [[nodiscard]] juce::String activateOnlineText() const + { + return strings.activateOnline.isNotEmpty() ? strings.activateOnline + : juce::String("Activate online"); + } + [[nodiscard]] juce::String activateOfflineText() const + { + return strings.activateOffline.isNotEmpty() ? strings.activateOffline + : juce::String("No internet? Activate offline"); + } + [[nodiscard]] juce::URL activationUrlResolved() const + { + if (activationUrl.toString(false).isNotEmpty()) + return activationUrl; + return juce::URL(endpoint.trimCharactersAtEnd("/") + "/activate"); + } + [[nodiscard]] juce::String activationUrlDisplay() const + { + // Host + path without the scheme, e.g. "your-tenant.moonbase.sh/activate". + return activationUrlResolved().toString(false).fromFirstOccurrenceOf("://", false, false); + } + + //== Helpers =============================================================== + // Returns an empty string when the connection fields are well-formed, or a + // human-readable reason when they're not. The controller surfaces this as an + // Error state rather than constructing the SDK with a broken configuration. + [[nodiscard]] juce::String validate() const + { + if (endpoint.trim().isEmpty()) + return "No activation endpoint configured (set config.endpoint)."; + if (! endpoint.trim().startsWithIgnoreCase("http")) + return "Activation endpoint must be an http(s) URL (set config.endpoint)."; + if (productId.trim().isEmpty()) + return "No product id configured (set config.productId)."; + if (publicKey.trim().isEmpty()) + return "No public key configured (set config.publicKey)."; + return {}; + } + + [[nodiscard]] juce::File resolvedLicenseFile() const + { + if (licenseFile != juce::File()) + return licenseFile; + + const auto resolvedManufacturer = resolvedManufacturerName(); + const auto manufacturer = resolvedManufacturer.isNotEmpty() ? resolvedManufacturer : juce::String("Moonbase"); + const auto product = resolvedProductName(); + return juce::File::getSpecialLocation(juce::File::userApplicationDataDirectory) + .getChildFile(manufacturer) + .getChildFile(product) + .getChildFile("license.mb"); + } + + [[nodiscard]] moonbase::licensing_options toLicensingOptions() const + { + moonbase::licensing_options options; + options.endpoint = endpoint.toStdString(); + options.product_id = productId.toStdString(); + options.public_key = publicKey.toStdString(); + if (accountId.isNotEmpty()) + options.account_id = accountId.toStdString(); + if (applicationVersion.isNotEmpty()) + options.application_version = applicationVersion.toStdString(); + #if defined (JucePlugin_VersionString) + else + options.application_version = JucePlugin_VersionString; // a plugin has no JUCEApplication to read it from + #endif + + // Identify this client as the JUCE module (appended to the base client's + // User-Agent), with the JUCE version + OS for support/analytics. + juce::String clientInfo; + clientInfo << "moonbase-juce/" << MOONBASE_LICENSING_VERSION + << " (" << juce::SystemStats::getJUCEVersion() + << "; " << juce::SystemStats::getOperatingSystemName() << ")"; + options.client_info = clientInfo.toStdString(); + + options.online_validation_grace_period = onlineGracePeriod; + options.online_validation_min_interval = onlineCheckInterval; + options.http_connect_timeout = httpConnectTimeout; + options.http_request_timeout = httpRequestTimeout; + + // Explicit metadata first (so it wins on key collisions), then the + // opt-in JUCE analytics capture, then the caller's last-word hook. + for (const auto& entry : metadata) + options.metadata.emplace(entry.first, entry.second); + if (analytics.enabled) + applyJuceMetadata(options, analytics); + if (onCollectMetadata) + onCollectMetadata(options.metadata); + + return options; + } +}; + +} // namespace moonbase::juce_integration diff --git a/modules/moonbase_licensing/juce/ActivationController.cpp b/modules/moonbase_licensing/juce/ActivationController.cpp new file mode 100644 index 0000000..601e609 --- /dev/null +++ b/modules/moonbase_licensing/juce/ActivationController.cpp @@ -0,0 +1,745 @@ +// Implementation of ActivationController. Compiled as part of the single +// moonbase_licensing module translation unit. + +#include "ActivationController.h" +#include "juce_fingerprint_provider.h" +#include "juce_http_transport.h" + +namespace moonbase::juce_integration { + +namespace { +constexpr int kPollIntervalMs = 1500; +} // namespace + +ActivationController::ActivationController(ActivationConfig config) + : config_(std::move(config)) +{ + // Fail loud, not late: a missing/malformed endpoint, product id or public + // key becomes a clear Error state instead of a confusing "license invalid" + // (or a throw out of the consumer's component constructor) further down. + configError_ = config_.validate(); + if (configError_.isNotEmpty()) + { + screen_ = Screen::Error; + statusMessage_ = configError_; + return; // licensing_ stays null; entry points guard via ensureReady() + } + + const auto file = config_.resolvedLicenseFile(); + file.getParentDirectory().createDirectory(); + + auto store = std::make_shared( + std::filesystem::path(file.getFullPathName().toStdString())); + auto fingerprint = std::make_shared(); + auto transport = std::make_shared(); + + try + { + licensing_ = std::make_shared( + config_.toLicensingOptions(), std::move(store), fingerprint, transport); + } + catch (const std::exception& ex) + { + // The SDK parses the public key on construction; surface a bad key here. + configError_ = juce::String("Invalid activation configuration: ") + ex.what(); + screen_ = Screen::Error; + statusMessage_ = configError_; + return; + } + + cancelInFlight_ = [transport] { transport->cancel(); }; + setDeviceLabel(juce::String(fingerprint->device_name())); +} + +ActivationController::ActivationController(ActivationConfig config, + std::shared_ptr licensing, + juce::String deviceName, + std::function cancelInFlight) + : config_(std::move(config)), licensing_(std::move(licensing)), + cancelInFlight_(std::move(cancelInFlight)) +{ + jassert(licensing_ != nullptr); + setDeviceLabel(std::move(deviceName)); +} + +ActivationController::~ActivationController() +{ + stopTimer(); + // Unblock any in-flight request, then wait for the workers to finish, so no + // detached thread keeps running module code after we (and possibly the + // plugin binary) are gone. cancelInFlight_ makes the drain near-instant. + if (cancelInFlight_) + cancelInFlight_(); + threadPool_.removeAllJobs(true, 5000); +} + +void ActivationController::setDeviceLabel(juce::String deviceName) +{ + deviceLabel_ = deviceName.trim(); + if (deviceLabel_.isEmpty()) + deviceLabel_ = "This device"; + deviceLabel_ << juce::String::fromUTF8(" \xc2\xb7 ") << shortPlatformName(); // middle dot +} + +void ActivationController::emitDiagnostic(const juce::String& message) +{ + // Always visible in debug builds; routed to the host's sink when provided. + // Call only on the message thread (the config callback expects that). + DBG("[moonbase] " << message); + if (config_.onDiagnostic) + config_.onDiagnostic(message); +} + +bool ActivationController::ensureReady() +{ + if (licensing_ != nullptr) + return true; + + const auto reason = configError_.isNotEmpty() ? configError_ + : juce::String("Activation is not configured."); + emitDiagnostic("Operation ignored: " + reason); + setScreen(Screen::Error, reason); + return false; +} + +//============================================================================== +void ActivationController::start() +{ + if (! ensureReady()) + return; + + setScreen(Screen::Loading); + + const auto generation = ++generation_; + juce::WeakReference safe(this); + auto licensing = licensing_; + + threadPool_.addJob([safe, generation, licensing]() mutable + { + std::optional result; + std::optional expiredTrial; + bool deleteExpiredOffline = false; + juce::String diag; + try + { + auto stored = licensing->store().load_local_license(); + if (stored) + { + // Peek without throwing on a past `exp` so we can tell an expired + // offline license (permanently dead, never refreshable) apart + // from an online token (refreshable within its grace period) and + // from an untrusted token (bad signature/device -> leave it). + std::optional peek; + try + { + peek = licensing->validate_token_local_allow_expired(stored->token); + } + catch (const std::exception& ex) + { + // Tampered / foreign / unparseable -> locked, but left on disk. + diag = juce::String("Stored token rejected (not valid for this device): ") + ex.what(); + } + + if (peek && peek->method == moonbase::activation_method::offline) + { + const bool expired = peek->expires_at + && *peek->expires_at < std::chrono::system_clock::now(); + if (expired) + { + deleteExpiredOffline = true; // remove the dead file below + diag = "Stored offline license has expired; removing it."; + } + else + result = std::move(peek); + } + else if (peek) + { + try + { + // Validates the local token first (throws immediately if + // already expired, with no network call) and otherwise + // re-checks against the server when past the throttle. + result = licensing->validate_token_online(stored->token); + } + catch (const moonbase::license_expired_error& ex) + { + // Expired either locally or per the server's response. A + // trial routes to the Expired screen (using the token we + // have for display); the plugin stays locked either way. + diag = juce::String("Stored license has expired: ") + ex.what(); + if (peek->trial) + expiredTrial = std::move(peek); + else + result = std::nullopt; + } + catch (const std::exception& ex) + { + // Invalid / unreachable-past-grace -> locked. + diag = juce::String("Re-validating stored license failed: ") + ex.what(); + result = std::nullopt; + } + } + } + } + catch (const std::exception& ex) + { + diag = juce::String("Loading stored license failed: ") + ex.what(); + result = std::nullopt; + } + + juce::MessageManager::callAsync([safe, generation, result, expiredTrial, deleteExpiredOffline, diag]() mutable + { + auto* self = safe.get(); + if (self == nullptr || generation != self->generation_.load()) + return; + if (diag.isNotEmpty()) + self->emitDiagnostic(diag); + if (deleteExpiredOffline) + self->deleteStoredLicense(); + if (expiredTrial) + self->showTrialExpired(std::move(*expiredTrial)); + else + self->applyLicense(std::move(result)); + }); + }); +} + +//============================================================================== +void ActivationController::beginOnlineActivation() +{ + if (! ensureReady()) + return; + + setScreen(Screen::BrowserWait); + + const auto generation = ++generation_; + juce::WeakReference safe(this); + auto licensing = licensing_; + + threadPool_.addJob([safe, generation, licensing]() mutable + { + std::optional request; + juce::String error; + try + { + request = licensing->request_activation(); + } + catch (const std::exception& ex) + { + error = ex.what(); + } + + juce::MessageManager::callAsync([safe, generation, request, error]() mutable + { + auto* self = safe.get(); + if (self == nullptr || generation != self->generation_.load()) + return; + + if (! request) + { + self->emitDiagnostic("request_activation failed: " + error); + self->setScreen(Screen::Error, + "Couldn't reach Moonbase to start activation. " + error); + return; + } + + self->pendingRequest_ = request; + juce::URL(juce::String(request->browser_url)).launchInDefaultBrowser(); + self->startTimer(kPollIntervalMs); + }); + }); +} + +void ActivationController::cancelActivation() +{ + stopTimer(); + pendingRequest_.reset(); + ++generation_; + pollInFlight_ = false; + showWelcome(); +} + +void ActivationController::refreshLicense(bool force, std::function onComplete) +{ + if (! ensureReady()) + { + if (onComplete) onComplete(false); + return; + } + + // Nothing to refresh, or an offline license (permanent + server-untracked). + if (! license_ || license_->method == moonbase::activation_method::offline) + { + if (license_ && license_->method == moonbase::activation_method::offline) + emitDiagnostic("refreshLicense: offline licenses are not re-validated online."); + if (onComplete) onComplete(false); + return; + } + + const auto generation = ++generation_; + const auto token = license_->token; + const auto currentLicense = *license_; // for the expired-trial case (re-validation can't return it) + const bool wasTrial = license_->trial; + juce::WeakReference safe(this); + auto licensing = licensing_; + + threadPool_.addJob([safe, generation, token, currentLicense, wasTrial, licensing, force, onComplete]() mutable + { + std::optional refreshed; + bool expired = false; + juce::String diag; + try + { + if (force) + { + // Bypass the min-interval throttle by hitting the API directly. + refreshed = licensing->client().validate_token_online(token); + } + else + { + // Throttled + grace-period aware (skips the network if recent). + // Suppress the SDK's own background-thread persist; we persist on + // the message thread below so a stale write can't resurrect a + // license the user cleared while this refresh was in flight. + refreshed = licensing->validate_token_online(token, [] { return false; }); + } + } + catch (const moonbase::license_expired_error& ex) + { + // Re-validation says it has ended (e.g. a trial that was still valid + // locally). Distinct from a network blip: this should lock. + expired = true; + diag = ex.what(); + } + catch (const std::exception& ex) + { + diag = ex.what(); + } + + juce::MessageManager::callAsync([safe, generation, refreshed, expired, currentLicense, wasTrial, + licensing, diag, onComplete]() mutable + { + auto* self = safe.get(); + if (self == nullptr || generation != self->generation_.load()) + { + // Superseded (e.g. deactivate / clearLicense bumped the + // generation). Drop the result and, crucially, do not persist. + if (onComplete) onComplete(false); + return; + } + + if (refreshed) + { + // Persist here on the message thread: serialized against + // deactivate()/clearLicense() and gated by the generation check + // above, so a stale refresh cannot recreate a cleared license. + try + { + auto guard = licensing->store().lock_for_update(); + licensing->store().store_local_license(*refreshed); + } + catch (const moonbase::storage_error& ex) + { + self->emitDiagnostic(juce::String("Refreshed, but couldn't persist the license: ") + ex.what()); + } + + // Updates license_, re-routes the screen if needed, and notifies + // listeners (onActivationChanged) so the host can reload features. + self->applyLicense(std::move(refreshed)); + if (onComplete) onComplete(true); + } + else if (expired && wasTrial) + { + // The trial ended per the server: lock and show the Expired + // screen (using the trial we held, since the throw returns none). + self->emitDiagnostic("Trial ended on re-validation: " + diag); + self->showTrialExpired(currentLicense); + if (onComplete) onComplete(false); + } + else + { + // Keep the current license on other failures (a network blip must + // not lock the user out); just report the underlying reason. + self->emitDiagnostic("Online re-validation failed: " + diag); + if (onComplete) onComplete(false); + } + }); + }); +} + +void ActivationController::timerCallback() +{ + if (pollInFlight_ || ! pendingRequest_) + return; + + pollInFlight_ = true; + const auto generation = generation_.load(); + const auto request = *pendingRequest_; + juce::WeakReference safe(this); + auto licensing = licensing_; + + threadPool_.addJob([safe, generation, request, licensing]() mutable + { + std::optional fulfilled; + bool fatal = false; + juce::String error; + juce::String transient; + try + { + fulfilled = licensing->get_requested_activation(request); + } + catch (const moonbase::license_invalid_error& ex) { fatal = true; error = ex.what(); } + catch (const moonbase::license_expired_error& ex) { fatal = true; error = ex.what(); } + catch (const std::exception& ex) + { + // Transient transport/5xx error — keep polling. + transient = ex.what(); + } + + juce::MessageManager::callAsync([safe, generation, fulfilled, fatal, error, transient]() mutable + { + auto* self = safe.get(); + if (self == nullptr) + return; + self->pollInFlight_ = false; + if (generation != self->generation_.load()) + return; // cancelled or superseded + + if (fatal) + { + self->emitDiagnostic("Activation rejected during polling: " + error); + self->stopTimer(); + self->pendingRequest_.reset(); + self->setScreen(Screen::Error, "Activation was rejected. " + error); + } + else if (fulfilled) + { + self->onActivationFulfilled(std::move(*fulfilled)); + } + else if (transient.isNotEmpty()) + { + self->emitDiagnostic("Activation poll transient error (still waiting): " + transient); + } + }); + }); +} + +void ActivationController::onActivationFulfilled(moonbase::license value) +{ + stopTimer(); + pendingRequest_.reset(); + ++generation_; + + try + { + auto guard = licensing_->store().lock_for_update(); + licensing_->store().store_local_license(value); + } + catch (const moonbase::storage_error& ex) + { + // Activated but couldn't persist; still unlock for this session. + emitDiagnostic(juce::String("Activated, but couldn't persist the license: ") + ex.what()); + } + + setLicense(std::move(value)); + setScreen(Screen::Success); +} + +//============================================================================== +bool ActivationController::saveOfflineRequest(const juce::File& destination) +{ + if (! ensureReady()) + return false; + + try + { + const auto token = licensing_->generate_device_token(); + if (destination.replaceWithText(juce::String(token))) + { + offlineRequestSaved_ = true; + offlineError_.clear(); + sendChangeMessage(); + return true; + } + emitDiagnostic("Couldn't write the machine file to " + destination.getFullPathName()); + } + catch (const std::exception& ex) + { + emitDiagnostic(juce::String("Generating the machine file failed: ") + ex.what()); + } + + offlineError_ = "Couldn't write the request file."; + sendChangeMessage(); + return false; +} + +void ActivationController::setOfflineResponse(const juce::File& responseFile) +{ + offlineResponse_ = responseFile; + offlineError_.clear(); + sendChangeMessage(); +} + +void ActivationController::activateOffline() +{ + if (! ensureReady()) + return; + + if (offlineResponse_ == juce::File()) + { + offlineError_ = "Add the response file from moonbase.sh to continue."; + setScreen(Screen::Offline); + return; + } + + const auto contents = offlineResponse_.loadFileAsString(); + ++generation_; + + try + { + auto licenseValue = licensing_->read_offline_license(contents.trim().toStdString()); + try + { + auto guard = licensing_->store().lock_for_update(); + licensing_->store().store_local_license(licenseValue); + } + catch (const moonbase::storage_error& ex) + { + emitDiagnostic(juce::String("Activated offline, but couldn't persist the license: ") + ex.what()); + } + + setLicense(std::move(licenseValue)); + offlineError_.clear(); + setScreen(Screen::Success); + } + catch (const std::exception& ex) + { + emitDiagnostic(juce::String("Offline license file rejected: ") + ex.what()); + offlineError_ = "That response file isn't valid for this device."; + setScreen(Screen::Offline); + } +} + +//============================================================================== +void ActivationController::deactivate() +{ + if (! ensureReady()) + return; + + if (! license_) + { + showWelcome(); + return; + } + + // Offline / trial licenses can't be revoked server-side — local forget. + if (license_->method == moonbase::activation_method::offline || license_->trial) + { + clearLicense(); + return; + } + + busy_ = true; + statusMessage_.clear(); // progress is shown by the inline spinner in the deactivate button + sendChangeMessage(); + + const auto generation = ++generation_; + const auto token = license_->token; + const auto activationId = juce::String(license_->activation_id); + juce::WeakReference safe(this); + auto licensing = licensing_; + + threadPool_.addJob([safe, generation, token, activationId, licensing]() mutable + { + enum class Outcome { Revoked, NotRevokable, Unreachable }; + Outcome outcome = Outcome::Revoked; + juce::String diag; + try + { + licensing->revoke_activation(token); + } + catch (const moonbase::operation_not_supported_error& ex) { outcome = Outcome::NotRevokable; diag = ex.what(); } + catch (const moonbase::license_invalid_error&) { outcome = Outcome::Revoked; } + catch (const moonbase::license_expired_error&) { outcome = Outcome::Revoked; } + catch (const std::exception& ex) { outcome = Outcome::Unreachable; diag = ex.what(); } + + const int outcomeCode = static_cast(outcome); + juce::MessageManager::callAsync([safe, generation, outcomeCode, activationId, diag]() mutable + { + auto* self = safe.get(); + if (self == nullptr || generation != self->generation_.load()) + return; + + self->busy_ = false; + switch (static_cast(outcomeCode)) + { + case Outcome::Revoked: + self->deleteStoredMatching(activationId); + self->applyLicense(std::nullopt); + break; + case Outcome::NotRevokable: + self->clearLicense(); + break; + case Outcome::Unreachable: + self->emitDiagnostic("revoke_activation couldn't reach Moonbase: " + diag); + self->setScreen(Screen::Details, + "Couldn't reach Moonbase to deactivate. Try again when online."); + break; + } + }); + }); +} + +void ActivationController::clearLicense() +{ + ++generation_; + deleteStoredLicense(); + applyLicense(std::nullopt); +} + +void ActivationController::deleteStoredLicense() +{ + try + { + auto guard = licensing_->store().lock_for_update(); + licensing_->store().delete_local_license(); + } + catch (const moonbase::storage_error&) + { + } +} + +void ActivationController::deleteStoredMatching(const juce::String& activationId) +{ + try + { + auto guard = licensing_->store().lock_for_update(); + if (auto stored = licensing_->store().load_local_license(); + stored && juce::String(stored->activation_id) == activationId) + { + licensing_->store().delete_local_license(); + } + } + catch (const moonbase::storage_error&) + { + } +} + +//============================================================================== +void ActivationController::showWelcome() +{ + offlineResponse_ = juce::File(); + offlineRequestSaved_ = false; + offlineError_.clear(); + setScreen(Screen::Welcome); +} + +void ActivationController::showOffline() +{ + offlineError_.clear(); + setScreen(Screen::Offline); +} + +void ActivationController::showDetails() +{ + // For a trial license "details" is the trial view; keep them consistent. + setScreen(screenForCurrentLicense()); +} + +void ActivationController::setPreviewState(Screen screen, std::optional license, + juce::String previewError, bool busy) +{ + ++generation_; // drop any in-flight start()/async continuation + stopTimer(); + pollInFlight_ = false; + busy_ = busy; + if (screen == Screen::Expired) + { + // The Expired screen is locked: keep the license out of license_ so + // gating stays off; the passed license backs the view's display. + expiredTrial_ = std::move(license); + setLicense(std::nullopt); + } + else + { + setLicense(std::move(license)); + expiredTrial_.reset(); + } + offlineError_ = previewError; + screen_ = screen; + statusMessage_.clear(); + // Synchronous so a snapshot harness sees the new screen immediately without + // pumping the message loop. Must be called on the message thread. + sendSynchronousChangeMessage(); +} + +//============================================================================== +int ActivationController::trialDaysRemaining() const +{ + if (! license_ || ! license_->expires_at) + return 0; + + const auto seconds = std::chrono::duration_cast( + *license_->expires_at - std::chrono::system_clock::now()) + .count(); + if (seconds <= 0) + return 0; + return static_cast((seconds + 86399) / 86400); // ceil to whole days +} + +//============================================================================== +void ActivationController::setScreen(Screen newScreen, const juce::String& message) +{ + screen_ = newScreen; + statusMessage_ = message; + sendChangeMessage(); +} + +void ActivationController::setLicense(std::optional value) +{ + license_ = std::move(value); + // Publish for the audio thread (this always runs on the message thread). + licensed_.store(license_.has_value(), std::memory_order_release); +} + +ActivationController::Screen ActivationController::screenForCurrentLicense() const +{ + if (! license_) + return Screen::Welcome; + // A backend-granted trial license shows the trial view (days left / unlock); + // a full license shows the details view. + return license_->trial ? Screen::Trial : Screen::Details; +} + +void ActivationController::applyLicense(std::optional value) +{ + setLicense(std::move(value)); + expiredTrial_.reset(); + statusMessage_.clear(); + setScreen(screenForCurrentLicense()); +} + +void ActivationController::showTrialExpired(moonbase::license expired) +{ + // The plugin must stay locked, so license_ stays empty; the ended trial is + // held separately for the Expired screen to show the product + end date. + expiredTrial_ = std::move(expired); + setLicense(std::nullopt); + statusMessage_.clear(); + setScreen(Screen::Expired); +} + +juce::String ActivationController::shortPlatformName() +{ +#if JUCE_MAC + return "macOS"; +#elif JUCE_WINDOWS + return "Windows"; +#elif JUCE_LINUX + return "Linux"; +#else + return juce::SystemStats::getOperatingSystemName(); +#endif +} + +} // namespace moonbase::juce_integration diff --git a/modules/moonbase_licensing/juce/ActivationController.h b/modules/moonbase_licensing/juce/ActivationController.h new file mode 100644 index 0000000..eb73717 --- /dev/null +++ b/modules/moonbase_licensing/juce/ActivationController.h @@ -0,0 +1,170 @@ +#pragma once + +// Headless activation state machine that drives the built-in UI by talking to +// the moonbase::licensing API directly (no juce::OnlineUnlockStatus). Network +// calls run on background threads; all state mutation + change notifications +// happen on the message thread, gated by a generation counter so a slow request +// can never clobber a newer state (cancel, a fresh activation, deactivate). +// +// Observe it as a juce::ChangeBroadcaster: on every change, read screen() and +// license() and repaint. + +#include +#include +#include +#include +#include + +#include + +#include + +#include "ActivationConfig.h" + +namespace moonbase::juce_integration { + +class ActivationController : private juce::Timer, + public juce::ChangeBroadcaster +{ +public: + enum class Screen + { + Loading, // validating any stored license at startup + Welcome, // not activated — choose online / trial / offline + BrowserWait, // browser activation in progress, polling + Success, // just activated + Offline, // offline request/response file flow + Trial, // a valid trial license is loaded + Expired, // a trial license that has ended (plugin locked; see expiredTrial()) + Details, // a valid full license is loaded + Error // an operation failed; statusMessage() has detail + }; + + explicit ActivationController(ActivationConfig config); + + // Test seam: drive the state machine against an injected licensing built + // with fake store / transport / fingerprint. The primary constructor builds + // the real dependencies from the config; this one takes them ready-made. + // cancelInFlight (optional) is invoked on teardown to interrupt a blocking + // request so workers can't outlive the controller (the real path wires it to + // the HTTP transport's cancel()). + ActivationController(ActivationConfig config, + std::shared_ptr licensing, + juce::String deviceName = "Test Device", + std::function cancelInFlight = {}); + + ~ActivationController() override; + + //== Lifecycle ============================================================= + // Loads + validates any stored license and routes to the right screen. + void start(); + + //== Online activation ===================================================== + void beginOnlineActivation(); // request + open browser + poll + void cancelActivation(); // stop polling, back to Welcome + + //== Re-validation ========================================================= + // Re-check the current license against the server and refresh its entitlements + // (sub-product ownership, properties, expiry, seats) in place. Call this after + // a purchase so newly granted features load without an app restart. Runs async + // and silently (no screen change); on success the license updates and + // onActivationChanged fires so you can reload features. `force` bypasses the + // online-validation throttle (use it right after a purchase). No-op for offline + // licenses. The optional callback runs on the message thread with the outcome. + void refreshLicense(bool force = true, std::function onComplete = {}); + + //== Offline activation ==================================================== + bool saveOfflineRequest(const juce::File& destination); // writes the device token + void setOfflineResponse(const juce::File& responseFile); + [[nodiscard]] bool hasOfflineResponse() const noexcept { return offlineResponse_ != juce::File(); } + [[nodiscard]] juce::String offlineResponseName() const { return offlineResponse_.getFileName(); } + void activateOffline(); // validate response locally + persist + + //== Deactivate / forget =================================================== + void deactivate(); // server-side revoke (async); local forget fallback + void clearLicense(); // local-only forget + + //== Navigation (screens not driven purely by license state) =============== + void showWelcome(); + void showOffline(); + void showDetails(); + + // Force a screen (with an optional synthetic license / offline error) with no + // network and no stored state. For previews, design iteration and snapshot + // tests — not part of the normal activation flow. + void setPreviewState(Screen screen, + std::optional license = std::nullopt, + juce::String previewError = {}, + bool busy = false); + + //== Accessors ============================================================= + [[nodiscard]] Screen screen() const noexcept { return screen_; } + [[nodiscard]] const std::optional& license() const noexcept { return license_; } + // Audio-thread-safe "is there a valid license" flag, updated on the message + // thread whenever the license changes. Read it from processBlock for gating + // without a ChangeListener: `if (! controller.licensedFlag().load()) ...`. + [[nodiscard]] const std::atomic& licensedFlag() const noexcept { return licensed_; } + // The ended trial backing the Expired screen. license() stays empty in this + // state (the plugin is locked); this is for display only. + [[nodiscard]] const std::optional& expiredTrial() const noexcept { return expiredTrial_; } + [[nodiscard]] juce::String statusMessage() const { return statusMessage_; } + [[nodiscard]] juce::String offlineError() const { return offlineError_; } + [[nodiscard]] juce::String deviceLabel() const { return deviceLabel_; } + [[nodiscard]] const ActivationConfig& config() const noexcept { return config_; } + [[nodiscard]] bool isBusy() const noexcept { return busy_; } + [[nodiscard]] bool offlineRequestSaved() const noexcept { return offlineRequestSaved_; } + + // Trial display helpers (valid only when a trial license is loaded). + [[nodiscard]] int trialDaysRemaining() const; + +private: + void timerCallback() override; + + void setScreen(Screen newScreen, const juce::String& message = {}); + void setLicense(std::optional value); // updates license_ + licensedFlag() + void applyLicense(std::optional value); + void showTrialExpired(moonbase::license expired); // locks + routes to the Expired screen + [[nodiscard]] Screen screenForCurrentLicense() const; // Welcome / Trial / Details + void onActivationFulfilled(moonbase::license value); + void deleteStoredMatching(const juce::String& activationId); + void deleteStoredLicense(); // best-effort delete of the local license file + void setDeviceLabel(juce::String deviceName); + void emitDiagnostic(const juce::String& message); // message-thread only + bool ensureReady(); // false (+ Error screen) when misconfigured / not built + + static juce::String shortPlatformName(); + + ActivationConfig config_; + std::shared_ptr licensing_; + + // Network/file work runs here instead of detached threads, so the destructor + // can drain workers (after cancelInFlight_ unblocks any in-flight request); + // nothing outlives the controller. + std::function cancelInFlight_; + juce::ThreadPool threadPool_ { 2 }; + + Screen screen_ = Screen::Loading; + std::optional license_; + std::atomic licensed_{false}; // mirror of license_.has_value() for the audio thread + std::optional expiredTrial_; // display-only backing for the Expired screen + std::optional pendingRequest_; + + juce::String statusMessage_; + juce::String offlineError_; + juce::String deviceLabel_; + juce::String configError_; // non-empty when the config failed validation/build + juce::File offlineResponse_; + bool offlineRequestSaved_ = false; + + bool busy_ = false; + bool pollInFlight_ = false; + + // Bumped by every state-changing entry point; async continuations capture it + // and no-op if it has moved on by the time they run on the message thread. + std::atomic generation_{0}; + + JUCE_DECLARE_WEAK_REFERENCEABLE(ActivationController) + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(ActivationController) +}; + +} // namespace moonbase::juce_integration diff --git a/modules/moonbase_licensing/juce/JuceMetadata.h b/modules/moonbase_licensing/juce/JuceMetadata.h new file mode 100644 index 0000000..c0eed05 --- /dev/null +++ b/modules/moonbase_licensing/juce/JuceMetadata.h @@ -0,0 +1,129 @@ +#pragma once + +// Optional analytics / telemetry capture for the native module, mirroring the +// reference bridge's applyJuceMetadata(). When enabled (config.analytics.enabled), +// the controller fills moonbase::licensing_options::metadata from juce::SystemStats +// (OS, CPU, JUCE version, memory), the DAW host + plugin format (when +// juce_audio_processors is in the build), and optionally locale + app version. +// The map is sent with activation and validation requests. + +#include +#include + +#include + +#include +#include + +// Host/plugin metadata needs juce_audio_processors. Gate on JUCE's own +// "is this module linked" macro, not __has_include: JUCE puts every fetched +// module's headers on the include path, so the header is present even when the +// module isn't linked, and referencing juce::PluginHostType would then fail to +// link. A consumer can still force it with MOONBASE_JUCE_HAS_AUDIO_PROCESSORS=1. +#if !defined(MOONBASE_JUCE_HAS_AUDIO_PROCESSORS) + #if defined(JUCE_MODULE_AVAILABLE_juce_audio_processors) && JUCE_MODULE_AVAILABLE_juce_audio_processors + #define MOONBASE_JUCE_HAS_AUDIO_PROCESSORS 1 + #else + #define MOONBASE_JUCE_HAS_AUDIO_PROCESSORS 0 + #endif +#endif + +#if MOONBASE_JUCE_HAS_AUDIO_PROCESSORS + #include +#endif + +namespace moonbase::juce_integration { + +// What to capture. Toggle the whole thing with `enabled`; the rest narrows it. +struct AnalyticsOptions +{ + bool enabled = false; // master switch (opt in) + bool includeSystemInfo = true; // OS, CPU, JUCE version, memory + bool includeHostInfo = true; // DAW host + plugin format (needs juce_audio_processors) + bool includeLocaleInfo = false; // language / region — opt in + bool includeAppVersion = true; // also fills application_version when unset +}; + +namespace detail { + +inline void putIfAbsent(std::map& map, + std::string key, + std::string value) +{ + if (value.empty()) + return; + map.emplace(std::move(key), std::move(value)); +} + +#if MOONBASE_JUCE_HAS_AUDIO_PROCESSORS +inline std::string pluginFormatTag(juce::AudioProcessor::WrapperType wrapper) +{ + switch (wrapper) + { + case juce::AudioProcessor::wrapperType_VST: return "VST"; + case juce::AudioProcessor::wrapperType_VST3: return "VST3"; + case juce::AudioProcessor::wrapperType_AudioUnit: return "AU"; + case juce::AudioProcessor::wrapperType_AudioUnitv3: return "AUv3"; + case juce::AudioProcessor::wrapperType_AAX: return "AAX"; + case juce::AudioProcessor::wrapperType_Standalone: return "Standalone"; + case juce::AudioProcessor::wrapperType_LV2: return "LV2"; + case juce::AudioProcessor::wrapperType_Unity: return "Unity"; + case juce::AudioProcessor::wrapperType_Undefined: + default: return "Unknown"; + } +} +#endif + +} // namespace detail + +// Fills options.metadata (and, optionally, application_version) from JUCE. Keys +// are namespaced "juce.*"; existing keys are never overwritten, so anything you +// set explicitly in config.metadata wins. +inline void applyJuceMetadata(moonbase::licensing_options& options, const AnalyticsOptions& opts) +{ + auto& meta = options.metadata; + + if (opts.includeSystemInfo) + { + detail::putIfAbsent(meta, "juce.version", juce::SystemStats::getJUCEVersion().toStdString()); + detail::putIfAbsent(meta, "juce.os", juce::SystemStats::getOperatingSystemName().toStdString()); + meta.emplace("juce.os.is64Bit", juce::SystemStats::isOperatingSystem64Bit() ? "true" : "false"); + detail::putIfAbsent(meta, "juce.cpu.model", juce::SystemStats::getCpuModel().trim().toStdString()); + detail::putIfAbsent(meta, "juce.cpu.vendor", juce::SystemStats::getCpuVendor().trim().toStdString()); + meta.emplace("juce.cpu.cores", std::to_string(juce::SystemStats::getNumPhysicalCpus())); + meta.emplace("juce.cpu.threads", std::to_string(juce::SystemStats::getNumCpus())); + meta.emplace("juce.memoryMb", std::to_string(juce::SystemStats::getMemorySizeInMegabytes())); + } + +#if MOONBASE_JUCE_HAS_AUDIO_PROCESSORS + if (opts.includeHostInfo) + { + const juce::PluginHostType host; + detail::putIfAbsent(meta, "juce.host.description", juce::String(host.getHostDescription()).toStdString()); + meta.emplace("juce.host.format", detail::pluginFormatTag(juce::PluginHostType::getPluginLoadedAs())); + } +#else + (void) opts.includeHostInfo; +#endif + + if (opts.includeLocaleInfo) + { + detail::putIfAbsent(meta, "juce.locale.display", juce::SystemStats::getDisplayLanguage().toStdString()); + detail::putIfAbsent(meta, "juce.locale.user", juce::SystemStats::getUserLanguage().toStdString()); + detail::putIfAbsent(meta, "juce.locale.region", juce::SystemStats::getUserRegion().toStdString()); + } + + if (auto* app = juce::JUCEApplicationBase::getInstance()) + { + detail::putIfAbsent(meta, "juce.app.name", app->getApplicationName().toStdString()); + + if (opts.includeAppVersion && ! options.application_version) + { + const auto version = app->getApplicationVersion().toStdString(); + if (! version.empty()) + options.application_version = version; + } + } +} + +} // namespace moonbase::juce_integration diff --git a/modules/moonbase_licensing/juce/LicenseGate.h b/modules/moonbase_licensing/juce/LicenseGate.h new file mode 100644 index 0000000..f934a19 --- /dev/null +++ b/modules/moonbase_licensing/juce/LicenseGate.h @@ -0,0 +1,64 @@ +#pragma once + +// Click-free audio gate for license enforcement. Hold one in your processor and +// call process() from processBlock, passing the licensed flag (e.g. +// controller.licensedFlag().load()). It ramps the buffer to silence when +// unlicensed (and back up when licensed) over a short fade, so activating or +// deactivating mid-stream doesn't pop. Pure float math: needs no audio modules, +// and keeps gating in your hands (the module never silences audio itself). +// +// gate.prepare (sampleRate); +// gate.reset (controller.licensedFlag().load()); // optional: avoid a first-block fade +// ... +// gate.process (buffer.getArrayOfWritePointers(), buffer.getNumChannels(), +// buffer.getNumSamples(), controller.licensedFlag().load()); + +#include + +namespace moonbase::juce_integration { + +class LicenseGate +{ +public: + // Call from prepareToPlay. fadeMs is the activate/deactivate ramp length. + void prepare(double sampleRate, double fadeMs = 8.0) noexcept + { + const auto fadeSamples = juce::jmax(1.0, sampleRate * (fadeMs / 1000.0)); + step_ = (float) (1.0 / fadeSamples); + } + + // Snap fully open/closed with no ramp (e.g. from prepareToPlay, once the + // initial license state is known, to skip a fade-in on the first block). + void reset(bool licensed) noexcept { gain_ = licensed ? 1.0f : 0.0f; } + + // Apply gating in place on the audio thread. Fully licensed: a no-op. Fully + // unlicensed: the buffer is cleared. In between: a per-sample ramp. + void process(float* const* channels, int numChannels, int numSamples, bool licensed) noexcept + { + const float target = licensed ? 1.0f : 0.0f; + + if (juce::exactlyEqual(gain_, target)) + { + if (juce::exactlyEqual(target, 0.0f)) + for (int ch = 0; ch < numChannels; ++ch) + juce::zeromem(channels[ch], sizeof(float) * (size_t) numSamples); + return; // target == 1: pass through untouched + } + + for (int n = 0; n < numSamples; ++n) + { + gain_ = gain_ < target ? juce::jmin(target, gain_ + step_) + : juce::jmax(target, gain_ - step_); + for (int ch = 0; ch < numChannels; ++ch) + channels[ch][n] *= gain_; + } + } + + [[nodiscard]] float currentGain() const noexcept { return gain_; } + +private: + float gain_ = 0.0f; // starts closed: fail safe until the license is confirmed + float step_ = 0.01f; +}; + +} // namespace moonbase::juce_integration diff --git a/modules/moonbase_licensing/juce/juce_fingerprint_provider.h b/modules/moonbase_licensing/juce/juce_fingerprint_provider.h new file mode 100644 index 0000000..a505227 --- /dev/null +++ b/modules/moonbase_licensing/juce/juce_fingerprint_provider.h @@ -0,0 +1,31 @@ +#pragma once + +// Device fingerprint sourced from juce::SystemStats::getUniqueDeviceID(), which +// JUCE derives from stable hardware identifiers (JUCE 7+). Used by default so +// the module never shells out to ioreg/dmidecode from inside a sandboxed plugin +// host. Pick one provider when you ship and keep it — changing it changes the +// device id Moonbase sees and invalidates existing activations. + +#include + +#include + +#include + +namespace moonbase::juce_integration { + +class juce_fingerprint_provider : public moonbase::fingerprint_provider +{ +public: + [[nodiscard]] std::string device_name() const override + { + return juce::SystemStats::getComputerName().toStdString(); + } + + [[nodiscard]] std::string device_id() const override + { + return juce::SystemStats::getUniqueDeviceID().toStdString(); + } +}; + +} // namespace moonbase::juce_integration diff --git a/modules/moonbase_licensing/juce/juce_http_transport.h b/modules/moonbase_licensing/juce/juce_http_transport.h new file mode 100644 index 0000000..5e34457 --- /dev/null +++ b/modules/moonbase_licensing/juce/juce_http_transport.h @@ -0,0 +1,112 @@ +#pragma once + +// HTTP transport for the Moonbase SDK built on juce::WebInputStream, so the +// module needs no CURL. Build JUCE with JUCE_USE_CURL=0 to keep CURL out +// entirely (the transport uses the platform stack: WinHTTP / NSURLSession / +// libcurl-free POSIX backends). + +#include +#include + +#include + +#include + +namespace moonbase::juce_integration { + +class juce_http_transport : public moonbase::http_transport +{ +public: + [[nodiscard]] moonbase::http_response send(const moonbase::http_request& request) override + { + const bool isPost = (request.method == "POST"); + + juce::URL url(juce::String(request.url)); + if (isPost) + url = url.withPOSTData(juce::String::fromUTF8(request.body.data(), + static_cast(request.body.size()))); + + juce::String extraHeaders; + for (const auto& [key, value] : request.headers) + extraHeaders << juce::String(key) << ": " << juce::String(value) << "\r\n"; + + // juce::WebInputStream exposes a single connection timeout; the SDK's + // separate request timeout has no equivalent here. + auto timeout = request.connect_timeout.count() > 0 + ? request.connect_timeout + : request.request_timeout; + const int timeoutMs = timeout.count() > 0 ? static_cast(timeout.count()) : 30000; + + // addParametersToRequestBody = false is the WebInputStream equivalent of + // ParameterHandling::inAddress: the SDK bakes its query string + // (?format=JWT&platform=…) into the URL and the POST body must stay + // exactly our payload. With `true` JUCE would form-encode those params + // into the body, corrupting the JSON/token and triggering server 400s. + // + // We construct the WebInputStream ourselves (rather than + // URL::createInputStream) so a torn-down controller can cancel() a + // blocking read instead of leaving a detached worker running module code + // after the plugin is destroyed/unloaded (scanning, pluginval). + auto stream = std::make_unique(url, /*addParametersToRequestBody*/ false); + stream->withExtraHeaders(extraHeaders); + stream->withConnectionTimeout(timeoutMs); + + { + const juce::ScopedLock sl(streamLock); + if (cancelled) + throw moonbase::api_error(0, "HTTP request to " + request.url + " was cancelled"); + active = stream.get(); + } + + const bool connected = stream->connect(nullptr); + + juce::String body; + int statusCode = 0; + juce::StringPairArray responseHeaders; + if (connected) + { + body = stream->readEntireStreamAsString(); + statusCode = stream->getStatusCode(); + responseHeaders = stream->getResponseHeaders(); + } + + { + const juce::ScopedLock sl(streamLock); + active = nullptr; + } + + if (! connected) + { + // Match curl_http_transport: a transport-level failure surfaces as + // api_error with status 0, which the SDK's grace-period logic treats + // as "unreachable" (this also covers a cancelled connect()). + throw moonbase::api_error( + 0, "HTTP request to " + request.url + " failed (could not connect)"); + } + + moonbase::http_response response; + response.body = body.toStdString(); + response.status_code = static_cast(statusCode); + for (const auto& key : responseHeaders.getAllKeys()) + response.headers[key.toStdString()] = responseHeaders[key].toStdString(); + return response; + } + + // Interrupt any in-flight request and refuse further ones. Safe to call from + // another thread; the controller calls it on teardown so a blocking read + // cannot outlive the controller (or the plugin binary). + void cancel() + { + const juce::ScopedLock sl(streamLock); + cancelled = true; + if (active != nullptr) + active->cancel(); + } + +private: + juce::CriticalSection streamLock; + juce::WebInputStream* active = nullptr; // valid only between connect() and the read completing + bool cancelled = false; +}; + +} // namespace moonbase::juce_integration diff --git a/modules/moonbase_licensing/juce/ui/ActivationComponent.cpp b/modules/moonbase_licensing/juce/ui/ActivationComponent.cpp new file mode 100644 index 0000000..9eadd93 --- /dev/null +++ b/modules/moonbase_licensing/juce/ui/ActivationComponent.cpp @@ -0,0 +1,2103 @@ +#include "ActivationComponent.h" +#include "ActivationLookAndFeel.h" + +#include + +#include +#include + +namespace moonbase::juce_integration { + +using juce::Colour; +using juce::Graphics; +using juce::Justification; +using juce::Rectangle; + +//============================================================================== +// Shared drawing helpers +namespace { + +constexpr float kPi = juce::MathConstants::pi; +constexpr float kTwoPi = juce::MathConstants::twoPi; + +// juce::String(const char*) is ASCII-only, so non-ASCII glyphs (…, ·, —) must be +// decoded explicitly as UTF-8 or they mojibake. +inline juce::String u8(const char* utf8) { return juce::String::fromUTF8(utf8); } +const juce::String kMidDot = juce::String::fromUTF8(" \xc2\xb7 "); // " middot " + +void drawSunLogo(Graphics& g, Rectangle area, Colour accent) +{ + const auto c = area.getCentre(); + const float s = juce::jmin(area.getWidth(), area.getHeight()); + const float ring = s * 0.34f; + const float core = s * 0.12f; + const float lineW = juce::jmax(1.4f, s * 0.055f); + + g.setColour(accent.withAlpha(0.9f)); + g.drawEllipse(c.x - ring, c.y - ring, ring * 2.0f, ring * 2.0f, lineW); + g.setColour(accent); + g.fillEllipse(c.x - core, c.y - core, core * 2.0f, core * 2.0f); + + juce::Path rays; + const float inner = s * 0.40f; + const float outer = s * 0.49f; + for (int i = 0; i < 8; ++i) + { + const float a = kTwoPi * (float) i / 8.0f; + rays.startNewSubPath(c.x + std::cos(a) * inner, c.y + std::sin(a) * inner); + rays.lineTo(c.x + std::cos(a) * outer, c.y + std::sin(a) * outer); + } + g.strokePath(rays, juce::PathStrokeType(lineW, juce::PathStrokeType::curved, + juce::PathStrokeType::rounded)); +} + +// Draws the product/manufacturer lockup. Uses the merchant-supplied logo when given, +// otherwise a generated sun mark. Returns the row height. +int drawBrand(Graphics& g, const ActivationLookAndFeel& lnf, const juce::Drawable* customLogo, + Rectangle row, const juce::String& product, const juce::String& manufacturer, + float logoPx, float titlePx, bool muted = false) +{ + auto area = row; + auto logoArea = area.removeFromLeft((int) logoPx).toFloat().withSizeKeepingCentre(logoPx, logoPx); + if (customLogo != nullptr) + customLogo->drawWithin(g, logoArea, juce::RectanglePlacement::centred, muted ? 0.55f : 1.0f); + else + drawSunLogo(g, logoArea, muted ? lnf.palette.textSecondary : lnf.accent); + + area.removeFromLeft(13); + g.setColour(lnf.palette.textPrimary); + g.setFont(lnf.heading(titlePx)); + const int titleH = (int) titlePx + 4; + g.drawText(product, area.removeFromTop(titleH), Justification::bottomLeft); + if (manufacturer.isNotEmpty()) + { + g.setColour(lnf.palette.textSecondary); + g.setFont(lnf.body(11.5f)); + g.drawText(manufacturer, area.removeFromTop(16), Justification::topLeft); + } + return row.getHeight(); +} + +} // namespace + +//============================================================================== +// Buttons +class StyledButton : public juce::Button, + private juce::Timer +{ +public: + enum class Style { accent, ghost, danger }; + + StyledButton(ActivationLookAndFeel& l, Style s, const juce::String& buttonText, + std::unique_ptr ic = {}) + : juce::Button(buttonText), lnf(l), style(s), icon(std::move(ic)) + { + } + + ~StyledButton() override { stopTimer(); } + + // Cross-fade the label out and a small inline spinner in (or back out) while + // an async action runs. Clicks are gated while busy so the work can't refire. + void setBusy(bool shouldBeBusy) + { + if (busy == shouldBeBusy) + return; + busy = shouldBeBusy; + setEnabled(! busy); + startTimerHz(60); + } + + // Reduce-motion / snapshot variant: settle to the target frame, no timer. + void setBusyImmediate(bool shouldBeBusy) + { + busy = shouldBeBusy; + setEnabled(! busy); + busyAmount = busy ? 1.0f : 0.0f; + spinPhase = 0.0f; + stopTimer(); + repaint(); + } + + void paintButton(Graphics& g, bool over, bool down) override + { + auto r = getLocalBounds().toFloat(); + const auto& p = lnf.palette; + const float radius = 9.0f; + + if (style == Style::accent) + { + g.setColour(lnf.accent.withAlpha(0.30f)); + g.fillRoundedRectangle(r.translated(0.0f, 4.0f).reduced(6.0f, 2.0f), radius); + auto col = lnf.accent; + if (over) col = col.brighter(0.12f); + if (down) col = col.darker(0.05f); + g.setColour(col); + g.fillRoundedRectangle(r, radius); + textColour = juce::Colours::white; + } + else if (style == Style::danger) + { + g.setColour(over ? p.dangerBorder : p.dangerFill); + g.fillRoundedRectangle(r, radius); + g.setColour(p.dangerBorder); + g.drawRoundedRectangle(r.reduced(0.5f), radius, 1.0f); + textColour = p.error; + } + else + { + g.setColour(over ? p.ghostHover : p.ghostFill); + g.fillRoundedRectangle(r, radius); + g.setColour(p.ghostBorder); + g.drawRoundedRectangle(r.reduced(0.5f), radius, 1.0f); + textColour = p.textPrimary; + } + + const auto label = getButtonText(); + auto font = lnf.heading(14.0f); + g.setFont(font); + const float iconSize = 16.0f; + const float gap = icon ? 8.0f : 0.0f; + const float textW = juce::GlyphArrangement::getStringWidth(font, label); + const float totalW = textW + (icon ? iconSize + gap : 0.0f); + float x = (r.getWidth() - totalW) * 0.5f; + + const float labelAlpha = 1.0f - busyAmount; + if (labelAlpha > 0.0f) + { + if (icon) + { + icon->drawWithin(g, { x, r.getCentreY() - iconSize * 0.5f, iconSize, iconSize }, + juce::RectanglePlacement::centred, labelAlpha); + x += iconSize + gap; + } + g.setColour(textColour.withMultipliedAlpha(labelAlpha)); + g.drawText(label, Rectangle(x, 0.0f, textW + 2.0f, r.getHeight()), + Justification::centredLeft); + } + + if (busyAmount > 0.0f) + drawBusySpinner(g, r, textColour, busyAmount); + } + + void timerCallback() override + { + const float target = busy ? 1.0f : 0.0f; + const float step = 1.0f / 9.0f; // ~150ms cross-fade between label and spinner + busyAmount = busyAmount < target ? juce::jmin(target, busyAmount + step) + : juce::jmax(target, busyAmount - step); + + if (busy || busyAmount > 0.0f) + { + spinPhase += 1.0f / 66.0f; // ~1.1s per revolution + if (spinPhase >= 1.0f) + spinPhase -= 1.0f; + } + repaint(); + + if (! busy && busyAmount <= 0.0f) + { + busyAmount = 0.0f; + stopTimer(); + } + } + +private: + void drawBusySpinner(Graphics& g, Rectangle area, Colour colour, float amount) + { + const float d = juce::jmin(18.0f, area.getHeight() - 12.0f) * juce::jlimit(0.5f, 1.0f, amount); + auto sb = Rectangle(0, 0, d, d).withCentre(area.getCentre()); + const float thickness = 2.2f; + auto ring = sb.reduced(thickness * 0.5f); + + juce::Path track; + track.addEllipse(ring); + g.setColour(colour.withMultipliedAlpha(0.22f * amount)); + g.strokePath(track, juce::PathStrokeType(thickness)); + + juce::Path arc; + const float start = spinPhase * kTwoPi; + arc.addCentredArc(ring.getCentreX(), ring.getCentreY(), ring.getWidth() * 0.5f, + ring.getHeight() * 0.5f, 0.0f, start, start + kPi * 0.7f, true); + g.setColour(colour.withMultipliedAlpha(amount)); + g.strokePath(arc, juce::PathStrokeType(thickness, juce::PathStrokeType::curved, + juce::PathStrokeType::rounded)); + } + + ActivationLookAndFeel& lnf; + Style style; + std::unique_ptr icon; + Colour textColour; + bool busy = false; + float busyAmount = 0.0f; // eased 0..1: label <-> spinner + float spinPhase = 0.0f; +}; + +class LinkButton : public juce::Button +{ +public: + LinkButton(ActivationLookAndFeel& l, const juce::String& buttonText, Colour c, + std::unique_ptr ic = {}, Justification just = Justification::centred) + : juce::Button(buttonText), lnf(l), label(buttonText), colour(c), icon(std::move(ic)), justification(just) + { + } + + void paintButton(Graphics& g, bool over, bool /*down*/) override + { + auto r = getLocalBounds().toFloat(); + auto font = lnf.body(12.5f); + g.setFont(font); + const float iconSize = 14.0f; + const float gap = icon ? 6.0f : 0.0f; + const float textW = juce::GlyphArrangement::getStringWidth(font, label); + const float totalW = textW + (icon ? iconSize + gap : 0.0f); + float x = justification.testFlags(Justification::left) + ? 0.0f + : (r.getWidth() - totalW) * 0.5f; + const auto col = over ? colour.brighter(0.25f) : colour; + if (icon) + { + icon->drawWithin(g, { x, r.getCentreY() - iconSize * 0.5f, iconSize, iconSize }, + juce::RectanglePlacement::centred, 1.0f); + x += iconSize + gap; + } + g.setColour(col); + g.drawText(label, Rectangle(x, 0.0f, textW + 2.0f, r.getHeight()), + Justification::centredLeft); + } + +private: + ActivationLookAndFeel& lnf; + juce::String label; + Colour colour; + std::unique_ptr icon; + Justification justification; +}; + +//============================================================================== +// Offline response-file target: click to browse, or drag a file onto it. +class DropZone : public juce::Component, + public juce::FileDragAndDropTarget +{ +public: + DropZone(ActivationLookAndFeel& l, juce::String fileNoun, juce::String fileExtension) + : lnf(l), noun(std::move(fileNoun)), extension(std::move(fileExtension)) + { + emptyIcon = icons::fromStroke(icons::fileQuestion, l.palette.textSecondary, 1.6f); + fileIcon = icons::fromStroke(icons::checkCircle, l.palette.success, 1.7f); + setMouseCursor(juce::MouseCursor::PointingHandCursor); + } + + std::function onBrowse; // clicked + std::function onFile; // file dropped + + void setFileName(const juce::String& name) + { + if (name != fileName) + { + fileName = name; + repaint(); + } + } + + void paint(Graphics& g) override + { + auto r = getLocalBounds().toFloat().reduced(1.0f); + const bool has = fileName.isNotEmpty(); + + g.setColour(dragOver ? lnf.palette.ghostHover : Colour(0x06ffffff)); + g.fillRoundedRectangle(r, 10.0f); + + const auto borderCol = dragOver ? lnf.accent + : has ? lnf.palette.success.withAlpha(0.45f) + : lnf.palette.ghostBorder; + if (dragOver || has) + { + g.setColour(borderCol); + g.drawRoundedRectangle(r, 10.0f, 1.5f); + } + else + { + juce::Path outline; + outline.addRoundedRectangle(r, 10.0f); + juce::Path dashed; + const float dashes[] = { 5.0f, 4.0f }; + juce::PathStrokeType(1.5f).createDashedStroke(dashed, outline, dashes, 2); + g.setColour(borderCol); + g.fillPath(dashed); + } + + const juce::String line1 = has ? fileName : ("Drop your " + noun + " here"); + const juce::String line2 = has ? juce::String("Click to choose a different file") + : juce::String("or click to browse"); + auto fontMain = has ? lnf.mono(12.5f) : lnf.heading(13.0f); + auto fontSub = lnf.body(11.5f); + + const float iconS = 22.0f, gap = 10.0f; + const float avail = r.getWidth() - iconS - gap - 20.0f; + const float textW = juce::jmin(avail, juce::jmax(juce::GlyphArrangement::getStringWidth(fontMain, line1), + juce::GlyphArrangement::getStringWidth(fontSub, line2))); + const float groupW = iconS + gap + textW; + float gx = r.getCentreX() - groupW * 0.5f; + const float cy = r.getCentreY(); + + if (auto* icon = has ? fileIcon.get() : emptyIcon.get()) + icon->drawWithin(g, { gx, cy - iconS * 0.5f, iconS, iconS }, juce::RectanglePlacement::centred, 1.0f); + const float tx = gx + iconS + gap; + g.setColour(has ? lnf.palette.textPrimary : lnf.palette.textBody); + g.setFont(fontMain); + g.drawText(line1, Rectangle(tx, cy - 16.0f, textW + 2.0f, 17.0f), Justification::bottomLeft); + g.setColour(lnf.palette.textSecondary); + g.setFont(fontSub); + g.drawText(line2, Rectangle(tx, cy + 1.0f, textW + 2.0f, 15.0f), Justification::topLeft); + } + + void mouseUp(const juce::MouseEvent& e) override + { + if (onBrowse != nullptr && getLocalBounds().contains(e.getPosition())) + onBrowse(); + } + + bool isInterestedInFileDrag(const juce::StringArray& files) override + { + return matchingFile(files) != juce::File(); + } + void fileDragEnter(const juce::StringArray& files, int, int) override + { + setDragOver(matchingFile(files) != juce::File()); + } + void fileDragExit(const juce::StringArray&) override { setDragOver(false); } + void filesDropped(const juce::StringArray& files, int, int) override + { + setDragOver(false); + const auto file = matchingFile(files); + if (file != juce::File() && onFile != nullptr) + onFile(file); + } + +private: + void setDragOver(bool o) + { + if (o != dragOver) + { + dragOver = o; + repaint(); + } + } + + // First dropped file matching the accepted extension (any, if unset). + [[nodiscard]] juce::File matchingFile(const juce::StringArray& files) const + { + for (const auto& f : files) + { + juce::File file(f); + if (extension.isEmpty() || file.hasFileExtension(extension)) + return file; + } + return {}; + } + + ActivationLookAndFeel& lnf; + juce::String noun; + juce::String extension; + std::unique_ptr emptyIcon, fileIcon; + juce::String fileName; + bool dragOver = false; +}; + +//============================================================================== +// Small round close (X) button shown in the panel's top-right when the flow can +// be dismissed. +class CloseButton : public juce::Button +{ +public: + explicit CloseButton(ActivationLookAndFeel& l) : juce::Button("Close"), lnf(l) + { + icon = icons::fromStroke(icons::cross, l.palette.textSecondary, 1.8f); + setWantsKeyboardFocus(false); + } + + void paintButton(Graphics& g, bool over, bool down) override + { + auto r = getLocalBounds().toFloat(); + if (over || down) + { + g.setColour(down ? lnf.palette.ghostBorder : lnf.palette.ghostHover); + g.fillEllipse(r); + } + if (icon != nullptr) + icon->drawWithin(g, r.reduced(8.0f), juce::RectanglePlacement::centred, over ? 1.0f : 0.85f); + } + +private: + ActivationLookAndFeel& lnf; + std::unique_ptr icon; +}; + +//============================================================================== +// "Licensing secured by [mark] moonbase" footer. The Moonbase mark + name are a +// clickable link to moonbase.sh (pointing cursor + brighten on hover). +class MoonbaseBadge : public juce::Component +{ +public: + explicit MoonbaseBadge(ActivationLookAndFeel& l) : lnf(l) + { + mark = icons::moonbaseMark(l.palette.textBright); + lock = icons::fromStroke(icons::lock, l.palette.textMuted, 1.5f); + } + + void paint(Graphics& g) override + { + auto area = getLocalBounds().toFloat(); + g.setColour(lnf.palette.hairline); + g.fillRect(area.removeFromTop(1.0f)); + + const auto fontA = lnf.body(11.0f); + const auto fontB = lnf.body(12.0f); + const juce::String prefix = "Licensing secured by"; + const juce::String name = "moonbase"; + const float w1 = juce::GlyphArrangement::getStringWidth(fontA, prefix); + const float w2 = juce::GlyphArrangement::getStringWidth(fontB, name); + const float lockS = 14.0f, markS = 14.0f, g1 = 6.0f, g2 = 8.0f, g3 = 7.0f; + const float total = lockS + g1 + w1 + g2 + markS + g3 + w2; + + const float cy = area.getCentreY(); + float x = (float) getWidth() * 0.5f - total * 0.5f; + + if (lock != nullptr) + lock->drawWithin(g, { x, cy - lockS * 0.5f, lockS, lockS }, juce::RectanglePlacement::centred, 1.0f); + x += lockS + g1; + g.setColour(lnf.palette.textMuted); + g.setFont(fontA); + g.drawText(prefix, Rectangle(x, cy - 9.0f, w1 + 2.0f, 18.0f), Justification::centredLeft); + x += w1 + g2; + + const float linkX = x; // mark + name form the clickable link + if (mark != nullptr) + mark->drawWithin(g, { x, cy - markS * 0.5f, markS, markS }, juce::RectanglePlacement::centred, + hovering ? 1.0f : 0.85f); + x += markS + g3; + g.setColour(hovering ? lnf.palette.textPrimary : lnf.palette.textBright); + g.setFont(fontB); + g.drawText(name, Rectangle(x, cy - 9.0f, w2 + 2.0f, 18.0f), Justification::centredLeft); + + linkArea = Rectangle(linkX, cy - 11.0f, (x + w2) - linkX, 22.0f); + } + + void mouseMove(const juce::MouseEvent& e) override { updateHover(e.position); } + void mouseEnter(const juce::MouseEvent& e) override { updateHover(e.position); } + void mouseExit(const juce::MouseEvent&) override + { + setHover(false); + setMouseCursor(juce::MouseCursor::NormalCursor); + } + void mouseUp(const juce::MouseEvent& e) override + { + if (linkArea.contains(e.position)) + juce::URL("https://moonbase.sh").launchInDefaultBrowser(); + } + +private: + void updateHover(juce::Point p) + { + const bool over = linkArea.contains(p); + setMouseCursor(over ? juce::MouseCursor::PointingHandCursor : juce::MouseCursor::NormalCursor); + setHover(over); + } + void setHover(bool h) + { + if (h != hovering) + { + hovering = h; + repaint(); + } + } + + ActivationLookAndFeel& lnf; + std::unique_ptr mark, lock; + juce::Rectangle linkArea; + bool hovering = false; +}; + +//============================================================================== +// Screen base +struct ScreenView : juce::Component +{ + ScreenView(ActivationController& c, ActivationLookAndFeel& l) : controller(c), lnf(l) {} + virtual void refresh() {} + + ActivationController& controller; + ActivationLookAndFeel& lnf; + std::function onCloseRequested; + int headerRightInset = 0; // space to keep clear at the header's right (close button) +}; + +//============================================================================== +// Welcome +class WelcomeView : public ScreenView +{ +public: + WelcomeView(ActivationController& c, ActivationLookAndFeel& l) : ScreenView(c, l) + { + const auto& cfg = controller.config(); + activate = std::make_unique(l, StyledButton::Style::accent, cfg.activateOnlineText()); + activate->onClick = [this] { controller.beginOnlineActivation(); }; + addAndMakeVisible(*activate); + + if (cfg.enableOffline) + { + offline = std::make_unique( + l, cfg.activateOfflineText(), l.palette.textSecondary, + icons::fromStroke(icons::offlineGlobe, l.palette.textSecondary, 1.7f), + Justification::left); + offline->onClick = [this] { controller.showOffline(); }; + addAndMakeVisible(*offline); + } + } + + void refresh() override { repaint(); } // pick up Error-state status messages + + void paint(Graphics& g) override + { + const auto& cfg = controller.config(); + auto r = getLocalBounds(); + drawBrand(g, lnf, cfg.logo.get(), r.removeFromTop(40), cfg.resolvedProductName(), cfg.resolvedManufacturerName(), + 38.0f, 17.0f); + r.removeFromTop(24); + + g.setColour(lnf.palette.textPrimary); + g.setFont(lnf.heading(27.0f)); + g.drawFittedText(cfg.welcomeTitleText(), r.removeFromTop(34), Justification::topLeft, 1); + r.removeFromTop(8); + + g.setColour(lnf.palette.textSecondary); + g.setFont(lnf.body(14.0f)); + g.drawFittedText(cfg.welcomeBodyText(), r.removeFromTop(46), Justification::topLeft, 3, 1.0f); + + // Surface activation errors here (the Error screen reuses this view). + if (controller.statusMessage().isNotEmpty()) + { + auto errArea = getLocalBounds().removeFromBottom(38); + if (auto warn = icons::fromStroke(icons::warning, lnf.palette.error, 1.8f)) + warn->drawWithin(g, errArea.removeFromLeft(20).toFloat().withSizeKeepingCentre(16, 16), + juce::RectanglePlacement::centred, 1.0f); + g.setColour(lnf.palette.error); + g.setFont(lnf.body(12.5f)); + g.drawFittedText(controller.statusMessage(), errArea, Justification::centredLeft, 2, 1.0f); + } + } + + void resized() override + { + auto r = getLocalBounds(); + r.removeFromTop(40 + 24 + 34 + 8 + 46 + 22); + const int bw = juce::jmin(r.getWidth(), 360); + activate->setBounds(r.removeFromTop(46).withWidth(bw)); + r.removeFromTop(24); + if (offline) + offline->setBounds(r.removeFromTop(20).withWidth(juce::jmin(bw, 280))); + } + +private: + std::unique_ptr activate; + std::unique_ptr offline; +}; + +//============================================================================== +// Browser wait +class BrowserWaitView : public ScreenView, + private juce::Timer +{ +public: + BrowserWaitView(ActivationController& c, ActivationLookAndFeel& l) : ScreenView(c, l) + { + cancel = std::make_unique(l, StyledButton::Style::ghost, "Cancel activation"); + cancel->onClick = [this] { controller.cancelActivation(); }; + addAndMakeVisible(*cancel); + monitorIcon = icons::fromStroke(icons::monitor, l.palette.textSecondary, 1.6f); + } + + ~BrowserWaitView() override { stopTimer(); } + + // Self-drive the spinner with a timer so it animates regardless of the + // shared animation updater — only while this screen is visible. + void visibilityChanged() override + { + if (isVisible()) + startTimerHz(60); + else + stopTimer(); + } + + void timerCallback() override + { + spinPhase += 1.0f / 72.0f; // ~1.2s per revolution + if (spinPhase >= 1.0f) + spinPhase -= 1.0f; + if (! spinnerRepaint.isEmpty()) + repaint(spinnerRepaint); + else + repaint(); + } + + void paint(Graphics& g) override + { + auto r = getLocalBounds(); + + // Reserve the bottom for the cancel button (placed in resized) and the + // device chip above it, so they never overlap on a shorter window. + r.removeFromBottom(46); // cancel button row + r.removeFromBottom(14); + auto chipRow = r.removeFromBottom(34); + r.removeFromBottom(18); + + // Spinner at the top of the remaining space. + drawSpinner(g, r.removeFromTop(juce::jmin(116, juce::jmax(86, r.getHeight() / 2)))); + + // Heading + wrapping subtitle fill the middle. + g.setColour(lnf.palette.textPrimary); + g.setFont(lnf.heading(22.0f)); + g.drawText("Waiting for activation", r.removeFromTop(30), Justification::centred); + r.removeFromTop(8); + g.setColour(lnf.palette.textSecondary); + g.setFont(lnf.body(14.0f)); + auto sub = r.reduced(4, 0); + // minimumHorizontalScale = 1.0f forces wrapping instead of squashing glyphs. + g.drawFittedText("We opened your browser to finish activation. Sign in and confirm this " + "device, then we'll pick it up automatically.", + sub.getX(), sub.getY(), sub.getWidth(), sub.getHeight(), + Justification::centredTop, 5, 1.0f); + + drawChip(g, chipRow); + } + + void resized() override + { + cancel->setBounds(getLocalBounds().removeFromBottom(46) + .withSizeKeepingCentre(juce::jmin(getWidth(), 320), 46)); + } + +private: + void drawSpinner(Graphics& g, Rectangle area) + { + const float diameter = 58.0f; + auto sb = Rectangle(0, 0, diameter, diameter) + .withCentre({ (float) getWidth() * 0.5f, (float) area.getCentreY() }); + spinnerRepaint = sb.expanded(10.0f).getSmallestIntegerContainer(); + + g.setColour(lnf.accent.withAlpha(0.16f)); + g.fillEllipse(sb.expanded(7.0f)); + + const float thickness = 4.0f; + auto ring = sb.reduced(thickness * 0.5f + 1.0f); + juce::Path track; + track.addEllipse(ring); + g.setColour(Colour(0x26ffffff)); + g.strokePath(track, juce::PathStrokeType(thickness)); + + juce::Path arc; + const float start = spinPhase * kTwoPi; + arc.addCentredArc(ring.getCentreX(), ring.getCentreY(), ring.getWidth() * 0.5f, + ring.getHeight() * 0.5f, 0.0f, start, start + kPi * 0.6f, true); + g.setColour(Colour(0xff82cef1)); + g.strokePath(arc, juce::PathStrokeType(thickness, juce::PathStrokeType::curved, + juce::PathStrokeType::rounded)); + } + + void drawChip(Graphics& g, Rectangle row) + { + const auto chipText = controller.deviceLabel(); + auto chipFont = lnf.mono(12.5f); + const float tw = juce::GlyphArrangement::getStringWidth(chipFont, chipText); + auto chip = Rectangle(0, 0, juce::jmin((float) getWidth(), tw + 52.0f), 32.0f) + .withCentre({ (float) getWidth() * 0.5f, (float) row.getCentreY() }); + g.setColour(Colour(0x0affffff)); + g.fillRoundedRectangle(chip, 16.0f); + g.setColour(lnf.palette.ghostBorder); + g.drawRoundedRectangle(chip, 16.0f, 1.0f); + if (monitorIcon != nullptr) + monitorIcon->drawWithin(g, chip.removeFromLeft(34).reduced(9.0f), + juce::RectanglePlacement::centred, 1.0f); + g.setColour(lnf.palette.textBright); + g.setFont(chipFont); + g.drawText(chipText, chip, Justification::centredLeft); + } + + std::unique_ptr cancel; + std::unique_ptr monitorIcon; + Rectangle spinnerRepaint; + float spinPhase = 0.0f; +}; + +//============================================================================== +// Success +class SuccessView : public ScreenView +{ +public: + SuccessView(ActivationController& c, ActivationLookAndFeel& l) : ScreenView(c, l) + { + open = std::make_unique(l, StyledButton::Style::accent, "Open " + c.config().resolvedProductName()); + open->onClick = [this] { if (onCloseRequested) onCloseRequested(); }; + addAndMakeVisible(*open); + + details = std::make_unique(l, "View license details", l.palette.link); + details->onClick = [this] { controller.showDetails(); }; + addAndMakeVisible(*details); + + check = icons::fromStroke(icons::check, l.palette.success, 2.4f); + } + + void setPop(float p) { pop = p; repaint(); } + void refresh() override + { + open->setButtonText("Open " + controller.config().resolvedProductName()); + repaint(); + } + + void paint(Graphics& g) override + { + auto r = getLocalBounds(); + + // Reserve the bottom for the buttons (placed in resized) and anchor the + // license card just above them, so the card never slides under the + // buttons when the window is short. + r.removeFromBottom(20 + 14 + 46); // details link + gap + open button + r.removeFromBottom(14); // gap above the card + auto cardRow = r.removeFromBottom(86); + r.removeFromBottom(12); // gap below the subtitle + + // Check badge, scaling down if vertical space is tight. + const int badgeH = juce::jlimit(44, 96, r.getHeight() - 76); + auto badge = r.removeFromTop(badgeH); + const float d = juce::jmin(66.0f, (float) badge.getHeight() - 8.0f); + auto circle = Rectangle(0, 0, d, d) + .withCentre({ (float) getWidth() * 0.5f, (float) badge.getCentreY() }); + auto scaled = circle.withSizeKeepingCentre(circle.getWidth() * pop, circle.getHeight() * pop); + g.setColour(lnf.palette.successFill); + g.fillEllipse(scaled); + g.setColour(lnf.palette.successBorder); + g.drawEllipse(scaled, 1.0f); + if (check && pop > 0.3f) + check->drawWithin(g, scaled.reduced(scaled.getWidth() * 0.3f), + juce::RectanglePlacement::centred, juce::jlimit(0.0f, 1.0f, pop)); + + g.setColour(lnf.palette.textPrimary); + g.setFont(lnf.heading(25.0f)); + g.drawText("You're all set", r.removeFromTop(32), Justification::centred); + r.removeFromTop(6); + g.setColour(lnf.palette.textSecondary); + g.setFont(lnf.body(14.0f)); + g.drawFittedText(controller.config().resolvedProductName() + + " is activated on this machine. Your license is linked to your account.", + r.getX(), r.getY(), r.getWidth(), r.getHeight(), Justification::centredTop, 2, 1.0f); + + // Mini card (anchored above the buttons). + auto card = cardRow.withSizeKeepingCentre(juce::jmin(380, getWidth()), + juce::jmin(86, cardRow.getHeight())); + g.setColour(Colour(0x06ffffff)); + g.fillRoundedRectangle(card.toFloat(), 12.0f); + g.setColour(lnf.palette.panelBorder); + g.drawRoundedRectangle(card.toFloat(), 12.0f, 1.0f); + auto inner = card.reduced(18, 14); + drawCardRow(g, inner.removeFromTop(28), "Plan", planText()); + g.setColour(lnf.palette.hairline); + g.fillRect(inner.removeFromTop(1)); + drawCardRow(g, inner.removeFromTop(28), "Licensed to", licensedToText()); + } + + void resized() override + { + auto r = getLocalBounds(); + details->setBounds(r.removeFromBottom(20)); + r.removeFromBottom(14); + open->setBounds(r.removeFromBottom(46).withSizeKeepingCentre(juce::jmin(320, getWidth()), 46)); + } + +private: + juce::String planText() const + { + if (auto& lic = controller.license()) + return controller.config().resolvedProductName() + kMidDot + + (lic->trial ? juce::String("Trial") : juce::String("Full license")); + return controller.config().resolvedProductName(); + } + juce::String licensedToText() const + { + if (auto& lic = controller.license()) + return juce::String(lic->issued_to.email); + return {}; + } + void drawCardRow(Graphics& g, Rectangle row, const juce::String& key, const juce::String& value) + { + g.setColour(lnf.palette.textSecondary); + g.setFont(lnf.body(12.5f)); + g.drawText(key, row, Justification::centredLeft); + g.setColour(lnf.palette.textPrimary); + g.setFont(lnf.heading(13.0f)); + g.drawText(value, row, Justification::centredRight); + } + + std::unique_ptr open; + std::unique_ptr details; + std::unique_ptr check; + float pop = 1.0f; +}; + +//============================================================================== +// Offline +class OfflineView : public ScreenView +{ +public: + OfflineView(ActivationController& c, ActivationLookAndFeel& l) : ScreenView(c, l) + { + back = std::make_unique(l, "Back", l.palette.textSecondary, + icons::fromStroke(icons::back, l.palette.textSecondary, 2.0f), + Justification::left); + back->onClick = [this] { controller.showWelcome(); }; + addAndMakeVisible(*back); + + saveMachine = std::make_unique( + l, StyledButton::Style::ghost, u8("Save machine file\xe2\x80\xa6"), + icons::fromStroke(icons::fileDown, Colour(0xff82cef1), 1.6f)); + saveMachine->onClick = [this] { chooseMachineFileLocation(); }; + addAndMakeVisible(*saveMachine); + + dropZone = std::make_unique(l, "license file", "mb"); + dropZone->onBrowse = [this] { chooseLicenseFile(); }; + dropZone->onFile = [this](juce::File f) { controller.setOfflineResponse(f); }; + addAndMakeVisible(*dropZone); + + urlLink = std::make_unique( + l, controller.config().activationUrlDisplay(), l.palette.link, + icons::fromStroke(icons::externalLink, l.palette.link, 1.6f), Justification::left); + urlLink->onClick = [this] { controller.config().activationUrlResolved().launchInDefaultBrowser(); }; + urlLink->setMouseCursor(juce::MouseCursor::PointingHandCursor); + addAndMakeVisible(*urlLink); + + activate = std::make_unique(l, StyledButton::Style::accent, "Activate offline"); + activate->onClick = [this] { controller.activateOffline(); }; + addAndMakeVisible(*activate); + } + + void refresh() override + { + dropZone->setFileName(controller.hasOfflineResponse() ? controller.offlineResponseName() + : juce::String()); + repaint(); + } + + void paint(Graphics& g) override + { + const auto l = computeLayout(); + + g.setColour(lnf.palette.textPrimary); + g.setFont(lnf.heading(23.0f)); + g.drawText("Offline activation", l.heading, Justification::topLeft); + + g.setColour(lnf.palette.textSecondary); + g.setFont(lnf.body(14.0f)); + g.drawFittedText("Save your machine file, exchange it for a license file on an " + "internet-connected device, then load that file back here.", + l.subtitle.getX(), l.subtitle.getY(), l.subtitle.getWidth(), + l.subtitle.getHeight(), Justification::topLeft, 2, 1.0f); + + drawStep(g, l.step1, 1, "Save your machine file"); + drawStep(g, l.step2, 2, "Add the license file"); + + if (controller.offlineError().isNotEmpty() && ! l.error.isEmpty()) + { + auto line = l.error; + if (auto w = icons::fromStroke(icons::warning, lnf.palette.error, 1.8f)) + w->drawWithin(g, line.removeFromLeft(20).toFloat().withSizeKeepingCentre(15.0f, 15.0f), + juce::RectanglePlacement::centred, 1.0f); + line.removeFromLeft(4); + g.setColour(lnf.palette.error); + g.setFont(lnf.body(12.5f)); + g.drawFittedText(controller.offlineError(), line.getX(), line.getY(), line.getWidth(), + line.getHeight(), Justification::centredLeft, 2, 1.0f); + } + } + + void resized() override + { + const auto l = computeLayout(); + back->setBounds(l.back.withWidth(juce::jmin(90, getWidth()))); + urlLink->setBounds(l.urlLink); + saveMachine->setBounds(l.saveBtn); + dropZone->setBounds(l.dropZone); + activate->setBounds(l.activate); + } + +private: + struct OfflineLayout + { + Rectangle back, heading, subtitle, urlLink, step1, saveBtn, step2, dropZone, error, activate; + }; + + // Single source of truth for the layout so paint() and resized() never + // disagree. Flows top-down with the Activate button (and any error line) + // reserved at the bottom; compresses gracefully on shorter windows. + OfflineLayout computeLayout() + { + auto r = getLocalBounds(); + OfflineLayout l; + l.back = r.removeFromTop(22); + r.removeFromTop(2); + l.heading = r.removeFromTop(26); + r.removeFromTop(2); + l.subtitle = r.removeFromTop(34); + r.removeFromTop(4); + l.urlLink = r.removeFromTop(18); + r.removeFromTop(8); + + l.activate = r.removeFromBottom(44); + r.removeFromBottom(8); + if (controller.offlineError().isNotEmpty()) + { + l.error = r.removeFromBottom(30); + r.removeFromBottom(6); + } + + l.step1 = r.removeFromTop(20); + r.removeFromTop(4); + l.saveBtn = r.removeFromTop(40); + r.removeFromTop(10); + l.step2 = r.removeFromTop(20); + r.removeFromTop(4); + l.dropZone = r.removeFromTop(52); + return l; + } + + void drawStep(Graphics& g, Rectangle row, int number, const juce::String& text) + { + auto badge = row.removeFromLeft(20).withSizeKeepingCentre(20, 20).toFloat(); + g.setColour(lnf.accent); + g.fillEllipse(badge); + g.setColour(juce::Colours::white); + g.setFont(lnf.heading(11.0f)); + g.drawText(juce::String(number), badge, Justification::centred); + row.removeFromLeft(10); + g.setColour(lnf.palette.textBright); + g.setFont(lnf.heading(12.5f)); + g.drawText(text, row, Justification::centredLeft); + } + + void chooseMachineFileLocation() + { + chooser = std::make_unique( + "Save machine file", + juce::File::getSpecialLocation(juce::File::userDesktopDirectory) + .getChildFile(controller.config().resolvedProductName() + " machine file.dt"), + "*.dt"); + chooser->launchAsync(juce::FileBrowserComponent::saveMode + | juce::FileBrowserComponent::canSelectFiles + | juce::FileBrowserComponent::warnAboutOverwriting, + [this](const juce::FileChooser& fc) + { + auto file = fc.getResult(); + if (file != juce::File()) + controller.saveOfflineRequest(file); + }); + } + + void chooseLicenseFile() + { + chooser = std::make_unique( + "Choose license file", + juce::File::getSpecialLocation(juce::File::userDesktopDirectory), "*.mb"); + chooser->launchAsync(juce::FileBrowserComponent::openMode + | juce::FileBrowserComponent::canSelectFiles, + [this](const juce::FileChooser& fc) + { + auto file = fc.getResult(); + if (file != juce::File()) + controller.setOfflineResponse(file); + }); + } + + std::unique_ptr back, urlLink; + std::unique_ptr saveMachine, activate; + std::unique_ptr dropZone; + std::unique_ptr chooser; +}; + +//============================================================================== +// Trial +// Trial feature rows, evenly spaced. Sits inside a Viewport so it scrolls when +// the list is longer than the available area; centres vertically when it fits. +class TrialFeatureList : public juce::Component +{ +public: + TrialFeatureList(ActivationController& c, ActivationLookAndFeel& l) : controller(c), lnf(l) {} + + static constexpr int rowHeight = 28; + + [[nodiscard]] int contentHeight() const + { + return (int) controller.config().trialFeatures.size() * rowHeight; + } + + void paint(Graphics& g) override + { + const auto& feats = controller.config().trialFeatures; + if (feats.empty()) + return; + // Centre the block when there is slack (it fits); top-align when it + // overflows (the viewport scrolls). Either way the rows are even. + const int block = (int) feats.size() * rowHeight; + int y = juce::jmax(0, (getHeight() - block) / 2); + for (const auto& f : feats) + { + drawFeature(g, { 0, y, getWidth(), rowHeight }, f); + y += rowHeight; + } + } + +private: + void drawFeature(Graphics& g, Rectangle row, const TrialFeature& f) + { + auto iconArea = row.removeFromLeft(17).withSizeKeepingCentre(17, 17).toFloat(); + const auto colour = f.included ? lnf.palette.success : lnf.palette.textMuted; + if (auto ic = icons::fromStroke(f.included ? icons::check : icons::cross, colour, 2.0f)) + ic->drawWithin(g, iconArea, juce::RectanglePlacement::centred, 1.0f); + row.removeFromLeft(10); + g.setColour(f.included ? lnf.palette.textBody : lnf.palette.textMuted); + g.setFont(lnf.body(13.0f)); + g.drawText(f.label, row, Justification::centredLeft); + } + + ActivationController& controller; + ActivationLookAndFeel& lnf; +}; + +class TrialView : public ScreenView +{ +public: + TrialView(ActivationController& c, ActivationLookAndFeel& l) : ScreenView(c, l) + { + unlock = std::make_unique(l, StyledButton::Style::accent, "Unlock full version", + icons::fromStroke(icons::lock, juce::Colours::white, 1.8f)); + unlock->onClick = [this] { controller.beginOnlineActivation(); }; + addAndMakeVisible(*unlock); + + cont = std::make_unique(l, "Continue trial", l.palette.textSecondary); + cont->onClick = [this] { if (onCloseRequested) onCloseRequested(); }; + addAndMakeVisible(*cont); + + featureList = std::make_unique(c, l); + featuresViewport.setViewedComponent(featureList.get(), false); + featuresViewport.setScrollBarsShown(true, false); // vertical only, when needed + featuresViewport.setScrollBarThickness(11); + // Keep the scroll affordance clearly visible (not autohidden / faint) so + // an overflowing list obviously reads as a scrollable field. + auto& scrollbar = featuresViewport.getVerticalScrollBar(); + scrollbar.setAutoHide(false); + scrollbar.setColour(juce::ScrollBar::thumbColourId, Colour(0x80ffffff)); + scrollbar.setColour(juce::ScrollBar::trackColourId, Colour(0x1affffff)); + addAndMakeVisible(featuresViewport); + } + + void refresh() override { repaint(); } + + void paint(Graphics& g) override + { + auto r = getLocalBounds(); + // Reserve the bottom for the buttons so the feature list never slides + // under them on a short window. + r.removeFromBottom(46 + 14 + 20 + 14); // unlock + gap + continue + gap + + auto headerRow = r.removeFromTop(40); + auto pill = headerRow.removeFromRight(150); + drawBrand(g, lnf, controller.config().logo.get(), headerRow, controller.config().resolvedProductName(), + controller.config().resolvedManufacturerName(), 34.0f, 15.0f); + + const int days = controller.trialDaysRemaining(); + auto pillText = u8("Trial") + kMidDot + juce::String(days) + (days == 1 ? " day left" : " days left"); + auto pf = lnf.heading(10.5f); + const float pw = juce::GlyphArrangement::getStringWidth(pf, pillText) + 22.0f; + auto pb = Rectangle(0, 0, pw, 22).withCentre( + { (float) (pill.getRight() - headerRightInset) - pw * 0.5f, (float) headerRow.getCentreY() }); + g.setColour(lnf.palette.trial); + g.fillRoundedRectangle(pb, 11.0f); + g.setColour(Colour(0xff131519)); + g.setFont(pf); + g.drawText(pillText.toUpperCase(), pb, Justification::centred); + + r.removeFromTop(26); + g.setColour(lnf.palette.textPrimary); + g.setFont(lnf.heading(24.0f)); + g.drawText("You're on a free trial", r.removeFromTop(30), Justification::topLeft); + r.removeFromTop(8); + g.setColour(lnf.palette.textSecondary); + g.setFont(lnf.body(14.0f)); + const int total = controller.config().trialLengthDays; + { + auto subtitle = r.removeFromTop(40); + g.drawFittedText(juce::String(days) + " of " + juce::String(total) + + " days remaining. Unlock the full version any time to keep everything.", + subtitle.getX(), subtitle.getY(), subtitle.getWidth(), subtitle.getHeight(), + Justification::topLeft, 2, 1.0f); + } + + // progress bar + r.removeFromTop(14); + auto bar = r.removeFromTop(6); + g.setColour(Colour(0x12ffffff)); + g.fillRoundedRectangle(bar.toFloat(), 3.0f); + const float frac = total > 0 ? juce::jlimit(0.0f, 1.0f, (float) days / (float) total) : 0.0f; + auto fill = bar.toFloat().withWidth(bar.getWidth() * frac); + g.setGradientFill(juce::ColourGradient(lnf.palette.trial, fill.getX(), 0, + Colour(0xfff5c542), fill.getRight(), 0, false)); + g.fillRoundedRectangle(fill, 3.0f); + + // When the list is longer than the band, frame it as a bounded, scrollable + // field so the scroll affordance is unmistakable (resized() insets the + // viewport + scrollbar inside this frame). When it fits, the rows render + // as a plain centred checklist (per the design). + const auto fa = featureArea(); + if (featureList != nullptr && featureList->contentHeight() > fa.getHeight()) + { + g.setColour(Colour(0x06ffffff)); + g.fillRoundedRectangle(fa.toFloat(), 10.0f); + g.setColour(lnf.palette.panelBorder); + g.drawRoundedRectangle(fa.toFloat().reduced(0.5f), 10.0f, 1.0f); + } + } + + void resized() override + { + auto r = getLocalBounds(); + cont->setBounds(r.removeFromBottom(20)); + r.removeFromBottom(14); + unlock->setBounds(r.removeFromBottom(46)); + + const auto area = featureArea(); + const int content = featureList->contentHeight(); + const bool scrolls = content > area.getHeight(); + // Overflow -> inset the viewport inside the framed field (padding + a + // gutter for the scrollbar); fits -> fill the band so the rows centre. + const auto viewportArea = scrolls ? area.reduced(12, 10) : area; + featuresViewport.setBounds(viewportArea); + featureList->setSize(scrolls ? juce::jmax(0, viewportArea.getWidth() - 13) : viewportArea.getWidth(), + scrolls ? content : viewportArea.getHeight()); + } + +private: + // The middle band between the top copy/progress bar and the bottom buttons. + // The top/bottom amounts mirror the fixed layout in paint() / resized(). + [[nodiscard]] Rectangle featureArea() const + { + auto a = getLocalBounds(); + a.removeFromTop(40 + 26 + 30 + 8 + 40 + 14 + 6); // brand .. progress bar + a.removeFromTop(20); // gap below the bar + a.removeFromBottom(46 + 14 + 20 + 18); // unlock + gap + continue + breathing room + return a; + } + + juce::Viewport featuresViewport; + std::unique_ptr featureList; + std::unique_ptr unlock; + std::unique_ptr cont; +}; + +//============================================================================== +// Trial expired (locked: the plugin stays bypassed until activated) +class ExpiredView : public ScreenView +{ +public: + ExpiredView(ActivationController& c, ActivationLookAndFeel& l) : ScreenView(c, l) + { + unlock = std::make_unique(l, StyledButton::Style::accent, "Unlock full version", + icons::fromStroke(icons::lock, juce::Colours::white, 1.8f)); + unlock->onClick = [this] { controller.beginOnlineActivation(); }; + addAndMakeVisible(*unlock); + + offline = std::make_unique(l, "Activate offline instead", l.palette.textSecondary); + offline->onClick = [this] { controller.showOffline(); }; + addChildComponent(*offline); // visibility set in refresh() + + warnIcon = icons::fromStroke(icons::warning, l.palette.error, 1.8f); + } + + void refresh() override + { + offline->setVisible(controller.config().enableOffline); + repaint(); + } + + void paint(Graphics& g) override + { + auto r = getLocalBounds(); + const bool withOffline = controller.config().enableOffline; + + // Reserve the bottom for the buttons so content never slides under them. + r.removeFromBottom(46); // unlock button + if (withOffline) + r.removeFromBottom(14 + 20); // gap + offline link + + // Header: a muted brand lockup + a red "Trial expired" pill. + auto headerRow = r.removeFromTop(40); + auto pillSlot = headerRow.removeFromRight(150); + drawBrand(g, lnf, controller.config().logo.get(), headerRow, + controller.config().resolvedProductName(), controller.config().resolvedManufacturerName(), + 34.0f, 15.0f, /*muted*/ true); + drawExpiredPill(g, pillSlot, headerRow.getCentreY()); + + r.removeFromTop(26); + g.setColour(lnf.palette.textPrimary); + g.setFont(lnf.heading(24.0f)); + g.drawText("Your trial has ended", r.removeFromTop(30), Justification::topLeft); + r.removeFromTop(8); + + g.setColour(lnf.palette.textSecondary); + g.setFont(lnf.body(14.0f)); + { + auto subtitle = r.removeFromTop(40); + g.drawFittedText(bodyText(), subtitle.getX(), subtitle.getY(), subtitle.getWidth(), + subtitle.getHeight(), Justification::topLeft, 2, 1.0f); + } + + // Full, red progress bar (the trial bar at 100%). + r.removeFromTop(14); + auto bar = r.removeFromTop(6); + g.setColour(Colour(0x12ffffff)); + g.fillRoundedRectangle(bar.toFloat(), 3.0f); + g.setGradientFill(juce::ColourGradient(Colour(0xffb9444c), (float) bar.getX(), 0.0f, + Colour(0xffdc5050), (float) bar.getRight(), 0.0f, false)); + g.fillRoundedRectangle(bar.toFloat(), 3.0f); + + // Red "audio is bypassed" callout. + r.removeFromTop(22); + auto callout = r.removeFromTop(juce::jmin(66, r.getHeight())); + g.setColour(Colour(0x12dc5050)); + g.fillRoundedRectangle(callout.toFloat(), 10.0f); + g.setColour(Colour(0x38dc5050)); + g.drawRoundedRectangle(callout.toFloat(), 10.0f, 1.0f); + auto inner = callout.reduced(15, 12); + auto iconArea = inner.removeFromLeft(18).withSizeKeepingCentre(18, 18).toFloat(); + if (warnIcon != nullptr) + warnIcon->drawWithin(g, iconArea, juce::RectanglePlacement::centred, 1.0f); + inner.removeFromLeft(11); + g.setColour(lnf.palette.textBody); + g.setFont(lnf.body(13.0f)); + g.drawFittedText("Audio processing is bypassed. Existing projects still load, but " + + controller.config().resolvedProductName() + + " won't affect your sound until you activate.", + inner.getX(), inner.getY(), inner.getWidth(), inner.getHeight(), + Justification::centredLeft, 3, 1.0f); + } + + void resized() override + { + auto r = getLocalBounds(); + if (controller.config().enableOffline) + { + offline->setBounds(r.removeFromBottom(20)); + r.removeFromBottom(14); + } + unlock->setBounds(r.removeFromBottom(46)); + } + +private: + juce::String bodyText() const + { + const int len = controller.config().trialLengthDays; + juce::String date; + if (auto& lic = controller.expiredTrial(); lic && lic->expires_at) + { + const auto ms = std::chrono::duration_cast( + lic->expires_at->time_since_epoch()).count(); + date = juce::Time((juce::int64) ms).toString(true, false); + } + juce::String s("Your "); + if (len > 0) + s << len << "-day "; + s << "trial of " << controller.config().resolvedProductName(); + if (date.isNotEmpty()) + s << " ended on " << date; + s << ". Unlock the full version to keep using the plugin."; + return s; + } + + void drawExpiredPill(Graphics& g, Rectangle slot, int centreY) + { + const juce::String label = juce::String("Trial expired").toUpperCase(); + auto pf = lnf.heading(10.5f); + const float tw = juce::GlyphArrangement::getStringWidth(pf, label); + const float leftPad = 11.0f, dotD = 6.0f, gap = 7.0f, rightPad = 12.0f; + const float pw = leftPad + dotD + gap + tw + rightPad; + auto pb = Rectangle(0, 0, pw, 22.0f).withCentre( + { (float) (slot.getRight() - headerRightInset) - pw * 0.5f, (float) centreY }); + g.setColour(Colour(0x1edc5050)); + g.fillRoundedRectangle(pb, 11.0f); + g.setColour(Colour(0x52dc5050)); + g.drawRoundedRectangle(pb, 11.0f, 1.0f); + g.setColour(lnf.palette.error); + g.fillEllipse(pb.getX() + leftPad, pb.getCentreY() - dotD * 0.5f, dotD, dotD); + g.setFont(pf); + g.drawText(label, + Rectangle(pb.getX() + leftPad + dotD + gap, pb.getY(), tw + 2.0f, pb.getHeight()), + Justification::centredLeft); + } + + std::unique_ptr unlock; + std::unique_ptr offline; + std::unique_ptr warnIcon; +}; + +//============================================================================== +// License details +class DetailsView : public ScreenView +{ +public: + DetailsView(ActivationController& c, ActivationLookAndFeel& l) : ScreenView(c, l) + { + deactivate = std::make_unique(l, StyledButton::Style::danger, + "Deactivate this device"); + deactivate->onClick = [this] { controller.deactivate(); }; + addAndMakeVisible(*deactivate); + } + + void refresh() override + { + // Offline licenses are permanent: no seats, no server-side revoke. + deactivate->setVisible(! isOffline()); + if (controller.config().reduceMotion) + deactivate->setBusyImmediate(controller.isBusy()); + else + deactivate->setBusy(controller.isBusy()); + repaint(); + } + + // If we slide away mid-revoke (e.g. it succeeded and we return to Welcome), + // refresh() won't fire again here, so clear the spinner as we leave. + void visibilityChanged() override + { + if (! isVisible()) + deactivate->setBusy(false); + } + + void paint(Graphics& g) override + { + auto r = getLocalBounds(); + const bool offline = isOffline(); + + // Reserve the bottom: the seat / deactivate box for online licenses, or a + // short permanence note for offline ones (which have no seats/revoke). + Rectangle bottomBox; + if (offline) + { + bottomBox = r.removeFromBottom(22); + r.removeFromBottom(14); + } + else + { + bottomBox = r.removeFromBottom(58); + r.removeFromBottom(14); + } + + auto headerRow = r.removeFromTop(40); + drawBrand(g, lnf, controller.config().logo.get(), headerRow, controller.config().resolvedProductName(), + controller.config().resolvedManufacturerName(), 34.0f, 15.0f); + drawActivePill(g, headerRow); + r.removeFromTop(22); + + // The busy state is shown by the inline spinner in the deactivate button, + // so only surface a real error message here. + if (controller.statusMessage().isNotEmpty() && ! controller.isBusy()) + { + auto msg = r.removeFromBottom(20); + r.removeFromBottom(8); + g.setColour(lnf.palette.error); + g.setFont(lnf.body(12.5f)); + g.drawFittedText(controller.statusMessage(), msg.getX(), msg.getY(), msg.getWidth(), + msg.getHeight(), Justification::topLeft, 2, 1.0f); + } + + // Info card fills the remaining middle, capped at its natural height. + auto card = r.removeFromTop(juce::jmin(5 * 38, r.getHeight())); + g.setColour(Colour(0x06ffffff)); + g.fillRoundedRectangle(card.toFloat(), 12.0f); + g.setColour(lnf.palette.panelBorder); + g.drawRoundedRectangle(card.toFloat(), 12.0f, 1.0f); + + auto inner = card.reduced(16, 0); + if (auto& lic = controller.license()) + { + drawRow(g, inner.removeFromTop(38), "Licensed to", + lic->issued_to.name.empty() ? lic->issued_to.email : juce::String(lic->issued_to.name)); + divider(g, inner); + drawRow(g, inner.removeFromTop(38), "Email", juce::String(lic->issued_to.email)); + divider(g, inner); + drawRow(g, inner.removeFromTop(38), "Plan", + juce::String(lic->licensed_product.name) + (lic->trial ? kMidDot + "trial" : kMidDot + "license")); + divider(g, inner); + drawRow(g, inner.removeFromTop(38), "Activation", juce::String(moonbase::to_string(lic->method))); + divider(g, inner); + drawRow(g, inner.removeFromTop(38), "Expires", expiryText(*lic)); + } + + if (offline) + { + auto& lic = controller.license(); + const bool hasExpiry = lic && lic->expires_at.has_value(); + g.setColour(lnf.palette.textSecondary); + g.setFont(lnf.body(12.5f)); + g.drawText(hasExpiry + ? "Activated offline" + kMidDot + "valid until " + expiryText(*lic) + : "Activated offline" + kMidDot + "this license is permanent", + bottomBox, Justification::centred); + } + else + { + drawSeatBox(g, bottomBox); + } + } + + void resized() override + { + // Always position it (visibility is driven by refresh() on state change). + auto content = getLocalBounds().removeFromBottom(58).reduced(16, 12); + const int bw = juce::jmin(180, content.getWidth() / 2); + deactivate->setBounds(content.removeFromRight(bw).withSizeKeepingCentre(bw, 32)); + } + +private: + struct Seats { int used; int total; }; + + [[nodiscard]] bool isOffline() const + { + auto& lic = controller.license(); + return lic && lic->method == moonbase::activation_method::offline; + } + + [[nodiscard]] std::optional seats() const + { + auto& lic = controller.license(); + if (! lic || ! lic->seat_count || *lic->seat_count <= 0) + return std::nullopt; + const int total = (int) *lic->seat_count; + const int used = lic->seats_used ? (int) *lic->seats_used : 0; + return Seats{ juce::jlimit(0, total, used), total }; + } + + void drawSeatBox(Graphics& g, Rectangle box) + { + g.setColour(Colour(0x06ffffff)); + g.fillRoundedRectangle(box.toFloat(), 12.0f); + g.setColour(lnf.palette.panelBorder); + g.drawRoundedRectangle(box.toFloat(), 12.0f, 1.0f); + + auto inner = box.reduced(16, 12); + const int bw = juce::jmin(180, inner.getWidth() / 2); + inner.removeFromRight(bw + 14); // make room for the deactivate button + + if (auto s = seats()) + { + g.setColour(lnf.palette.textBright); + g.setFont(lnf.heading(12.5f)); + const auto label = juce::String("Activations") + kMidDot + juce::String(s->used) + " of " + + juce::String(s->total) + (s->total == 1 ? " device" : " devices"); + g.drawText(label, inner.removeFromTop(18), Justification::centredLeft); + inner.removeFromTop(8); + drawSeatBar(g, inner.removeFromTop(6), s->used, s->total); + } + else + { + g.setColour(lnf.palette.textSecondary); + g.setFont(lnf.body(12.5f)); + g.drawText("This device's activation", inner, Justification::centredLeft); + } + } + + void drawSeatBar(Graphics& g, Rectangle row, int used, int total) + { + auto r = row.toFloat(); + if (total <= 6) + { + const float gap = 6.0f; + const float segW = juce::jmin(36.0f, (r.getWidth() - gap * (float) (total - 1)) / (float) total); + float x = r.getX(); + for (int i = 0; i < total; ++i) + { + g.setColour(i < used ? lnf.accent : Colour(0x1affffff)); + g.fillRoundedRectangle(x, r.getY(), segW, r.getHeight(), r.getHeight() * 0.5f); + x += segW + gap; + } + } + else + { + g.setColour(Colour(0x1affffff)); + g.fillRoundedRectangle(r, r.getHeight() * 0.5f); + const float frac = juce::jlimit(0.0f, 1.0f, (float) used / (float) total); + g.setColour(lnf.accent); + g.fillRoundedRectangle(r.withWidth(r.getWidth() * frac), r.getHeight() * 0.5f); + } + } + + void drawActivePill(Graphics& g, Rectangle headerRow) + { + const juce::String label = "Active"; + auto font = lnf.heading(11.0f); + const float tw = juce::GlyphArrangement::getStringWidth(font, label); + const float leftPad = 11.0f, dotD = 6.0f, gap = 7.0f, rightPad = 12.0f; + const float pw = leftPad + dotD + gap + tw + rightPad; + auto pb = Rectangle(0, 0, pw, 24.0f) + .withCentre({ (float) (headerRow.getRight() - headerRightInset) - pw * 0.5f, + (float) headerRow.getCentreY() }); + g.setColour(lnf.palette.successFill); + g.fillRoundedRectangle(pb, 12.0f); + g.setColour(lnf.palette.successBorder); + g.drawRoundedRectangle(pb, 12.0f, 1.0f); + g.setColour(lnf.palette.success); + g.fillEllipse(pb.getX() + leftPad, pb.getCentreY() - dotD * 0.5f, dotD, dotD); + g.setFont(font); + g.drawText(label, Rectangle(pb.getX() + leftPad + dotD + gap, pb.getY(), tw + 2.0f, pb.getHeight()), + Justification::centredLeft); + } + + static juce::String expiryText(const moonbase::license& lic) + { + if (! lic.expires_at) + return "Never"; + const auto ms = std::chrono::duration_cast( + lic.expires_at->time_since_epoch()) + .count(); + return juce::Time((juce::int64) ms).toString(true, false); + } + void divider(Graphics& g, Rectangle& area) + { + g.setColour(lnf.palette.hairline); + g.fillRect(area.removeFromTop(1)); + } + void drawRow(Graphics& g, Rectangle row, const juce::String& key, const juce::String& value) + { + g.setColour(lnf.palette.textSecondary); + g.setFont(lnf.body(12.5f)); + g.drawText(key, row, Justification::centredLeft); + g.setColour(lnf.palette.textBody); + g.setFont(lnf.heading(13.0f)); + g.drawText(value, row, Justification::centredRight); + } + + std::unique_ptr deactivate; +}; + +//============================================================================== +// Impl +struct ActivationComponent::Impl : public juce::ChangeListener, + private juce::Timer +{ + // Owns a controller built from the config. + Impl(ActivationComponent& o, ActivationConfig cfg) + : owner(o), + ownedController(std::make_unique(std::move(cfg))), + controller(*ownedController), + lnf(controller.config().accent) + { + init(/*ownsController=*/true); + } + + // Shares an externally-owned controller (e.g. one living in the processor). + Impl(ActivationComponent& o, ActivationController& existing) + : owner(o), + ownedController(nullptr), + controller(existing), + lnf(controller.config().accent) + { + init(/*ownsController=*/false); + } + + void init(bool ownsController) + { + // The views live in a clipping host so directional slides are masked to + // the content area instead of bleeding over the panel chrome. + screenHost.setInterceptsMouseClicks(false, true); + owner.addAndMakeVisible(screenHost); + + welcome = std::make_unique(controller, lnf); + browser = std::make_unique(controller, lnf); + success = std::make_unique(controller, lnf); + offline = std::make_unique(controller, lnf); + trial = std::make_unique(controller, lnf); + expired = std::make_unique(controller, lnf); + details = std::make_unique(controller, lnf); + + for (auto* v : views()) + { + screenHost.addChildComponent(*v); + v->onCloseRequested = [this] { if (owner.onClose) owner.onClose(); }; + } + + closeButton = std::make_unique(lnf); + closeButton->onClick = [this] { if (owner.onClose) owner.onClose(); }; + owner.addChildComponent(*closeButton); // shown only when owner.onClose is set + + moonbaseBadge = std::make_unique(lnf); + owner.addChildComponent(*moonbaseBadge); // shown only when config.showMoonbaseBadge + + buildAnimators(); + // Drive the JUCE 8 animators from a timer rather than a + // VBlankAnimatorUpdater: the latter did not deliver ticks reliably for a + // freshly-shown plugin/app window. update() uses the hi-res clock. + startTimerHz(60); + controller.addChangeListener(this); + + if (ownsController) + controller.start(); // load any stored license + route + else + changeListenerCallback(nullptr); // shared + already started: sync to its current state + } + + ~Impl() override + { + stopTimer(); + controller.removeChangeListener(this); + } + + void timerCallback() override { updater.update(); } + + std::vector views() + { + return { welcome.get(), browser.get(), success.get(), offline.get(), trial.get(), expired.get(), details.get() }; + } + + ScreenView* viewFor(ActivationController::Screen s) + { + using S = ActivationController::Screen; + switch (s) + { + case S::Welcome: return welcome.get(); + case S::BrowserWait: return browser.get(); + case S::Success: return success.get(); + case S::Offline: return offline.get(); + case S::Trial: return trial.get(); + case S::Expired: return expired.get(); + case S::Details: return details.get(); + case S::Error: return welcome.get(); + case S::Loading: default: return nullptr; + } + } + + void changeListenerCallback(juce::ChangeBroadcaster*) override + { + const auto screen = controller.screen(); + auto* next = viewFor(screen); + if (next != active) + { + const int dir = directionFor(lastScreen, screen); + + // Finish any still-running previous transition cleanly. + if (outgoing != nullptr && outgoing != active && outgoing != next) + { + outgoing->setVisible(false); + outgoing->setTopLeftPosition(0, 0); + outgoing->setAlpha(1.0f); + } + + outgoing = active; + active = next; + if (active != nullptr) + { + active->refresh(); + active->setVisible(true); + active->toFront(false); + } + layoutActive(); + startTransition(dir); + } + else if (active != nullptr) + { + active->refresh(); + } + lastScreen = screen; + + if (controller.screen() == ActivationController::Screen::Success) + { + if (controller.config().reduceMotion || ! successAnim) + { + success->setPop(1.0f); + } + else + { + success->setPop(0.0f); + successAnim->start(); + } + } + + updateCloseButton(); + owner.repaint(); + if (owner.onActivationChanged) + owner.onActivationChanged(controller.license().has_value()); + } + + //== Animation ============================================================ + void buildAnimators() + { + // runningInfinitely() makes progress climb past 1.0 without wrapping, so + // wrap into [0,1) ourselves for a continuous loop. + glowAnim.emplace(juce::ValueAnimatorBuilder{} + .withDurationMs(5000.0) + .runningInfinitely() + .withValueChangedCallback([this](float v) + { + glowPhase = (float) std::fmod(v, 1.0); + owner.repaint(glowRegion); + }) + .build()); + + // Directional slide + cross-fade between screens, clipped to screenHost. + transitionAnim.emplace(juce::ValueAnimatorBuilder{} + .withDurationMs(380.0) + .withEasing(juce::Easings::createEaseInOutCubic()) + .withValueChangedCallback([this](float v) { applyTransition(v); }) + .withOnCompleteCallback([this] { finishTransition(); }) + .build()); + + successAnim.emplace(juce::ValueAnimatorBuilder{} + .withDurationMs(520.0) + .withEasing(juce::Easings::createEaseOutBack()) + .withValueChangedCallback([this](float v) { success->setPop(v); }) + .build()); + + // Modal appear/dismiss: fades + scales the whole overlay (scrim + panel). + appearAnim.emplace(juce::ValueAnimatorBuilder{} + .withDurationMs(260.0) + .withEasing(juce::Easings::createEaseOut()) + .withValueChangedCallback([this](float v) + { + appear_ = appearStart_ + + (appearTarget_ - appearStart_) * v; + applyAppear(); + }) + .withOnCompleteCallback([this] + { + appear_ = appearTarget_; + applyAppear(); + if (appearTarget_ <= 0.0f) + { + owner.setVisible(false); + appear_ = 1.0f; // reset for next show + applyAppear(); + } + }) + .build()); + + updater.addAnimator(*glowAnim); + updater.addAnimator(*transitionAnim); + updater.addAnimator(*successAnim); + updater.addAnimator(*appearAnim); + glowAnim->start(); + } + + void appear() + { + owner.setVisible(true); + owner.toFront(true); + if (controller.config().reduceMotion || ! appearAnim) + { + appear_ = 1.0f; + applyAppear(); + return; + } + appear_ = 0.0f; + applyAppear(); + appearStart_ = 0.0f; + appearTarget_ = 1.0f; + appearAnim->start(); + } + + void dismiss() + { + if (controller.config().reduceMotion || ! appearAnim) + { + owner.setVisible(false); + appear_ = 1.0f; + applyAppear(); + return; + } + appearStart_ = appear_; + appearTarget_ = 0.0f; + appearAnim->start(); + } + + // Combined panel scale: appear/dismiss animation * fit-to-window down-scale. + [[nodiscard]] float panelScale() const { return appearScale_ * fitScale_; } + + void applyAppear() + { + owner.setAlpha(juce::jlimit(0.0f, 1.0f, appear_)); + appearScale_ = 0.94f + 0.06f * appear_; + const float s = panelScale(); + const auto pivot = panelBounds.toFloat().getCentre(); + const auto transform = s >= 0.999f + ? juce::AffineTransform() + : juce::AffineTransform::scale(s, s, pivot.x, pivot.y); + screenHost.setTransform(transform); + if (closeButton != nullptr) + closeButton->setTransform(transform); + if (moonbaseBadge != nullptr) + moonbaseBadge->setTransform(transform); + owner.repaint(); + } + + // Screen "depth": entry screens are shallow, in-flow screens deeper. Moving + // deeper slides the new view in from the right (forward); shallower slides + // it in from the left (back). + static int depth(ActivationController::Screen s) + { + using S = ActivationController::Screen; + switch (s) + { + case S::Loading: case S::Welcome: case S::Error: return 0; + case S::Offline: case S::BrowserWait: return 1; + case S::Trial: case S::Success: case S::Details: case S::Expired: return 2; + } + return 0; + } + + static int directionFor(ActivationController::Screen from, ActivationController::Screen to) + { + return depth(to) >= depth(from) ? 1 : -1; + } + + void startTransition(int dir) + { + slideDir = dir; + const int w = screenHost.getWidth(); + + if (active == nullptr) + { + if (outgoing != nullptr) { outgoing->setVisible(false); outgoing = nullptr; } + return; + } + + if (controller.config().reduceMotion || ! transitionAnim || w <= 0) + { + finishTransition(); + return; + } + + active->setAlpha(0.0f); + active->setTopLeftPosition(dir * w, 0); // start off-screen (right if forward) + if (outgoing != nullptr) + { + outgoing->setAlpha(1.0f); + outgoing->setTopLeftPosition(0, 0); + } + transitionAnim->start(); + } + + void applyTransition(float v) + { + const int w = screenHost.getWidth(); + if (active != nullptr) + { + active->setAlpha(v); + active->setTopLeftPosition((int) std::lround((1.0f - v) * (float) slideDir * (float) w), 0); + } + if (outgoing != nullptr) + { + outgoing->setAlpha(1.0f - v); + outgoing->setTopLeftPosition((int) std::lround(-v * (float) slideDir * (float) w), 0); + } + } + + void finishTransition() + { + if (active != nullptr) + { + active->setTopLeftPosition(0, 0); + active->setAlpha(1.0f); + } + if (outgoing != nullptr) + { + outgoing->setVisible(false); + outgoing->setTopLeftPosition(0, 0); + outgoing->setAlpha(1.0f); + outgoing = nullptr; + } + } + + //== Layout / paint ======================================================= + void layout() + { + auto b = owner.getLocalBounds(); + + // Responsive: the panel scales with the window between sensible min/max + // bounds, with margins + interior padding that scale too. + constexpr int minPanelW = 320, minPanelH = 420, minMargin = 16; + const int hMargin = juce::jlimit(16, 48, b.getWidth() / 12); + const int vMargin = juce::jlimit(16, 48, b.getHeight() / 12); + const int panelW = juce::jlimit(minPanelW, 600, b.getWidth() - hMargin * 2); + const int panelH = juce::jlimit(minPanelH, 660, b.getHeight() - vMargin * 2); + + // When the host is smaller than the modal's minimum footprint, scale the + // whole modal down to fit rather than clipping the panel. + const int needW = panelW + minMargin * 2; + const int needH = panelH + minMargin * 2; + fitScale_ = juce::jmin(1.0f, (float) b.getWidth() / (float) needW, + (float) b.getHeight() / (float) needH); + + panelBounds = Rectangle(0, 0, panelW, panelH) + .withCentre({ b.getCentreX(), b.getCentreY() }); + glowRegion = Rectangle(panelBounds.getX(), panelBounds.getY() - 6, panelBounds.getWidth(), 14); + + const int padX = juce::jlimit(22, 44, panelW / 14); + const int padY = juce::jlimit(20, 34, panelH / 16); + auto content = panelBounds.reduced(padX, padY); + if (controller.config().showMoonbaseBadge) + content.removeFromBottom(44); + contentArea = content; + updateCloseButton(); + + if (moonbaseBadge != nullptr) + { + moonbaseBadge->setVisible(controller.config().showMoonbaseBadge); + moonbaseBadge->setBounds(panelBounds.getX(), panelBounds.getBottom() - 40, + panelBounds.getWidth(), 40); + } + + screenHost.setBounds(contentArea); + layoutActive(); + applyAppear(); // re-applies the appear * fit scale for the new bounds + } + + // The flow is dismissable only once a valid license is loaded — until then + // the modal stays up to lock the host. So the close button appears only when + // the host wired onClose AND there is a license. + void updateCloseButton() + { + const bool canClose = (owner.onClose != nullptr) && controller.license().has_value(); + constexpr int closeSize = 28; + if (closeButton != nullptr) + { + closeButton->setVisible(canClose); + closeButton->setBounds(contentArea.getRight() - closeSize, contentArea.getY() + 6, + closeSize, closeSize); + closeButton->toFront(false); + } + const int inset = canClose ? (closeSize + 10) : 0; + for (auto* v : views()) + v->headerRightInset = inset; + } + + void layoutActive() + { + // Views fill the (clipping) host; transitions offset them horizontally. + for (auto* v : views()) + v->setBounds(screenHost.getLocalBounds()); + } + + void paintChrome(Graphics& g) + { + auto b = owner.getLocalBounds().toFloat(); + if (controller.config().overlayBackdrop) + { + // Modal over a host (e.g. a plugin editor): dim what's behind so the + // app shows through instead of an opaque takeover. + g.fillAll(juce::Colours::black.withAlpha(0.58f)); + } + else + { + juce::ColourGradient bg(lnf.palette.backgroundTop, b.getCentreX(), -b.getHeight() * 0.1f, + lnf.palette.backgroundBottom, b.getCentreX(), b.getHeight(), true); + bg.addColour(0.5, lnf.palette.backgroundMid); + g.setGradientFill(bg); + g.fillRect(b); + } + + auto panel = panelBounds.toFloat(); + + // Scale the panel by the appear/dismiss animation and the fit-to-window + // down-scale (the scrim, drawn above, only fades via the component alpha). + const float s = panelScale(); + juce::Graphics::ScopedSaveState saved(g); + if (s < 0.999f) + g.addTransform(juce::AffineTransform::scale(s, s, panel.getCentreX(), panel.getCentreY())); + + // soft outer shadow + g.setColour(juce::Colours::black.withAlpha(0.45f)); + g.fillRoundedRectangle(panel.translated(0, 14).expanded(2.0f), 18.0f); + + juce::ColourGradient pg(lnf.palette.panelTop, panel.getX(), panel.getY(), + lnf.palette.panelBottom, panel.getRight(), panel.getBottom(), false); + pg.addColour(0.58, lnf.palette.panelMid); + g.setGradientFill(pg); + g.fillRoundedRectangle(panel, 16.0f); + g.setColour(lnf.palette.panelBorder); + g.drawRoundedRectangle(panel, 16.0f, 1.0f); + + // signature top-edge glow (breathing) + const float breathe = 0.85f + 0.15f * (0.5f + 0.5f * std::sin(glowPhase * kTwoPi)); + auto glowLine = Rectangle(panel.getX() + panel.getWidth() * 0.18f, panel.getY() - 0.5f, + panel.getWidth() * 0.64f, 2.0f); + juce::ColourGradient gg(lnf.accent.withAlpha(0.0f), glowLine.getX(), 0, + lnf.accent.withAlpha(0.0f), glowLine.getRight(), 0, false); + gg.addColour(0.5, Colour(0xff82cef1).withAlpha(breathe)); + g.setGradientFill(gg); + g.fillRoundedRectangle(glowLine, 1.0f); + // The "secured by moonbase" footer is the MoonbaseBadge child component. + } + + ActivationComponent& owner; + std::unique_ptr ownedController; // null when sharing an external controller + ActivationController& controller; + ActivationLookAndFeel lnf; + + juce::Component screenHost; + + std::unique_ptr welcome; + std::unique_ptr browser; + std::unique_ptr success; + std::unique_ptr offline; + std::unique_ptr trial; + std::unique_ptr expired; + std::unique_ptr details; + ScreenView* active = nullptr; + ScreenView* outgoing = nullptr; + ActivationController::Screen lastScreen = ActivationController::Screen::Loading; + int slideDir = 1; + + Rectangle panelBounds, contentArea, glowRegion; + + std::unique_ptr closeButton; + std::unique_ptr moonbaseBadge; + + juce::AnimatorUpdater updater; + std::optional glowAnim, transitionAnim, successAnim, appearAnim; + float glowPhase = 0.0f; + + // Modal appear/dismiss animation state. + float appear_ = 1.0f; // 1 = fully shown, 0 = hidden + float appearScale_ = 1.0f; // panel scale derived from appear_ + float appearStart_ = 0.0f; + float appearTarget_ = 1.0f; + float fitScale_ = 1.0f; // down-scale applied when the host is smaller than the modal minimum +}; + +//============================================================================== +ActivationComponent::ActivationComponent(ActivationConfig config) + : impl(std::make_unique(*this, std::move(config))) +{ + setSize(defaultWidth, defaultHeight); +} + +ActivationComponent::ActivationComponent(ActivationController& sharedController) + : impl(std::make_unique(*this, sharedController)) +{ + setSize(defaultWidth, defaultHeight); +} + +ActivationComponent::~ActivationComponent() = default; + +ActivationController& ActivationComponent::controller() { return impl->controller; } + +void ActivationComponent::appear() { impl->appear(); } + +void ActivationComponent::dismiss() { impl->dismiss(); } + +void ActivationComponent::paint(Graphics& g) { impl->paintChrome(g); } + +void ActivationComponent::resized() { impl->layout(); } + +} // namespace moonbase::juce_integration diff --git a/modules/moonbase_licensing/juce/ui/ActivationComponent.h b/modules/moonbase_licensing/juce/ui/ActivationComponent.h new file mode 100644 index 0000000..c79878c --- /dev/null +++ b/modules/moonbase_licensing/juce/ui/ActivationComponent.h @@ -0,0 +1,68 @@ +#pragma once + +// The built-in activation window. Construct it with an ActivationConfig and add +// it to a window (or use ActivationDialog). It owns an ActivationController, +// renders the Solstice-style flow, and drives every transition with JUCE 8's +// animation API. Native end to end — it talks to the moonbase::licensing API +// directly, not juce::OnlineUnlockStatus. + +#include +#include + +#include + +#include "../ActivationConfig.h" +#include "../ActivationController.h" + +namespace moonbase::juce_integration { + +class ActivationComponent : public juce::Component +{ +public: + // Builds and owns its own ActivationController from the config. + explicit ActivationComponent(ActivationConfig config); + + // Shares a controller owned elsewhere (e.g. one living in your AudioProcessor + // so audio-thread gating survives the editor's lifetime) — both processor and + // editor then see one license, with no hand-rolled re-sync. The controller + // must outlive this component, and the owner is responsible for calling + // controller.start(); the component reflects its current state. + explicit ActivationComponent(ActivationController& sharedController); + + ~ActivationComponent() override; + + // Called when the user dismisses the flow from a "done" state — the + // Welcome "no thanks" is not offered, so this fires from Success ("Open …") + // and Trial ("Continue"). ActivationDialog wires this to close the window. + std::function onClose; + + // Fired whenever activation state settles (true once a valid license is + // loaded). Handy for gating: enable your plugin when this reports true. + std::function onActivationChanged; + + [[nodiscard]] ActivationController& controller(); + + // Show/hide as a modal with a smooth fade + scale of both the panel and the + // backdrop. Use these (instead of setVisible) when overlaying a host app: + // appear() -> setVisible(true) and animate in + // dismiss() -> animate out, then setVisible(false) + // "Open", the close button, and a successful activation call onClose; wire + // onClose to dismiss(). + void appear(); + void dismiss(); + + // Preferred window size for this design. + static constexpr int defaultWidth = 660; + static constexpr int defaultHeight = 600; + + void paint(juce::Graphics&) override; + void resized() override; + +private: + struct Impl; + std::unique_ptr impl; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(ActivationComponent) +}; + +} // namespace moonbase::juce_integration diff --git a/modules/moonbase_licensing/juce/ui/ActivationDialog.cpp b/modules/moonbase_licensing/juce/ui/ActivationDialog.cpp new file mode 100644 index 0000000..1b8e42d --- /dev/null +++ b/modules/moonbase_licensing/juce/ui/ActivationDialog.cpp @@ -0,0 +1,48 @@ +#include "ActivationDialog.h" + +namespace moonbase::juce_integration { + +namespace { + +class ActivationDialogWindow : public juce::DocumentWindow +{ +public: + ActivationDialogWindow(ActivationConfig config, std::function onClosedIn) + : juce::DocumentWindow("Activate " + config.resolvedProductName(), + juce::Colour(0xff04060b), juce::DocumentWindow::closeButton), + onClosed(std::move(onClosedIn)) + { + auto* comp = new ActivationComponent(std::move(config)); + comp->onClose = [this] { closeButtonPressed(); }; + + setUsingNativeTitleBar(true); + setContentOwned(comp, true); + setResizable(false, false); + centreWithSize(comp->getWidth(), comp->getHeight()); + setVisible(true); + toFront(true); + } + + void closeButtonPressed() override + { + bool activated = false; + if (auto* comp = dynamic_cast(getContentComponent())) + activated = comp->controller().license().has_value(); + if (onClosed) + onClosed(activated); + delete this; // self-owned + } + +private: + std::function onClosed; + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(ActivationDialogWindow) +}; + +} // namespace + +void ActivationDialog::show(ActivationConfig config, std::function onClosed) +{ + new ActivationDialogWindow(std::move(config), std::move(onClosed)); +} + +} // namespace moonbase::juce_integration diff --git a/modules/moonbase_licensing/juce/ui/ActivationDialog.h b/modules/moonbase_licensing/juce/ui/ActivationDialog.h new file mode 100644 index 0000000..bd2808c --- /dev/null +++ b/modules/moonbase_licensing/juce/ui/ActivationDialog.h @@ -0,0 +1,27 @@ +#pragma once + +// One-call helper to pop the activation flow in its own window. For a plugin +// editor you'll usually embed ActivationComponent directly instead; this is for +// standalone apps and "Activate…" menu items. + +#include +#include + +#include + +#include "../ActivationConfig.h" +#include "ActivationComponent.h" + +namespace moonbase::juce_integration { + +class ActivationDialog +{ +public: + // Opens a centered, self-owning window hosting an ActivationComponent. The + // window closes when the user dismisses the flow (Open / Continue) or via + // the title-bar close button; onClosed reports whether a valid license is + // active at close time. + static void show(ActivationConfig config, std::function onClosed = {}); +}; + +} // namespace moonbase::juce_integration diff --git a/modules/moonbase_licensing/juce/ui/ActivationLookAndFeel.cpp b/modules/moonbase_licensing/juce/ui/ActivationLookAndFeel.cpp new file mode 100644 index 0000000..e67fd60 --- /dev/null +++ b/modules/moonbase_licensing/juce/ui/ActivationLookAndFeel.cpp @@ -0,0 +1,93 @@ +#include "ActivationLookAndFeel.h" + +namespace moonbase::juce_integration::icons { + +static juce::String hexOf(juce::Colour c) +{ + return juce::String::formatted("#%02x%02x%02x", c.getRed(), c.getGreen(), c.getBlue()); +} + +std::unique_ptr fromStroke(const juce::String& pathData, + juce::Colour colour, + float strokeWidth, + float viewBox) +{ + juce::String svg; + svg << "" + << ""; + + if (auto xml = juce::XmlDocument::parse(svg)) + return juce::Drawable::createFromSVG(*xml); + return nullptr; +} + +std::unique_ptr fromFill(const juce::String& pathData, + juce::Colour colour, + float viewBox) +{ + juce::String svg; + svg << "" + << ""; + + if (auto xml = juce::XmlDocument::parse(svg)) + return juce::Drawable::createFromSVG(*xml); + return nullptr; +} + +// The Moonbase brand mark, verbatim from the marketing site's +// images/logos/moonbase.svg. Recoloured to a single colour at load time. +static const char* const kMoonbaseSvg = R"SVG( + + + + + + + +)SVG"; + +std::unique_ptr moonbaseMark(juce::Colour colour) +{ + const auto svg = juce::String(kMoonbaseSvg).replace("FILLCOLOR", hexOf(colour)); + if (auto xml = juce::XmlDocument::parse(svg)) + return juce::Drawable::createFromSVG(*xml); + return nullptr; +} + +// Heroicons-style 24x24 stroke paths, taken from the Solstice design. +const char* const offlineGlobe = + "M9.348 14.652a3.75 3.75 0 010-5.304m5.304 0a3.75 3.75 0 010 5.304m-7.425 2.121a6.75 6.75 0 010-9.546" + "m9.546 0a6.75 6.75 0 010 9.546M5.106 18.894c-3.808-3.807-3.808-9.98 0-13.788m13.788 0c3.808 3.807 " + "3.808 9.98 0 13.788M12 12h.008v.008H12V12z"; +const char* const back = "M15.75 19.5 8.25 12l7.5-7.5"; +const char* const upload = + "M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 " + "0 0 0-3.375-3.375H8.25m.75 12 3 3m0 0 3-3m-3 3v-6m-1.5-9H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 " + ".621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9z"; +const char* const fileDown = upload; +const char* const checkCircle = "M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0z"; +const char* const fileQuestion = + "M7.5 7.5h-.75A2.25 2.25 0 0 0 4.5 9.75v7.5a2.25 2.25 0 0 0 2.25 2.25h7.5a2.25 2.25 0 0 0 2.25-2.25v-7.5" + "a2.25 2.25 0 0 0-2.25-2.25h-.75m-6 3.75 3-3m0 0 3 3m-3-3v11.25m6-2.25h.75a2.25 2.25 0 0 0 2.25-2.25v-7.5" + "a2.25 2.25 0 0 0-2.25-2.25h-.75"; +const char* const warning = + "M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 " + "3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z"; +const char* const lock = + "M16.5 10.5V6.75a4.5 4.5 0 1 0-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 0 0 2.25-2.25v-6.75a2.25 2.25 0 0 " + "0-2.25-2.25H6.75a2.25 2.25 0 0 0-2.25 2.25v6.75a2.25 2.25 0 0 0 2.25 2.25z"; +const char* const externalLink = + "M13.5 6H5.25A2.25 2.25 0 0 0 3 8.25v10.5A2.25 2.25 0 0 0 5.25 21h10.5A2.25 2.25 0 0 0 18 18.75V10.5" + "m-10.5 6L21 3m0 0h-5.25M21 3v5.25"; +const char* const monitor = + "M9 17.25v1.007a3 3 0 0 1-.879 2.122L7.5 21h9l-.621-.621A3 3 0 0 1 15 18.257V17.25m6-12V15a2.25 2.25 0 " + "0 1-2.25 2.25H5.25A2.25 2.25 0 0 1 3 15V5.25m18 0A2.25 2.25 0 0 0 18.75 3H5.25A2.25 2.25 0 0 0 3 5.25" + "m18 0V12a2.25 2.25 0 0 1-2.25 2.25H5.25A2.25 2.25 0 0 1 3 12V5.25"; +const char* const check = "M4.5 12.75l6 6 9-13.5"; +const char* const cross = "M6 18 18 6M6 6l12 12"; + +} // namespace moonbase::juce_integration::icons diff --git a/modules/moonbase_licensing/juce/ui/ActivationLookAndFeel.h b/modules/moonbase_licensing/juce/ui/ActivationLookAndFeel.h new file mode 100644 index 0000000..1f0856e --- /dev/null +++ b/modules/moonbase_licensing/juce/ui/ActivationLookAndFeel.h @@ -0,0 +1,114 @@ +#pragma once + +// Palette, fonts and icon helpers for the built-in activation UI, ported from +// the "Solstice Activation" design. The accent colour comes from +// ActivationConfig; everything else is a themeable token here. Subclass or +// mutate the palette to re-skin the whole flow. + +#include + +#include +#include + +namespace moonbase::juce_integration { + +struct ActivationPalette +{ + // Backdrop + plugin window. + juce::Colour backgroundTop { 0xff0e1626 }; + juce::Colour backgroundMid { 0xff070a11 }; + juce::Colour backgroundBottom { 0xff04060b }; + juce::Colour panelTop { 0xff0d121c }; + juce::Colour panelMid { 0xff080b13 }; + juce::Colour panelBottom { 0xff06090f }; + juce::Colour panelBorder { 0x14ffffff }; + juce::Colour hairline { 0x1affffff }; + + // Text. + juce::Colour textPrimary { 0xfff5f8fb }; + juce::Colour textBody { 0xffcdd8e6 }; + juce::Colour textBright { 0xff9fb3cc }; + juce::Colour textSecondary { 0xff768aa4 }; + juce::Colour textMuted { 0xff5a6b82 }; + + // Controls. + juce::Colour ghostFill { 0x0affffff }; + juce::Colour ghostBorder { 0x21ffffff }; + juce::Colour ghostHover { 0x14ffffff }; + juce::Colour link { 0xff6aa8ff }; + + // Status. + juce::Colour success { 0xff34d27b }; + juce::Colour successFill { 0x2416a34a }; + juce::Colour successBorder { 0x5916a34a }; + juce::Colour trial { 0xffeab308 }; + juce::Colour error { 0xfff08a8a }; + juce::Colour dangerFill { 0x14dc5050 }; + juce::Colour dangerBorder { 0x4cdc5050 }; +}; + +class ActivationLookAndFeel : public juce::LookAndFeel_V4 +{ +public: + explicit ActivationLookAndFeel(juce::Colour accentColour = juce::Colour(0xff186cdc)) + : accent(accentColour) + { + setColour(juce::ResizableWindow::backgroundColourId, palette.backgroundBottom); + } + + juce::Colour accent; + ActivationPalette palette; + + // Fonts: Inter ~ default sans, Space Mono ~ default monospaced. Bundle real + // typefaces with juce_add_binary_data and swap these if you want exact + // fidelity. + [[nodiscard]] juce::Font heading(float height) const + { + return juce::Font(juce::FontOptions().withHeight(height).withStyle("Bold")); + } + [[nodiscard]] juce::Font body(float height) const + { + return juce::Font(juce::FontOptions().withHeight(height)); + } + [[nodiscard]] juce::Font mono(float height) const + { + return juce::Font(juce::FontOptions(juce::Font::getDefaultMonospacedFontName(), height, + juce::Font::plain)); + } +}; + +//============================================================================== +// Icon helper: builds a juce::Drawable from inline SVG data, stroked (or +// filled) in a chosen colour. The design's icons are 24x24 stroke paths. +namespace icons { + +std::unique_ptr fromStroke(const juce::String& pathData, + juce::Colour colour, + float strokeWidth = 1.7f, + float viewBox = 24.0f); + +std::unique_ptr fromFill(const juce::String& pathData, + juce::Colour colour, + float viewBox = 24.0f); + +// The official Moonbase brand mark (from the marketing site's +// images/logos/moonbase.svg), recoloured to a single colour. +std::unique_ptr moonbaseMark(juce::Colour colour); + +// Path data strings (24x24 viewBox unless noted) used across the screens. +extern const char* const offlineGlobe; +extern const char* const back; +extern const char* const upload; +extern const char* const fileDown; +extern const char* const checkCircle; +extern const char* const fileQuestion; +extern const char* const warning; +extern const char* const lock; +extern const char* const externalLink; +extern const char* const monitor; +extern const char* const check; +extern const char* const cross; + +} // namespace icons + +} // namespace moonbase::juce_integration diff --git a/modules/moonbase_licensing/moonbase/client.hpp b/modules/moonbase_licensing/moonbase/client.hpp new file mode 100644 index 0000000..a7a656f --- /dev/null +++ b/modules/moonbase_licensing/moonbase/client.hpp @@ -0,0 +1,260 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include + +#include "moonbase/detail/url.hpp" +#include "moonbase/errors.hpp" +#include "moonbase/fingerprint.hpp" +#include "moonbase/http.hpp" +#include "moonbase/types.hpp" +#include "moonbase/validator.hpp" + +namespace moonbase { + +namespace detail { + +inline std::string version_string() +{ +#ifdef MOONBASE_CPP_VERSION + return MOONBASE_CPP_VERSION; +#else + return "0.0.0"; +#endif +} + +inline std::string request_path(const licensing_options& options) +{ + return trim_trailing_slashes(options.endpoint) + + "/api/client/activations/" + + url_encode(options.product_id) + + "/request"; +} + +inline std::string validate_path(const licensing_options& options) +{ + return trim_trailing_slashes(options.endpoint) + + "/api/client/licenses/" + + url_encode(options.product_id) + + "/validate"; +} + +inline std::string revoke_path(const licensing_options& options) +{ + return trim_trailing_slashes(options.endpoint) + + "/api/client/licenses/" + + url_encode(options.product_id) + + "/revoke"; +} + +inline std::map client_query(const licensing_options& options) +{ + std::map query{{"format", "JWT"}}; + if (options.target_platform != platform::unknown) { + query["platform"] = to_string(options.target_platform); + } + if (options.application_version) { + query["appVersion"] = *options.application_version; + } + for (const auto& [key, value] : options.metadata) { + if (!value.empty()) { + query["meta[" + key + "]"] = value; + } + } + return query; +} + +inline std::map default_headers(const licensing_options& options, + const std::string& content_type = {}) +{ + std::string user_agent = "moonbase-cpp/" + version_string(); + if (options.client_info && !options.client_info->empty()) { + user_agent += " " + *options.client_info; + } + std::map headers{ + {"Accept", "application/json, application/jwt, text/plain"}, + {"User-Agent", user_agent}, + {"x-mb-client", "moonbase-cpp"}, + }; + if (!content_type.empty()) { + headers["Content-Type"] = content_type; + } + return headers; +} + +inline void throw_for_problem(long status_code, const std::string& body) +{ + std::string title; + std::string detail; + std::string error_type; + + if (!body.empty()) { + try { + const auto problem = nlohmann::json::parse(body); + if (problem.contains("title") && problem.at("title").is_string()) { + title = problem.at("title").get(); + } + if (problem.contains("detail") && problem.at("detail").is_string()) { + detail = problem.at("detail").get(); + } + if (problem.contains("errorType")) { + if (problem.at("errorType").is_string()) { + error_type = problem.at("errorType").get(); + } else if (problem.at("errorType").is_number_integer()) { + error_type = std::to_string(problem.at("errorType").get()); + } + } + } catch (const std::exception&) { + } + } + + const auto message = !detail.empty() + ? detail + : (!title.empty() ? title : "Moonbase API request failed with HTTP " + std::to_string(status_code)); + + if (error_type == "LicenseExpired" || error_type == "4" || + title.find("expired") != std::string::npos || + detail.find("expired") != std::string::npos) { + throw license_expired_error(message); + } + + if (status_code == 400) { + throw license_invalid_error(message); + } + + throw api_error(static_cast(status_code), message, title, detail); +} + +} // namespace detail + +class license_client { +public: + license_client( + licensing_options options, + std::shared_ptr fingerprints, + std::shared_ptr validator, + std::shared_ptr transport) + : options_(std::move(options)), + fingerprints_(std::move(fingerprints)), + validator_(std::move(validator)), + transport_(std::move(transport)) + { + if (!fingerprints_) { + throw configuration_error("A fingerprint provider is required"); + } + if (!validator_) { + throw configuration_error("A license validator is required"); + } + if (!transport_) { + throw configuration_error("An HTTP transport is required"); + } + } + + [[nodiscard]] activation_request request_activation() const + { + const auto url = detail::append_query( + detail::request_path(options_), + detail::client_query(options_)); + + const auto payload = nlohmann::json{ + {"deviceName", fingerprints_->device_name()}, + {"deviceSignature", fingerprints_->device_id()}, + }; + + http_request request; + request.method = "POST"; + request.url = url; + request.headers = detail::default_headers(options_, "application/json"); + request.connect_timeout = options_.http_connect_timeout; + request.request_timeout = options_.http_request_timeout; + request.body = payload.dump(); + + const auto response = transport_->send(request); + if (response.status_code < 200 || response.status_code >= 300) { + detail::throw_for_problem(response.status_code, response.body); + } + + try { + return nlohmann::json::parse(response.body).get(); + } catch (const std::exception& ex) { + throw api_error( + static_cast(response.status_code), + std::string("Could not parse activation response: ") + ex.what()); + } + } + + [[nodiscard]] license validate_token_online(std::string_view token) const + { + const auto url = detail::append_query( + detail::validate_path(options_), + detail::client_query(options_)); + + http_request request; + request.method = "POST"; + request.url = url; + request.headers = detail::default_headers(options_, "text/plain"); + request.connect_timeout = options_.http_connect_timeout; + request.request_timeout = options_.http_request_timeout; + request.body = std::string(token); + + const auto response = transport_->send(request); + if (response.status_code < 200 || response.status_code >= 300) { + detail::throw_for_problem(response.status_code, response.body); + } + return validator_->validate_token(response.body); + } + + void revoke_activation(std::string_view token) const + { + const auto url = detail::append_query( + detail::revoke_path(options_), + detail::client_query(options_)); + + http_request request; + request.method = "POST"; + request.url = url; + request.headers = detail::default_headers(options_, "text/plain"); + request.connect_timeout = options_.http_connect_timeout; + request.request_timeout = options_.http_request_timeout; + request.body = std::string(token); + + const auto response = transport_->send(request); + if (response.status_code < 200 || response.status_code >= 300) { + detail::throw_for_problem(response.status_code, response.body); + } + } + + [[nodiscard]] std::optional get_requested_activation( + const activation_request& activation) const + { + http_request request; + request.method = "GET"; + request.url = activation.request_url; + request.headers = detail::default_headers(options_); + request.connect_timeout = options_.http_connect_timeout; + request.request_timeout = options_.http_request_timeout; + + const auto response = transport_->send(request); + if (response.status_code == 204 || response.status_code == 404) { + return std::nullopt; + } + if (response.status_code < 200 || response.status_code >= 300) { + detail::throw_for_problem(response.status_code, response.body); + } + return validator_->validate_token(response.body); + } + +private: + licensing_options options_; + std::shared_ptr fingerprints_; + std::shared_ptr validator_; + std::shared_ptr transport_; +}; + +} // namespace moonbase diff --git a/modules/moonbase_licensing/moonbase/default_fingerprint.hpp b/modules/moonbase_licensing/moonbase/default_fingerprint.hpp new file mode 100644 index 0000000..470096d --- /dev/null +++ b/modules/moonbase_licensing/moonbase/default_fingerprint.hpp @@ -0,0 +1,372 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(_WIN32) +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#else +#include +#endif + +#include "moonbase/detail/crypto/crypto.hpp" +#include "moonbase/fingerprint.hpp" + +namespace moonbase { + +class default_fingerprint_provider : public fingerprint_provider { +public: + using identity_parameter = std::pair; + + [[nodiscard]] static std::string platform_tag() + { +#if defined(__APPLE__) + return "mac"; +#elif defined(_WIN32) + return "windows"; +#elif defined(__ANDROID__) + return "android"; +#elif defined(__linux__) + return "linux"; +#elif defined(__FreeBSD__) || defined(__OpenBSD__) || defined(__NetBSD__) || defined(__DragonFly__) + return "bsd"; +#else + return "unknown"; +#endif + } + + [[nodiscard]] static std::string hash_identity_parameters( + const std::vector& parameters, + std::string_view platform = platform_tag()) + { + std::string material; + material += "moonbase-cpp:fingerprint:v1\n"; + material += "platform="; + material.append(platform.data(), platform.size()); + material += "\n"; + + for (const auto& parameter : parameters) { + auto name = trim_ascii(parameter.first); + auto value = trim_ascii(parameter.second); + if (!name.empty() && !value.empty()) { + material += name; + material += "="; + material += value; + material += "\n"; + } + } + + return detail::sha256_hex(material); + } + + [[nodiscard]] static std::vector identity_parameters() + { + std::vector parameters; + +#if defined(_WIN32) + append_windows_identity_parameters(parameters); +#elif defined(__APPLE__) + auto uuid = trim_ascii(command_output( + "ioreg -rd1 -c IOPlatformExpertDevice 2>/dev/null | " + "awk -F\\\" '/IOPlatformUUID/{print $4; exit}'")); + uuid.erase(std::remove(uuid.begin(), uuid.end(), '-'), uuid.end()); + append_parameter(parameters, "ioPlatformUuid", uuid); +#elif defined(__linux__) && !defined(__ANDROID__) + const auto board_serial = trim_ascii(read_file("/sys/class/dmi/id/board_serial")); + if (!board_serial.empty()) { + append_parameter(parameters, "boardSerial", board_serial); + } else { + append_parameter(parameters, "biosDate", read_file("/sys/class/dmi/id/bios_date")); + append_parameter(parameters, "biosRelease", read_file("/sys/class/dmi/id/bios_release")); + append_parameter(parameters, "biosVendor", read_file("/sys/class/dmi/id/bios_vendor")); + append_parameter(parameters, "biosVersion", read_file("/sys/class/dmi/id/bios_version")); + } + + const auto cpu_data = command_output("lscpu 2>/dev/null"); + if (!cpu_data.empty()) { + append_parameter(parameters, "cpuFamily", linux_cpu_field(cpu_data, "CPU family:")); + append_parameter(parameters, "cpuModel", linux_cpu_field(cpu_data, "Model:")); + append_parameter(parameters, "cpuModelName", linux_cpu_field(cpu_data, "Model name:")); + append_parameter(parameters, "cpuVendor", linux_cpu_field(cpu_data, "Vendor ID:")); + } +#endif + + return parameters; + } + + [[nodiscard]] std::string device_name() const override + { +#if defined(_WIN32) + char buffer[128]{}; + DWORD size = static_cast(sizeof(buffer)) - 1; + if (GetComputerNameExA(ComputerNamePhysicalDnsHostname, buffer, &size)) { + return std::string(buffer, size); + } + return {}; +#else + std::array buffer{}; + if (gethostname(buffer.data(), buffer.size() - 1) == 0) { + auto name = std::string(buffer.data()); +#if defined(__APPLE__) + const auto suffix = std::string(".local"); + if (name.size() >= suffix.size()) { + auto tail = name.substr(name.size() - suffix.size()); + std::transform(tail.begin(), tail.end(), tail.begin(), [](unsigned char c) { + return static_cast(std::tolower(c)); + }); + if (tail == suffix) { + name.erase(name.size() - suffix.size()); + } + } +#endif + return name; + } + return {}; +#endif + } + + [[nodiscard]] std::string device_id() const override + { + auto parameters = identity_parameters(); + if (parameters.empty()) { + append_parameter(parameters, "deviceName", device_name()); + } + return hash_identity_parameters(parameters); + } + +private: + [[nodiscard]] static std::string read_file(const std::string& path) + { + std::ifstream file(path); + if (!file) { + return {}; + } + std::ostringstream out; + out << file.rdbuf(); + return out.str(); + } + + [[nodiscard]] static std::string command_output(const std::string& command) + { +#if defined(_WIN32) + (void)command; + return {}; +#else + std::array buffer{}; + std::string result; + std::unique_ptr pipe(popen(command.c_str(), "r"), pclose); + if (!pipe) { + return {}; + } + while (fgets(buffer.data(), static_cast(buffer.size()), pipe.get()) != nullptr) { + result += buffer.data(); + } + return result; +#endif + } + + [[nodiscard]] static std::string trim_ascii(std::string value) + { + while (!value.empty() && + (value.back() == '\n' || value.back() == '\r' || value.back() == ' ' || value.back() == '\t')) { + value.pop_back(); + } + while (!value.empty() && + (value.front() == '\n' || value.front() == '\r' || value.front() == ' ' || value.front() == '\t')) { + value.erase(value.begin()); + } + return value; + } + + [[nodiscard]] static std::string linux_cpu_field(const std::string& lscpu_output, const std::string& key) + { + const auto key_index = lscpu_output.find(key); + if (key_index == std::string::npos) { + return {}; + } + + const auto colon = lscpu_output.find(':', key_index); + if (colon == std::string::npos) { + return {}; + } + + const auto end = lscpu_output.find('\n', colon); + return trim_ascii(lscpu_output.substr( + colon + 1, + end == std::string::npos ? std::string::npos : end - colon - 1)); + } + + static void append_parameter( + std::vector& parameters, + std::string name, + std::string value) + { + name = trim_ascii(std::move(name)); + value = trim_ascii(std::move(value)); + if (!name.empty() && !value.empty()) { + parameters.emplace_back(std::move(name), std::move(value)); + } + } + +#if defined(_WIN32) + [[nodiscard]] static std::string windows_string_from_offset( + const std::vector& content, + const std::vector& strings, + std::size_t byte_offset) + { + if (byte_offset >= content.size()) { + return {}; + } + + const auto index = static_cast(content[byte_offset]); + if (index == 0 || index > strings.size()) { + return {}; + } + + return std::string(strings[index - 1]); + } + + [[nodiscard]] static std::size_t windows_bounded_string_length(const char* value, std::size_t max_length) + { + std::size_t length = 0; + while (length < max_length && value[length] != '\0') { + ++length; + } + return length; + } + + static void append_windows_identity_parameters(std::vector& parameters) + { + constexpr DWORD signature = + static_cast('R') | + (static_cast('S') << 8U) | + (static_cast('M') << 16U) | + (static_cast('B') << 24U); + + const auto table_size = GetSystemFirmwareTable(signature, 0, nullptr, 0); + if (table_size == 0) { + return; + } + + std::vector smbios(table_size); + if (GetSystemFirmwareTable(signature, 0, smbios.data(), table_size) != table_size) { + return; + } + + struct raw_smbios_data { + std::uint8_t unused[4]; + std::uint32_t length; + }; + + struct smbios_header { + std::uint8_t id; + std::uint8_t length; + std::uint16_t handle; + }; + + if (smbios.size() < sizeof(raw_smbios_data)) { + return; + } + + raw_smbios_data raw{}; + std::memcpy(&raw, smbios.data(), sizeof(raw)); + if (smbios.size() < sizeof(raw_smbios_data) + raw.length) { + return; + } + + std::vector content( + smbios.begin() + static_cast(sizeof(raw_smbios_data)), + smbios.begin() + static_cast(sizeof(raw_smbios_data) + raw.length)); + + std::size_t offset = 0; + while (offset < content.size()) { + if (content.size() - offset < sizeof(smbios_header)) { + break; + } + + smbios_header header{}; + std::memcpy(&header, content.data() + offset, sizeof(header)); + if (header.length == 0 || content.size() - offset < header.length) { + break; + } + + std::vector strings; + auto string_offset = offset + header.length; + while (string_offset < content.size()) { + const auto* str = reinterpret_cast(content.data() + string_offset); + const auto max_length = content.size() - string_offset; + const auto length = windows_bounded_string_length(str, max_length); + if (length == 0) { + break; + } + strings.emplace_back(str, length); + string_offset += std::min(length + 1, max_length); + } + + const auto end_of_table = std::min( + content.size(), + std::max(offset + static_cast(header.length) + 2, string_offset + 1)); + + const auto from_offset = [&](std::size_t byte_offset) { + return windows_string_from_offset(content, strings, offset + byte_offset); + }; + + switch (header.id) { + case 1: { + append_parameter(parameters, "systemManufacturer", from_offset(0x04)); + append_parameter(parameters, "systemProductName", from_offset(0x05)); + + if (offset + 0x08 + 16 <= content.size()) { + std::ostringstream hex; + hex << std::uppercase << std::hex << std::setfill('0'); + for (std::size_t index = 0; index != 16; ++index) { + hex << std::setw(2) << static_cast(content[offset + 0x08 + index]); + } + append_parameter(parameters, "systemUuid", hex.str()); + } + break; + } + + case 2: + append_parameter(parameters, "baseboardManufacturer", from_offset(0x04)); + append_parameter(parameters, "baseboardProduct", from_offset(0x05)); + append_parameter(parameters, "baseboardVersion", from_offset(0x06)); + append_parameter(parameters, "baseboardSerialNumber", from_offset(0x07)); + append_parameter(parameters, "baseboardAssetTag", from_offset(0x08)); + break; + + case 4: + append_parameter(parameters, "processorManufacturer", from_offset(0x07)); + append_parameter(parameters, "processorVersion", from_offset(0x10)); + append_parameter(parameters, "processorAssetTag", from_offset(0x21)); + append_parameter(parameters, "processorPartNumber", from_offset(0x22)); + break; + + default: + break; + } + + offset = end_of_table; + } + } +#endif +}; + +} // namespace moonbase diff --git a/modules/moonbase_licensing/moonbase/detail/base64.hpp b/modules/moonbase_licensing/moonbase/detail/base64.hpp new file mode 100644 index 0000000..b15eca7 --- /dev/null +++ b/modules/moonbase_licensing/moonbase/detail/base64.hpp @@ -0,0 +1,141 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace moonbase::detail { + +inline std::string base64_encode(const unsigned char* data, std::size_t length) +{ + static constexpr char table[] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + + std::string out; + out.reserve(((length + 2) / 3) * 4); + + for (std::size_t i = 0; i < length; i += 3) { + const auto a = static_cast(data[i]); + const auto b = i + 1 < length ? static_cast(data[i + 1]) : 0U; + const auto c = i + 2 < length ? static_cast(data[i + 2]) : 0U; + const auto triple = (a << 16U) | (b << 8U) | c; + + out.push_back(table[(triple >> 18U) & 0x3FU]); + out.push_back(table[(triple >> 12U) & 0x3FU]); + out.push_back(i + 1 < length ? table[(triple >> 6U) & 0x3FU] : '='); + out.push_back(i + 2 < length ? table[triple & 0x3FU] : '='); + } + + return out; +} + +inline std::string base64_encode(const std::vector& data) +{ + return base64_encode(data.data(), data.size()); +} + +inline std::vector base64_decode(std::string_view input) +{ + static constexpr unsigned char invalid = 0xFFU; + static const auto reverse_table = [] { + std::array table{}; + table.fill(invalid); + const std::string alphabet = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + for (std::size_t i = 0; i < alphabet.size(); ++i) { + table[static_cast(alphabet[i])] = static_cast(i); + } + return table; + }(); + + std::string normalized; + normalized.reserve(input.size()); + for (const char c : input) { + if (!std::isspace(static_cast(c))) { + normalized.push_back(c); + } + } + + if (normalized.empty()) { + return {}; + } + if (normalized.size() % 4 != 0) { + throw std::runtime_error("Invalid base64 length"); + } + + std::vector out; + out.reserve((normalized.size() / 4) * 3); + + for (std::size_t i = 0; i < normalized.size(); i += 4) { + unsigned char values[4]{}; + int padding = 0; + for (int j = 0; j < 4; ++j) { + const auto c = normalized[i + static_cast(j)]; + if (c == '=') { + values[j] = 0; + ++padding; + continue; + } + const auto v = reverse_table[static_cast(c)]; + if (v == invalid) { + throw std::runtime_error("Invalid base64 character"); + } + values[j] = v; + } + + const auto triple = + (static_cast(values[0]) << 18U) | + (static_cast(values[1]) << 12U) | + (static_cast(values[2]) << 6U) | + static_cast(values[3]); + + out.push_back(static_cast((triple >> 16U) & 0xFFU)); + if (padding < 2) { + out.push_back(static_cast((triple >> 8U) & 0xFFU)); + } + if (padding < 1) { + out.push_back(static_cast(triple & 0xFFU)); + } + } + + return out; +} + +inline std::string base64url_encode(const unsigned char* data, std::size_t length) +{ + auto out = base64_encode(data, length); + std::replace(out.begin(), out.end(), '+', '-'); + std::replace(out.begin(), out.end(), '/', '_'); + while (!out.empty() && out.back() == '=') { + out.pop_back(); + } + return out; +} + +inline std::string base64url_encode(const std::vector& data) +{ + return base64url_encode(data.data(), data.size()); +} + +inline std::vector base64url_decode(std::string_view input) +{ + std::string normalized(input); + std::replace(normalized.begin(), normalized.end(), '-', '+'); + std::replace(normalized.begin(), normalized.end(), '_', '/'); + while (normalized.size() % 4 != 0) { + normalized.push_back('='); + } + return base64_decode(normalized); +} + +inline std::string bytes_to_string(const std::vector& bytes) +{ + return {reinterpret_cast(bytes.data()), bytes.size()}; +} + +} // namespace moonbase::detail diff --git a/modules/moonbase_licensing/moonbase/detail/crypto/apple_backend.hpp b/modules/moonbase_licensing/moonbase/detail/crypto/apple_backend.hpp new file mode 100644 index 0000000..53809fc --- /dev/null +++ b/modules/moonbase_licensing/moonbase/detail/crypto/apple_backend.hpp @@ -0,0 +1,129 @@ +#pragma once + +// Apple crypto backend — Security.framework + CommonCrypto. Used on macOS/iOS +// when MOONBASE_CRYPTO_NATIVE is set (the JUCE module), so the module needs no +// OpenSSL. RS256 verification uses SecKeyVerifySignature with the "Message" +// PKCS#1 v1.5 SHA-256 algorithm, which hashes the input internally. + +#include +#include +#include +#include +#include + +#include +#include + +#include "moonbase/detail/crypto/der.hpp" +#include "moonbase/errors.hpp" + +namespace moonbase::detail::crypto { + +inline std::array sha256_raw(const unsigned char* data, std::size_t length) +{ + std::array digest{}; + CC_SHA256(data, static_cast(length), digest.data()); + return digest; +} + +namespace apple_detail { + +// RAII for CoreFoundation handles (CFRelease on destruction). +template +class cf_ref { +public: + cf_ref() = default; + explicit cf_ref(T ref) : ref_(ref) {} + cf_ref(const cf_ref&) = delete; + cf_ref& operator=(const cf_ref&) = delete; + cf_ref(cf_ref&& other) noexcept : ref_(other.ref_) { other.ref_ = nullptr; } + cf_ref& operator=(cf_ref&& other) noexcept + { + if (this != &other) { + reset(); + ref_ = other.ref_; + other.ref_ = nullptr; + } + return *this; + } + ~cf_ref() { reset(); } + + [[nodiscard]] T get() const noexcept { return ref_; } + explicit operator bool() const noexcept { return ref_ != nullptr; } + +private: + void reset() + { + if (ref_ != nullptr) { + CFRelease(ref_); + ref_ = nullptr; + } + } + T ref_ = nullptr; +}; + +inline cf_ref make_data(const unsigned char* bytes, std::size_t length) +{ + return cf_ref( + CFDataCreate(kCFAllocatorDefault, bytes, static_cast(length))); +} + +} // namespace apple_detail + +class rsa_public_key { +public: + explicit rsa_public_key(const std::string& key_material) + { + const std::vector pkcs1 = der::normalize_to_pkcs1(key_material); + auto key_data = apple_detail::make_data(pkcs1.data(), pkcs1.size()); + if (!key_data) { + throw license_invalid_error("Public key is not a supported RSA public key"); + } + + const void* keys[] = {kSecAttrKeyType, kSecAttrKeyClass}; + const void* values[] = {kSecAttrKeyTypeRSA, kSecAttrKeyClassPublic}; + apple_detail::cf_ref attributes( + CFDictionaryCreate(kCFAllocatorDefault, keys, values, 2, + &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks)); + + CFErrorRef error = nullptr; + SecKeyRef key = SecKeyCreateWithData(key_data.get(), attributes.get(), &error); + if (key == nullptr) { + if (error != nullptr) { + CFRelease(error); + } + throw license_invalid_error("Public key is not a supported RSA public key"); + } + key_ = apple_detail::cf_ref(key); + } + + void verify_rs256(std::string_view signing_input, + const std::vector& signature) const + { + auto message = apple_detail::make_data( + reinterpret_cast(signing_input.data()), signing_input.size()); + auto sig = apple_detail::make_data(signature.data(), signature.size()); + if (!message || !sig) { + throw license_invalid_error("License token signature is not valid"); + } + + CFErrorRef error = nullptr; + const Boolean ok = SecKeyVerifySignature( + key_.get(), + kSecKeyAlgorithmRSASignatureMessagePKCS1v15SHA256, + message.get(), + sig.get(), + &error); + if (error != nullptr) { + CFRelease(error); + } + if (ok != true) { + throw license_invalid_error("License token signature is not valid"); + } + } + +private: + apple_detail::cf_ref key_; +}; + +} // namespace moonbase::detail::crypto diff --git a/modules/moonbase_licensing/moonbase/detail/crypto/crypto.hpp b/modules/moonbase_licensing/moonbase/detail/crypto/crypto.hpp new file mode 100644 index 0000000..34dd530 --- /dev/null +++ b/modules/moonbase_licensing/moonbase/detail/crypto/crypto.hpp @@ -0,0 +1,74 @@ +#pragma once + +// Crypto backend selector for Moonbase's RS256 JWT verification and the device +// fingerprint hash. The backend is chosen at compile time: +// +// * Default (no macro set): OpenSSL — keeps existing non-JUCE consumers and +// the test suite byte-for-byte unchanged. +// * Define MOONBASE_CRYPTO_NATIVE: use the OS-native backend — Security.framework +// on Apple, CNG/bcrypt on Windows, system libcrypto (OpenSSL) on Linux. This +// is what the JUCE module defines so it pulls in no third-party crypto. +// * Or force a specific backend by defining MOONBASE_CRYPTO_BACKEND to one of +// MOONBASE_CRYPTO_OPENSSL / MOONBASE_CRYPTO_APPLE / MOONBASE_CRYPTO_WINDOWS. +// +// Whichever backend is active provides, in namespace moonbase::detail::crypto: +// std::array sha256_raw(const unsigned char*, std::size_t); +// class rsa_public_key { rsa_public_key(const std::string&); +// void verify_rs256(std::string_view, const std::vector&) const; }; + +#include +#include +#include +#include + +#define MOONBASE_CRYPTO_OPENSSL 1 +#define MOONBASE_CRYPTO_APPLE 2 +#define MOONBASE_CRYPTO_WINDOWS 3 + +#if !defined(MOONBASE_CRYPTO_BACKEND) +#if defined(MOONBASE_CRYPTO_NATIVE) +#if defined(__APPLE__) +#define MOONBASE_CRYPTO_BACKEND MOONBASE_CRYPTO_APPLE +#elif defined(_WIN32) +#define MOONBASE_CRYPTO_BACKEND MOONBASE_CRYPTO_WINDOWS +#else +#define MOONBASE_CRYPTO_BACKEND MOONBASE_CRYPTO_OPENSSL +#endif +#else +#define MOONBASE_CRYPTO_BACKEND MOONBASE_CRYPTO_OPENSSL +#endif +#endif + +#if MOONBASE_CRYPTO_BACKEND == MOONBASE_CRYPTO_APPLE +#include "moonbase/detail/crypto/apple_backend.hpp" +#elif MOONBASE_CRYPTO_BACKEND == MOONBASE_CRYPTO_WINDOWS +#include "moonbase/detail/crypto/windows_backend.hpp" +#else +#include "moonbase/detail/crypto/openssl_backend.hpp" +#endif + +namespace moonbase::detail { + +// The RSA public key type the validator holds, resolved to the active backend. +using crypto::rsa_public_key; + +// Lowercase hex SHA-256 of the material. The hex formatting lives here (shared) +// so the device-fingerprint output is byte-identical across all backends — any +// change would invalidate already-issued licenses whose `sig` claim was derived +// from the previous hash. +inline std::string sha256_hex(std::string_view material) +{ + const std::array digest = crypto::sha256_raw( + reinterpret_cast(material.data()), material.size()); + + static constexpr char hex[] = "0123456789abcdef"; + std::string out; + out.reserve(digest.size() * 2); + for (const unsigned char byte : digest) { + out.push_back(hex[byte >> 4U]); + out.push_back(hex[byte & 0x0FU]); + } + return out; +} + +} // namespace moonbase::detail diff --git a/modules/moonbase_licensing/moonbase/detail/crypto/der.hpp b/modules/moonbase_licensing/moonbase/detail/crypto/der.hpp new file mode 100644 index 0000000..622f59e --- /dev/null +++ b/modules/moonbase_licensing/moonbase/detail/crypto/der.hpp @@ -0,0 +1,180 @@ +#pragma once + +// Minimal DER/TLV reader, just enough to normalize an RSA public key into +// PKCS#1 `RSAPublicKey` form and (for the Windows CNG backend) split it into +// its modulus and exponent. Shared by the Apple and Windows crypto backends so +// they accept exactly the same key inputs the OpenSSL backend does: PEM SPKI +// (`-----BEGIN PUBLIC KEY-----`), PEM PKCS#1 (`-----BEGIN RSA PUBLIC KEY-----`), +// and raw base64 of either DER encoding. +// +// The OpenSSL backend does not use this file — it lets OpenSSL parse the key. + +#include +#include +#include +#include +#include + +#include "moonbase/detail/base64.hpp" +#include "moonbase/errors.hpp" + +namespace moonbase::detail::crypto::der { + +struct cursor { + const unsigned char* p; + const unsigned char* end; +}; + +struct tlv { + unsigned char tag; + const unsigned char* content; + std::size_t length; +}; + +inline unsigned char read_byte(cursor& c) +{ + if (c.p >= c.end) { + throw license_invalid_error("Public key is not a supported RSA public key"); + } + return *c.p++; +} + +// Definite-length encoding, short and long form. RSA-2048 keys push the inner +// structures past 127 bytes, so long-form (0x82 hi lo) is the common case, not +// a corner case — handle it or real keys mis-parse. +inline std::size_t read_length(cursor& c) +{ + const unsigned char first = read_byte(c); + if ((first & 0x80U) == 0) { + return first; + } + const unsigned count = first & 0x7FU; + if (count == 0 || count > sizeof(std::size_t)) { + throw license_invalid_error("Public key is not a supported RSA public key"); + } + std::size_t length = 0; + for (unsigned i = 0; i < count; ++i) { + length = (length << 8U) | read_byte(c); + } + return length; +} + +inline tlv read_tlv(cursor& c) +{ + const unsigned char tag = read_byte(c); + const std::size_t length = read_length(c); + // Bounds-check before advancing: a short- or long-form length larger than + // the bytes that remain is a malformed key, not a licence to read past the + // decoded buffer. + if (static_cast(c.end - c.p) < length) { + throw license_invalid_error("Public key is not a supported RSA public key"); + } + const unsigned char* content = c.p; + c.p += length; + return tlv{tag, content, length}; +} + +// Strip the PEM armor (if any) and base64-decode to raw DER bytes. +inline std::vector decode_key_bytes(const std::string& key_material) +{ + if (key_material.find("-----BEGIN") != std::string::npos) { + std::string body; + std::istringstream stream(key_material); + std::string line; + bool inside = false; + while (std::getline(stream, line)) { + if (line.find("-----BEGIN") != std::string::npos) { + inside = true; + continue; + } + if (line.find("-----END") != std::string::npos) { + break; + } + if (inside) { + body += line; + } + } + return base64_decode(body); + } + return base64_decode(key_material); +} + +// Normalize any accepted key shape to PKCS#1 `RSAPublicKey` DER +// (`SEQUENCE { INTEGER n, INTEGER e }`). +inline std::vector normalize_to_pkcs1(const std::string& key_material) +{ + std::vector der = decode_key_bytes(key_material); + if (der.empty()) { + throw license_invalid_error("Public key is not a supported RSA public key"); + } + + const unsigned char* begin = der.data(); + cursor top{der.data(), der.data() + der.size()}; + const tlv outer = read_tlv(top); + if (outer.tag != 0x30) { + throw license_invalid_error("Public key is not a supported RSA public key"); + } + const std::size_t outer_total = static_cast(top.p - begin); + + // Peek the first element inside the outer SEQUENCE to discriminate. + cursor inner{outer.content, outer.content + outer.length}; + cursor peek = inner; + const tlv first = read_tlv(peek); + + if (first.tag == 0x02) { + // INTEGER first => already PKCS#1 RSAPublicKey. Return the outer TLV. + return std::vector(der.begin(), + der.begin() + static_cast(outer_total)); + } + + if (first.tag == 0x30) { + // SEQUENCE first => SPKI: SEQUENCE { AlgorithmIdentifier, BIT STRING }. + read_tlv(inner); // skip AlgorithmIdentifier + const tlv bitstring = read_tlv(inner); + if (bitstring.tag != 0x03 || bitstring.length < 1) { + throw license_invalid_error("Public key is not a supported RSA public key"); + } + // First content byte of a BIT STRING is the unused-bit count (0 here); + // the remainder is the embedded PKCS#1 RSAPublicKey DER. + const unsigned char* pk = bitstring.content + 1; + const std::size_t pk_len = bitstring.length - 1; + return std::vector(pk, pk + pk_len); + } + + throw license_invalid_error("Public key is not a supported RSA public key"); +} + +struct rsa_components { + std::vector modulus; // big-endian, sign byte stripped + std::vector exponent; // big-endian, sign byte stripped +}; + +// Split PKCS#1 `SEQUENCE { INTEGER n, INTEGER e }` into raw big-endian (n, e), +// stripping the leading 0x00 sign byte DER prepends when the high bit is set. +inline rsa_components parse_rsa_pkcs1(const std::vector& pkcs1) +{ + cursor top{pkcs1.data(), pkcs1.data() + pkcs1.size()}; + const tlv seq = read_tlv(top); + if (seq.tag != 0x30) { + throw license_invalid_error("Public key is not a supported RSA public key"); + } + cursor in{seq.content, seq.content + seq.length}; + const tlv modulus = read_tlv(in); + const tlv exponent = read_tlv(in); + if (modulus.tag != 0x02 || exponent.tag != 0x02) { + throw license_invalid_error("Public key is not a supported RSA public key"); + } + + const auto strip = [](const unsigned char* p, std::size_t len) { + while (len > 1 && p[0] == 0x00) { + ++p; + --len; + } + return std::vector(p, p + len); + }; + + return rsa_components{strip(modulus.content, modulus.length), + strip(exponent.content, exponent.length)}; +} + +} // namespace moonbase::detail::crypto::der diff --git a/modules/moonbase_licensing/moonbase/detail/crypto/openssl_backend.hpp b/modules/moonbase_licensing/moonbase/detail/crypto/openssl_backend.hpp new file mode 100644 index 0000000..9ab2d08 --- /dev/null +++ b/modules/moonbase_licensing/moonbase/detail/crypto/openssl_backend.hpp @@ -0,0 +1,165 @@ +#pragma once + +// OpenSSL crypto backend — the default. Selected unless a different +// MOONBASE_CRYPTO_BACKEND is configured. This is the path existing non-JUCE +// consumers (and the test suite) compile, so its behavior and error messages +// are kept identical to the original inline implementation in validator.hpp. + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "moonbase/detail/base64.hpp" +#include "moonbase/errors.hpp" + +namespace moonbase::detail::crypto { + +inline std::array sha256_raw(const unsigned char* data, std::size_t length) +{ + std::array digest{}; + SHA256(data, length, digest.data()); + return digest; +} + +namespace openssl_detail { + +#if defined(__clang__) || defined(__GNUC__) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" +#endif + +using evp_pkey_ptr = std::unique_ptr; +using bio_ptr = std::unique_ptr; +using evp_md_ctx_ptr = std::unique_ptr; + +inline evp_pkey_ptr make_empty_pkey() +{ + return evp_pkey_ptr(nullptr, EVP_PKEY_free); +} + +inline bio_ptr make_memory_bio(const std::string& value) +{ + return bio_ptr(BIO_new_mem_buf(value.data(), static_cast(value.size())), BIO_free); +} + +inline evp_pkey_ptr read_pem_public_key(const std::string& public_key) +{ + { + auto bio = make_memory_bio(public_key); + if (bio) { + if (auto* pkey = PEM_read_bio_PUBKEY(bio.get(), nullptr, nullptr, nullptr)) { + return evp_pkey_ptr(pkey, EVP_PKEY_free); + } + } + } + + { + auto bio = make_memory_bio(public_key); + if (bio) { + if (auto* rsa = PEM_read_bio_RSAPublicKey(bio.get(), nullptr, nullptr, nullptr)) { + auto* pkey = EVP_PKEY_new(); + if (!pkey) { + RSA_free(rsa); + throw license_invalid_error("Could not allocate RSA public key"); + } + if (EVP_PKEY_assign_RSA(pkey, rsa) != 1) { + RSA_free(rsa); + EVP_PKEY_free(pkey); + throw license_invalid_error("Could not assign RSA public key"); + } + return evp_pkey_ptr(pkey, EVP_PKEY_free); + } + } + } + + return make_empty_pkey(); +} + +inline evp_pkey_ptr read_der_public_key(const std::vector& der) +{ + const unsigned char* cursor = der.data(); + if (auto* pkey = d2i_PUBKEY(nullptr, &cursor, static_cast(der.size()))) { + return evp_pkey_ptr(pkey, EVP_PKEY_free); + } + + cursor = der.data(); + if (auto* rsa = d2i_RSAPublicKey(nullptr, &cursor, static_cast(der.size()))) { + auto* pkey = EVP_PKEY_new(); + if (!pkey) { + RSA_free(rsa); + throw license_invalid_error("Could not allocate RSA public key"); + } + if (EVP_PKEY_assign_RSA(pkey, rsa) != 1) { + RSA_free(rsa); + EVP_PKEY_free(pkey); + throw license_invalid_error("Could not assign RSA public key"); + } + return evp_pkey_ptr(pkey, EVP_PKEY_free); + } + + return make_empty_pkey(); +} + +#if defined(__clang__) || defined(__GNUC__) +#pragma GCC diagnostic pop +#endif + +inline evp_pkey_ptr load_public_key(const std::string& public_key) +{ + if (public_key.find("-----BEGIN") != std::string::npos) { + auto pkey = read_pem_public_key(public_key); + if (pkey) { + return pkey; + } + } + + try { + auto der = base64_decode(public_key); + auto pkey = read_der_public_key(der); + if (pkey) { + return pkey; + } + } catch (const std::exception&) { + } + + throw license_invalid_error("Public key is not a supported RSA public key"); +} + +} // namespace openssl_detail + +class rsa_public_key { +public: + explicit rsa_public_key(const std::string& key_material) + : key_(openssl_detail::load_public_key(key_material)) + { + } + + void verify_rs256(std::string_view signing_input, + const std::vector& signature) const + { + openssl_detail::evp_md_ctx_ptr ctx(EVP_MD_CTX_new(), EVP_MD_CTX_free); + if (!ctx) { + throw license_invalid_error("Could not initialize signature verifier"); + } + + if (EVP_DigestVerifyInit(ctx.get(), nullptr, EVP_sha256(), nullptr, key_.get()) != 1 || + EVP_DigestVerifyUpdate(ctx.get(), signing_input.data(), signing_input.size()) != 1 || + EVP_DigestVerifyFinal(ctx.get(), signature.data(), signature.size()) != 1) { + throw license_invalid_error("License token signature is not valid"); + } + } + +private: + openssl_detail::evp_pkey_ptr key_; +}; + +} // namespace moonbase::detail::crypto diff --git a/modules/moonbase_licensing/moonbase/detail/crypto/windows_backend.hpp b/modules/moonbase_licensing/moonbase/detail/crypto/windows_backend.hpp new file mode 100644 index 0000000..af4b57e --- /dev/null +++ b/modules/moonbase_licensing/moonbase/detail/crypto/windows_backend.hpp @@ -0,0 +1,135 @@ +#pragma once + +// Windows crypto backend — CNG (bcrypt). Used on Windows when +// MOONBASE_CRYPTO_NATIVE is set (the JUCE module), so the module needs no +// OpenSSL. Unlike the Apple "Message" algorithm, BCryptVerifySignature wants a +// pre-computed SHA-256 digest plus a PKCS#1 padding descriptor. + +#include +#include +#include +#include +#include +#include + +#ifndef NOMINMAX +#define NOMINMAX +#endif +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include + +#include + +#include "moonbase/detail/crypto/der.hpp" +#include "moonbase/errors.hpp" + +namespace moonbase::detail::crypto { + +inline std::array sha256_raw(const unsigned char* data, std::size_t length) +{ + std::array digest{}; + + BCRYPT_ALG_HANDLE algorithm = nullptr; + if (!BCRYPT_SUCCESS(BCryptOpenAlgorithmProvider(&algorithm, BCRYPT_SHA256_ALGORITHM, nullptr, 0))) { + throw license_invalid_error("Could not initialize signature verifier"); + } + + BCRYPT_HASH_HANDLE hash = nullptr; + NTSTATUS status = BCryptCreateHash(algorithm, &hash, nullptr, 0, nullptr, 0, 0); + if (BCRYPT_SUCCESS(status)) { + status = BCryptHashData(hash, const_cast(reinterpret_cast(data)), + static_cast(length), 0); + } + if (BCRYPT_SUCCESS(status)) { + status = BCryptFinishHash(hash, digest.data(), static_cast(digest.size()), 0); + } + if (hash != nullptr) { + BCryptDestroyHash(hash); + } + BCryptCloseAlgorithmProvider(algorithm, 0); + + if (!BCRYPT_SUCCESS(status)) { + throw license_invalid_error("Could not initialize signature verifier"); + } + return digest; +} + +class rsa_public_key { +public: + explicit rsa_public_key(const std::string& key_material) + { + const std::vector pkcs1 = der::normalize_to_pkcs1(key_material); + const der::rsa_components components = der::parse_rsa_pkcs1(pkcs1); + + if (!BCRYPT_SUCCESS(BCryptOpenAlgorithmProvider(&algorithm_, BCRYPT_RSA_ALGORITHM, nullptr, 0))) { + throw license_invalid_error("Public key is not a supported RSA public key"); + } + + BCRYPT_RSAKEY_BLOB header{}; + header.Magic = BCRYPT_RSAPUBLIC_MAGIC; + header.BitLength = static_cast(components.modulus.size() * 8U); + header.cbPublicExp = static_cast(components.exponent.size()); + header.cbModulus = static_cast(components.modulus.size()); + header.cbPrime1 = 0; + header.cbPrime2 = 0; + + std::vector blob(sizeof(header) + components.exponent.size() + + components.modulus.size()); + std::memcpy(blob.data(), &header, sizeof(header)); + std::memcpy(blob.data() + sizeof(header), components.exponent.data(), + components.exponent.size()); + std::memcpy(blob.data() + sizeof(header) + components.exponent.size(), + components.modulus.data(), components.modulus.size()); + + const NTSTATUS status = BCryptImportKeyPair( + algorithm_, nullptr, BCRYPT_RSAPUBLIC_BLOB, &key_, + blob.data(), static_cast(blob.size()), 0); + if (!BCRYPT_SUCCESS(status)) { + BCryptCloseAlgorithmProvider(algorithm_, 0); + algorithm_ = nullptr; + throw license_invalid_error("Public key is not a supported RSA public key"); + } + } + + rsa_public_key(const rsa_public_key&) = delete; + rsa_public_key& operator=(const rsa_public_key&) = delete; + rsa_public_key(rsa_public_key&&) = delete; + rsa_public_key& operator=(rsa_public_key&&) = delete; + + ~rsa_public_key() + { + if (key_ != nullptr) { + BCryptDestroyKey(key_); + } + if (algorithm_ != nullptr) { + BCryptCloseAlgorithmProvider(algorithm_, 0); + } + } + + void verify_rs256(std::string_view signing_input, + const std::vector& signature) const + { + const std::array digest = sha256_raw( + reinterpret_cast(signing_input.data()), signing_input.size()); + + BCRYPT_PKCS1_PADDING_INFO padding{}; + padding.pszAlgId = BCRYPT_SHA256_ALGORITHM; + + const NTSTATUS status = BCryptVerifySignature( + key_, &padding, + const_cast(digest.data()), static_cast(digest.size()), + const_cast(signature.data()), static_cast(signature.size()), + BCRYPT_PAD_PKCS1); + if (!BCRYPT_SUCCESS(status)) { + throw license_invalid_error("License token signature is not valid"); + } + } + +private: + BCRYPT_ALG_HANDLE algorithm_ = nullptr; + BCRYPT_KEY_HANDLE key_ = nullptr; +}; + +} // namespace moonbase::detail::crypto diff --git a/modules/moonbase_licensing/moonbase/detail/file_lock.hpp b/modules/moonbase_licensing/moonbase/detail/file_lock.hpp new file mode 100644 index 0000000..44761b6 --- /dev/null +++ b/modules/moonbase_licensing/moonbase/detail/file_lock.hpp @@ -0,0 +1,148 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "moonbase/errors.hpp" + +#if defined(_WIN32) +#include +#include +#include +#include +#include +#else +#include +#include +#include +#include +#endif + +namespace moonbase::detail { + +// RAII exclusive lock on a regular file. Callers pass a dedicated sidecar +// path (e.g. ".lock") — the file is created on first acquire and +// is not assumed to contain anything, so it can outlive any data file the +// caller may later unlink. That separation is what makes the lock safe +// against delete_local_license() on POSIX, where flocking the data file +// would orphan the lock on the unlinked inode. +// +// POSIX: flock(LOCK_EX) on the file's descriptor. Locks are per-open-file +// description and cooperative; only callers that go through this class +// observe each other. +// Windows: CRT _locking(_LK_LOCK) on a single-byte range. The range is +// required by the API but otherwise arbitrary; all callers using this class +// lock the same range. _locking accepts ranges past EOF, so we deliberately +// do not _chsize_s the file — SetEndOfFile on a sibling handle would race +// against the byte lock held by an earlier acquirer and surface as EACCES. +// This intentionally avoids in public SDK headers because +// store.hpp includes this detail header. +// +// Throws moonbase::storage_error on unrecoverable open/lock failures. +class file_lock { +public: + explicit file_lock(const std::filesystem::path& path) + { + const auto parent = path.parent_path(); + if (!parent.empty()) { + std::error_code ec; + std::filesystem::create_directories(parent, ec); + // Directory creation failure is non-fatal here: if the parent + // can't be created the subsequent open will fail with a clearer + // error. + } + +#if defined(_WIN32) + auto fd = -1; + const auto open_error = ::_wsopen_s( + &fd, + path.wstring().c_str(), + _O_RDWR | _O_CREAT | _O_BINARY, + _SH_DENYNO, + _S_IREAD | _S_IWRITE); + if (open_error != 0) { + throw storage_error( + "Could not open license lock file: " + + std::string(std::strerror(open_error))); + } + fd_ = fd; + + if (::_locking(fd_, _LK_LOCK, lock_range_size_) != 0) { + const auto err = errno; + ::_close(fd_); + fd_ = -1; + throw storage_error( + "Could not acquire license file lock: " + + std::string(std::strerror(err))); + } +#else + fd_ = ::open(path.c_str(), O_RDWR | O_CREAT | O_CLOEXEC, 0644); + if (fd_ < 0) { + throw storage_error( + std::string("Could not open license lock file: ") + + std::strerror(errno)); + } + + if (::flock(fd_, LOCK_EX) != 0) { + const auto err = errno; + ::close(fd_); + fd_ = -1; + throw storage_error( + std::string("Could not acquire license file lock: ") + + std::strerror(err)); + } +#endif + } + + file_lock(const file_lock&) = delete; + file_lock& operator=(const file_lock&) = delete; + + file_lock(file_lock&& other) noexcept + { + fd_ = other.fd_; + other.fd_ = -1; + } + + file_lock& operator=(file_lock&& other) noexcept + { + if (this != &other) { + release(); + fd_ = other.fd_; + other.fd_ = -1; + } + return *this; + } + + ~file_lock() { release(); } + +private: + void release() noexcept + { +#if defined(_WIN32) + if (fd_ >= 0) { + ::_lseek(fd_, 0, SEEK_SET); + ::_locking(fd_, _LK_UNLCK, lock_range_size_); + ::_close(fd_); + fd_ = -1; + } +#else + if (fd_ >= 0) { + ::flock(fd_, LOCK_UN); + ::close(fd_); + fd_ = -1; + } +#endif + } + +#if defined(_WIN32) + static constexpr long lock_range_size_ = 1; +#endif + int fd_ = -1; +}; + +} // namespace moonbase::detail diff --git a/modules/moonbase_licensing/moonbase/detail/time.hpp b/modules/moonbase_licensing/moonbase/detail/time.hpp new file mode 100644 index 0000000..d9b37fb --- /dev/null +++ b/modules/moonbase_licensing/moonbase/detail/time.hpp @@ -0,0 +1,159 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace moonbase::detail { + +inline std::time_t timegm_utc(std::tm* tm) +{ +#if defined(_WIN32) + return _mkgmtime(tm); +#else + return timegm(tm); +#endif +} + +inline std::chrono::system_clock::time_point from_epoch_seconds(long long seconds) +{ + return std::chrono::system_clock::time_point{std::chrono::seconds{seconds}}; +} + +inline long long to_epoch_seconds(std::chrono::system_clock::time_point time) +{ + return std::chrono::duration_cast( + time.time_since_epoch()).count(); +} + +inline int parse_fixed_digits(const std::string& value, std::size_t offset, std::size_t count) +{ + if (offset + count > value.size()) { + throw std::runtime_error("Invalid ISO-8601 timestamp"); + } + + int result = 0; + for (std::size_t index = 0; index != count; ++index) { + const auto c = value[offset + index]; + if (!std::isdigit(static_cast(c))) { + throw std::runtime_error("Invalid ISO-8601 timestamp"); + } + result = (result * 10) + (c - '0'); + } + return result; +} + +inline void require_timestamp_char(const std::string& value, std::size_t offset, char expected) +{ + if (offset >= value.size() || value[offset] != expected) { + throw std::runtime_error("Invalid ISO-8601 timestamp"); + } +} + +inline bool utc_fields_match( + std::time_t epoch, + int year, + int month, + int day, + int hour, + int minute, + int second) +{ + std::tm normalized{}; +#if defined(_WIN32) + if (gmtime_s(&normalized, &epoch) != 0) { + return false; + } +#else + if (gmtime_r(&epoch, &normalized) == nullptr) { + return false; + } +#endif + + return normalized.tm_year == year - 1900 && + normalized.tm_mon == month - 1 && + normalized.tm_mday == day && + normalized.tm_hour == hour && + normalized.tm_min == minute && + normalized.tm_sec == second; +} + +inline std::chrono::system_clock::time_point parse_iso8601_utc(const std::string& value) +{ + if (value.size() < 20) { + throw std::runtime_error("Invalid ISO-8601 timestamp"); + } + + const auto year = parse_fixed_digits(value, 0, 4); + require_timestamp_char(value, 4, '-'); + const auto month = parse_fixed_digits(value, 5, 2); + require_timestamp_char(value, 7, '-'); + const auto day = parse_fixed_digits(value, 8, 2); + require_timestamp_char(value, 10, 'T'); + const auto hour = parse_fixed_digits(value, 11, 2); + require_timestamp_char(value, 13, ':'); + const auto minute = parse_fixed_digits(value, 14, 2); + require_timestamp_char(value, 16, ':'); + const auto second = parse_fixed_digits(value, 17, 2); + + auto cursor = std::size_t{19}; + if (cursor < value.size() && value[cursor] == '.') { + ++cursor; + const auto fraction_start = cursor; + while (cursor < value.size() && std::isdigit(static_cast(value[cursor]))) { + ++cursor; + } + if (cursor == fraction_start) { + throw std::runtime_error("Invalid ISO-8601 timestamp"); + } + } + + if (cursor >= value.size() || value[cursor] != 'Z' || cursor + 1 != value.size()) { + throw std::runtime_error("Invalid ISO-8601 timestamp"); + } + + if (month < 1 || month > 12 || + day < 1 || day > 31 || + hour < 0 || hour > 23 || + minute < 0 || minute > 59 || + second < 0 || second > 59) { + throw std::runtime_error("Invalid ISO-8601 timestamp"); + } + + std::tm tm{}; + tm.tm_year = year - 1900; + tm.tm_mon = month - 1; + tm.tm_mday = day; + tm.tm_hour = hour; + tm.tm_min = minute; + tm.tm_sec = second; + tm.tm_isdst = 0; + + const auto epoch = timegm_utc(&tm); + if (!utc_fields_match(epoch, year, month, day, hour, minute, second)) { + throw std::runtime_error("Invalid UTC timestamp"); + } + return std::chrono::system_clock::from_time_t(epoch); +} + +inline std::string format_iso8601_utc(std::chrono::system_clock::time_point time) +{ + const auto epoch = std::chrono::system_clock::to_time_t(time); + std::tm tm{}; +#if defined(_WIN32) + gmtime_s(&tm, &epoch); +#else + gmtime_r(&epoch, &tm); +#endif + + std::ostringstream out; + out << std::put_time(&tm, "%Y-%m-%dT%H:%M:%SZ"); + return out.str(); +} + +} // namespace moonbase::detail diff --git a/modules/moonbase_licensing/moonbase/detail/url.hpp b/modules/moonbase_licensing/moonbase/detail/url.hpp new file mode 100644 index 0000000..577d6ad --- /dev/null +++ b/modules/moonbase_licensing/moonbase/detail/url.hpp @@ -0,0 +1,56 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace moonbase::detail { + +inline std::string trim_trailing_slashes(std::string value) +{ + while (!value.empty() && value.back() == '/') { + value.pop_back(); + } + return value; +} + +inline std::string url_encode(std::string_view value) +{ + std::ostringstream out; + out << std::uppercase << std::hex; + for (const char ch : value) { + const auto c = static_cast(ch); + if (std::isalnum(c) || c == '-' || c == '_' || c == '.' || c == '~') { + out << static_cast(c); + } else { + out << '%' << std::setw(2) << std::setfill('0') << static_cast(c); + } + } + return out.str(); +} + +inline std::string append_query( + const std::string& base_url, + const std::map& query) +{ + if (query.empty()) { + return base_url; + } + + std::ostringstream out; + out << base_url << (base_url.find('?') == std::string::npos ? '?' : '&'); + bool first = true; + for (const auto& [key, value] : query) { + if (!first) { + out << '&'; + } + first = false; + out << url_encode(key) << '=' << url_encode(value); + } + return out.str(); +} + +} // namespace moonbase::detail diff --git a/modules/moonbase_licensing/moonbase/errors.hpp b/modules/moonbase_licensing/moonbase/errors.hpp new file mode 100644 index 0000000..e7df001 --- /dev/null +++ b/modules/moonbase_licensing/moonbase/errors.hpp @@ -0,0 +1,91 @@ +#pragma once + +#include +#include +#include + +namespace moonbase { + +enum class error_type { + api_error, + license_invalid, + license_expired, + storage_error, + configuration_error, + operation_not_supported, +}; + +class moonbase_error : public std::runtime_error { +public: + moonbase_error(error_type type, std::string message) + : std::runtime_error(std::move(message)), type_(type) + { + } + + [[nodiscard]] error_type type() const noexcept { return type_; } + +private: + error_type type_; +}; + +class configuration_error : public moonbase_error { +public: + explicit configuration_error(const std::string& message) + : moonbase_error(error_type::configuration_error, message) + { + } +}; + +class api_error : public moonbase_error { +public: + api_error(int status_code, std::string message, std::string title = {}, std::string detail = {}) + : moonbase_error(error_type::api_error, std::move(message)), + status_code_(status_code), + title_(std::move(title)), + detail_(std::move(detail)) + { + } + + [[nodiscard]] int status_code() const noexcept { return status_code_; } + [[nodiscard]] const std::string& title() const noexcept { return title_; } + [[nodiscard]] const std::string& detail() const noexcept { return detail_; } + +private: + int status_code_; + std::string title_; + std::string detail_; +}; + +class license_invalid_error : public moonbase_error { +public: + explicit license_invalid_error(const std::string& message) + : moonbase_error(error_type::license_invalid, message) + { + } +}; + +class license_expired_error : public moonbase_error { +public: + explicit license_expired_error(const std::string& message) + : moonbase_error(error_type::license_expired, message) + { + } +}; + +class storage_error : public moonbase_error { +public: + explicit storage_error(const std::string& message) + : moonbase_error(error_type::storage_error, message) + { + } +}; + +class operation_not_supported_error : public moonbase_error { +public: + explicit operation_not_supported_error(const std::string& message) + : moonbase_error(error_type::operation_not_supported, message) + { + } +}; + +} // namespace moonbase diff --git a/modules/moonbase_licensing/moonbase/fingerprint.hpp b/modules/moonbase_licensing/moonbase/fingerprint.hpp new file mode 100644 index 0000000..9d511ab --- /dev/null +++ b/modules/moonbase_licensing/moonbase/fingerprint.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include +#include + +namespace moonbase { + +class fingerprint_provider { +public: + virtual ~fingerprint_provider() = default; + [[nodiscard]] virtual std::string device_name() const = 0; + [[nodiscard]] virtual std::string device_id() const = 0; +}; + +class static_fingerprint_provider : public fingerprint_provider { +public: + static_fingerprint_provider(std::string name, std::string id) + : name_(std::move(name)), id_(std::move(id)) + { + } + + [[nodiscard]] std::string device_name() const override { return name_; } + [[nodiscard]] std::string device_id() const override { return id_; } + +private: + std::string name_; + std::string id_; +}; + +} // namespace moonbase diff --git a/modules/moonbase_licensing/moonbase/http.hpp b/modules/moonbase_licensing/moonbase/http.hpp new file mode 100644 index 0000000..5dc2e86 --- /dev/null +++ b/modules/moonbase_licensing/moonbase/http.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include +#include +#include + +namespace moonbase { + +struct http_request { + std::string method = "GET"; + std::string url; + std::map headers; + std::chrono::milliseconds connect_timeout{0}; + std::chrono::milliseconds request_timeout{0}; + std::string body; +}; + +struct http_response { + long status_code = 0; + std::map headers; + std::string body; +}; + +class http_transport { +public: + virtual ~http_transport() = default; + [[nodiscard]] virtual http_response send(const http_request& request) = 0; +}; + +} // namespace moonbase diff --git a/modules/moonbase_licensing/moonbase/http_curl.hpp b/modules/moonbase_licensing/moonbase/http_curl.hpp new file mode 100644 index 0000000..eec0169 --- /dev/null +++ b/modules/moonbase_licensing/moonbase/http_curl.hpp @@ -0,0 +1,114 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include + +#include "moonbase/errors.hpp" +#include "moonbase/http.hpp" + +namespace moonbase { + +class curl_http_transport : public http_transport { +public: + curl_http_transport() + { + static const auto init_result = curl_global_init(CURL_GLOBAL_DEFAULT); + if (init_result != CURLE_OK) { + throw api_error(0, curl_easy_strerror(init_result)); + } + } + + [[nodiscard]] http_response send(const http_request& request) override + { + std::unique_ptr curl(curl_easy_init(), curl_easy_cleanup); + if (!curl) { + throw api_error(0, "Could not initialize curl"); + } + + http_response response; + curl_easy_setopt(curl.get(), CURLOPT_URL, request.url.c_str()); + curl_easy_setopt(curl.get(), CURLOPT_FOLLOWLOCATION, 1L); + curl_easy_setopt(curl.get(), CURLOPT_CONNECTTIMEOUT_MS, timeout_milliseconds(request.connect_timeout)); + curl_easy_setopt(curl.get(), CURLOPT_TIMEOUT_MS, timeout_milliseconds(request.request_timeout)); + curl_easy_setopt(curl.get(), CURLOPT_WRITEFUNCTION, &curl_http_transport::write_body); + curl_easy_setopt(curl.get(), CURLOPT_WRITEDATA, &response.body); + curl_easy_setopt(curl.get(), CURLOPT_HEADERFUNCTION, &curl_http_transport::write_header); + curl_easy_setopt(curl.get(), CURLOPT_HEADERDATA, &response.headers); + + if (request.method == "POST") { + curl_easy_setopt(curl.get(), CURLOPT_POST, 1L); + curl_easy_setopt(curl.get(), CURLOPT_POSTFIELDS, request.body.c_str()); + curl_easy_setopt(curl.get(), CURLOPT_POSTFIELDSIZE, request.body.size()); + } else if (request.method != "GET") { + curl_easy_setopt(curl.get(), CURLOPT_CUSTOMREQUEST, request.method.c_str()); + if (!request.body.empty()) { + curl_easy_setopt(curl.get(), CURLOPT_POSTFIELDS, request.body.c_str()); + curl_easy_setopt(curl.get(), CURLOPT_POSTFIELDSIZE, request.body.size()); + } + } + + curl_slist* raw_headers = nullptr; + for (const auto& [key, value] : request.headers) { + const auto header = key + ": " + value; + raw_headers = curl_slist_append(raw_headers, header.c_str()); + } + std::unique_ptr headers(raw_headers, curl_slist_free_all); + if (headers) { + curl_easy_setopt(curl.get(), CURLOPT_HTTPHEADER, headers.get()); + } + + const auto result = curl_easy_perform(curl.get()); + if (result != CURLE_OK) { + throw api_error(0, curl_easy_strerror(result)); + } + curl_easy_getinfo(curl.get(), CURLINFO_RESPONSE_CODE, &response.status_code); + return response; + } + +private: + static long timeout_milliseconds(std::chrono::milliseconds timeout) + { + if (timeout.count() <= 0) { + return 0L; + } + if (timeout.count() > std::numeric_limits::max()) { + throw api_error(0, "HTTP timeout is too large"); + } + return static_cast(timeout.count()); + } + + static std::size_t write_body(char* ptr, std::size_t size, std::size_t nmemb, void* userdata) + { + const auto total = size * nmemb; + auto* body = static_cast(userdata); + body->append(ptr, total); + return total; + } + + static std::size_t write_header(char* buffer, std::size_t size, std::size_t nitems, void* userdata) + { + const auto total = size * nitems; + std::string line(buffer, total); + const auto separator = line.find(':'); + if (separator != std::string::npos) { + auto key = line.substr(0, separator); + auto value = line.substr(separator + 1); + while (!value.empty() && (value.front() == ' ' || value.front() == '\t')) { + value.erase(value.begin()); + } + while (!value.empty() && (value.back() == '\r' || value.back() == '\n')) { + value.pop_back(); + } + auto* headers = static_cast*>(userdata); + (*headers)[std::move(key)] = std::move(value); + } + return total; + } +}; + +} // namespace moonbase diff --git a/modules/moonbase_licensing/moonbase/licensing.hpp b/modules/moonbase_licensing/moonbase/licensing.hpp new file mode 100644 index 0000000..37fc0a6 --- /dev/null +++ b/modules/moonbase_licensing/moonbase/licensing.hpp @@ -0,0 +1,271 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "moonbase/client.hpp" +#include "moonbase/default_fingerprint.hpp" +#include "moonbase/detail/base64.hpp" +#include "moonbase/errors.hpp" +#include "moonbase/fingerprint.hpp" +#include "moonbase/http.hpp" +#ifndef MOONBASE_DISABLE_CURL_TRANSPORT +#include "moonbase/http_curl.hpp" +#endif +#include "moonbase/store.hpp" +#include "moonbase/types.hpp" +#include "moonbase/validator.hpp" + +namespace moonbase { + +class licensing { +public: + explicit licensing( + licensing_options options, + std::shared_ptr store = nullptr, + std::shared_ptr fingerprints = nullptr, + std::shared_ptr transport = nullptr) + : options_(normalize_and_validate(std::move(options))), + store_(std::move(store)), + fingerprints_(std::move(fingerprints)), + transport_(std::move(transport)) + { + if (!store_) { + store_ = std::make_shared(); + } + if (!fingerprints_) { + fingerprints_ = std::make_shared(); + } + if (!transport_) { +#ifdef MOONBASE_DISABLE_CURL_TRANSPORT + throw configuration_error( + "An HTTP transport is required (built without the bundled CURL transport)"); +#else + transport_ = std::make_shared(); +#endif + } + validator_ = std::make_shared(options_, fingerprints_); + client_ = std::make_shared(options_, fingerprints_, validator_, transport_); + } + + [[nodiscard]] activation_request request_activation() const + { + return client_->request_activation(); + } + + [[nodiscard]] std::optional get_requested_activation( + const activation_request& activation) const + { + return client_->get_requested_activation(activation); + } + + [[nodiscard]] license validate_token_local(std::string_view token) const + { + return validator_->validate_token(token); + } + + // Same as validate_token_local but does not throw on a past `exp`. Lets a + // caller inspect an aged-out token (e.g. to learn its activation method and + // expiry) and decide what to do with it, rather than only seeing the throw. + [[nodiscard]] license validate_token_local_allow_expired(std::string_view token) const + { + return validator_->validate_token_allow_expired(token); + } + + // Emits the device token ("machine file") for the offline activation flow. + // The returned string is a base64-encoded JSON descriptor of this device + + // product; write it to a file (conventionally with a ".dt" extension) that + // the customer uploads at https://.moonbase.sh/activate to obtain an + // offline-activated license token in return. + [[nodiscard]] std::string generate_device_token() const + { + const nlohmann::json payload{ + {"id", fingerprints_->device_id()}, + {"name", fingerprints_->device_name()}, + {"productId", options_.product_id}, + // The Moonbase API expects this to always be "JWT". + {"format", "JWT"}, + }; + const auto json = payload.dump(); + return detail::base64_encode( + reinterpret_cast(json.data()), json.size()); + } + + // Reads back the offline-activated license token the customer downloaded + // from the portal (the raw JWT contents of the license file). Validates it + // locally — signature, audience, issuer, device fingerprint, and expiry — + // and requires the token to have been issued via offline activation. + // Offline tokens are never re-validated against the API, so there is no + // online counterpart. Persist the result with + // store().store_local_license(...), mirroring the online flow. + [[nodiscard]] license read_offline_license(std::string_view token) const + { + auto local = validator_->validate_token(token); + if (local.method != activation_method::offline) { + throw license_invalid_error( + "License token is not an offline-activated license"); + } + return local; + } + + // should_persist (optional): evaluated inside the SDK's locks immediately + // before the refreshed license would be written to the store. When it + // returns false, the SDK skips the persist but still returns the + // refreshed license. Lets callers cancel writes that have been invalidated + // by concurrent state changes (e.g. the user pressed "Deactivate" while a + // background revalidation was in flight) without dropping the file lock + // in between — the predicate runs while the lock is still held, so a + // racing clearLicense() either bumped its flag before the predicate read + // (we skip), or after (we persist and the clear's own delete-under-lock + // runs strictly after our release). + [[nodiscard]] license validate_token_online( + std::string_view token, + std::function should_persist = {}) const + { + auto local = validator_->validate_token(token); + + if (local.method == activation_method::offline) { + return local; + } + + // Cross-instance deduplication: many DAW plugin instances frequently + // call this concurrently (project load with N plugin instances; in + // sandboxed hosts each instance is its own process). Layered guards: + // 1. Process-local mutex: serializes same-process callers cheaply. + // 2. File lock from the store: serializes across processes that + // share the license file. + // 3. Reload-under-lock + persist-on-success: the second waiter + // observes the fresh validated_at the first writer just wrote + // and short-circuits via the throttle. + std::lock_guard in_process_guard(validate_mutex_); + auto cross_process_guard = store_->lock_for_update(); + + license freshest = local; + try { + if (auto stored = store_->load_local_license(); + stored && stored->activation_id == local.activation_id) { + auto candidate = validator_->validate_token(stored->token); + if (candidate.validated_at > freshest.validated_at) { + freshest = std::move(candidate); + } + } + } catch (const std::exception&) { + // A corrupt or unparseable store entry must not block validation; + // fall back to the caller-supplied token. + } + + const auto age = std::chrono::system_clock::now() - freshest.validated_at; + + // The throttle is only allowed to skip the API while we're also + // inside the grace period — otherwise a min_interval longer than the + // grace period would silently extend "max age without an online + // check" past its advertised limit. + if (age < options_.online_validation_min_interval + && age <= options_.online_validation_grace_period) { + return freshest; + } + + try { + auto refreshed = client_->validate_token_online(token); + // Persist so sibling processes/instances see the fresh + // validated_at and hit the throttle on their next call. Storage + // failures are best-effort — we already have a valid license to + // return. + if (!should_persist || should_persist()) { + try { + store_->store_local_license(refreshed); + } catch (const storage_error&) { + } + } + return refreshed; + } catch (const license_invalid_error&) { + throw; + } catch (const license_expired_error&) { + throw; + } catch (const std::exception&) { + if (age <= options_.online_validation_grace_period) { + return freshest; + } + throw; + } + } + + void revoke_activation(std::string_view token) const + { + // allow_expired: a long-running app may click "Deactivate" after the + // local token has aged out, but the server-side seat is still + // allocated until we POST to /revoke. + const auto local = validator_->validate_token_allow_expired(token); + + if (local.method == activation_method::offline) { + throw operation_not_supported_error( + "Offline-activated licenses cannot be revoked"); + } + if (local.trial) { + throw operation_not_supported_error( + "Trial licenses cannot be revoked"); + } + + client_->revoke_activation(token); + + // Store cleanup is best-effort — the server-side seat is already + // freed, so a local IO failure must not surface as a revoke failure + // (callers would retry against a token the server no longer knows). + // + // Hold the cross-process update lock for the load/delete window so an + // in-flight validate_token_online in a sibling instance can't slip a + // persist between our read and our delete (which would resurrect the + // license the user just revoked). + try { + auto cross_process_guard = store_->lock_for_update(); + if (auto stored = store_->load_local_license(); + stored && stored->activation_id == local.activation_id) { + store_->delete_local_license(); + } + } catch (const storage_error&) { + } + } + + [[nodiscard]] license_store& store() noexcept { return *store_; } + [[nodiscard]] const license_store& store() const noexcept { return *store_; } + + [[nodiscard]] license_client& client() noexcept { return *client_; } + [[nodiscard]] const license_client& client() const noexcept { return *client_; } + + [[nodiscard]] license_validator& validator() noexcept { return *validator_; } + [[nodiscard]] const license_validator& validator() const noexcept { return *validator_; } + + [[nodiscard]] moonbase::fingerprint_provider& fingerprint() noexcept { return *fingerprints_; } + [[nodiscard]] const moonbase::fingerprint_provider& fingerprint() const noexcept { return *fingerprints_; } + +private: + static licensing_options normalize_and_validate(licensing_options options) + { + options.endpoint = detail::trim_trailing_slashes(options.endpoint); + if (options.endpoint.empty()) { + throw configuration_error("Moonbase endpoint is required"); + } + if (options.product_id.empty()) { + throw configuration_error("Moonbase product_id is required"); + } + if (options.public_key.empty()) { + throw configuration_error("Moonbase public_key is required"); + } + return options; + } + + licensing_options options_; + std::shared_ptr store_; + std::shared_ptr fingerprints_; + std::shared_ptr transport_; + std::shared_ptr validator_; + std::shared_ptr client_; + mutable std::mutex validate_mutex_; +}; + +} // namespace moonbase diff --git a/modules/moonbase_licensing/moonbase/moonbase.hpp b/modules/moonbase_licensing/moonbase/moonbase.hpp new file mode 100644 index 0000000..54999f7 --- /dev/null +++ b/modules/moonbase_licensing/moonbase/moonbase.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include "moonbase/client.hpp" +#include "moonbase/default_fingerprint.hpp" +#include "moonbase/errors.hpp" +#include "moonbase/fingerprint.hpp" +#include "moonbase/http.hpp" +#ifndef MOONBASE_DISABLE_CURL_TRANSPORT +#include "moonbase/http_curl.hpp" +#endif +#include "moonbase/licensing.hpp" +#include "moonbase/store.hpp" +#include "moonbase/types.hpp" +#include "moonbase/validator.hpp" diff --git a/modules/moonbase_licensing/moonbase/store.hpp b/modules/moonbase_licensing/moonbase/store.hpp new file mode 100644 index 0000000..2d04f07 --- /dev/null +++ b/modules/moonbase_licensing/moonbase/store.hpp @@ -0,0 +1,171 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include + +#include "moonbase/detail/file_lock.hpp" +#include "moonbase/errors.hpp" +#include "moonbase/types.hpp" + +namespace moonbase { + +// Opaque RAII handle returned by license_store::lock_for_update(). Holding it +// guarantees exclusive access to the underlying store for the lifetime of the +// handle, including across processes when the store is backed by a shared +// resource (e.g. a file). +class store_lock_guard { +public: + virtual ~store_lock_guard() = default; +}; + +class license_store { +public: + virtual ~license_store() = default; + [[nodiscard]] virtual std::optional load_local_license() = 0; + virtual void store_local_license(const license& value) = 0; + virtual void delete_local_license() = 0; + + // Acquires an exclusive lock spanning the load/validate/store critical + // section in licensing::validate_token_online. Returns nullptr if the + // store does not require coordination (e.g. an in-memory store used by a + // single SDK instance); the SDK then relies on its in-process mutex only. + [[nodiscard]] virtual std::unique_ptr lock_for_update() + { + return nullptr; + } +}; + +class memory_license_store : public license_store { +public: + [[nodiscard]] std::optional load_local_license() override + { + std::lock_guard guard(mutex_); + return value_; + } + + void store_local_license(const license& value) override + { + std::lock_guard guard(mutex_); + value_ = value; + } + + void delete_local_license() override + { + std::lock_guard guard(mutex_); + value_.reset(); + } + + // In-process serialization for the validate→persist critical section. + // Without this, a concurrent clearLicense() running on another thread + // (the SDK's validate_mutex_ only serializes validate calls, not external + // mutations of the store) could delete between should_persist returning + // true and the actual store_local_license call, resurrecting the cleared + // license in memory. + // + // Recursive so callers (notably validate_token_online) can keep using + // load_local_license / store_local_license on the same thread while + // holding the guard. + [[nodiscard]] std::unique_ptr lock_for_update() override + { + return std::make_unique(mutex_); + } + +private: + class memory_store_lock : public store_lock_guard { + public: + explicit memory_store_lock(std::recursive_mutex& mutex) : guard_(mutex) {} + + private: + std::lock_guard guard_; + }; + + std::recursive_mutex mutex_; + std::optional value_; +}; + +class file_license_store : public license_store { +public: + explicit file_license_store( + std::filesystem::path path = std::filesystem::current_path() / "license.mb") + : path_(std::move(path)) + { + } + + [[nodiscard]] const std::filesystem::path& path() const noexcept { return path_; } + + [[nodiscard]] std::optional load_local_license() override + { + if (!std::filesystem::exists(path_)) { + return std::nullopt; + } + + std::ifstream file(path_); + if (!file) { + throw storage_error("Could not open local license file for reading"); + } + + try { + nlohmann::json json; + file >> json; + return json.get(); + } catch (const std::exception& ex) { + throw storage_error(std::string("Could not parse local license file: ") + ex.what()); + } + } + + void store_local_license(const license& value) override + { + const auto parent = path_.parent_path(); + if (!parent.empty()) { + std::filesystem::create_directories(parent); + } + + std::ofstream file(path_, std::ios::trunc); + if (!file) { + throw storage_error("Could not open local license file for writing"); + } + file << nlohmann::json(value).dump(2); + } + + void delete_local_license() override + { + std::error_code error; + std::filesystem::remove(path_, error); + if (error) { + throw storage_error("Could not delete local license file: " + error.message()); + } + } + + [[nodiscard]] std::unique_ptr lock_for_update() override + { + // Lock a sidecar file, not the license file itself. delete_local_license + // unlinks the license path, which on POSIX would orphan an flock on + // that inode: a sibling instance could then open a fresh inode at the + // same path and acquire an independent lock, defeating the + // serialization. The sidecar is created on first use and never + // deleted by us, so its inode is stable for the lifetime of the + // process tree. + auto lock_path = path_; + lock_path += ".lock"; + return std::make_unique(lock_path); + } + +private: + class file_store_lock : public store_lock_guard { + public: + explicit file_store_lock(const std::filesystem::path& path) : lock_(path) {} + + private: + detail::file_lock lock_; + }; + + std::filesystem::path path_; +}; + +} // namespace moonbase diff --git a/modules/moonbase_licensing/moonbase/types.hpp b/modules/moonbase_licensing/moonbase/types.hpp new file mode 100644 index 0000000..f3cf3db --- /dev/null +++ b/modules/moonbase_licensing/moonbase/types.hpp @@ -0,0 +1,256 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include + +#include "moonbase/detail/time.hpp" +#include "moonbase/errors.hpp" + +namespace moonbase { + +enum class activation_method { + online, + offline, +}; + +enum class platform { + unknown, + windows, + linux_os, + mac, +}; + +inline std::string to_string(activation_method method) +{ + switch (method) { + case activation_method::online: + return "Online"; + case activation_method::offline: + return "Offline"; + } + return "Online"; +} + +inline activation_method activation_method_from_string(const std::string& value) +{ + if (value == "Online" || value == "online") { + return activation_method::online; + } + if (value == "Offline" || value == "offline") { + return activation_method::offline; + } + throw license_invalid_error("Unknown activation method: " + value); +} + +inline std::string to_string(platform value) +{ + switch (value) { + case platform::windows: + return "Windows"; + case platform::linux_os: + return "Linux"; + case platform::mac: + return "Mac"; + case platform::unknown: + return "Unknown"; + } + return "Unknown"; +} + +inline platform current_platform() +{ +#if defined(_WIN32) + return platform::windows; +#elif defined(__APPLE__) + return platform::mac; +#elif defined(__linux__) + return platform::linux_os; +#else + return platform::unknown; +#endif +} + +struct product { + std::string id; + std::string name; + std::optional current_release_version; + nlohmann::json properties = nlohmann::json::object(); +}; + +struct user { + std::string id; + std::string name; + std::string email; + nlohmann::json properties = nlohmann::json::object(); +}; + +struct license { + std::string id; + std::string activation_id; + bool trial = false; + activation_method method = activation_method::online; + product licensed_product; + user issued_to; + std::chrono::system_clock::time_point issued_at{}; + std::optional expires_at; + std::chrono::system_clock::time_point validated_at{}; + std::vector owned_sub_product_ids; + std::optional subscription_id; + std::optional seat_count; // total activation seats (from token claims), if provided + std::optional seats_used; // seats currently in use, if provided + nlohmann::json properties = nlohmann::json::object(); + std::string token; +}; + +struct activation_request { + std::string id; + std::string request_url; + std::string browser_url; +}; + +struct licensing_options { + std::string endpoint; + std::string product_id; + std::string public_key; + std::optional account_id; + platform target_platform = current_platform(); + std::optional application_version; + // Identifies a higher-level integration built on top of the SDK (e.g. the + // JUCE module). Appended to the User-Agent after "moonbase-cpp/" so + // the server can tell which client made the request. + std::optional client_info; + std::map metadata; + std::chrono::milliseconds http_connect_timeout{std::chrono::seconds{10}}; + std::chrono::milliseconds http_request_timeout{std::chrono::seconds{30}}; + std::chrono::seconds online_validation_grace_period{std::chrono::hours(24 * 7)}; + std::chrono::seconds online_validation_min_interval{std::chrono::minutes(5)}; +}; + +inline void to_json(nlohmann::json& json, const product& value) +{ + json = nlohmann::json{ + {"id", value.id}, + {"name", value.name}, + {"properties", value.properties}, + }; + if (value.current_release_version) { + json["currentReleaseVersion"] = *value.current_release_version; + } +} + +inline void from_json(const nlohmann::json& json, product& value) +{ + json.at("id").get_to(value.id); + json.at("name").get_to(value.name); + if (json.contains("currentReleaseVersion") && !json.at("currentReleaseVersion").is_null()) { + value.current_release_version = json.at("currentReleaseVersion").get(); + } else { + value.current_release_version.reset(); + } + value.properties = json.value("properties", nlohmann::json::object()); +} + +inline void to_json(nlohmann::json& json, const user& value) +{ + json = nlohmann::json{ + {"id", value.id}, + {"name", value.name}, + {"email", value.email}, + {"properties", value.properties}, + }; +} + +inline void from_json(const nlohmann::json& json, user& value) +{ + json.at("id").get_to(value.id); + json.at("name").get_to(value.name); + json.at("email").get_to(value.email); + value.properties = json.value("properties", nlohmann::json::object()); +} + +inline void to_json(nlohmann::json& json, const license& value) +{ + json = nlohmann::json{ + {"id", value.id}, + {"activationId", value.activation_id}, + {"trial", value.trial}, + {"activationMethod", to_string(value.method)}, + {"product", value.licensed_product}, + {"issuedTo", value.issued_to}, + {"issuedAt", detail::format_iso8601_utc(value.issued_at)}, + {"validatedAt", detail::format_iso8601_utc(value.validated_at)}, + {"ownedSubProductIds", value.owned_sub_product_ids}, + {"properties", value.properties}, + {"token", value.token}, + }; + if (value.expires_at) { + json["expiresAt"] = detail::format_iso8601_utc(*value.expires_at); + } + if (value.subscription_id) { + json["subscriptionId"] = *value.subscription_id; + } + if (value.seat_count) { + json["seatCount"] = *value.seat_count; + } + if (value.seats_used) { + json["seatsUsed"] = *value.seats_used; + } +} + +inline void from_json(const nlohmann::json& json, license& value) +{ + json.at("id").get_to(value.id); + json.at("activationId").get_to(value.activation_id); + json.at("trial").get_to(value.trial); + value.method = activation_method_from_string(json.at("activationMethod").get()); + json.at("product").get_to(value.licensed_product); + json.at("issuedTo").get_to(value.issued_to); + value.issued_at = detail::parse_iso8601_utc(json.at("issuedAt").get()); + if (json.contains("expiresAt") && !json.at("expiresAt").is_null()) { + value.expires_at = detail::parse_iso8601_utc(json.at("expiresAt").get()); + } else { + value.expires_at.reset(); + } + value.validated_at = detail::parse_iso8601_utc(json.at("validatedAt").get()); + value.owned_sub_product_ids = json.value("ownedSubProductIds", std::vector{}); + if (json.contains("subscriptionId") && !json.at("subscriptionId").is_null()) { + value.subscription_id = json.at("subscriptionId").get(); + } else { + value.subscription_id.reset(); + } + if (json.contains("seatCount") && !json.at("seatCount").is_null()) { + value.seat_count = json.at("seatCount").get(); + } else { + value.seat_count.reset(); + } + if (json.contains("seatsUsed") && !json.at("seatsUsed").is_null()) { + value.seats_used = json.at("seatsUsed").get(); + } else { + value.seats_used.reset(); + } + value.properties = json.value("properties", nlohmann::json::object()); + json.at("token").get_to(value.token); +} + +inline void to_json(nlohmann::json& json, const activation_request& value) +{ + json = nlohmann::json{ + {"id", value.id}, + {"request", value.request_url}, + {"browser", value.browser_url}, + }; +} + +inline void from_json(const nlohmann::json& json, activation_request& value) +{ + json.at("id").get_to(value.id); + json.at("request").get_to(value.request_url); + json.at("browser").get_to(value.browser_url); +} + +} // namespace moonbase diff --git a/modules/moonbase_licensing/moonbase/validator.hpp b/modules/moonbase_licensing/moonbase/validator.hpp new file mode 100644 index 0000000..6854581 --- /dev/null +++ b/modules/moonbase_licensing/moonbase/validator.hpp @@ -0,0 +1,319 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include "moonbase/detail/base64.hpp" +#include "moonbase/detail/crypto/crypto.hpp" +#include "moonbase/detail/time.hpp" +#include "moonbase/errors.hpp" +#include "moonbase/fingerprint.hpp" +#include "moonbase/types.hpp" + +namespace moonbase { + +namespace detail { + +inline std::vector split_jwt(std::string_view token) +{ + std::vector parts; + std::size_t start = 0; + while (start <= token.size()) { + const auto next = token.find('.', start); + if (next == std::string_view::npos) { + parts.emplace_back(token.substr(start)); + break; + } + parts.emplace_back(token.substr(start, next - start)); + start = next + 1; + } + + if (parts.size() != 3 || parts[0].empty() || parts[1].empty() || parts[2].empty()) { + throw license_invalid_error("License token is malformed"); + } + return parts; +} + +inline std::string trim_ascii_whitespace(std::string value) +{ + while (!value.empty() && std::isspace(static_cast(value.front()))) { + value.erase(value.begin()); + } + while (!value.empty() && std::isspace(static_cast(value.back()))) { + value.pop_back(); + } + return value; +} + +inline nlohmann::json decode_jwt_json(const std::string& encoded, const char* label) +{ + try { + return nlohmann::json::parse(bytes_to_string(base64url_decode(encoded))); + } catch (const std::exception& ex) { + throw license_invalid_error(std::string("License token ") + label + " is malformed: " + ex.what()); + } +} + +inline bool has_audience(const nlohmann::json& payload, const std::string& expected) +{ + if (!payload.contains("aud")) { + return false; + } + const auto& aud = payload.at("aud"); + if (aud.is_string()) { + return aud.get() == expected; + } + if (aud.is_array()) { + return std::any_of(aud.begin(), aud.end(), [&](const nlohmann::json& item) { + return item.is_string() && item.get() == expected; + }); + } + return false; +} + +inline std::string require_string(const nlohmann::json& json, const char* key) +{ + if (!json.contains(key) || !json.at(key).is_string()) { + throw license_invalid_error(std::string("License token is missing string claim ") + key); + } + return json.at(key).get(); +} + +inline std::optional optional_string(const nlohmann::json& json, const char* key) +{ + if (!json.contains(key) || json.at(key).is_null()) { + return std::nullopt; + } + if (!json.at(key).is_string()) { + throw license_invalid_error(std::string("License token claim is not a string: ") + key); + } + return json.at(key).get(); +} + +inline bool require_boolish(const nlohmann::json& json, const char* key) +{ + if (!json.contains(key)) { + throw license_invalid_error(std::string("License token is missing boolean claim ") + key); + } + const auto& value = json.at(key); + if (value.is_boolean()) { + return value.get(); + } + if (value.is_string()) { + const auto text = value.get(); + if (text == "true" || text == "True") { + return true; + } + if (text == "false" || text == "False") { + return false; + } + } + throw license_invalid_error(std::string("License token claim is not boolean: ") + key); +} + +inline long long require_integer(const nlohmann::json& json, const char* key) +{ + if (!json.contains(key) || !json.at(key).is_number_integer()) { + throw license_invalid_error(std::string("License token is missing integer claim ") + key); + } + return json.at(key).get(); +} + +inline std::optional optional_integer(const nlohmann::json& json, const char* key) +{ + if (!json.contains(key) || json.at(key).is_null()) { + return std::nullopt; + } + if (!json.at(key).is_number_integer()) { + throw license_invalid_error(std::string("License token claim is not an integer: ") + key); + } + return json.at(key).get(); +} + +inline std::vector split_claim_list(const std::optional& value) +{ + std::vector result; + if (!value || value->empty()) { + return result; + } + std::stringstream stream(*value); + std::string item; + while (std::getline(stream, item, ',')) { + if (!item.empty()) { + result.push_back(item); + } + } + return result; +} + +inline std::chrono::system_clock::time_point require_validation_time(const nlohmann::json& payload) +{ + if (auto epoch = optional_integer(payload, "validated")) { + return from_epoch_seconds(*epoch); + } + if (auto iso = optional_string(payload, "ver")) { + try { + return parse_iso8601_utc(*iso); + } catch (const std::exception& ex) { + throw license_invalid_error( + std::string("License token validation timestamp is malformed: ") + ex.what()); + } + } + throw license_invalid_error("License token is missing validation timestamp"); +} + +inline nlohmann::json object_claim_or_empty(const nlohmann::json& payload, const char* key) +{ + if (!payload.contains(key) || payload.at(key).is_null()) { + return nlohmann::json::object(); + } + if (!payload.at(key).is_object()) { + throw license_invalid_error(std::string("License token claim is not an object: ") + key); + } + return payload.at(key); +} + +} // namespace detail + +class license_validator { +public: + license_validator(licensing_options options, std::shared_ptr fingerprints) + : options_(std::move(options)), + fingerprints_(std::move(fingerprints)), + key_(options_.public_key) + { + if (!fingerprints_) { + throw configuration_error("A fingerprint provider is required"); + } + } + + [[nodiscard]] license validate_token(std::string_view token) const + { + return validate_token_internal(token, false); + } + + // Same as validate_token, but does not throw on a past `exp`. Intended for + // operations like revoke where the seat may still be allocated server-side + // even after the local token has aged out. All other checks (signature, + // audience, issuer, device match) still apply. + [[nodiscard]] license validate_token_allow_expired(std::string_view token) const + { + return validate_token_internal(token, true); + } + +private: + [[nodiscard]] license validate_token_internal(std::string_view token, bool allow_expired) const + { + const auto token_string = detail::trim_ascii_whitespace(std::string(token)); + const auto parts = detail::split_jwt(token_string); + const auto header = detail::decode_jwt_json(parts[0], "header"); + const auto payload = detail::decode_jwt_json(parts[1], "payload"); + + if (!header.contains("alg") || header.at("alg") != "RS256") { + throw license_invalid_error("License token algorithm is not supported"); + } + + const auto signing_input = parts[0] + "." + parts[1]; + std::vector signature; + try { + signature = detail::base64url_decode(parts[2]); + } catch (const std::exception& ex) { + throw license_invalid_error( + std::string("License token signature is malformed: ") + ex.what()); + } + key_.verify_rs256(signing_input, signature); + + if (!detail::has_audience(payload, options_.product_id)) { + throw license_invalid_error("License token audience does not match the configured product"); + } + + if (options_.account_id) { + if (!payload.contains("iss") || + !payload.at("iss").is_string() || + payload.at("iss").get() != *options_.account_id) { + throw license_invalid_error("License token issuer does not match the configured account"); + } + } + + license result; + result.activation_id = detail::require_string(payload, "id"); + result.id = detail::require_string(payload, "l:id"); + result.trial = detail::require_boolish(payload, "trial"); + result.method = activation_method_from_string(detail::require_string(payload, "method")); + result.issued_at = detail::from_epoch_seconds(detail::require_integer(payload, "iat")); + if (auto exp = detail::optional_integer(payload, "exp")) { + result.expires_at = detail::from_epoch_seconds(*exp); + } + result.validated_at = detail::require_validation_time(payload); + result.token = token_string; + + result.licensed_product.id = detail::require_string(payload, "p:id"); + result.licensed_product.name = detail::require_string(payload, "p:name"); + result.licensed_product.current_release_version = detail::optional_string(payload, "p:rel"); + result.licensed_product.properties = detail::object_claim_or_empty(payload, "p:properties"); + + result.owned_sub_product_ids = detail::split_claim_list(detail::optional_string(payload, "sp:owned")); + result.subscription_id = detail::optional_string(payload, "s:id"); + + result.issued_to.id = detail::require_string(payload, "u:id"); + result.issued_to.name = detail::require_string(payload, "u:name"); + result.issued_to.email = detail::require_string(payload, "u:email"); + result.issued_to.properties = detail::object_claim_or_empty(payload, "u:properties"); + result.properties = result.trial + ? detail::object_claim_or_empty(payload, "t:properties") + : detail::object_claim_or_empty(payload, "l:properties"); + + // Seat counts. The backend issues these as the claims p:seats:total / + // p:seats:used (p:seats:remaining is derivable). Fall back to a couple of + // alternative spellings and to the license properties object. + result.seat_count = detail::optional_integer(payload, "p:seats:total"); + result.seats_used = detail::optional_integer(payload, "p:seats:used"); + if (!result.seat_count) { + result.seat_count = detail::optional_integer(payload, "seats"); + } + if (!result.seats_used) { + result.seats_used = detail::optional_integer(payload, "seatsUsed"); + } + const auto seat_from_properties = [&result](const char* key) -> std::optional { + if (result.properties.contains(key) && result.properties.at(key).is_number_integer()) { + return result.properties.at(key).get(); + } + return std::nullopt; + }; + if (!result.seat_count) { + result.seat_count = seat_from_properties("seats"); + } + if (!result.seats_used) { + result.seats_used = seat_from_properties("seatsUsed"); + } + + if (!allow_expired && result.expires_at + && *result.expires_at < std::chrono::system_clock::now()) { + throw license_expired_error("License has expired"); + } + + const auto expected_signature = fingerprints_->device_id(); + const auto actual_signature = detail::require_string(payload, "sig"); + if (actual_signature != expected_signature) { + throw license_invalid_error("License does not match the current device"); + } + + return result; + } + + licensing_options options_; + std::shared_ptr fingerprints_; + detail::rsa_public_key key_; +}; + +} // namespace moonbase diff --git a/modules/moonbase_licensing/moonbase_licensing.cpp b/modules/moonbase_licensing/moonbase_licensing.cpp new file mode 100644 index 0000000..db27ddd --- /dev/null +++ b/modules/moonbase_licensing/moonbase_licensing.cpp @@ -0,0 +1,23 @@ +// Single compilation unit for the moonbase_licensing JUCE module. +// +// On Apple platforms JUCE compiles moonbase_licensing.mm instead, which simply +// #includes this file so the same body builds as Objective-C++ (the Apple +// crypto backend uses Security.framework). Everything the module defines is +// funneled through here so there is exactly one translation unit. + +#ifdef MOONBASE_LICENSING_H_INCLUDED + /* When you add this cpp file to your project, you mustn't include it in a file + where you've already included any other headers - just put it inside a file + on its own, possibly with your config flags preceding it, but don't include + anything else. That also includes avoiding any automatic prefix header files + that the compiler may be using. */ + #error "Incorrect use of JUCE cpp file" +#endif + +#include "moonbase_licensing.h" + +//============================================================================== +#include "juce/ActivationController.cpp" +#include "juce/ui/ActivationLookAndFeel.cpp" +#include "juce/ui/ActivationComponent.cpp" +#include "juce/ui/ActivationDialog.cpp" diff --git a/modules/moonbase_licensing/moonbase_licensing.h b/modules/moonbase_licensing/moonbase_licensing.h new file mode 100644 index 0000000..e194bf4 --- /dev/null +++ b/modules/moonbase_licensing/moonbase_licensing.h @@ -0,0 +1,77 @@ +/******************************************************************************* + The block below describes the properties of this module, and is read by + the Projucer to automatically generate project code that uses it. + For details about the syntax and how to create or use a module, see the + JUCE Module Format.md file. + + + BEGIN_JUCE_MODULE_DECLARATION + + ID: moonbase_licensing + vendor: Moonbase + version: 3.1.0 + name: Moonbase Licensing + description: Moonbase license activation for JUCE apps and plugins, with a built-in activation UI. Talks to the Moonbase API natively — no juce::OnlineUnlockStatus. Zero third-party dependencies: JUCE WebInputStream transport, bundled nlohmann/json, and OS-native RS256 verification (Security.framework / CNG / libcrypto). + website: https://moonbase.sh + license: MIT + minimumCppStandard: 17 + + dependencies: juce_core juce_events juce_data_structures juce_graphics juce_gui_basics juce_animation + OSXFrameworks: Security + iOSFrameworks: Security + windowsLibs: bcrypt + linuxLibs: crypto + searchpaths: . vendor + + END_JUCE_MODULE_DECLARATION + +*******************************************************************************/ + +#pragma once +#define MOONBASE_LICENSING_H_INCLUDED + +//============================================================================== +// Module configuration. Both are forced on so the module pulls in no +// third-party crypto/HTTP — define them yourself before this header to override. + +#ifndef MOONBASE_CRYPTO_NATIVE + #define MOONBASE_CRYPTO_NATIVE 1 // Security.framework / CNG / system libcrypto +#endif + +#ifndef MOONBASE_DISABLE_CURL_TRANSPORT + #define MOONBASE_DISABLE_CURL_TRANSPORT 1 // use the JUCE WebInputStream transport instead +#endif + +// Module version (keep in sync with the `version:` field above). Also used as +// the SDK version when it isn't otherwise defined for this build, so the base +// client's User-Agent reports a real version instead of 0.0.0. +#ifndef MOONBASE_LICENSING_VERSION + #define MOONBASE_LICENSING_VERSION "3.1.0" +#endif +#ifndef MOONBASE_CPP_VERSION + #define MOONBASE_CPP_VERSION MOONBASE_LICENSING_VERSION +#endif + +//============================================================================== +#include +#include +#include +#include +#include +#include + +// The Moonbase C++ SDK (header-only). Resolved from this module's own copy via +// the `.` searchpath; nlohmann/json from the `vendor` searchpath. +#include + +//============================================================================== +// Native integration + built-in UI. +#include "juce/juce_http_transport.h" +#include "juce/juce_fingerprint_provider.h" +#include "juce/JuceMetadata.h" +#include "juce/LicenseGate.h" +#include "juce/ActivationConfig.h" +#include "juce/ActivationController.h" +#include "juce/ui/ActivationLookAndFeel.h" +#include "juce/ui/ActivationComponent.h" +#include "juce/ui/ActivationDialog.h" diff --git a/modules/moonbase_licensing/moonbase_licensing.mm b/modules/moonbase_licensing/moonbase_licensing.mm new file mode 100644 index 0000000..3f56e33 --- /dev/null +++ b/modules/moonbase_licensing/moonbase_licensing.mm @@ -0,0 +1,5 @@ +// Apple compilation unit. JUCE compiles this instead of moonbase_licensing.cpp +// on macOS/iOS so the module body builds as Objective-C++; the Apple crypto +// backend (Security.framework / CommonCrypto) and any Cocoa-touching helpers +// compile here. +#include "moonbase_licensing.cpp" diff --git a/modules/moonbase_licensing/vendor/nlohmann/json.hpp b/modules/moonbase_licensing/vendor/nlohmann/json.hpp new file mode 100644 index 0000000..8b72ea6 --- /dev/null +++ b/modules/moonbase_licensing/vendor/nlohmann/json.hpp @@ -0,0 +1,24765 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.11.3 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann +// SPDX-License-Identifier: MIT + +/****************************************************************************\ + * Note on documentation: The source files contain links to the online * + * documentation of the public API at https://json.nlohmann.me. This URL * + * contains the most recent documentation and should also be applicable to * + * previous versions; documentation for deprecated functions is not * + * removed, but marked deprecated. See "Generate documentation" section in * + * file docs/README.md. * +\****************************************************************************/ + +#ifndef INCLUDE_NLOHMANN_JSON_HPP_ +#define INCLUDE_NLOHMANN_JSON_HPP_ + +#include // all_of, find, for_each +#include // nullptr_t, ptrdiff_t, size_t +#include // hash, less +#include // initializer_list +#ifndef JSON_NO_IO + #include // istream, ostream +#endif // JSON_NO_IO +#include // random_access_iterator_tag +#include // unique_ptr +#include // string, stoi, to_string +#include // declval, forward, move, pair, swap +#include // vector + +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.11.3 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +#include + +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.11.3 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +// This file contains all macro definitions affecting or depending on the ABI + +#ifndef JSON_SKIP_LIBRARY_VERSION_CHECK + #if defined(NLOHMANN_JSON_VERSION_MAJOR) && defined(NLOHMANN_JSON_VERSION_MINOR) && defined(NLOHMANN_JSON_VERSION_PATCH) + #if NLOHMANN_JSON_VERSION_MAJOR != 3 || NLOHMANN_JSON_VERSION_MINOR != 11 || NLOHMANN_JSON_VERSION_PATCH != 3 + #warning "Already included a different version of the library!" + #endif + #endif +#endif + +#define NLOHMANN_JSON_VERSION_MAJOR 3 // NOLINT(modernize-macro-to-enum) +#define NLOHMANN_JSON_VERSION_MINOR 11 // NOLINT(modernize-macro-to-enum) +#define NLOHMANN_JSON_VERSION_PATCH 3 // NOLINT(modernize-macro-to-enum) + +#ifndef JSON_DIAGNOSTICS + #define JSON_DIAGNOSTICS 0 +#endif + +#ifndef JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON + #define JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON 0 +#endif + +#if JSON_DIAGNOSTICS + #define NLOHMANN_JSON_ABI_TAG_DIAGNOSTICS _diag +#else + #define NLOHMANN_JSON_ABI_TAG_DIAGNOSTICS +#endif + +#if JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON + #define NLOHMANN_JSON_ABI_TAG_LEGACY_DISCARDED_VALUE_COMPARISON _ldvcmp +#else + #define NLOHMANN_JSON_ABI_TAG_LEGACY_DISCARDED_VALUE_COMPARISON +#endif + +#ifndef NLOHMANN_JSON_NAMESPACE_NO_VERSION + #define NLOHMANN_JSON_NAMESPACE_NO_VERSION 0 +#endif + +// Construct the namespace ABI tags component +#define NLOHMANN_JSON_ABI_TAGS_CONCAT_EX(a, b) json_abi ## a ## b +#define NLOHMANN_JSON_ABI_TAGS_CONCAT(a, b) \ + NLOHMANN_JSON_ABI_TAGS_CONCAT_EX(a, b) + +#define NLOHMANN_JSON_ABI_TAGS \ + NLOHMANN_JSON_ABI_TAGS_CONCAT( \ + NLOHMANN_JSON_ABI_TAG_DIAGNOSTICS, \ + NLOHMANN_JSON_ABI_TAG_LEGACY_DISCARDED_VALUE_COMPARISON) + +// Construct the namespace version component +#define NLOHMANN_JSON_NAMESPACE_VERSION_CONCAT_EX(major, minor, patch) \ + _v ## major ## _ ## minor ## _ ## patch +#define NLOHMANN_JSON_NAMESPACE_VERSION_CONCAT(major, minor, patch) \ + NLOHMANN_JSON_NAMESPACE_VERSION_CONCAT_EX(major, minor, patch) + +#if NLOHMANN_JSON_NAMESPACE_NO_VERSION +#define NLOHMANN_JSON_NAMESPACE_VERSION +#else +#define NLOHMANN_JSON_NAMESPACE_VERSION \ + NLOHMANN_JSON_NAMESPACE_VERSION_CONCAT(NLOHMANN_JSON_VERSION_MAJOR, \ + NLOHMANN_JSON_VERSION_MINOR, \ + NLOHMANN_JSON_VERSION_PATCH) +#endif + +// Combine namespace components +#define NLOHMANN_JSON_NAMESPACE_CONCAT_EX(a, b) a ## b +#define NLOHMANN_JSON_NAMESPACE_CONCAT(a, b) \ + NLOHMANN_JSON_NAMESPACE_CONCAT_EX(a, b) + +#ifndef NLOHMANN_JSON_NAMESPACE +#define NLOHMANN_JSON_NAMESPACE \ + nlohmann::NLOHMANN_JSON_NAMESPACE_CONCAT( \ + NLOHMANN_JSON_ABI_TAGS, \ + NLOHMANN_JSON_NAMESPACE_VERSION) +#endif + +#ifndef NLOHMANN_JSON_NAMESPACE_BEGIN +#define NLOHMANN_JSON_NAMESPACE_BEGIN \ + namespace nlohmann \ + { \ + inline namespace NLOHMANN_JSON_NAMESPACE_CONCAT( \ + NLOHMANN_JSON_ABI_TAGS, \ + NLOHMANN_JSON_NAMESPACE_VERSION) \ + { +#endif + +#ifndef NLOHMANN_JSON_NAMESPACE_END +#define NLOHMANN_JSON_NAMESPACE_END \ + } /* namespace (inline namespace) NOLINT(readability/namespace) */ \ + } // namespace nlohmann +#endif + +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.11.3 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +#include // transform +#include // array +#include // forward_list +#include // inserter, front_inserter, end +#include // map +#include // string +#include // tuple, make_tuple +#include // is_arithmetic, is_same, is_enum, underlying_type, is_convertible +#include // unordered_map +#include // pair, declval +#include // valarray + +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.11.3 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +#include // nullptr_t +#include // exception +#if JSON_DIAGNOSTICS + #include // accumulate +#endif +#include // runtime_error +#include // to_string +#include // vector + +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.11.3 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +#include // array +#include // size_t +#include // uint8_t +#include // string + +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.11.3 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +#include // declval, pair +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.11.3 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +#include + +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.11.3 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +// #include + + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ + +template struct make_void +{ + using type = void; +}; +template using void_t = typename make_void::type; + +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END + + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ + +// https://en.cppreference.com/w/cpp/experimental/is_detected +struct nonesuch +{ + nonesuch() = delete; + ~nonesuch() = delete; + nonesuch(nonesuch const&) = delete; + nonesuch(nonesuch const&&) = delete; + void operator=(nonesuch const&) = delete; + void operator=(nonesuch&&) = delete; +}; + +template class Op, + class... Args> +struct detector +{ + using value_t = std::false_type; + using type = Default; +}; + +template class Op, class... Args> +struct detector>, Op, Args...> +{ + using value_t = std::true_type; + using type = Op; +}; + +template class Op, class... Args> +using is_detected = typename detector::value_t; + +template class Op, class... Args> +struct is_detected_lazy : is_detected { }; + +template class Op, class... Args> +using detected_t = typename detector::type; + +template class Op, class... Args> +using detected_or = detector; + +template class Op, class... Args> +using detected_or_t = typename detected_or::type; + +template class Op, class... Args> +using is_detected_exact = std::is_same>; + +template class Op, class... Args> +using is_detected_convertible = + std::is_convertible, To>; + +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END + +// #include + + +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.11.3 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann +// SPDX-FileCopyrightText: 2016-2021 Evan Nemerson +// SPDX-License-Identifier: MIT + +/* Hedley - https://nemequ.github.io/hedley + * Created by Evan Nemerson + */ + +#if !defined(JSON_HEDLEY_VERSION) || (JSON_HEDLEY_VERSION < 15) +#if defined(JSON_HEDLEY_VERSION) + #undef JSON_HEDLEY_VERSION +#endif +#define JSON_HEDLEY_VERSION 15 + +#if defined(JSON_HEDLEY_STRINGIFY_EX) + #undef JSON_HEDLEY_STRINGIFY_EX +#endif +#define JSON_HEDLEY_STRINGIFY_EX(x) #x + +#if defined(JSON_HEDLEY_STRINGIFY) + #undef JSON_HEDLEY_STRINGIFY +#endif +#define JSON_HEDLEY_STRINGIFY(x) JSON_HEDLEY_STRINGIFY_EX(x) + +#if defined(JSON_HEDLEY_CONCAT_EX) + #undef JSON_HEDLEY_CONCAT_EX +#endif +#define JSON_HEDLEY_CONCAT_EX(a,b) a##b + +#if defined(JSON_HEDLEY_CONCAT) + #undef JSON_HEDLEY_CONCAT +#endif +#define JSON_HEDLEY_CONCAT(a,b) JSON_HEDLEY_CONCAT_EX(a,b) + +#if defined(JSON_HEDLEY_CONCAT3_EX) + #undef JSON_HEDLEY_CONCAT3_EX +#endif +#define JSON_HEDLEY_CONCAT3_EX(a,b,c) a##b##c + +#if defined(JSON_HEDLEY_CONCAT3) + #undef JSON_HEDLEY_CONCAT3 +#endif +#define JSON_HEDLEY_CONCAT3(a,b,c) JSON_HEDLEY_CONCAT3_EX(a,b,c) + +#if defined(JSON_HEDLEY_VERSION_ENCODE) + #undef JSON_HEDLEY_VERSION_ENCODE +#endif +#define JSON_HEDLEY_VERSION_ENCODE(major,minor,revision) (((major) * 1000000) + ((minor) * 1000) + (revision)) + +#if defined(JSON_HEDLEY_VERSION_DECODE_MAJOR) + #undef JSON_HEDLEY_VERSION_DECODE_MAJOR +#endif +#define JSON_HEDLEY_VERSION_DECODE_MAJOR(version) ((version) / 1000000) + +#if defined(JSON_HEDLEY_VERSION_DECODE_MINOR) + #undef JSON_HEDLEY_VERSION_DECODE_MINOR +#endif +#define JSON_HEDLEY_VERSION_DECODE_MINOR(version) (((version) % 1000000) / 1000) + +#if defined(JSON_HEDLEY_VERSION_DECODE_REVISION) + #undef JSON_HEDLEY_VERSION_DECODE_REVISION +#endif +#define JSON_HEDLEY_VERSION_DECODE_REVISION(version) ((version) % 1000) + +#if defined(JSON_HEDLEY_GNUC_VERSION) + #undef JSON_HEDLEY_GNUC_VERSION +#endif +#if defined(__GNUC__) && defined(__GNUC_PATCHLEVEL__) + #define JSON_HEDLEY_GNUC_VERSION JSON_HEDLEY_VERSION_ENCODE(__GNUC__, __GNUC_MINOR__, __GNUC_PATCHLEVEL__) +#elif defined(__GNUC__) + #define JSON_HEDLEY_GNUC_VERSION JSON_HEDLEY_VERSION_ENCODE(__GNUC__, __GNUC_MINOR__, 0) +#endif + +#if defined(JSON_HEDLEY_GNUC_VERSION_CHECK) + #undef JSON_HEDLEY_GNUC_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_GNUC_VERSION) + #define JSON_HEDLEY_GNUC_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_GNUC_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_GNUC_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_MSVC_VERSION) + #undef JSON_HEDLEY_MSVC_VERSION +#endif +#if defined(_MSC_FULL_VER) && (_MSC_FULL_VER >= 140000000) && !defined(__ICL) + #define JSON_HEDLEY_MSVC_VERSION JSON_HEDLEY_VERSION_ENCODE(_MSC_FULL_VER / 10000000, (_MSC_FULL_VER % 10000000) / 100000, (_MSC_FULL_VER % 100000) / 100) +#elif defined(_MSC_FULL_VER) && !defined(__ICL) + #define JSON_HEDLEY_MSVC_VERSION JSON_HEDLEY_VERSION_ENCODE(_MSC_FULL_VER / 1000000, (_MSC_FULL_VER % 1000000) / 10000, (_MSC_FULL_VER % 10000) / 10) +#elif defined(_MSC_VER) && !defined(__ICL) + #define JSON_HEDLEY_MSVC_VERSION JSON_HEDLEY_VERSION_ENCODE(_MSC_VER / 100, _MSC_VER % 100, 0) +#endif + +#if defined(JSON_HEDLEY_MSVC_VERSION_CHECK) + #undef JSON_HEDLEY_MSVC_VERSION_CHECK +#endif +#if !defined(JSON_HEDLEY_MSVC_VERSION) + #define JSON_HEDLEY_MSVC_VERSION_CHECK(major,minor,patch) (0) +#elif defined(_MSC_VER) && (_MSC_VER >= 1400) + #define JSON_HEDLEY_MSVC_VERSION_CHECK(major,minor,patch) (_MSC_FULL_VER >= ((major * 10000000) + (minor * 100000) + (patch))) +#elif defined(_MSC_VER) && (_MSC_VER >= 1200) + #define JSON_HEDLEY_MSVC_VERSION_CHECK(major,minor,patch) (_MSC_FULL_VER >= ((major * 1000000) + (minor * 10000) + (patch))) +#else + #define JSON_HEDLEY_MSVC_VERSION_CHECK(major,minor,patch) (_MSC_VER >= ((major * 100) + (minor))) +#endif + +#if defined(JSON_HEDLEY_INTEL_VERSION) + #undef JSON_HEDLEY_INTEL_VERSION +#endif +#if defined(__INTEL_COMPILER) && defined(__INTEL_COMPILER_UPDATE) && !defined(__ICL) + #define JSON_HEDLEY_INTEL_VERSION JSON_HEDLEY_VERSION_ENCODE(__INTEL_COMPILER / 100, __INTEL_COMPILER % 100, __INTEL_COMPILER_UPDATE) +#elif defined(__INTEL_COMPILER) && !defined(__ICL) + #define JSON_HEDLEY_INTEL_VERSION JSON_HEDLEY_VERSION_ENCODE(__INTEL_COMPILER / 100, __INTEL_COMPILER % 100, 0) +#endif + +#if defined(JSON_HEDLEY_INTEL_VERSION_CHECK) + #undef JSON_HEDLEY_INTEL_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_INTEL_VERSION) + #define JSON_HEDLEY_INTEL_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_INTEL_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_INTEL_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_INTEL_CL_VERSION) + #undef JSON_HEDLEY_INTEL_CL_VERSION +#endif +#if defined(__INTEL_COMPILER) && defined(__INTEL_COMPILER_UPDATE) && defined(__ICL) + #define JSON_HEDLEY_INTEL_CL_VERSION JSON_HEDLEY_VERSION_ENCODE(__INTEL_COMPILER, __INTEL_COMPILER_UPDATE, 0) +#endif + +#if defined(JSON_HEDLEY_INTEL_CL_VERSION_CHECK) + #undef JSON_HEDLEY_INTEL_CL_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_INTEL_CL_VERSION) + #define JSON_HEDLEY_INTEL_CL_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_INTEL_CL_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_INTEL_CL_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_PGI_VERSION) + #undef JSON_HEDLEY_PGI_VERSION +#endif +#if defined(__PGI) && defined(__PGIC__) && defined(__PGIC_MINOR__) && defined(__PGIC_PATCHLEVEL__) + #define JSON_HEDLEY_PGI_VERSION JSON_HEDLEY_VERSION_ENCODE(__PGIC__, __PGIC_MINOR__, __PGIC_PATCHLEVEL__) +#endif + +#if defined(JSON_HEDLEY_PGI_VERSION_CHECK) + #undef JSON_HEDLEY_PGI_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_PGI_VERSION) + #define JSON_HEDLEY_PGI_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_PGI_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_PGI_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_SUNPRO_VERSION) + #undef JSON_HEDLEY_SUNPRO_VERSION +#endif +#if defined(__SUNPRO_C) && (__SUNPRO_C > 0x1000) + #define JSON_HEDLEY_SUNPRO_VERSION JSON_HEDLEY_VERSION_ENCODE((((__SUNPRO_C >> 16) & 0xf) * 10) + ((__SUNPRO_C >> 12) & 0xf), (((__SUNPRO_C >> 8) & 0xf) * 10) + ((__SUNPRO_C >> 4) & 0xf), (__SUNPRO_C & 0xf) * 10) +#elif defined(__SUNPRO_C) + #define JSON_HEDLEY_SUNPRO_VERSION JSON_HEDLEY_VERSION_ENCODE((__SUNPRO_C >> 8) & 0xf, (__SUNPRO_C >> 4) & 0xf, (__SUNPRO_C) & 0xf) +#elif defined(__SUNPRO_CC) && (__SUNPRO_CC > 0x1000) + #define JSON_HEDLEY_SUNPRO_VERSION JSON_HEDLEY_VERSION_ENCODE((((__SUNPRO_CC >> 16) & 0xf) * 10) + ((__SUNPRO_CC >> 12) & 0xf), (((__SUNPRO_CC >> 8) & 0xf) * 10) + ((__SUNPRO_CC >> 4) & 0xf), (__SUNPRO_CC & 0xf) * 10) +#elif defined(__SUNPRO_CC) + #define JSON_HEDLEY_SUNPRO_VERSION JSON_HEDLEY_VERSION_ENCODE((__SUNPRO_CC >> 8) & 0xf, (__SUNPRO_CC >> 4) & 0xf, (__SUNPRO_CC) & 0xf) +#endif + +#if defined(JSON_HEDLEY_SUNPRO_VERSION_CHECK) + #undef JSON_HEDLEY_SUNPRO_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_SUNPRO_VERSION) + #define JSON_HEDLEY_SUNPRO_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_SUNPRO_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_SUNPRO_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_EMSCRIPTEN_VERSION) + #undef JSON_HEDLEY_EMSCRIPTEN_VERSION +#endif +#if defined(__EMSCRIPTEN__) + #define JSON_HEDLEY_EMSCRIPTEN_VERSION JSON_HEDLEY_VERSION_ENCODE(__EMSCRIPTEN_major__, __EMSCRIPTEN_minor__, __EMSCRIPTEN_tiny__) +#endif + +#if defined(JSON_HEDLEY_EMSCRIPTEN_VERSION_CHECK) + #undef JSON_HEDLEY_EMSCRIPTEN_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_EMSCRIPTEN_VERSION) + #define JSON_HEDLEY_EMSCRIPTEN_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_EMSCRIPTEN_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_EMSCRIPTEN_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_ARM_VERSION) + #undef JSON_HEDLEY_ARM_VERSION +#endif +#if defined(__CC_ARM) && defined(__ARMCOMPILER_VERSION) + #define JSON_HEDLEY_ARM_VERSION JSON_HEDLEY_VERSION_ENCODE(__ARMCOMPILER_VERSION / 1000000, (__ARMCOMPILER_VERSION % 1000000) / 10000, (__ARMCOMPILER_VERSION % 10000) / 100) +#elif defined(__CC_ARM) && defined(__ARMCC_VERSION) + #define JSON_HEDLEY_ARM_VERSION JSON_HEDLEY_VERSION_ENCODE(__ARMCC_VERSION / 1000000, (__ARMCC_VERSION % 1000000) / 10000, (__ARMCC_VERSION % 10000) / 100) +#endif + +#if defined(JSON_HEDLEY_ARM_VERSION_CHECK) + #undef JSON_HEDLEY_ARM_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_ARM_VERSION) + #define JSON_HEDLEY_ARM_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_ARM_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_ARM_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_IBM_VERSION) + #undef JSON_HEDLEY_IBM_VERSION +#endif +#if defined(__ibmxl__) + #define JSON_HEDLEY_IBM_VERSION JSON_HEDLEY_VERSION_ENCODE(__ibmxl_version__, __ibmxl_release__, __ibmxl_modification__) +#elif defined(__xlC__) && defined(__xlC_ver__) + #define JSON_HEDLEY_IBM_VERSION JSON_HEDLEY_VERSION_ENCODE(__xlC__ >> 8, __xlC__ & 0xff, (__xlC_ver__ >> 8) & 0xff) +#elif defined(__xlC__) + #define JSON_HEDLEY_IBM_VERSION JSON_HEDLEY_VERSION_ENCODE(__xlC__ >> 8, __xlC__ & 0xff, 0) +#endif + +#if defined(JSON_HEDLEY_IBM_VERSION_CHECK) + #undef JSON_HEDLEY_IBM_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_IBM_VERSION) + #define JSON_HEDLEY_IBM_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_IBM_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_IBM_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_TI_VERSION) + #undef JSON_HEDLEY_TI_VERSION +#endif +#if \ + defined(__TI_COMPILER_VERSION__) && \ + ( \ + defined(__TMS470__) || defined(__TI_ARM__) || \ + defined(__MSP430__) || \ + defined(__TMS320C2000__) \ + ) +#if (__TI_COMPILER_VERSION__ >= 16000000) + #define JSON_HEDLEY_TI_VERSION JSON_HEDLEY_VERSION_ENCODE(__TI_COMPILER_VERSION__ / 1000000, (__TI_COMPILER_VERSION__ % 1000000) / 1000, (__TI_COMPILER_VERSION__ % 1000)) +#endif +#endif + +#if defined(JSON_HEDLEY_TI_VERSION_CHECK) + #undef JSON_HEDLEY_TI_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_TI_VERSION) + #define JSON_HEDLEY_TI_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_TI_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_TI_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_TI_CL2000_VERSION) + #undef JSON_HEDLEY_TI_CL2000_VERSION +#endif +#if defined(__TI_COMPILER_VERSION__) && defined(__TMS320C2000__) + #define JSON_HEDLEY_TI_CL2000_VERSION JSON_HEDLEY_VERSION_ENCODE(__TI_COMPILER_VERSION__ / 1000000, (__TI_COMPILER_VERSION__ % 1000000) / 1000, (__TI_COMPILER_VERSION__ % 1000)) +#endif + +#if defined(JSON_HEDLEY_TI_CL2000_VERSION_CHECK) + #undef JSON_HEDLEY_TI_CL2000_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_TI_CL2000_VERSION) + #define JSON_HEDLEY_TI_CL2000_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_TI_CL2000_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_TI_CL2000_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_TI_CL430_VERSION) + #undef JSON_HEDLEY_TI_CL430_VERSION +#endif +#if defined(__TI_COMPILER_VERSION__) && defined(__MSP430__) + #define JSON_HEDLEY_TI_CL430_VERSION JSON_HEDLEY_VERSION_ENCODE(__TI_COMPILER_VERSION__ / 1000000, (__TI_COMPILER_VERSION__ % 1000000) / 1000, (__TI_COMPILER_VERSION__ % 1000)) +#endif + +#if defined(JSON_HEDLEY_TI_CL430_VERSION_CHECK) + #undef JSON_HEDLEY_TI_CL430_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_TI_CL430_VERSION) + #define JSON_HEDLEY_TI_CL430_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_TI_CL430_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_TI_CL430_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_TI_ARMCL_VERSION) + #undef JSON_HEDLEY_TI_ARMCL_VERSION +#endif +#if defined(__TI_COMPILER_VERSION__) && (defined(__TMS470__) || defined(__TI_ARM__)) + #define JSON_HEDLEY_TI_ARMCL_VERSION JSON_HEDLEY_VERSION_ENCODE(__TI_COMPILER_VERSION__ / 1000000, (__TI_COMPILER_VERSION__ % 1000000) / 1000, (__TI_COMPILER_VERSION__ % 1000)) +#endif + +#if defined(JSON_HEDLEY_TI_ARMCL_VERSION_CHECK) + #undef JSON_HEDLEY_TI_ARMCL_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_TI_ARMCL_VERSION) + #define JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_TI_ARMCL_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_TI_CL6X_VERSION) + #undef JSON_HEDLEY_TI_CL6X_VERSION +#endif +#if defined(__TI_COMPILER_VERSION__) && defined(__TMS320C6X__) + #define JSON_HEDLEY_TI_CL6X_VERSION JSON_HEDLEY_VERSION_ENCODE(__TI_COMPILER_VERSION__ / 1000000, (__TI_COMPILER_VERSION__ % 1000000) / 1000, (__TI_COMPILER_VERSION__ % 1000)) +#endif + +#if defined(JSON_HEDLEY_TI_CL6X_VERSION_CHECK) + #undef JSON_HEDLEY_TI_CL6X_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_TI_CL6X_VERSION) + #define JSON_HEDLEY_TI_CL6X_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_TI_CL6X_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_TI_CL6X_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_TI_CL7X_VERSION) + #undef JSON_HEDLEY_TI_CL7X_VERSION +#endif +#if defined(__TI_COMPILER_VERSION__) && defined(__C7000__) + #define JSON_HEDLEY_TI_CL7X_VERSION JSON_HEDLEY_VERSION_ENCODE(__TI_COMPILER_VERSION__ / 1000000, (__TI_COMPILER_VERSION__ % 1000000) / 1000, (__TI_COMPILER_VERSION__ % 1000)) +#endif + +#if defined(JSON_HEDLEY_TI_CL7X_VERSION_CHECK) + #undef JSON_HEDLEY_TI_CL7X_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_TI_CL7X_VERSION) + #define JSON_HEDLEY_TI_CL7X_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_TI_CL7X_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_TI_CL7X_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_TI_CLPRU_VERSION) + #undef JSON_HEDLEY_TI_CLPRU_VERSION +#endif +#if defined(__TI_COMPILER_VERSION__) && defined(__PRU__) + #define JSON_HEDLEY_TI_CLPRU_VERSION JSON_HEDLEY_VERSION_ENCODE(__TI_COMPILER_VERSION__ / 1000000, (__TI_COMPILER_VERSION__ % 1000000) / 1000, (__TI_COMPILER_VERSION__ % 1000)) +#endif + +#if defined(JSON_HEDLEY_TI_CLPRU_VERSION_CHECK) + #undef JSON_HEDLEY_TI_CLPRU_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_TI_CLPRU_VERSION) + #define JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_TI_CLPRU_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_CRAY_VERSION) + #undef JSON_HEDLEY_CRAY_VERSION +#endif +#if defined(_CRAYC) + #if defined(_RELEASE_PATCHLEVEL) + #define JSON_HEDLEY_CRAY_VERSION JSON_HEDLEY_VERSION_ENCODE(_RELEASE_MAJOR, _RELEASE_MINOR, _RELEASE_PATCHLEVEL) + #else + #define JSON_HEDLEY_CRAY_VERSION JSON_HEDLEY_VERSION_ENCODE(_RELEASE_MAJOR, _RELEASE_MINOR, 0) + #endif +#endif + +#if defined(JSON_HEDLEY_CRAY_VERSION_CHECK) + #undef JSON_HEDLEY_CRAY_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_CRAY_VERSION) + #define JSON_HEDLEY_CRAY_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_CRAY_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_CRAY_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_IAR_VERSION) + #undef JSON_HEDLEY_IAR_VERSION +#endif +#if defined(__IAR_SYSTEMS_ICC__) + #if __VER__ > 1000 + #define JSON_HEDLEY_IAR_VERSION JSON_HEDLEY_VERSION_ENCODE((__VER__ / 1000000), ((__VER__ / 1000) % 1000), (__VER__ % 1000)) + #else + #define JSON_HEDLEY_IAR_VERSION JSON_HEDLEY_VERSION_ENCODE(__VER__ / 100, __VER__ % 100, 0) + #endif +#endif + +#if defined(JSON_HEDLEY_IAR_VERSION_CHECK) + #undef JSON_HEDLEY_IAR_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_IAR_VERSION) + #define JSON_HEDLEY_IAR_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_IAR_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_IAR_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_TINYC_VERSION) + #undef JSON_HEDLEY_TINYC_VERSION +#endif +#if defined(__TINYC__) + #define JSON_HEDLEY_TINYC_VERSION JSON_HEDLEY_VERSION_ENCODE(__TINYC__ / 1000, (__TINYC__ / 100) % 10, __TINYC__ % 100) +#endif + +#if defined(JSON_HEDLEY_TINYC_VERSION_CHECK) + #undef JSON_HEDLEY_TINYC_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_TINYC_VERSION) + #define JSON_HEDLEY_TINYC_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_TINYC_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_TINYC_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_DMC_VERSION) + #undef JSON_HEDLEY_DMC_VERSION +#endif +#if defined(__DMC__) + #define JSON_HEDLEY_DMC_VERSION JSON_HEDLEY_VERSION_ENCODE(__DMC__ >> 8, (__DMC__ >> 4) & 0xf, __DMC__ & 0xf) +#endif + +#if defined(JSON_HEDLEY_DMC_VERSION_CHECK) + #undef JSON_HEDLEY_DMC_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_DMC_VERSION) + #define JSON_HEDLEY_DMC_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_DMC_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_DMC_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_COMPCERT_VERSION) + #undef JSON_HEDLEY_COMPCERT_VERSION +#endif +#if defined(__COMPCERT_VERSION__) + #define JSON_HEDLEY_COMPCERT_VERSION JSON_HEDLEY_VERSION_ENCODE(__COMPCERT_VERSION__ / 10000, (__COMPCERT_VERSION__ / 100) % 100, __COMPCERT_VERSION__ % 100) +#endif + +#if defined(JSON_HEDLEY_COMPCERT_VERSION_CHECK) + #undef JSON_HEDLEY_COMPCERT_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_COMPCERT_VERSION) + #define JSON_HEDLEY_COMPCERT_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_COMPCERT_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_COMPCERT_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_PELLES_VERSION) + #undef JSON_HEDLEY_PELLES_VERSION +#endif +#if defined(__POCC__) + #define JSON_HEDLEY_PELLES_VERSION JSON_HEDLEY_VERSION_ENCODE(__POCC__ / 100, __POCC__ % 100, 0) +#endif + +#if defined(JSON_HEDLEY_PELLES_VERSION_CHECK) + #undef JSON_HEDLEY_PELLES_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_PELLES_VERSION) + #define JSON_HEDLEY_PELLES_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_PELLES_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_PELLES_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_MCST_LCC_VERSION) + #undef JSON_HEDLEY_MCST_LCC_VERSION +#endif +#if defined(__LCC__) && defined(__LCC_MINOR__) + #define JSON_HEDLEY_MCST_LCC_VERSION JSON_HEDLEY_VERSION_ENCODE(__LCC__ / 100, __LCC__ % 100, __LCC_MINOR__) +#endif + +#if defined(JSON_HEDLEY_MCST_LCC_VERSION_CHECK) + #undef JSON_HEDLEY_MCST_LCC_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_MCST_LCC_VERSION) + #define JSON_HEDLEY_MCST_LCC_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_MCST_LCC_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_MCST_LCC_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_GCC_VERSION) + #undef JSON_HEDLEY_GCC_VERSION +#endif +#if \ + defined(JSON_HEDLEY_GNUC_VERSION) && \ + !defined(__clang__) && \ + !defined(JSON_HEDLEY_INTEL_VERSION) && \ + !defined(JSON_HEDLEY_PGI_VERSION) && \ + !defined(JSON_HEDLEY_ARM_VERSION) && \ + !defined(JSON_HEDLEY_CRAY_VERSION) && \ + !defined(JSON_HEDLEY_TI_VERSION) && \ + !defined(JSON_HEDLEY_TI_ARMCL_VERSION) && \ + !defined(JSON_HEDLEY_TI_CL430_VERSION) && \ + !defined(JSON_HEDLEY_TI_CL2000_VERSION) && \ + !defined(JSON_HEDLEY_TI_CL6X_VERSION) && \ + !defined(JSON_HEDLEY_TI_CL7X_VERSION) && \ + !defined(JSON_HEDLEY_TI_CLPRU_VERSION) && \ + !defined(__COMPCERT__) && \ + !defined(JSON_HEDLEY_MCST_LCC_VERSION) + #define JSON_HEDLEY_GCC_VERSION JSON_HEDLEY_GNUC_VERSION +#endif + +#if defined(JSON_HEDLEY_GCC_VERSION_CHECK) + #undef JSON_HEDLEY_GCC_VERSION_CHECK +#endif +#if defined(JSON_HEDLEY_GCC_VERSION) + #define JSON_HEDLEY_GCC_VERSION_CHECK(major,minor,patch) (JSON_HEDLEY_GCC_VERSION >= JSON_HEDLEY_VERSION_ENCODE(major, minor, patch)) +#else + #define JSON_HEDLEY_GCC_VERSION_CHECK(major,minor,patch) (0) +#endif + +#if defined(JSON_HEDLEY_HAS_ATTRIBUTE) + #undef JSON_HEDLEY_HAS_ATTRIBUTE +#endif +#if \ + defined(__has_attribute) && \ + ( \ + (!defined(JSON_HEDLEY_IAR_VERSION) || JSON_HEDLEY_IAR_VERSION_CHECK(8,5,9)) \ + ) +# define JSON_HEDLEY_HAS_ATTRIBUTE(attribute) __has_attribute(attribute) +#else +# define JSON_HEDLEY_HAS_ATTRIBUTE(attribute) (0) +#endif + +#if defined(JSON_HEDLEY_GNUC_HAS_ATTRIBUTE) + #undef JSON_HEDLEY_GNUC_HAS_ATTRIBUTE +#endif +#if defined(__has_attribute) + #define JSON_HEDLEY_GNUC_HAS_ATTRIBUTE(attribute,major,minor,patch) JSON_HEDLEY_HAS_ATTRIBUTE(attribute) +#else + #define JSON_HEDLEY_GNUC_HAS_ATTRIBUTE(attribute,major,minor,patch) JSON_HEDLEY_GNUC_VERSION_CHECK(major,minor,patch) +#endif + +#if defined(JSON_HEDLEY_GCC_HAS_ATTRIBUTE) + #undef JSON_HEDLEY_GCC_HAS_ATTRIBUTE +#endif +#if defined(__has_attribute) + #define JSON_HEDLEY_GCC_HAS_ATTRIBUTE(attribute,major,minor,patch) JSON_HEDLEY_HAS_ATTRIBUTE(attribute) +#else + #define JSON_HEDLEY_GCC_HAS_ATTRIBUTE(attribute,major,minor,patch) JSON_HEDLEY_GCC_VERSION_CHECK(major,minor,patch) +#endif + +#if defined(JSON_HEDLEY_HAS_CPP_ATTRIBUTE) + #undef JSON_HEDLEY_HAS_CPP_ATTRIBUTE +#endif +#if \ + defined(__has_cpp_attribute) && \ + defined(__cplusplus) && \ + (!defined(JSON_HEDLEY_SUNPRO_VERSION) || JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,15,0)) + #define JSON_HEDLEY_HAS_CPP_ATTRIBUTE(attribute) __has_cpp_attribute(attribute) +#else + #define JSON_HEDLEY_HAS_CPP_ATTRIBUTE(attribute) (0) +#endif + +#if defined(JSON_HEDLEY_HAS_CPP_ATTRIBUTE_NS) + #undef JSON_HEDLEY_HAS_CPP_ATTRIBUTE_NS +#endif +#if !defined(__cplusplus) || !defined(__has_cpp_attribute) + #define JSON_HEDLEY_HAS_CPP_ATTRIBUTE_NS(ns,attribute) (0) +#elif \ + !defined(JSON_HEDLEY_PGI_VERSION) && \ + !defined(JSON_HEDLEY_IAR_VERSION) && \ + (!defined(JSON_HEDLEY_SUNPRO_VERSION) || JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,15,0)) && \ + (!defined(JSON_HEDLEY_MSVC_VERSION) || JSON_HEDLEY_MSVC_VERSION_CHECK(19,20,0)) + #define JSON_HEDLEY_HAS_CPP_ATTRIBUTE_NS(ns,attribute) JSON_HEDLEY_HAS_CPP_ATTRIBUTE(ns::attribute) +#else + #define JSON_HEDLEY_HAS_CPP_ATTRIBUTE_NS(ns,attribute) (0) +#endif + +#if defined(JSON_HEDLEY_GNUC_HAS_CPP_ATTRIBUTE) + #undef JSON_HEDLEY_GNUC_HAS_CPP_ATTRIBUTE +#endif +#if defined(__has_cpp_attribute) && defined(__cplusplus) + #define JSON_HEDLEY_GNUC_HAS_CPP_ATTRIBUTE(attribute,major,minor,patch) __has_cpp_attribute(attribute) +#else + #define JSON_HEDLEY_GNUC_HAS_CPP_ATTRIBUTE(attribute,major,minor,patch) JSON_HEDLEY_GNUC_VERSION_CHECK(major,minor,patch) +#endif + +#if defined(JSON_HEDLEY_GCC_HAS_CPP_ATTRIBUTE) + #undef JSON_HEDLEY_GCC_HAS_CPP_ATTRIBUTE +#endif +#if defined(__has_cpp_attribute) && defined(__cplusplus) + #define JSON_HEDLEY_GCC_HAS_CPP_ATTRIBUTE(attribute,major,minor,patch) __has_cpp_attribute(attribute) +#else + #define JSON_HEDLEY_GCC_HAS_CPP_ATTRIBUTE(attribute,major,minor,patch) JSON_HEDLEY_GCC_VERSION_CHECK(major,minor,patch) +#endif + +#if defined(JSON_HEDLEY_HAS_BUILTIN) + #undef JSON_HEDLEY_HAS_BUILTIN +#endif +#if defined(__has_builtin) + #define JSON_HEDLEY_HAS_BUILTIN(builtin) __has_builtin(builtin) +#else + #define JSON_HEDLEY_HAS_BUILTIN(builtin) (0) +#endif + +#if defined(JSON_HEDLEY_GNUC_HAS_BUILTIN) + #undef JSON_HEDLEY_GNUC_HAS_BUILTIN +#endif +#if defined(__has_builtin) + #define JSON_HEDLEY_GNUC_HAS_BUILTIN(builtin,major,minor,patch) __has_builtin(builtin) +#else + #define JSON_HEDLEY_GNUC_HAS_BUILTIN(builtin,major,minor,patch) JSON_HEDLEY_GNUC_VERSION_CHECK(major,minor,patch) +#endif + +#if defined(JSON_HEDLEY_GCC_HAS_BUILTIN) + #undef JSON_HEDLEY_GCC_HAS_BUILTIN +#endif +#if defined(__has_builtin) + #define JSON_HEDLEY_GCC_HAS_BUILTIN(builtin,major,minor,patch) __has_builtin(builtin) +#else + #define JSON_HEDLEY_GCC_HAS_BUILTIN(builtin,major,minor,patch) JSON_HEDLEY_GCC_VERSION_CHECK(major,minor,patch) +#endif + +#if defined(JSON_HEDLEY_HAS_FEATURE) + #undef JSON_HEDLEY_HAS_FEATURE +#endif +#if defined(__has_feature) + #define JSON_HEDLEY_HAS_FEATURE(feature) __has_feature(feature) +#else + #define JSON_HEDLEY_HAS_FEATURE(feature) (0) +#endif + +#if defined(JSON_HEDLEY_GNUC_HAS_FEATURE) + #undef JSON_HEDLEY_GNUC_HAS_FEATURE +#endif +#if defined(__has_feature) + #define JSON_HEDLEY_GNUC_HAS_FEATURE(feature,major,minor,patch) __has_feature(feature) +#else + #define JSON_HEDLEY_GNUC_HAS_FEATURE(feature,major,minor,patch) JSON_HEDLEY_GNUC_VERSION_CHECK(major,minor,patch) +#endif + +#if defined(JSON_HEDLEY_GCC_HAS_FEATURE) + #undef JSON_HEDLEY_GCC_HAS_FEATURE +#endif +#if defined(__has_feature) + #define JSON_HEDLEY_GCC_HAS_FEATURE(feature,major,minor,patch) __has_feature(feature) +#else + #define JSON_HEDLEY_GCC_HAS_FEATURE(feature,major,minor,patch) JSON_HEDLEY_GCC_VERSION_CHECK(major,minor,patch) +#endif + +#if defined(JSON_HEDLEY_HAS_EXTENSION) + #undef JSON_HEDLEY_HAS_EXTENSION +#endif +#if defined(__has_extension) + #define JSON_HEDLEY_HAS_EXTENSION(extension) __has_extension(extension) +#else + #define JSON_HEDLEY_HAS_EXTENSION(extension) (0) +#endif + +#if defined(JSON_HEDLEY_GNUC_HAS_EXTENSION) + #undef JSON_HEDLEY_GNUC_HAS_EXTENSION +#endif +#if defined(__has_extension) + #define JSON_HEDLEY_GNUC_HAS_EXTENSION(extension,major,minor,patch) __has_extension(extension) +#else + #define JSON_HEDLEY_GNUC_HAS_EXTENSION(extension,major,minor,patch) JSON_HEDLEY_GNUC_VERSION_CHECK(major,minor,patch) +#endif + +#if defined(JSON_HEDLEY_GCC_HAS_EXTENSION) + #undef JSON_HEDLEY_GCC_HAS_EXTENSION +#endif +#if defined(__has_extension) + #define JSON_HEDLEY_GCC_HAS_EXTENSION(extension,major,minor,patch) __has_extension(extension) +#else + #define JSON_HEDLEY_GCC_HAS_EXTENSION(extension,major,minor,patch) JSON_HEDLEY_GCC_VERSION_CHECK(major,minor,patch) +#endif + +#if defined(JSON_HEDLEY_HAS_DECLSPEC_ATTRIBUTE) + #undef JSON_HEDLEY_HAS_DECLSPEC_ATTRIBUTE +#endif +#if defined(__has_declspec_attribute) + #define JSON_HEDLEY_HAS_DECLSPEC_ATTRIBUTE(attribute) __has_declspec_attribute(attribute) +#else + #define JSON_HEDLEY_HAS_DECLSPEC_ATTRIBUTE(attribute) (0) +#endif + +#if defined(JSON_HEDLEY_GNUC_HAS_DECLSPEC_ATTRIBUTE) + #undef JSON_HEDLEY_GNUC_HAS_DECLSPEC_ATTRIBUTE +#endif +#if defined(__has_declspec_attribute) + #define JSON_HEDLEY_GNUC_HAS_DECLSPEC_ATTRIBUTE(attribute,major,minor,patch) __has_declspec_attribute(attribute) +#else + #define JSON_HEDLEY_GNUC_HAS_DECLSPEC_ATTRIBUTE(attribute,major,minor,patch) JSON_HEDLEY_GNUC_VERSION_CHECK(major,minor,patch) +#endif + +#if defined(JSON_HEDLEY_GCC_HAS_DECLSPEC_ATTRIBUTE) + #undef JSON_HEDLEY_GCC_HAS_DECLSPEC_ATTRIBUTE +#endif +#if defined(__has_declspec_attribute) + #define JSON_HEDLEY_GCC_HAS_DECLSPEC_ATTRIBUTE(attribute,major,minor,patch) __has_declspec_attribute(attribute) +#else + #define JSON_HEDLEY_GCC_HAS_DECLSPEC_ATTRIBUTE(attribute,major,minor,patch) JSON_HEDLEY_GCC_VERSION_CHECK(major,minor,patch) +#endif + +#if defined(JSON_HEDLEY_HAS_WARNING) + #undef JSON_HEDLEY_HAS_WARNING +#endif +#if defined(__has_warning) + #define JSON_HEDLEY_HAS_WARNING(warning) __has_warning(warning) +#else + #define JSON_HEDLEY_HAS_WARNING(warning) (0) +#endif + +#if defined(JSON_HEDLEY_GNUC_HAS_WARNING) + #undef JSON_HEDLEY_GNUC_HAS_WARNING +#endif +#if defined(__has_warning) + #define JSON_HEDLEY_GNUC_HAS_WARNING(warning,major,minor,patch) __has_warning(warning) +#else + #define JSON_HEDLEY_GNUC_HAS_WARNING(warning,major,minor,patch) JSON_HEDLEY_GNUC_VERSION_CHECK(major,minor,patch) +#endif + +#if defined(JSON_HEDLEY_GCC_HAS_WARNING) + #undef JSON_HEDLEY_GCC_HAS_WARNING +#endif +#if defined(__has_warning) + #define JSON_HEDLEY_GCC_HAS_WARNING(warning,major,minor,patch) __has_warning(warning) +#else + #define JSON_HEDLEY_GCC_HAS_WARNING(warning,major,minor,patch) JSON_HEDLEY_GCC_VERSION_CHECK(major,minor,patch) +#endif + +#if \ + (defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L)) || \ + defined(__clang__) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(3,0,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_IAR_VERSION_CHECK(8,0,0) || \ + JSON_HEDLEY_PGI_VERSION_CHECK(18,4,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ + JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,7,0) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(2,0,1) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,1,0) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,0,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) || \ + JSON_HEDLEY_CRAY_VERSION_CHECK(5,0,0) || \ + JSON_HEDLEY_TINYC_VERSION_CHECK(0,9,17) || \ + JSON_HEDLEY_SUNPRO_VERSION_CHECK(8,0,0) || \ + (JSON_HEDLEY_IBM_VERSION_CHECK(10,1,0) && defined(__C99_PRAGMA_OPERATOR)) + #define JSON_HEDLEY_PRAGMA(value) _Pragma(#value) +#elif JSON_HEDLEY_MSVC_VERSION_CHECK(15,0,0) + #define JSON_HEDLEY_PRAGMA(value) __pragma(value) +#else + #define JSON_HEDLEY_PRAGMA(value) +#endif + +#if defined(JSON_HEDLEY_DIAGNOSTIC_PUSH) + #undef JSON_HEDLEY_DIAGNOSTIC_PUSH +#endif +#if defined(JSON_HEDLEY_DIAGNOSTIC_POP) + #undef JSON_HEDLEY_DIAGNOSTIC_POP +#endif +#if defined(__clang__) + #define JSON_HEDLEY_DIAGNOSTIC_PUSH _Pragma("clang diagnostic push") + #define JSON_HEDLEY_DIAGNOSTIC_POP _Pragma("clang diagnostic pop") +#elif JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) + #define JSON_HEDLEY_DIAGNOSTIC_PUSH _Pragma("warning(push)") + #define JSON_HEDLEY_DIAGNOSTIC_POP _Pragma("warning(pop)") +#elif JSON_HEDLEY_GCC_VERSION_CHECK(4,6,0) + #define JSON_HEDLEY_DIAGNOSTIC_PUSH _Pragma("GCC diagnostic push") + #define JSON_HEDLEY_DIAGNOSTIC_POP _Pragma("GCC diagnostic pop") +#elif \ + JSON_HEDLEY_MSVC_VERSION_CHECK(15,0,0) || \ + JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) + #define JSON_HEDLEY_DIAGNOSTIC_PUSH __pragma(warning(push)) + #define JSON_HEDLEY_DIAGNOSTIC_POP __pragma(warning(pop)) +#elif JSON_HEDLEY_ARM_VERSION_CHECK(5,6,0) + #define JSON_HEDLEY_DIAGNOSTIC_PUSH _Pragma("push") + #define JSON_HEDLEY_DIAGNOSTIC_POP _Pragma("pop") +#elif \ + JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,4,0) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(8,1,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) + #define JSON_HEDLEY_DIAGNOSTIC_PUSH _Pragma("diag_push") + #define JSON_HEDLEY_DIAGNOSTIC_POP _Pragma("diag_pop") +#elif JSON_HEDLEY_PELLES_VERSION_CHECK(2,90,0) + #define JSON_HEDLEY_DIAGNOSTIC_PUSH _Pragma("warning(push)") + #define JSON_HEDLEY_DIAGNOSTIC_POP _Pragma("warning(pop)") +#else + #define JSON_HEDLEY_DIAGNOSTIC_PUSH + #define JSON_HEDLEY_DIAGNOSTIC_POP +#endif + +/* JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_ is for + HEDLEY INTERNAL USE ONLY. API subject to change without notice. */ +#if defined(JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_) + #undef JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_ +#endif +#if defined(__cplusplus) +# if JSON_HEDLEY_HAS_WARNING("-Wc++98-compat") +# if JSON_HEDLEY_HAS_WARNING("-Wc++17-extensions") +# if JSON_HEDLEY_HAS_WARNING("-Wc++1z-extensions") +# define JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_(xpr) \ + JSON_HEDLEY_DIAGNOSTIC_PUSH \ + _Pragma("clang diagnostic ignored \"-Wc++98-compat\"") \ + _Pragma("clang diagnostic ignored \"-Wc++17-extensions\"") \ + _Pragma("clang diagnostic ignored \"-Wc++1z-extensions\"") \ + xpr \ + JSON_HEDLEY_DIAGNOSTIC_POP +# else +# define JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_(xpr) \ + JSON_HEDLEY_DIAGNOSTIC_PUSH \ + _Pragma("clang diagnostic ignored \"-Wc++98-compat\"") \ + _Pragma("clang diagnostic ignored \"-Wc++17-extensions\"") \ + xpr \ + JSON_HEDLEY_DIAGNOSTIC_POP +# endif +# else +# define JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_(xpr) \ + JSON_HEDLEY_DIAGNOSTIC_PUSH \ + _Pragma("clang diagnostic ignored \"-Wc++98-compat\"") \ + xpr \ + JSON_HEDLEY_DIAGNOSTIC_POP +# endif +# endif +#endif +#if !defined(JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_(x) x +#endif + +#if defined(JSON_HEDLEY_CONST_CAST) + #undef JSON_HEDLEY_CONST_CAST +#endif +#if defined(__cplusplus) +# define JSON_HEDLEY_CONST_CAST(T, expr) (const_cast(expr)) +#elif \ + JSON_HEDLEY_HAS_WARNING("-Wcast-qual") || \ + JSON_HEDLEY_GCC_VERSION_CHECK(4,6,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) +# define JSON_HEDLEY_CONST_CAST(T, expr) (__extension__ ({ \ + JSON_HEDLEY_DIAGNOSTIC_PUSH \ + JSON_HEDLEY_DIAGNOSTIC_DISABLE_CAST_QUAL \ + ((T) (expr)); \ + JSON_HEDLEY_DIAGNOSTIC_POP \ + })) +#else +# define JSON_HEDLEY_CONST_CAST(T, expr) ((T) (expr)) +#endif + +#if defined(JSON_HEDLEY_REINTERPRET_CAST) + #undef JSON_HEDLEY_REINTERPRET_CAST +#endif +#if defined(__cplusplus) + #define JSON_HEDLEY_REINTERPRET_CAST(T, expr) (reinterpret_cast(expr)) +#else + #define JSON_HEDLEY_REINTERPRET_CAST(T, expr) ((T) (expr)) +#endif + +#if defined(JSON_HEDLEY_STATIC_CAST) + #undef JSON_HEDLEY_STATIC_CAST +#endif +#if defined(__cplusplus) + #define JSON_HEDLEY_STATIC_CAST(T, expr) (static_cast(expr)) +#else + #define JSON_HEDLEY_STATIC_CAST(T, expr) ((T) (expr)) +#endif + +#if defined(JSON_HEDLEY_CPP_CAST) + #undef JSON_HEDLEY_CPP_CAST +#endif +#if defined(__cplusplus) +# if JSON_HEDLEY_HAS_WARNING("-Wold-style-cast") +# define JSON_HEDLEY_CPP_CAST(T, expr) \ + JSON_HEDLEY_DIAGNOSTIC_PUSH \ + _Pragma("clang diagnostic ignored \"-Wold-style-cast\"") \ + ((T) (expr)) \ + JSON_HEDLEY_DIAGNOSTIC_POP +# elif JSON_HEDLEY_IAR_VERSION_CHECK(8,3,0) +# define JSON_HEDLEY_CPP_CAST(T, expr) \ + JSON_HEDLEY_DIAGNOSTIC_PUSH \ + _Pragma("diag_suppress=Pe137") \ + JSON_HEDLEY_DIAGNOSTIC_POP +# else +# define JSON_HEDLEY_CPP_CAST(T, expr) ((T) (expr)) +# endif +#else +# define JSON_HEDLEY_CPP_CAST(T, expr) (expr) +#endif + +#if defined(JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED) + #undef JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED +#endif +#if JSON_HEDLEY_HAS_WARNING("-Wdeprecated-declarations") + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED _Pragma("clang diagnostic ignored \"-Wdeprecated-declarations\"") +#elif JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED _Pragma("warning(disable:1478 1786)") +#elif JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED __pragma(warning(disable:1478 1786)) +#elif JSON_HEDLEY_PGI_VERSION_CHECK(20,7,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED _Pragma("diag_suppress 1215,1216,1444,1445") +#elif JSON_HEDLEY_PGI_VERSION_CHECK(17,10,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED _Pragma("diag_suppress 1215,1444") +#elif JSON_HEDLEY_GCC_VERSION_CHECK(4,3,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED _Pragma("GCC diagnostic ignored \"-Wdeprecated-declarations\"") +#elif JSON_HEDLEY_MSVC_VERSION_CHECK(15,0,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED __pragma(warning(disable:4996)) +#elif JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED _Pragma("diag_suppress 1215,1444") +#elif \ + JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ + (JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,8,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \ + (JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \ + (JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \ + (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED _Pragma("diag_suppress 1291,1718") +#elif JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,13,0) && !defined(__cplusplus) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED _Pragma("error_messages(off,E_DEPRECATED_ATT,E_DEPRECATED_ATT_MESS)") +#elif JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,13,0) && defined(__cplusplus) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED _Pragma("error_messages(off,symdeprecated,symdeprecated2)") +#elif JSON_HEDLEY_IAR_VERSION_CHECK(8,0,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED _Pragma("diag_suppress=Pe1444,Pe1215") +#elif JSON_HEDLEY_PELLES_VERSION_CHECK(2,90,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED _Pragma("warn(disable:2241)") +#else + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_DEPRECATED +#endif + +#if defined(JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS) + #undef JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS +#endif +#if JSON_HEDLEY_HAS_WARNING("-Wunknown-pragmas") + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS _Pragma("clang diagnostic ignored \"-Wunknown-pragmas\"") +#elif JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS _Pragma("warning(disable:161)") +#elif JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS __pragma(warning(disable:161)) +#elif JSON_HEDLEY_PGI_VERSION_CHECK(17,10,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS _Pragma("diag_suppress 1675") +#elif JSON_HEDLEY_GCC_VERSION_CHECK(4,3,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS _Pragma("GCC diagnostic ignored \"-Wunknown-pragmas\"") +#elif JSON_HEDLEY_MSVC_VERSION_CHECK(15,0,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS __pragma(warning(disable:4068)) +#elif \ + JSON_HEDLEY_TI_VERSION_CHECK(16,9,0) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(8,0,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,3,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS _Pragma("diag_suppress 163") +#elif JSON_HEDLEY_TI_CL6X_VERSION_CHECK(8,0,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS _Pragma("diag_suppress 163") +#elif JSON_HEDLEY_IAR_VERSION_CHECK(8,0,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS _Pragma("diag_suppress=Pe161") +#elif JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS _Pragma("diag_suppress 161") +#else + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS +#endif + +#if defined(JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES) + #undef JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES +#endif +#if JSON_HEDLEY_HAS_WARNING("-Wunknown-attributes") + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES _Pragma("clang diagnostic ignored \"-Wunknown-attributes\"") +#elif JSON_HEDLEY_GCC_VERSION_CHECK(4,6,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES _Pragma("GCC diagnostic ignored \"-Wdeprecated-declarations\"") +#elif JSON_HEDLEY_INTEL_VERSION_CHECK(17,0,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES _Pragma("warning(disable:1292)") +#elif JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES __pragma(warning(disable:1292)) +#elif JSON_HEDLEY_MSVC_VERSION_CHECK(19,0,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES __pragma(warning(disable:5030)) +#elif JSON_HEDLEY_PGI_VERSION_CHECK(20,7,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES _Pragma("diag_suppress 1097,1098") +#elif JSON_HEDLEY_PGI_VERSION_CHECK(17,10,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES _Pragma("diag_suppress 1097") +#elif JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,14,0) && defined(__cplusplus) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES _Pragma("error_messages(off,attrskipunsup)") +#elif \ + JSON_HEDLEY_TI_VERSION_CHECK(18,1,0) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(8,3,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES _Pragma("diag_suppress 1173") +#elif JSON_HEDLEY_IAR_VERSION_CHECK(8,0,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES _Pragma("diag_suppress=Pe1097") +#elif JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES _Pragma("diag_suppress 1097") +#else + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_CPP_ATTRIBUTES +#endif + +#if defined(JSON_HEDLEY_DIAGNOSTIC_DISABLE_CAST_QUAL) + #undef JSON_HEDLEY_DIAGNOSTIC_DISABLE_CAST_QUAL +#endif +#if JSON_HEDLEY_HAS_WARNING("-Wcast-qual") + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_CAST_QUAL _Pragma("clang diagnostic ignored \"-Wcast-qual\"") +#elif JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_CAST_QUAL _Pragma("warning(disable:2203 2331)") +#elif JSON_HEDLEY_GCC_VERSION_CHECK(3,0,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_CAST_QUAL _Pragma("GCC diagnostic ignored \"-Wcast-qual\"") +#else + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_CAST_QUAL +#endif + +#if defined(JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNUSED_FUNCTION) + #undef JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNUSED_FUNCTION +#endif +#if JSON_HEDLEY_HAS_WARNING("-Wunused-function") + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNUSED_FUNCTION _Pragma("clang diagnostic ignored \"-Wunused-function\"") +#elif JSON_HEDLEY_GCC_VERSION_CHECK(3,4,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNUSED_FUNCTION _Pragma("GCC diagnostic ignored \"-Wunused-function\"") +#elif JSON_HEDLEY_MSVC_VERSION_CHECK(1,0,0) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNUSED_FUNCTION __pragma(warning(disable:4505)) +#elif JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNUSED_FUNCTION _Pragma("diag_suppress 3142") +#else + #define JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNUSED_FUNCTION +#endif + +#if defined(JSON_HEDLEY_DEPRECATED) + #undef JSON_HEDLEY_DEPRECATED +#endif +#if defined(JSON_HEDLEY_DEPRECATED_FOR) + #undef JSON_HEDLEY_DEPRECATED_FOR +#endif +#if \ + JSON_HEDLEY_MSVC_VERSION_CHECK(14,0,0) || \ + JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) + #define JSON_HEDLEY_DEPRECATED(since) __declspec(deprecated("Since " # since)) + #define JSON_HEDLEY_DEPRECATED_FOR(since, replacement) __declspec(deprecated("Since " #since "; use " #replacement)) +#elif \ + (JSON_HEDLEY_HAS_EXTENSION(attribute_deprecated_with_message) && !defined(JSON_HEDLEY_IAR_VERSION)) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(4,5,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(5,6,0) || \ + JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,13,0) || \ + JSON_HEDLEY_PGI_VERSION_CHECK(17,10,0) || \ + JSON_HEDLEY_TI_VERSION_CHECK(18,1,0) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(18,1,0) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(8,3,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,3,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_DEPRECATED(since) __attribute__((__deprecated__("Since " #since))) + #define JSON_HEDLEY_DEPRECATED_FOR(since, replacement) __attribute__((__deprecated__("Since " #since "; use " #replacement))) +#elif defined(__cplusplus) && (__cplusplus >= 201402L) + #define JSON_HEDLEY_DEPRECATED(since) JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_([[deprecated("Since " #since)]]) + #define JSON_HEDLEY_DEPRECATED_FOR(since, replacement) JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_([[deprecated("Since " #since "; use " #replacement)]]) +#elif \ + JSON_HEDLEY_HAS_ATTRIBUTE(deprecated) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(3,1,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ + JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ + (JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,8,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \ + (JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \ + (JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \ + (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) || \ + JSON_HEDLEY_IAR_VERSION_CHECK(8,10,0) + #define JSON_HEDLEY_DEPRECATED(since) __attribute__((__deprecated__)) + #define JSON_HEDLEY_DEPRECATED_FOR(since, replacement) __attribute__((__deprecated__)) +#elif \ + JSON_HEDLEY_MSVC_VERSION_CHECK(13,10,0) || \ + JSON_HEDLEY_PELLES_VERSION_CHECK(6,50,0) || \ + JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) + #define JSON_HEDLEY_DEPRECATED(since) __declspec(deprecated) + #define JSON_HEDLEY_DEPRECATED_FOR(since, replacement) __declspec(deprecated) +#elif JSON_HEDLEY_IAR_VERSION_CHECK(8,0,0) + #define JSON_HEDLEY_DEPRECATED(since) _Pragma("deprecated") + #define JSON_HEDLEY_DEPRECATED_FOR(since, replacement) _Pragma("deprecated") +#else + #define JSON_HEDLEY_DEPRECATED(since) + #define JSON_HEDLEY_DEPRECATED_FOR(since, replacement) +#endif + +#if defined(JSON_HEDLEY_UNAVAILABLE) + #undef JSON_HEDLEY_UNAVAILABLE +#endif +#if \ + JSON_HEDLEY_HAS_ATTRIBUTE(warning) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(4,3,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_UNAVAILABLE(available_since) __attribute__((__warning__("Not available until " #available_since))) +#else + #define JSON_HEDLEY_UNAVAILABLE(available_since) +#endif + +#if defined(JSON_HEDLEY_WARN_UNUSED_RESULT) + #undef JSON_HEDLEY_WARN_UNUSED_RESULT +#endif +#if defined(JSON_HEDLEY_WARN_UNUSED_RESULT_MSG) + #undef JSON_HEDLEY_WARN_UNUSED_RESULT_MSG +#endif +#if \ + JSON_HEDLEY_HAS_ATTRIBUTE(warn_unused_result) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(3,4,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ + (JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,8,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \ + (JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \ + (JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \ + (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) || \ + (JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,15,0) && defined(__cplusplus)) || \ + JSON_HEDLEY_PGI_VERSION_CHECK(17,10,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_WARN_UNUSED_RESULT __attribute__((__warn_unused_result__)) + #define JSON_HEDLEY_WARN_UNUSED_RESULT_MSG(msg) __attribute__((__warn_unused_result__)) +#elif (JSON_HEDLEY_HAS_CPP_ATTRIBUTE(nodiscard) >= 201907L) + #define JSON_HEDLEY_WARN_UNUSED_RESULT JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_([[nodiscard]]) + #define JSON_HEDLEY_WARN_UNUSED_RESULT_MSG(msg) JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_([[nodiscard(msg)]]) +#elif JSON_HEDLEY_HAS_CPP_ATTRIBUTE(nodiscard) + #define JSON_HEDLEY_WARN_UNUSED_RESULT JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_([[nodiscard]]) + #define JSON_HEDLEY_WARN_UNUSED_RESULT_MSG(msg) JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_([[nodiscard]]) +#elif defined(_Check_return_) /* SAL */ + #define JSON_HEDLEY_WARN_UNUSED_RESULT _Check_return_ + #define JSON_HEDLEY_WARN_UNUSED_RESULT_MSG(msg) _Check_return_ +#else + #define JSON_HEDLEY_WARN_UNUSED_RESULT + #define JSON_HEDLEY_WARN_UNUSED_RESULT_MSG(msg) +#endif + +#if defined(JSON_HEDLEY_SENTINEL) + #undef JSON_HEDLEY_SENTINEL +#endif +#if \ + JSON_HEDLEY_HAS_ATTRIBUTE(sentinel) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(4,0,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(5,4,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_SENTINEL(position) __attribute__((__sentinel__(position))) +#else + #define JSON_HEDLEY_SENTINEL(position) +#endif + +#if defined(JSON_HEDLEY_NO_RETURN) + #undef JSON_HEDLEY_NO_RETURN +#endif +#if JSON_HEDLEY_IAR_VERSION_CHECK(8,0,0) + #define JSON_HEDLEY_NO_RETURN __noreturn +#elif \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_NO_RETURN __attribute__((__noreturn__)) +#elif defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201112L + #define JSON_HEDLEY_NO_RETURN _Noreturn +#elif defined(__cplusplus) && (__cplusplus >= 201103L) + #define JSON_HEDLEY_NO_RETURN JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_([[noreturn]]) +#elif \ + JSON_HEDLEY_HAS_ATTRIBUTE(noreturn) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(3,2,0) || \ + JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,11,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ + JSON_HEDLEY_IBM_VERSION_CHECK(10,1,0) || \ + JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ + (JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,8,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \ + (JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \ + (JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \ + (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) || \ + JSON_HEDLEY_IAR_VERSION_CHECK(8,10,0) + #define JSON_HEDLEY_NO_RETURN __attribute__((__noreturn__)) +#elif JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,10,0) + #define JSON_HEDLEY_NO_RETURN _Pragma("does_not_return") +#elif \ + JSON_HEDLEY_MSVC_VERSION_CHECK(13,10,0) || \ + JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) + #define JSON_HEDLEY_NO_RETURN __declspec(noreturn) +#elif JSON_HEDLEY_TI_CL6X_VERSION_CHECK(6,0,0) && defined(__cplusplus) + #define JSON_HEDLEY_NO_RETURN _Pragma("FUNC_NEVER_RETURNS;") +#elif JSON_HEDLEY_COMPCERT_VERSION_CHECK(3,2,0) + #define JSON_HEDLEY_NO_RETURN __attribute((noreturn)) +#elif JSON_HEDLEY_PELLES_VERSION_CHECK(9,0,0) + #define JSON_HEDLEY_NO_RETURN __declspec(noreturn) +#else + #define JSON_HEDLEY_NO_RETURN +#endif + +#if defined(JSON_HEDLEY_NO_ESCAPE) + #undef JSON_HEDLEY_NO_ESCAPE +#endif +#if JSON_HEDLEY_HAS_ATTRIBUTE(noescape) + #define JSON_HEDLEY_NO_ESCAPE __attribute__((__noescape__)) +#else + #define JSON_HEDLEY_NO_ESCAPE +#endif + +#if defined(JSON_HEDLEY_UNREACHABLE) + #undef JSON_HEDLEY_UNREACHABLE +#endif +#if defined(JSON_HEDLEY_UNREACHABLE_RETURN) + #undef JSON_HEDLEY_UNREACHABLE_RETURN +#endif +#if defined(JSON_HEDLEY_ASSUME) + #undef JSON_HEDLEY_ASSUME +#endif +#if \ + JSON_HEDLEY_MSVC_VERSION_CHECK(13,10,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) + #define JSON_HEDLEY_ASSUME(expr) __assume(expr) +#elif JSON_HEDLEY_HAS_BUILTIN(__builtin_assume) + #define JSON_HEDLEY_ASSUME(expr) __builtin_assume(expr) +#elif \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,2,0) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(4,0,0) + #if defined(__cplusplus) + #define JSON_HEDLEY_ASSUME(expr) std::_nassert(expr) + #else + #define JSON_HEDLEY_ASSUME(expr) _nassert(expr) + #endif +#endif +#if \ + (JSON_HEDLEY_HAS_BUILTIN(__builtin_unreachable) && (!defined(JSON_HEDLEY_ARM_VERSION))) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(4,5,0) || \ + JSON_HEDLEY_PGI_VERSION_CHECK(18,10,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_IBM_VERSION_CHECK(13,1,5) || \ + JSON_HEDLEY_CRAY_VERSION_CHECK(10,0,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_UNREACHABLE() __builtin_unreachable() +#elif defined(JSON_HEDLEY_ASSUME) + #define JSON_HEDLEY_UNREACHABLE() JSON_HEDLEY_ASSUME(0) +#endif +#if !defined(JSON_HEDLEY_ASSUME) + #if defined(JSON_HEDLEY_UNREACHABLE) + #define JSON_HEDLEY_ASSUME(expr) JSON_HEDLEY_STATIC_CAST(void, ((expr) ? 1 : (JSON_HEDLEY_UNREACHABLE(), 1))) + #else + #define JSON_HEDLEY_ASSUME(expr) JSON_HEDLEY_STATIC_CAST(void, expr) + #endif +#endif +#if defined(JSON_HEDLEY_UNREACHABLE) + #if \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,2,0) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(4,0,0) + #define JSON_HEDLEY_UNREACHABLE_RETURN(value) return (JSON_HEDLEY_STATIC_CAST(void, JSON_HEDLEY_ASSUME(0)), (value)) + #else + #define JSON_HEDLEY_UNREACHABLE_RETURN(value) JSON_HEDLEY_UNREACHABLE() + #endif +#else + #define JSON_HEDLEY_UNREACHABLE_RETURN(value) return (value) +#endif +#if !defined(JSON_HEDLEY_UNREACHABLE) + #define JSON_HEDLEY_UNREACHABLE() JSON_HEDLEY_ASSUME(0) +#endif + +JSON_HEDLEY_DIAGNOSTIC_PUSH +#if JSON_HEDLEY_HAS_WARNING("-Wpedantic") + #pragma clang diagnostic ignored "-Wpedantic" +#endif +#if JSON_HEDLEY_HAS_WARNING("-Wc++98-compat-pedantic") && defined(__cplusplus) + #pragma clang diagnostic ignored "-Wc++98-compat-pedantic" +#endif +#if JSON_HEDLEY_GCC_HAS_WARNING("-Wvariadic-macros",4,0,0) + #if defined(__clang__) + #pragma clang diagnostic ignored "-Wvariadic-macros" + #elif defined(JSON_HEDLEY_GCC_VERSION) + #pragma GCC diagnostic ignored "-Wvariadic-macros" + #endif +#endif +#if defined(JSON_HEDLEY_NON_NULL) + #undef JSON_HEDLEY_NON_NULL +#endif +#if \ + JSON_HEDLEY_HAS_ATTRIBUTE(nonnull) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(3,3,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) + #define JSON_HEDLEY_NON_NULL(...) __attribute__((__nonnull__(__VA_ARGS__))) +#else + #define JSON_HEDLEY_NON_NULL(...) +#endif +JSON_HEDLEY_DIAGNOSTIC_POP + +#if defined(JSON_HEDLEY_PRINTF_FORMAT) + #undef JSON_HEDLEY_PRINTF_FORMAT +#endif +#if defined(__MINGW32__) && JSON_HEDLEY_GCC_HAS_ATTRIBUTE(format,4,4,0) && !defined(__USE_MINGW_ANSI_STDIO) + #define JSON_HEDLEY_PRINTF_FORMAT(string_idx,first_to_check) __attribute__((__format__(ms_printf, string_idx, first_to_check))) +#elif defined(__MINGW32__) && JSON_HEDLEY_GCC_HAS_ATTRIBUTE(format,4,4,0) && defined(__USE_MINGW_ANSI_STDIO) + #define JSON_HEDLEY_PRINTF_FORMAT(string_idx,first_to_check) __attribute__((__format__(gnu_printf, string_idx, first_to_check))) +#elif \ + JSON_HEDLEY_HAS_ATTRIBUTE(format) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(3,1,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(5,6,0) || \ + JSON_HEDLEY_IBM_VERSION_CHECK(10,1,0) || \ + JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ + (JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,8,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \ + (JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \ + (JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \ + (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_PRINTF_FORMAT(string_idx,first_to_check) __attribute__((__format__(__printf__, string_idx, first_to_check))) +#elif JSON_HEDLEY_PELLES_VERSION_CHECK(6,0,0) + #define JSON_HEDLEY_PRINTF_FORMAT(string_idx,first_to_check) __declspec(vaformat(printf,string_idx,first_to_check)) +#else + #define JSON_HEDLEY_PRINTF_FORMAT(string_idx,first_to_check) +#endif + +#if defined(JSON_HEDLEY_CONSTEXPR) + #undef JSON_HEDLEY_CONSTEXPR +#endif +#if defined(__cplusplus) + #if __cplusplus >= 201103L + #define JSON_HEDLEY_CONSTEXPR JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_(constexpr) + #endif +#endif +#if !defined(JSON_HEDLEY_CONSTEXPR) + #define JSON_HEDLEY_CONSTEXPR +#endif + +#if defined(JSON_HEDLEY_PREDICT) + #undef JSON_HEDLEY_PREDICT +#endif +#if defined(JSON_HEDLEY_LIKELY) + #undef JSON_HEDLEY_LIKELY +#endif +#if defined(JSON_HEDLEY_UNLIKELY) + #undef JSON_HEDLEY_UNLIKELY +#endif +#if defined(JSON_HEDLEY_UNPREDICTABLE) + #undef JSON_HEDLEY_UNPREDICTABLE +#endif +#if JSON_HEDLEY_HAS_BUILTIN(__builtin_unpredictable) + #define JSON_HEDLEY_UNPREDICTABLE(expr) __builtin_unpredictable((expr)) +#endif +#if \ + (JSON_HEDLEY_HAS_BUILTIN(__builtin_expect_with_probability) && !defined(JSON_HEDLEY_PGI_VERSION)) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(9,0,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) +# define JSON_HEDLEY_PREDICT(expr, value, probability) __builtin_expect_with_probability( (expr), (value), (probability)) +# define JSON_HEDLEY_PREDICT_TRUE(expr, probability) __builtin_expect_with_probability(!!(expr), 1 , (probability)) +# define JSON_HEDLEY_PREDICT_FALSE(expr, probability) __builtin_expect_with_probability(!!(expr), 0 , (probability)) +# define JSON_HEDLEY_LIKELY(expr) __builtin_expect (!!(expr), 1 ) +# define JSON_HEDLEY_UNLIKELY(expr) __builtin_expect (!!(expr), 0 ) +#elif \ + (JSON_HEDLEY_HAS_BUILTIN(__builtin_expect) && !defined(JSON_HEDLEY_INTEL_CL_VERSION)) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(3,0,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + (JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,15,0) && defined(__cplusplus)) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ + JSON_HEDLEY_IBM_VERSION_CHECK(10,1,0) || \ + JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,7,0) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(3,1,0) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,1,0) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(6,1,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) || \ + JSON_HEDLEY_TINYC_VERSION_CHECK(0,9,27) || \ + JSON_HEDLEY_CRAY_VERSION_CHECK(8,1,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) +# define JSON_HEDLEY_PREDICT(expr, expected, probability) \ + (((probability) >= 0.9) ? __builtin_expect((expr), (expected)) : (JSON_HEDLEY_STATIC_CAST(void, expected), (expr))) +# define JSON_HEDLEY_PREDICT_TRUE(expr, probability) \ + (__extension__ ({ \ + double hedley_probability_ = (probability); \ + ((hedley_probability_ >= 0.9) ? __builtin_expect(!!(expr), 1) : ((hedley_probability_ <= 0.1) ? __builtin_expect(!!(expr), 0) : !!(expr))); \ + })) +# define JSON_HEDLEY_PREDICT_FALSE(expr, probability) \ + (__extension__ ({ \ + double hedley_probability_ = (probability); \ + ((hedley_probability_ >= 0.9) ? __builtin_expect(!!(expr), 0) : ((hedley_probability_ <= 0.1) ? __builtin_expect(!!(expr), 1) : !!(expr))); \ + })) +# define JSON_HEDLEY_LIKELY(expr) __builtin_expect(!!(expr), 1) +# define JSON_HEDLEY_UNLIKELY(expr) __builtin_expect(!!(expr), 0) +#else +# define JSON_HEDLEY_PREDICT(expr, expected, probability) (JSON_HEDLEY_STATIC_CAST(void, expected), (expr)) +# define JSON_HEDLEY_PREDICT_TRUE(expr, probability) (!!(expr)) +# define JSON_HEDLEY_PREDICT_FALSE(expr, probability) (!!(expr)) +# define JSON_HEDLEY_LIKELY(expr) (!!(expr)) +# define JSON_HEDLEY_UNLIKELY(expr) (!!(expr)) +#endif +#if !defined(JSON_HEDLEY_UNPREDICTABLE) + #define JSON_HEDLEY_UNPREDICTABLE(expr) JSON_HEDLEY_PREDICT(expr, 1, 0.5) +#endif + +#if defined(JSON_HEDLEY_MALLOC) + #undef JSON_HEDLEY_MALLOC +#endif +#if \ + JSON_HEDLEY_HAS_ATTRIBUTE(malloc) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(3,1,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,11,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ + JSON_HEDLEY_IBM_VERSION_CHECK(12,1,0) || \ + JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ + (JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,8,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \ + (JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \ + (JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \ + (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_MALLOC __attribute__((__malloc__)) +#elif JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,10,0) + #define JSON_HEDLEY_MALLOC _Pragma("returns_new_memory") +#elif \ + JSON_HEDLEY_MSVC_VERSION_CHECK(14,0,0) || \ + JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) + #define JSON_HEDLEY_MALLOC __declspec(restrict) +#else + #define JSON_HEDLEY_MALLOC +#endif + +#if defined(JSON_HEDLEY_PURE) + #undef JSON_HEDLEY_PURE +#endif +#if \ + JSON_HEDLEY_HAS_ATTRIBUTE(pure) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(2,96,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,11,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ + JSON_HEDLEY_IBM_VERSION_CHECK(10,1,0) || \ + JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ + (JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,8,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \ + (JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \ + (JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \ + (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) || \ + JSON_HEDLEY_PGI_VERSION_CHECK(17,10,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) +# define JSON_HEDLEY_PURE __attribute__((__pure__)) +#elif JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,10,0) +# define JSON_HEDLEY_PURE _Pragma("does_not_write_global_data") +#elif defined(__cplusplus) && \ + ( \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(2,0,1) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(4,0,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) \ + ) +# define JSON_HEDLEY_PURE _Pragma("FUNC_IS_PURE;") +#else +# define JSON_HEDLEY_PURE +#endif + +#if defined(JSON_HEDLEY_CONST) + #undef JSON_HEDLEY_CONST +#endif +#if \ + JSON_HEDLEY_HAS_ATTRIBUTE(const) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(2,5,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,11,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ + JSON_HEDLEY_IBM_VERSION_CHECK(10,1,0) || \ + JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ + (JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,8,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \ + (JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \ + (JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \ + (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) || \ + JSON_HEDLEY_PGI_VERSION_CHECK(17,10,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_CONST __attribute__((__const__)) +#elif \ + JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,10,0) + #define JSON_HEDLEY_CONST _Pragma("no_side_effect") +#else + #define JSON_HEDLEY_CONST JSON_HEDLEY_PURE +#endif + +#if defined(JSON_HEDLEY_RESTRICT) + #undef JSON_HEDLEY_RESTRICT +#endif +#if defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L) && !defined(__cplusplus) + #define JSON_HEDLEY_RESTRICT restrict +#elif \ + JSON_HEDLEY_GCC_VERSION_CHECK(3,1,0) || \ + JSON_HEDLEY_MSVC_VERSION_CHECK(14,0,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ + JSON_HEDLEY_IBM_VERSION_CHECK(10,1,0) || \ + JSON_HEDLEY_PGI_VERSION_CHECK(17,10,0) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,2,4) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(8,1,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + (JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,14,0) && defined(__cplusplus)) || \ + JSON_HEDLEY_IAR_VERSION_CHECK(8,0,0) || \ + defined(__clang__) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_RESTRICT __restrict +#elif JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,3,0) && !defined(__cplusplus) + #define JSON_HEDLEY_RESTRICT _Restrict +#else + #define JSON_HEDLEY_RESTRICT +#endif + +#if defined(JSON_HEDLEY_INLINE) + #undef JSON_HEDLEY_INLINE +#endif +#if \ + (defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L)) || \ + (defined(__cplusplus) && (__cplusplus >= 199711L)) + #define JSON_HEDLEY_INLINE inline +#elif \ + defined(JSON_HEDLEY_GCC_VERSION) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(6,2,0) + #define JSON_HEDLEY_INLINE __inline__ +#elif \ + JSON_HEDLEY_MSVC_VERSION_CHECK(12,0,0) || \ + JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,1,0) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(3,1,0) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,2,0) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(8,0,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_INLINE __inline +#else + #define JSON_HEDLEY_INLINE +#endif + +#if defined(JSON_HEDLEY_ALWAYS_INLINE) + #undef JSON_HEDLEY_ALWAYS_INLINE +#endif +#if \ + JSON_HEDLEY_HAS_ATTRIBUTE(always_inline) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(4,0,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,11,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ + JSON_HEDLEY_IBM_VERSION_CHECK(10,1,0) || \ + JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ + (JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,8,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \ + (JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \ + (JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \ + (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) || \ + JSON_HEDLEY_IAR_VERSION_CHECK(8,10,0) +# define JSON_HEDLEY_ALWAYS_INLINE __attribute__((__always_inline__)) JSON_HEDLEY_INLINE +#elif \ + JSON_HEDLEY_MSVC_VERSION_CHECK(12,0,0) || \ + JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) +# define JSON_HEDLEY_ALWAYS_INLINE __forceinline +#elif defined(__cplusplus) && \ + ( \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(6,1,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) \ + ) +# define JSON_HEDLEY_ALWAYS_INLINE _Pragma("FUNC_ALWAYS_INLINE;") +#elif JSON_HEDLEY_IAR_VERSION_CHECK(8,0,0) +# define JSON_HEDLEY_ALWAYS_INLINE _Pragma("inline=forced") +#else +# define JSON_HEDLEY_ALWAYS_INLINE JSON_HEDLEY_INLINE +#endif + +#if defined(JSON_HEDLEY_NEVER_INLINE) + #undef JSON_HEDLEY_NEVER_INLINE +#endif +#if \ + JSON_HEDLEY_HAS_ATTRIBUTE(noinline) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(4,0,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,11,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ + JSON_HEDLEY_IBM_VERSION_CHECK(10,1,0) || \ + JSON_HEDLEY_TI_VERSION_CHECK(15,12,0) || \ + (JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(4,8,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_ARMCL_VERSION_CHECK(5,2,0) || \ + (JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL2000_VERSION_CHECK(6,4,0) || \ + (JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,0,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(4,3,0) || \ + (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) || \ + JSON_HEDLEY_TI_CL7X_VERSION_CHECK(1,2,0) || \ + JSON_HEDLEY_TI_CLPRU_VERSION_CHECK(2,1,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) || \ + JSON_HEDLEY_IAR_VERSION_CHECK(8,10,0) + #define JSON_HEDLEY_NEVER_INLINE __attribute__((__noinline__)) +#elif \ + JSON_HEDLEY_MSVC_VERSION_CHECK(13,10,0) || \ + JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) + #define JSON_HEDLEY_NEVER_INLINE __declspec(noinline) +#elif JSON_HEDLEY_PGI_VERSION_CHECK(10,2,0) + #define JSON_HEDLEY_NEVER_INLINE _Pragma("noinline") +#elif JSON_HEDLEY_TI_CL6X_VERSION_CHECK(6,0,0) && defined(__cplusplus) + #define JSON_HEDLEY_NEVER_INLINE _Pragma("FUNC_CANNOT_INLINE;") +#elif JSON_HEDLEY_IAR_VERSION_CHECK(8,0,0) + #define JSON_HEDLEY_NEVER_INLINE _Pragma("inline=never") +#elif JSON_HEDLEY_COMPCERT_VERSION_CHECK(3,2,0) + #define JSON_HEDLEY_NEVER_INLINE __attribute((noinline)) +#elif JSON_HEDLEY_PELLES_VERSION_CHECK(9,0,0) + #define JSON_HEDLEY_NEVER_INLINE __declspec(noinline) +#else + #define JSON_HEDLEY_NEVER_INLINE +#endif + +#if defined(JSON_HEDLEY_PRIVATE) + #undef JSON_HEDLEY_PRIVATE +#endif +#if defined(JSON_HEDLEY_PUBLIC) + #undef JSON_HEDLEY_PUBLIC +#endif +#if defined(JSON_HEDLEY_IMPORT) + #undef JSON_HEDLEY_IMPORT +#endif +#if defined(_WIN32) || defined(__CYGWIN__) +# define JSON_HEDLEY_PRIVATE +# define JSON_HEDLEY_PUBLIC __declspec(dllexport) +# define JSON_HEDLEY_IMPORT __declspec(dllimport) +#else +# if \ + JSON_HEDLEY_HAS_ATTRIBUTE(visibility) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(3,3,0) || \ + JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,11,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ + JSON_HEDLEY_IBM_VERSION_CHECK(13,1,0) || \ + ( \ + defined(__TI_EABI__) && \ + ( \ + (JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,2,0) && defined(__TI_GNU_ATTRIBUTE_SUPPORT__)) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(7,5,0) \ + ) \ + ) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) +# define JSON_HEDLEY_PRIVATE __attribute__((__visibility__("hidden"))) +# define JSON_HEDLEY_PUBLIC __attribute__((__visibility__("default"))) +# else +# define JSON_HEDLEY_PRIVATE +# define JSON_HEDLEY_PUBLIC +# endif +# define JSON_HEDLEY_IMPORT extern +#endif + +#if defined(JSON_HEDLEY_NO_THROW) + #undef JSON_HEDLEY_NO_THROW +#endif +#if \ + JSON_HEDLEY_HAS_ATTRIBUTE(nothrow) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(3,3,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_NO_THROW __attribute__((__nothrow__)) +#elif \ + JSON_HEDLEY_MSVC_VERSION_CHECK(13,1,0) || \ + JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) + #define JSON_HEDLEY_NO_THROW __declspec(nothrow) +#else + #define JSON_HEDLEY_NO_THROW +#endif + +#if defined(JSON_HEDLEY_FALL_THROUGH) + #undef JSON_HEDLEY_FALL_THROUGH +#endif +#if \ + JSON_HEDLEY_HAS_ATTRIBUTE(fallthrough) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(7,0,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_FALL_THROUGH __attribute__((__fallthrough__)) +#elif JSON_HEDLEY_HAS_CPP_ATTRIBUTE_NS(clang,fallthrough) + #define JSON_HEDLEY_FALL_THROUGH JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_([[clang::fallthrough]]) +#elif JSON_HEDLEY_HAS_CPP_ATTRIBUTE(fallthrough) + #define JSON_HEDLEY_FALL_THROUGH JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_([[fallthrough]]) +#elif defined(__fallthrough) /* SAL */ + #define JSON_HEDLEY_FALL_THROUGH __fallthrough +#else + #define JSON_HEDLEY_FALL_THROUGH +#endif + +#if defined(JSON_HEDLEY_RETURNS_NON_NULL) + #undef JSON_HEDLEY_RETURNS_NON_NULL +#endif +#if \ + JSON_HEDLEY_HAS_ATTRIBUTE(returns_nonnull) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(4,9,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_RETURNS_NON_NULL __attribute__((__returns_nonnull__)) +#elif defined(_Ret_notnull_) /* SAL */ + #define JSON_HEDLEY_RETURNS_NON_NULL _Ret_notnull_ +#else + #define JSON_HEDLEY_RETURNS_NON_NULL +#endif + +#if defined(JSON_HEDLEY_ARRAY_PARAM) + #undef JSON_HEDLEY_ARRAY_PARAM +#endif +#if \ + defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L) && \ + !defined(__STDC_NO_VLA__) && \ + !defined(__cplusplus) && \ + !defined(JSON_HEDLEY_PGI_VERSION) && \ + !defined(JSON_HEDLEY_TINYC_VERSION) + #define JSON_HEDLEY_ARRAY_PARAM(name) (name) +#else + #define JSON_HEDLEY_ARRAY_PARAM(name) +#endif + +#if defined(JSON_HEDLEY_IS_CONSTANT) + #undef JSON_HEDLEY_IS_CONSTANT +#endif +#if defined(JSON_HEDLEY_REQUIRE_CONSTEXPR) + #undef JSON_HEDLEY_REQUIRE_CONSTEXPR +#endif +/* JSON_HEDLEY_IS_CONSTEXPR_ is for + HEDLEY INTERNAL USE ONLY. API subject to change without notice. */ +#if defined(JSON_HEDLEY_IS_CONSTEXPR_) + #undef JSON_HEDLEY_IS_CONSTEXPR_ +#endif +#if \ + JSON_HEDLEY_HAS_BUILTIN(__builtin_constant_p) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(3,4,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_TINYC_VERSION_CHECK(0,9,19) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(4,1,0) || \ + JSON_HEDLEY_IBM_VERSION_CHECK(13,1,0) || \ + JSON_HEDLEY_TI_CL6X_VERSION_CHECK(6,1,0) || \ + (JSON_HEDLEY_SUNPRO_VERSION_CHECK(5,10,0) && !defined(__cplusplus)) || \ + JSON_HEDLEY_CRAY_VERSION_CHECK(8,1,0) || \ + JSON_HEDLEY_MCST_LCC_VERSION_CHECK(1,25,10) + #define JSON_HEDLEY_IS_CONSTANT(expr) __builtin_constant_p(expr) +#endif +#if !defined(__cplusplus) +# if \ + JSON_HEDLEY_HAS_BUILTIN(__builtin_types_compatible_p) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(3,4,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + JSON_HEDLEY_IBM_VERSION_CHECK(13,1,0) || \ + JSON_HEDLEY_CRAY_VERSION_CHECK(8,1,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(5,4,0) || \ + JSON_HEDLEY_TINYC_VERSION_CHECK(0,9,24) +#if defined(__INTPTR_TYPE__) + #define JSON_HEDLEY_IS_CONSTEXPR_(expr) __builtin_types_compatible_p(__typeof__((1 ? (void*) ((__INTPTR_TYPE__) ((expr) * 0)) : (int*) 0)), int*) +#else + #include + #define JSON_HEDLEY_IS_CONSTEXPR_(expr) __builtin_types_compatible_p(__typeof__((1 ? (void*) ((intptr_t) ((expr) * 0)) : (int*) 0)), int*) +#endif +# elif \ + ( \ + defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 201112L) && \ + !defined(JSON_HEDLEY_SUNPRO_VERSION) && \ + !defined(JSON_HEDLEY_PGI_VERSION) && \ + !defined(JSON_HEDLEY_IAR_VERSION)) || \ + (JSON_HEDLEY_HAS_EXTENSION(c_generic_selections) && !defined(JSON_HEDLEY_IAR_VERSION)) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(4,9,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(17,0,0) || \ + JSON_HEDLEY_IBM_VERSION_CHECK(12,1,0) || \ + JSON_HEDLEY_ARM_VERSION_CHECK(5,3,0) +#if defined(__INTPTR_TYPE__) + #define JSON_HEDLEY_IS_CONSTEXPR_(expr) _Generic((1 ? (void*) ((__INTPTR_TYPE__) ((expr) * 0)) : (int*) 0), int*: 1, void*: 0) +#else + #include + #define JSON_HEDLEY_IS_CONSTEXPR_(expr) _Generic((1 ? (void*) ((intptr_t) * 0) : (int*) 0), int*: 1, void*: 0) +#endif +# elif \ + defined(JSON_HEDLEY_GCC_VERSION) || \ + defined(JSON_HEDLEY_INTEL_VERSION) || \ + defined(JSON_HEDLEY_TINYC_VERSION) || \ + defined(JSON_HEDLEY_TI_ARMCL_VERSION) || \ + JSON_HEDLEY_TI_CL430_VERSION_CHECK(18,12,0) || \ + defined(JSON_HEDLEY_TI_CL2000_VERSION) || \ + defined(JSON_HEDLEY_TI_CL6X_VERSION) || \ + defined(JSON_HEDLEY_TI_CL7X_VERSION) || \ + defined(JSON_HEDLEY_TI_CLPRU_VERSION) || \ + defined(__clang__) +# define JSON_HEDLEY_IS_CONSTEXPR_(expr) ( \ + sizeof(void) != \ + sizeof(*( \ + 1 ? \ + ((void*) ((expr) * 0L) ) : \ +((struct { char v[sizeof(void) * 2]; } *) 1) \ + ) \ + ) \ + ) +# endif +#endif +#if defined(JSON_HEDLEY_IS_CONSTEXPR_) + #if !defined(JSON_HEDLEY_IS_CONSTANT) + #define JSON_HEDLEY_IS_CONSTANT(expr) JSON_HEDLEY_IS_CONSTEXPR_(expr) + #endif + #define JSON_HEDLEY_REQUIRE_CONSTEXPR(expr) (JSON_HEDLEY_IS_CONSTEXPR_(expr) ? (expr) : (-1)) +#else + #if !defined(JSON_HEDLEY_IS_CONSTANT) + #define JSON_HEDLEY_IS_CONSTANT(expr) (0) + #endif + #define JSON_HEDLEY_REQUIRE_CONSTEXPR(expr) (expr) +#endif + +#if defined(JSON_HEDLEY_BEGIN_C_DECLS) + #undef JSON_HEDLEY_BEGIN_C_DECLS +#endif +#if defined(JSON_HEDLEY_END_C_DECLS) + #undef JSON_HEDLEY_END_C_DECLS +#endif +#if defined(JSON_HEDLEY_C_DECL) + #undef JSON_HEDLEY_C_DECL +#endif +#if defined(__cplusplus) + #define JSON_HEDLEY_BEGIN_C_DECLS extern "C" { + #define JSON_HEDLEY_END_C_DECLS } + #define JSON_HEDLEY_C_DECL extern "C" +#else + #define JSON_HEDLEY_BEGIN_C_DECLS + #define JSON_HEDLEY_END_C_DECLS + #define JSON_HEDLEY_C_DECL +#endif + +#if defined(JSON_HEDLEY_STATIC_ASSERT) + #undef JSON_HEDLEY_STATIC_ASSERT +#endif +#if \ + !defined(__cplusplus) && ( \ + (defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 201112L)) || \ + (JSON_HEDLEY_HAS_FEATURE(c_static_assert) && !defined(JSON_HEDLEY_INTEL_CL_VERSION)) || \ + JSON_HEDLEY_GCC_VERSION_CHECK(6,0,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) || \ + defined(_Static_assert) \ + ) +# define JSON_HEDLEY_STATIC_ASSERT(expr, message) _Static_assert(expr, message) +#elif \ + (defined(__cplusplus) && (__cplusplus >= 201103L)) || \ + JSON_HEDLEY_MSVC_VERSION_CHECK(16,0,0) || \ + JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) +# define JSON_HEDLEY_STATIC_ASSERT(expr, message) JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_(static_assert(expr, message)) +#else +# define JSON_HEDLEY_STATIC_ASSERT(expr, message) +#endif + +#if defined(JSON_HEDLEY_NULL) + #undef JSON_HEDLEY_NULL +#endif +#if defined(__cplusplus) + #if __cplusplus >= 201103L + #define JSON_HEDLEY_NULL JSON_HEDLEY_DIAGNOSTIC_DISABLE_CPP98_COMPAT_WRAP_(nullptr) + #elif defined(NULL) + #define JSON_HEDLEY_NULL NULL + #else + #define JSON_HEDLEY_NULL JSON_HEDLEY_STATIC_CAST(void*, 0) + #endif +#elif defined(NULL) + #define JSON_HEDLEY_NULL NULL +#else + #define JSON_HEDLEY_NULL ((void*) 0) +#endif + +#if defined(JSON_HEDLEY_MESSAGE) + #undef JSON_HEDLEY_MESSAGE +#endif +#if JSON_HEDLEY_HAS_WARNING("-Wunknown-pragmas") +# define JSON_HEDLEY_MESSAGE(msg) \ + JSON_HEDLEY_DIAGNOSTIC_PUSH \ + JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS \ + JSON_HEDLEY_PRAGMA(message msg) \ + JSON_HEDLEY_DIAGNOSTIC_POP +#elif \ + JSON_HEDLEY_GCC_VERSION_CHECK(4,4,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) +# define JSON_HEDLEY_MESSAGE(msg) JSON_HEDLEY_PRAGMA(message msg) +#elif JSON_HEDLEY_CRAY_VERSION_CHECK(5,0,0) +# define JSON_HEDLEY_MESSAGE(msg) JSON_HEDLEY_PRAGMA(_CRI message msg) +#elif JSON_HEDLEY_IAR_VERSION_CHECK(8,0,0) +# define JSON_HEDLEY_MESSAGE(msg) JSON_HEDLEY_PRAGMA(message(msg)) +#elif JSON_HEDLEY_PELLES_VERSION_CHECK(2,0,0) +# define JSON_HEDLEY_MESSAGE(msg) JSON_HEDLEY_PRAGMA(message(msg)) +#else +# define JSON_HEDLEY_MESSAGE(msg) +#endif + +#if defined(JSON_HEDLEY_WARNING) + #undef JSON_HEDLEY_WARNING +#endif +#if JSON_HEDLEY_HAS_WARNING("-Wunknown-pragmas") +# define JSON_HEDLEY_WARNING(msg) \ + JSON_HEDLEY_DIAGNOSTIC_PUSH \ + JSON_HEDLEY_DIAGNOSTIC_DISABLE_UNKNOWN_PRAGMAS \ + JSON_HEDLEY_PRAGMA(clang warning msg) \ + JSON_HEDLEY_DIAGNOSTIC_POP +#elif \ + JSON_HEDLEY_GCC_VERSION_CHECK(4,8,0) || \ + JSON_HEDLEY_PGI_VERSION_CHECK(18,4,0) || \ + JSON_HEDLEY_INTEL_VERSION_CHECK(13,0,0) +# define JSON_HEDLEY_WARNING(msg) JSON_HEDLEY_PRAGMA(GCC warning msg) +#elif \ + JSON_HEDLEY_MSVC_VERSION_CHECK(15,0,0) || \ + JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) +# define JSON_HEDLEY_WARNING(msg) JSON_HEDLEY_PRAGMA(message(msg)) +#else +# define JSON_HEDLEY_WARNING(msg) JSON_HEDLEY_MESSAGE(msg) +#endif + +#if defined(JSON_HEDLEY_REQUIRE) + #undef JSON_HEDLEY_REQUIRE +#endif +#if defined(JSON_HEDLEY_REQUIRE_MSG) + #undef JSON_HEDLEY_REQUIRE_MSG +#endif +#if JSON_HEDLEY_HAS_ATTRIBUTE(diagnose_if) +# if JSON_HEDLEY_HAS_WARNING("-Wgcc-compat") +# define JSON_HEDLEY_REQUIRE(expr) \ + JSON_HEDLEY_DIAGNOSTIC_PUSH \ + _Pragma("clang diagnostic ignored \"-Wgcc-compat\"") \ + __attribute__((diagnose_if(!(expr), #expr, "error"))) \ + JSON_HEDLEY_DIAGNOSTIC_POP +# define JSON_HEDLEY_REQUIRE_MSG(expr,msg) \ + JSON_HEDLEY_DIAGNOSTIC_PUSH \ + _Pragma("clang diagnostic ignored \"-Wgcc-compat\"") \ + __attribute__((diagnose_if(!(expr), msg, "error"))) \ + JSON_HEDLEY_DIAGNOSTIC_POP +# else +# define JSON_HEDLEY_REQUIRE(expr) __attribute__((diagnose_if(!(expr), #expr, "error"))) +# define JSON_HEDLEY_REQUIRE_MSG(expr,msg) __attribute__((diagnose_if(!(expr), msg, "error"))) +# endif +#else +# define JSON_HEDLEY_REQUIRE(expr) +# define JSON_HEDLEY_REQUIRE_MSG(expr,msg) +#endif + +#if defined(JSON_HEDLEY_FLAGS) + #undef JSON_HEDLEY_FLAGS +#endif +#if JSON_HEDLEY_HAS_ATTRIBUTE(flag_enum) && (!defined(__cplusplus) || JSON_HEDLEY_HAS_WARNING("-Wbitfield-enum-conversion")) + #define JSON_HEDLEY_FLAGS __attribute__((__flag_enum__)) +#else + #define JSON_HEDLEY_FLAGS +#endif + +#if defined(JSON_HEDLEY_FLAGS_CAST) + #undef JSON_HEDLEY_FLAGS_CAST +#endif +#if JSON_HEDLEY_INTEL_VERSION_CHECK(19,0,0) +# define JSON_HEDLEY_FLAGS_CAST(T, expr) (__extension__ ({ \ + JSON_HEDLEY_DIAGNOSTIC_PUSH \ + _Pragma("warning(disable:188)") \ + ((T) (expr)); \ + JSON_HEDLEY_DIAGNOSTIC_POP \ + })) +#else +# define JSON_HEDLEY_FLAGS_CAST(T, expr) JSON_HEDLEY_STATIC_CAST(T, expr) +#endif + +#if defined(JSON_HEDLEY_EMPTY_BASES) + #undef JSON_HEDLEY_EMPTY_BASES +#endif +#if \ + (JSON_HEDLEY_MSVC_VERSION_CHECK(19,0,23918) && !JSON_HEDLEY_MSVC_VERSION_CHECK(20,0,0)) || \ + JSON_HEDLEY_INTEL_CL_VERSION_CHECK(2021,1,0) + #define JSON_HEDLEY_EMPTY_BASES __declspec(empty_bases) +#else + #define JSON_HEDLEY_EMPTY_BASES +#endif + +/* Remaining macros are deprecated. */ + +#if defined(JSON_HEDLEY_GCC_NOT_CLANG_VERSION_CHECK) + #undef JSON_HEDLEY_GCC_NOT_CLANG_VERSION_CHECK +#endif +#if defined(__clang__) + #define JSON_HEDLEY_GCC_NOT_CLANG_VERSION_CHECK(major,minor,patch) (0) +#else + #define JSON_HEDLEY_GCC_NOT_CLANG_VERSION_CHECK(major,minor,patch) JSON_HEDLEY_GCC_VERSION_CHECK(major,minor,patch) +#endif + +#if defined(JSON_HEDLEY_CLANG_HAS_ATTRIBUTE) + #undef JSON_HEDLEY_CLANG_HAS_ATTRIBUTE +#endif +#define JSON_HEDLEY_CLANG_HAS_ATTRIBUTE(attribute) JSON_HEDLEY_HAS_ATTRIBUTE(attribute) + +#if defined(JSON_HEDLEY_CLANG_HAS_CPP_ATTRIBUTE) + #undef JSON_HEDLEY_CLANG_HAS_CPP_ATTRIBUTE +#endif +#define JSON_HEDLEY_CLANG_HAS_CPP_ATTRIBUTE(attribute) JSON_HEDLEY_HAS_CPP_ATTRIBUTE(attribute) + +#if defined(JSON_HEDLEY_CLANG_HAS_BUILTIN) + #undef JSON_HEDLEY_CLANG_HAS_BUILTIN +#endif +#define JSON_HEDLEY_CLANG_HAS_BUILTIN(builtin) JSON_HEDLEY_HAS_BUILTIN(builtin) + +#if defined(JSON_HEDLEY_CLANG_HAS_FEATURE) + #undef JSON_HEDLEY_CLANG_HAS_FEATURE +#endif +#define JSON_HEDLEY_CLANG_HAS_FEATURE(feature) JSON_HEDLEY_HAS_FEATURE(feature) + +#if defined(JSON_HEDLEY_CLANG_HAS_EXTENSION) + #undef JSON_HEDLEY_CLANG_HAS_EXTENSION +#endif +#define JSON_HEDLEY_CLANG_HAS_EXTENSION(extension) JSON_HEDLEY_HAS_EXTENSION(extension) + +#if defined(JSON_HEDLEY_CLANG_HAS_DECLSPEC_DECLSPEC_ATTRIBUTE) + #undef JSON_HEDLEY_CLANG_HAS_DECLSPEC_DECLSPEC_ATTRIBUTE +#endif +#define JSON_HEDLEY_CLANG_HAS_DECLSPEC_ATTRIBUTE(attribute) JSON_HEDLEY_HAS_DECLSPEC_ATTRIBUTE(attribute) + +#if defined(JSON_HEDLEY_CLANG_HAS_WARNING) + #undef JSON_HEDLEY_CLANG_HAS_WARNING +#endif +#define JSON_HEDLEY_CLANG_HAS_WARNING(warning) JSON_HEDLEY_HAS_WARNING(warning) + +#endif /* !defined(JSON_HEDLEY_VERSION) || (JSON_HEDLEY_VERSION < X) */ + + +// This file contains all internal macro definitions (except those affecting ABI) +// You MUST include macro_unscope.hpp at the end of json.hpp to undef all of them + +// #include + + +// exclude unsupported compilers +#if !defined(JSON_SKIP_UNSUPPORTED_COMPILER_CHECK) + #if defined(__clang__) + #if (__clang_major__ * 10000 + __clang_minor__ * 100 + __clang_patchlevel__) < 30400 + #error "unsupported Clang version - see https://github.com/nlohmann/json#supported-compilers" + #endif + #elif defined(__GNUC__) && !(defined(__ICC) || defined(__INTEL_COMPILER)) + #if (__GNUC__ * 10000 + __GNUC_MINOR__ * 100 + __GNUC_PATCHLEVEL__) < 40800 + #error "unsupported GCC version - see https://github.com/nlohmann/json#supported-compilers" + #endif + #endif +#endif + +// C++ language standard detection +// if the user manually specified the used c++ version this is skipped +#if !defined(JSON_HAS_CPP_20) && !defined(JSON_HAS_CPP_17) && !defined(JSON_HAS_CPP_14) && !defined(JSON_HAS_CPP_11) + #if (defined(__cplusplus) && __cplusplus >= 202002L) || (defined(_MSVC_LANG) && _MSVC_LANG >= 202002L) + #define JSON_HAS_CPP_20 + #define JSON_HAS_CPP_17 + #define JSON_HAS_CPP_14 + #elif (defined(__cplusplus) && __cplusplus >= 201703L) || (defined(_HAS_CXX17) && _HAS_CXX17 == 1) // fix for issue #464 + #define JSON_HAS_CPP_17 + #define JSON_HAS_CPP_14 + #elif (defined(__cplusplus) && __cplusplus >= 201402L) || (defined(_HAS_CXX14) && _HAS_CXX14 == 1) + #define JSON_HAS_CPP_14 + #endif + // the cpp 11 flag is always specified because it is the minimal required version + #define JSON_HAS_CPP_11 +#endif + +#ifdef __has_include + #if __has_include() + #include + #endif +#endif + +#if !defined(JSON_HAS_FILESYSTEM) && !defined(JSON_HAS_EXPERIMENTAL_FILESYSTEM) + #ifdef JSON_HAS_CPP_17 + #if defined(__cpp_lib_filesystem) + #define JSON_HAS_FILESYSTEM 1 + #elif defined(__cpp_lib_experimental_filesystem) + #define JSON_HAS_EXPERIMENTAL_FILESYSTEM 1 + #elif !defined(__has_include) + #define JSON_HAS_EXPERIMENTAL_FILESYSTEM 1 + #elif __has_include() + #define JSON_HAS_FILESYSTEM 1 + #elif __has_include() + #define JSON_HAS_EXPERIMENTAL_FILESYSTEM 1 + #endif + + // std::filesystem does not work on MinGW GCC 8: https://sourceforge.net/p/mingw-w64/bugs/737/ + #if defined(__MINGW32__) && defined(__GNUC__) && __GNUC__ == 8 + #undef JSON_HAS_FILESYSTEM + #undef JSON_HAS_EXPERIMENTAL_FILESYSTEM + #endif + + // no filesystem support before GCC 8: https://en.cppreference.com/w/cpp/compiler_support + #if defined(__GNUC__) && !defined(__clang__) && __GNUC__ < 8 + #undef JSON_HAS_FILESYSTEM + #undef JSON_HAS_EXPERIMENTAL_FILESYSTEM + #endif + + // no filesystem support before Clang 7: https://en.cppreference.com/w/cpp/compiler_support + #if defined(__clang_major__) && __clang_major__ < 7 + #undef JSON_HAS_FILESYSTEM + #undef JSON_HAS_EXPERIMENTAL_FILESYSTEM + #endif + + // no filesystem support before MSVC 19.14: https://en.cppreference.com/w/cpp/compiler_support + #if defined(_MSC_VER) && _MSC_VER < 1914 + #undef JSON_HAS_FILESYSTEM + #undef JSON_HAS_EXPERIMENTAL_FILESYSTEM + #endif + + // no filesystem support before iOS 13 + #if defined(__IPHONE_OS_VERSION_MIN_REQUIRED) && __IPHONE_OS_VERSION_MIN_REQUIRED < 130000 + #undef JSON_HAS_FILESYSTEM + #undef JSON_HAS_EXPERIMENTAL_FILESYSTEM + #endif + + // no filesystem support before macOS Catalina + #if defined(__MAC_OS_X_VERSION_MIN_REQUIRED) && __MAC_OS_X_VERSION_MIN_REQUIRED < 101500 + #undef JSON_HAS_FILESYSTEM + #undef JSON_HAS_EXPERIMENTAL_FILESYSTEM + #endif + #endif +#endif + +#ifndef JSON_HAS_EXPERIMENTAL_FILESYSTEM + #define JSON_HAS_EXPERIMENTAL_FILESYSTEM 0 +#endif + +#ifndef JSON_HAS_FILESYSTEM + #define JSON_HAS_FILESYSTEM 0 +#endif + +#ifndef JSON_HAS_THREE_WAY_COMPARISON + #if defined(__cpp_impl_three_way_comparison) && __cpp_impl_three_way_comparison >= 201907L \ + && defined(__cpp_lib_three_way_comparison) && __cpp_lib_three_way_comparison >= 201907L + #define JSON_HAS_THREE_WAY_COMPARISON 1 + #else + #define JSON_HAS_THREE_WAY_COMPARISON 0 + #endif +#endif + +#ifndef JSON_HAS_RANGES + // ranges header shipping in GCC 11.1.0 (released 2021-04-27) has syntax error + #if defined(__GLIBCXX__) && __GLIBCXX__ == 20210427 + #define JSON_HAS_RANGES 0 + #elif defined(__cpp_lib_ranges) + #define JSON_HAS_RANGES 1 + #else + #define JSON_HAS_RANGES 0 + #endif +#endif + +#ifndef JSON_HAS_STATIC_RTTI + #if !defined(_HAS_STATIC_RTTI) || _HAS_STATIC_RTTI != 0 + #define JSON_HAS_STATIC_RTTI 1 + #else + #define JSON_HAS_STATIC_RTTI 0 + #endif +#endif + +#ifdef JSON_HAS_CPP_17 + #define JSON_INLINE_VARIABLE inline +#else + #define JSON_INLINE_VARIABLE +#endif + +#if JSON_HEDLEY_HAS_ATTRIBUTE(no_unique_address) + #define JSON_NO_UNIQUE_ADDRESS [[no_unique_address]] +#else + #define JSON_NO_UNIQUE_ADDRESS +#endif + +// disable documentation warnings on clang +#if defined(__clang__) + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wdocumentation" + #pragma clang diagnostic ignored "-Wdocumentation-unknown-command" +#endif + +// allow disabling exceptions +#if (defined(__cpp_exceptions) || defined(__EXCEPTIONS) || defined(_CPPUNWIND)) && !defined(JSON_NOEXCEPTION) + #define JSON_THROW(exception) throw exception + #define JSON_TRY try + #define JSON_CATCH(exception) catch(exception) + #define JSON_INTERNAL_CATCH(exception) catch(exception) +#else + #include + #define JSON_THROW(exception) std::abort() + #define JSON_TRY if(true) + #define JSON_CATCH(exception) if(false) + #define JSON_INTERNAL_CATCH(exception) if(false) +#endif + +// override exception macros +#if defined(JSON_THROW_USER) + #undef JSON_THROW + #define JSON_THROW JSON_THROW_USER +#endif +#if defined(JSON_TRY_USER) + #undef JSON_TRY + #define JSON_TRY JSON_TRY_USER +#endif +#if defined(JSON_CATCH_USER) + #undef JSON_CATCH + #define JSON_CATCH JSON_CATCH_USER + #undef JSON_INTERNAL_CATCH + #define JSON_INTERNAL_CATCH JSON_CATCH_USER +#endif +#if defined(JSON_INTERNAL_CATCH_USER) + #undef JSON_INTERNAL_CATCH + #define JSON_INTERNAL_CATCH JSON_INTERNAL_CATCH_USER +#endif + +// allow overriding assert +#if !defined(JSON_ASSERT) + #include // assert + #define JSON_ASSERT(x) assert(x) +#endif + +// allow to access some private functions (needed by the test suite) +#if defined(JSON_TESTS_PRIVATE) + #define JSON_PRIVATE_UNLESS_TESTED public +#else + #define JSON_PRIVATE_UNLESS_TESTED private +#endif + +/*! +@brief macro to briefly define a mapping between an enum and JSON +@def NLOHMANN_JSON_SERIALIZE_ENUM +@since version 3.4.0 +*/ +#define NLOHMANN_JSON_SERIALIZE_ENUM(ENUM_TYPE, ...) \ + template \ + inline void to_json(BasicJsonType& j, const ENUM_TYPE& e) \ + { \ + static_assert(std::is_enum::value, #ENUM_TYPE " must be an enum!"); \ + static const std::pair m[] = __VA_ARGS__; \ + auto it = std::find_if(std::begin(m), std::end(m), \ + [e](const std::pair& ej_pair) -> bool \ + { \ + return ej_pair.first == e; \ + }); \ + j = ((it != std::end(m)) ? it : std::begin(m))->second; \ + } \ + template \ + inline void from_json(const BasicJsonType& j, ENUM_TYPE& e) \ + { \ + static_assert(std::is_enum::value, #ENUM_TYPE " must be an enum!"); \ + static const std::pair m[] = __VA_ARGS__; \ + auto it = std::find_if(std::begin(m), std::end(m), \ + [&j](const std::pair& ej_pair) -> bool \ + { \ + return ej_pair.second == j; \ + }); \ + e = ((it != std::end(m)) ? it : std::begin(m))->first; \ + } + +// Ugly macros to avoid uglier copy-paste when specializing basic_json. They +// may be removed in the future once the class is split. + +#define NLOHMANN_BASIC_JSON_TPL_DECLARATION \ + template class ObjectType, \ + template class ArrayType, \ + class StringType, class BooleanType, class NumberIntegerType, \ + class NumberUnsignedType, class NumberFloatType, \ + template class AllocatorType, \ + template class JSONSerializer, \ + class BinaryType, \ + class CustomBaseClass> + +#define NLOHMANN_BASIC_JSON_TPL \ + basic_json + +// Macros to simplify conversion from/to types + +#define NLOHMANN_JSON_EXPAND( x ) x +#define NLOHMANN_JSON_GET_MACRO(_1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17, _18, _19, _20, _21, _22, _23, _24, _25, _26, _27, _28, _29, _30, _31, _32, _33, _34, _35, _36, _37, _38, _39, _40, _41, _42, _43, _44, _45, _46, _47, _48, _49, _50, _51, _52, _53, _54, _55, _56, _57, _58, _59, _60, _61, _62, _63, _64, NAME,...) NAME +#define NLOHMANN_JSON_PASTE(...) NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_GET_MACRO(__VA_ARGS__, \ + NLOHMANN_JSON_PASTE64, \ + NLOHMANN_JSON_PASTE63, \ + NLOHMANN_JSON_PASTE62, \ + NLOHMANN_JSON_PASTE61, \ + NLOHMANN_JSON_PASTE60, \ + NLOHMANN_JSON_PASTE59, \ + NLOHMANN_JSON_PASTE58, \ + NLOHMANN_JSON_PASTE57, \ + NLOHMANN_JSON_PASTE56, \ + NLOHMANN_JSON_PASTE55, \ + NLOHMANN_JSON_PASTE54, \ + NLOHMANN_JSON_PASTE53, \ + NLOHMANN_JSON_PASTE52, \ + NLOHMANN_JSON_PASTE51, \ + NLOHMANN_JSON_PASTE50, \ + NLOHMANN_JSON_PASTE49, \ + NLOHMANN_JSON_PASTE48, \ + NLOHMANN_JSON_PASTE47, \ + NLOHMANN_JSON_PASTE46, \ + NLOHMANN_JSON_PASTE45, \ + NLOHMANN_JSON_PASTE44, \ + NLOHMANN_JSON_PASTE43, \ + NLOHMANN_JSON_PASTE42, \ + NLOHMANN_JSON_PASTE41, \ + NLOHMANN_JSON_PASTE40, \ + NLOHMANN_JSON_PASTE39, \ + NLOHMANN_JSON_PASTE38, \ + NLOHMANN_JSON_PASTE37, \ + NLOHMANN_JSON_PASTE36, \ + NLOHMANN_JSON_PASTE35, \ + NLOHMANN_JSON_PASTE34, \ + NLOHMANN_JSON_PASTE33, \ + NLOHMANN_JSON_PASTE32, \ + NLOHMANN_JSON_PASTE31, \ + NLOHMANN_JSON_PASTE30, \ + NLOHMANN_JSON_PASTE29, \ + NLOHMANN_JSON_PASTE28, \ + NLOHMANN_JSON_PASTE27, \ + NLOHMANN_JSON_PASTE26, \ + NLOHMANN_JSON_PASTE25, \ + NLOHMANN_JSON_PASTE24, \ + NLOHMANN_JSON_PASTE23, \ + NLOHMANN_JSON_PASTE22, \ + NLOHMANN_JSON_PASTE21, \ + NLOHMANN_JSON_PASTE20, \ + NLOHMANN_JSON_PASTE19, \ + NLOHMANN_JSON_PASTE18, \ + NLOHMANN_JSON_PASTE17, \ + NLOHMANN_JSON_PASTE16, \ + NLOHMANN_JSON_PASTE15, \ + NLOHMANN_JSON_PASTE14, \ + NLOHMANN_JSON_PASTE13, \ + NLOHMANN_JSON_PASTE12, \ + NLOHMANN_JSON_PASTE11, \ + NLOHMANN_JSON_PASTE10, \ + NLOHMANN_JSON_PASTE9, \ + NLOHMANN_JSON_PASTE8, \ + NLOHMANN_JSON_PASTE7, \ + NLOHMANN_JSON_PASTE6, \ + NLOHMANN_JSON_PASTE5, \ + NLOHMANN_JSON_PASTE4, \ + NLOHMANN_JSON_PASTE3, \ + NLOHMANN_JSON_PASTE2, \ + NLOHMANN_JSON_PASTE1)(__VA_ARGS__)) +#define NLOHMANN_JSON_PASTE2(func, v1) func(v1) +#define NLOHMANN_JSON_PASTE3(func, v1, v2) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE2(func, v2) +#define NLOHMANN_JSON_PASTE4(func, v1, v2, v3) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE3(func, v2, v3) +#define NLOHMANN_JSON_PASTE5(func, v1, v2, v3, v4) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE4(func, v2, v3, v4) +#define NLOHMANN_JSON_PASTE6(func, v1, v2, v3, v4, v5) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE5(func, v2, v3, v4, v5) +#define NLOHMANN_JSON_PASTE7(func, v1, v2, v3, v4, v5, v6) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE6(func, v2, v3, v4, v5, v6) +#define NLOHMANN_JSON_PASTE8(func, v1, v2, v3, v4, v5, v6, v7) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE7(func, v2, v3, v4, v5, v6, v7) +#define NLOHMANN_JSON_PASTE9(func, v1, v2, v3, v4, v5, v6, v7, v8) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE8(func, v2, v3, v4, v5, v6, v7, v8) +#define NLOHMANN_JSON_PASTE10(func, v1, v2, v3, v4, v5, v6, v7, v8, v9) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE9(func, v2, v3, v4, v5, v6, v7, v8, v9) +#define NLOHMANN_JSON_PASTE11(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE10(func, v2, v3, v4, v5, v6, v7, v8, v9, v10) +#define NLOHMANN_JSON_PASTE12(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE11(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11) +#define NLOHMANN_JSON_PASTE13(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE12(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12) +#define NLOHMANN_JSON_PASTE14(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE13(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13) +#define NLOHMANN_JSON_PASTE15(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE14(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14) +#define NLOHMANN_JSON_PASTE16(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE15(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15) +#define NLOHMANN_JSON_PASTE17(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE16(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16) +#define NLOHMANN_JSON_PASTE18(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE17(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17) +#define NLOHMANN_JSON_PASTE19(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE18(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18) +#define NLOHMANN_JSON_PASTE20(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE19(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19) +#define NLOHMANN_JSON_PASTE21(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE20(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20) +#define NLOHMANN_JSON_PASTE22(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE21(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21) +#define NLOHMANN_JSON_PASTE23(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE22(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22) +#define NLOHMANN_JSON_PASTE24(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE23(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23) +#define NLOHMANN_JSON_PASTE25(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE24(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24) +#define NLOHMANN_JSON_PASTE26(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE25(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25) +#define NLOHMANN_JSON_PASTE27(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE26(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26) +#define NLOHMANN_JSON_PASTE28(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE27(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27) +#define NLOHMANN_JSON_PASTE29(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE28(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28) +#define NLOHMANN_JSON_PASTE30(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE29(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29) +#define NLOHMANN_JSON_PASTE31(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE30(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30) +#define NLOHMANN_JSON_PASTE32(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE31(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31) +#define NLOHMANN_JSON_PASTE33(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE32(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32) +#define NLOHMANN_JSON_PASTE34(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE33(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33) +#define NLOHMANN_JSON_PASTE35(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE34(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34) +#define NLOHMANN_JSON_PASTE36(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE35(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35) +#define NLOHMANN_JSON_PASTE37(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE36(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36) +#define NLOHMANN_JSON_PASTE38(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE37(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37) +#define NLOHMANN_JSON_PASTE39(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE38(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38) +#define NLOHMANN_JSON_PASTE40(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE39(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39) +#define NLOHMANN_JSON_PASTE41(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE40(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40) +#define NLOHMANN_JSON_PASTE42(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE41(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41) +#define NLOHMANN_JSON_PASTE43(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE42(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42) +#define NLOHMANN_JSON_PASTE44(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE43(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43) +#define NLOHMANN_JSON_PASTE45(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE44(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44) +#define NLOHMANN_JSON_PASTE46(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE45(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45) +#define NLOHMANN_JSON_PASTE47(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE46(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46) +#define NLOHMANN_JSON_PASTE48(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE47(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47) +#define NLOHMANN_JSON_PASTE49(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE48(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48) +#define NLOHMANN_JSON_PASTE50(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE49(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49) +#define NLOHMANN_JSON_PASTE51(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE50(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50) +#define NLOHMANN_JSON_PASTE52(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE51(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51) +#define NLOHMANN_JSON_PASTE53(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE52(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52) +#define NLOHMANN_JSON_PASTE54(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE53(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53) +#define NLOHMANN_JSON_PASTE55(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE54(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54) +#define NLOHMANN_JSON_PASTE56(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE55(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55) +#define NLOHMANN_JSON_PASTE57(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE56(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56) +#define NLOHMANN_JSON_PASTE58(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE57(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57) +#define NLOHMANN_JSON_PASTE59(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE58(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58) +#define NLOHMANN_JSON_PASTE60(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE59(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59) +#define NLOHMANN_JSON_PASTE61(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59, v60) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE60(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59, v60) +#define NLOHMANN_JSON_PASTE62(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59, v60, v61) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE61(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59, v60, v61) +#define NLOHMANN_JSON_PASTE63(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59, v60, v61, v62) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE62(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59, v60, v61, v62) +#define NLOHMANN_JSON_PASTE64(func, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59, v60, v61, v62, v63) NLOHMANN_JSON_PASTE2(func, v1) NLOHMANN_JSON_PASTE63(func, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15, v16, v17, v18, v19, v20, v21, v22, v23, v24, v25, v26, v27, v28, v29, v30, v31, v32, v33, v34, v35, v36, v37, v38, v39, v40, v41, v42, v43, v44, v45, v46, v47, v48, v49, v50, v51, v52, v53, v54, v55, v56, v57, v58, v59, v60, v61, v62, v63) + +#define NLOHMANN_JSON_TO(v1) nlohmann_json_j[#v1] = nlohmann_json_t.v1; +#define NLOHMANN_JSON_FROM(v1) nlohmann_json_j.at(#v1).get_to(nlohmann_json_t.v1); +#define NLOHMANN_JSON_FROM_WITH_DEFAULT(v1) nlohmann_json_t.v1 = nlohmann_json_j.value(#v1, nlohmann_json_default_obj.v1); + +/*! +@brief macro +@def NLOHMANN_DEFINE_TYPE_INTRUSIVE +@since version 3.9.0 +*/ +#define NLOHMANN_DEFINE_TYPE_INTRUSIVE(Type, ...) \ + friend void to_json(nlohmann::json& nlohmann_json_j, const Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) } \ + friend void from_json(const nlohmann::json& nlohmann_json_j, Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM, __VA_ARGS__)) } + +#define NLOHMANN_DEFINE_TYPE_INTRUSIVE_WITH_DEFAULT(Type, ...) \ + friend void to_json(nlohmann::json& nlohmann_json_j, const Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) } \ + friend void from_json(const nlohmann::json& nlohmann_json_j, Type& nlohmann_json_t) { const Type nlohmann_json_default_obj{}; NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM_WITH_DEFAULT, __VA_ARGS__)) } + +#define NLOHMANN_DEFINE_TYPE_INTRUSIVE_ONLY_SERIALIZE(Type, ...) \ + friend void to_json(nlohmann::json& nlohmann_json_j, const Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) } + +/*! +@brief macro +@def NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE +@since version 3.9.0 +*/ +#define NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE(Type, ...) \ + inline void to_json(nlohmann::json& nlohmann_json_j, const Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) } \ + inline void from_json(const nlohmann::json& nlohmann_json_j, Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM, __VA_ARGS__)) } + +#define NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_ONLY_SERIALIZE(Type, ...) \ + inline void to_json(nlohmann::json& nlohmann_json_j, const Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) } + +#define NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(Type, ...) \ + inline void to_json(nlohmann::json& nlohmann_json_j, const Type& nlohmann_json_t) { NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_TO, __VA_ARGS__)) } \ + inline void from_json(const nlohmann::json& nlohmann_json_j, Type& nlohmann_json_t) { const Type nlohmann_json_default_obj{}; NLOHMANN_JSON_EXPAND(NLOHMANN_JSON_PASTE(NLOHMANN_JSON_FROM_WITH_DEFAULT, __VA_ARGS__)) } + +// inspired from https://stackoverflow.com/a/26745591 +// allows to call any std function as if (e.g. with begin): +// using std::begin; begin(x); +// +// it allows using the detected idiom to retrieve the return type +// of such an expression +#define NLOHMANN_CAN_CALL_STD_FUNC_IMPL(std_name) \ + namespace detail { \ + using std::std_name; \ + \ + template \ + using result_of_##std_name = decltype(std_name(std::declval()...)); \ + } \ + \ + namespace detail2 { \ + struct std_name##_tag \ + { \ + }; \ + \ + template \ + std_name##_tag std_name(T&&...); \ + \ + template \ + using result_of_##std_name = decltype(std_name(std::declval()...)); \ + \ + template \ + struct would_call_std_##std_name \ + { \ + static constexpr auto const value = ::nlohmann::detail:: \ + is_detected_exact::value; \ + }; \ + } /* namespace detail2 */ \ + \ + template \ + struct would_call_std_##std_name : detail2::would_call_std_##std_name \ + { \ + } + +#ifndef JSON_USE_IMPLICIT_CONVERSIONS + #define JSON_USE_IMPLICIT_CONVERSIONS 1 +#endif + +#if JSON_USE_IMPLICIT_CONVERSIONS + #define JSON_EXPLICIT +#else + #define JSON_EXPLICIT explicit +#endif + +#ifndef JSON_DISABLE_ENUM_SERIALIZATION + #define JSON_DISABLE_ENUM_SERIALIZATION 0 +#endif + +#ifndef JSON_USE_GLOBAL_UDLS + #define JSON_USE_GLOBAL_UDLS 1 +#endif + +#if JSON_HAS_THREE_WAY_COMPARISON + #include // partial_ordering +#endif + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ + +/////////////////////////// +// JSON type enumeration // +/////////////////////////// + +/*! +@brief the JSON type enumeration + +This enumeration collects the different JSON types. It is internally used to +distinguish the stored values, and the functions @ref basic_json::is_null(), +@ref basic_json::is_object(), @ref basic_json::is_array(), +@ref basic_json::is_string(), @ref basic_json::is_boolean(), +@ref basic_json::is_number() (with @ref basic_json::is_number_integer(), +@ref basic_json::is_number_unsigned(), and @ref basic_json::is_number_float()), +@ref basic_json::is_discarded(), @ref basic_json::is_primitive(), and +@ref basic_json::is_structured() rely on it. + +@note There are three enumeration entries (number_integer, number_unsigned, and +number_float), because the library distinguishes these three types for numbers: +@ref basic_json::number_unsigned_t is used for unsigned integers, +@ref basic_json::number_integer_t is used for signed integers, and +@ref basic_json::number_float_t is used for floating-point numbers or to +approximate integers which do not fit in the limits of their respective type. + +@sa see @ref basic_json::basic_json(const value_t value_type) -- create a JSON +value with the default value for a given type + +@since version 1.0.0 +*/ +enum class value_t : std::uint8_t +{ + null, ///< null value + object, ///< object (unordered set of name/value pairs) + array, ///< array (ordered collection of values) + string, ///< string value + boolean, ///< boolean value + number_integer, ///< number value (signed integer) + number_unsigned, ///< number value (unsigned integer) + number_float, ///< number value (floating-point) + binary, ///< binary array (ordered collection of bytes) + discarded ///< discarded by the parser callback function +}; + +/*! +@brief comparison operator for JSON types + +Returns an ordering that is similar to Python: +- order: null < boolean < number < object < array < string < binary +- furthermore, each type is not smaller than itself +- discarded values are not comparable +- binary is represented as a b"" string in python and directly comparable to a + string; however, making a binary array directly comparable with a string would + be surprising behavior in a JSON file. + +@since version 1.0.0 +*/ +#if JSON_HAS_THREE_WAY_COMPARISON + inline std::partial_ordering operator<=>(const value_t lhs, const value_t rhs) noexcept // *NOPAD* +#else + inline bool operator<(const value_t lhs, const value_t rhs) noexcept +#endif +{ + static constexpr std::array order = {{ + 0 /* null */, 3 /* object */, 4 /* array */, 5 /* string */, + 1 /* boolean */, 2 /* integer */, 2 /* unsigned */, 2 /* float */, + 6 /* binary */ + } + }; + + const auto l_index = static_cast(lhs); + const auto r_index = static_cast(rhs); +#if JSON_HAS_THREE_WAY_COMPARISON + if (l_index < order.size() && r_index < order.size()) + { + return order[l_index] <=> order[r_index]; // *NOPAD* + } + return std::partial_ordering::unordered; +#else + return l_index < order.size() && r_index < order.size() && order[l_index] < order[r_index]; +#endif +} + +// GCC selects the built-in operator< over an operator rewritten from +// a user-defined spaceship operator +// Clang, MSVC, and ICC select the rewritten candidate +// (see GCC bug https://gcc.gnu.org/bugzilla/show_bug.cgi?id=105200) +#if JSON_HAS_THREE_WAY_COMPARISON && defined(__GNUC__) +inline bool operator<(const value_t lhs, const value_t rhs) noexcept +{ + return std::is_lt(lhs <=> rhs); // *NOPAD* +} +#endif + +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END + +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.11.3 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +// #include + + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ + +/*! +@brief replace all occurrences of a substring by another string + +@param[in,out] s the string to manipulate; changed so that all + occurrences of @a f are replaced with @a t +@param[in] f the substring to replace with @a t +@param[in] t the string to replace @a f + +@pre The search string @a f must not be empty. **This precondition is +enforced with an assertion.** + +@since version 2.0.0 +*/ +template +inline void replace_substring(StringType& s, const StringType& f, + const StringType& t) +{ + JSON_ASSERT(!f.empty()); + for (auto pos = s.find(f); // find first occurrence of f + pos != StringType::npos; // make sure f was found + s.replace(pos, f.size(), t), // replace with t, and + pos = s.find(f, pos + t.size())) // find next occurrence of f + {} +} + +/*! + * @brief string escaping as described in RFC 6901 (Sect. 4) + * @param[in] s string to escape + * @return escaped string + * + * Note the order of escaping "~" to "~0" and "/" to "~1" is important. + */ +template +inline StringType escape(StringType s) +{ + replace_substring(s, StringType{"~"}, StringType{"~0"}); + replace_substring(s, StringType{"/"}, StringType{"~1"}); + return s; +} + +/*! + * @brief string unescaping as described in RFC 6901 (Sect. 4) + * @param[in] s string to unescape + * @return unescaped string + * + * Note the order of escaping "~1" to "/" and "~0" to "~" is important. + */ +template +static void unescape(StringType& s) +{ + replace_substring(s, StringType{"~1"}, StringType{"/"}); + replace_substring(s, StringType{"~0"}, StringType{"~"}); +} + +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END + +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.11.3 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +#include // size_t + +// #include + + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ + +/// struct to capture the start position of the current token +struct position_t +{ + /// the total number of characters read + std::size_t chars_read_total = 0; + /// the number of characters read in the current line + std::size_t chars_read_current_line = 0; + /// the number of lines read + std::size_t lines_read = 0; + + /// conversion to size_t to preserve SAX interface + constexpr operator size_t() const + { + return chars_read_total; + } +}; + +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END + +// #include + +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.11.3 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann +// SPDX-FileCopyrightText: 2018 The Abseil Authors +// SPDX-License-Identifier: MIT + + + +#include // array +#include // size_t +#include // conditional, enable_if, false_type, integral_constant, is_constructible, is_integral, is_same, remove_cv, remove_reference, true_type +#include // index_sequence, make_index_sequence, index_sequence_for + +// #include + + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ + +template +using uncvref_t = typename std::remove_cv::type>::type; + +#ifdef JSON_HAS_CPP_14 + +// the following utilities are natively available in C++14 +using std::enable_if_t; +using std::index_sequence; +using std::make_index_sequence; +using std::index_sequence_for; + +#else + +// alias templates to reduce boilerplate +template +using enable_if_t = typename std::enable_if::type; + +// The following code is taken from https://github.com/abseil/abseil-cpp/blob/10cb35e459f5ecca5b2ff107635da0bfa41011b4/absl/utility/utility.h +// which is part of Google Abseil (https://github.com/abseil/abseil-cpp), licensed under the Apache License 2.0. + +//// START OF CODE FROM GOOGLE ABSEIL + +// integer_sequence +// +// Class template representing a compile-time integer sequence. An instantiation +// of `integer_sequence` has a sequence of integers encoded in its +// type through its template arguments (which is a common need when +// working with C++11 variadic templates). `absl::integer_sequence` is designed +// to be a drop-in replacement for C++14's `std::integer_sequence`. +// +// Example: +// +// template< class T, T... Ints > +// void user_function(integer_sequence); +// +// int main() +// { +// // user_function's `T` will be deduced to `int` and `Ints...` +// // will be deduced to `0, 1, 2, 3, 4`. +// user_function(make_integer_sequence()); +// } +template +struct integer_sequence +{ + using value_type = T; + static constexpr std::size_t size() noexcept + { + return sizeof...(Ints); + } +}; + +// index_sequence +// +// A helper template for an `integer_sequence` of `size_t`, +// `absl::index_sequence` is designed to be a drop-in replacement for C++14's +// `std::index_sequence`. +template +using index_sequence = integer_sequence; + +namespace utility_internal +{ + +template +struct Extend; + +// Note that SeqSize == sizeof...(Ints). It's passed explicitly for efficiency. +template +struct Extend, SeqSize, 0> +{ + using type = integer_sequence < T, Ints..., (Ints + SeqSize)... >; +}; + +template +struct Extend, SeqSize, 1> +{ + using type = integer_sequence < T, Ints..., (Ints + SeqSize)..., 2 * SeqSize >; +}; + +// Recursion helper for 'make_integer_sequence'. +// 'Gen::type' is an alias for 'integer_sequence'. +template +struct Gen +{ + using type = + typename Extend < typename Gen < T, N / 2 >::type, N / 2, N % 2 >::type; +}; + +template +struct Gen +{ + using type = integer_sequence; +}; + +} // namespace utility_internal + +// Compile-time sequences of integers + +// make_integer_sequence +// +// This template alias is equivalent to +// `integer_sequence`, and is designed to be a drop-in +// replacement for C++14's `std::make_integer_sequence`. +template +using make_integer_sequence = typename utility_internal::Gen::type; + +// make_index_sequence +// +// This template alias is equivalent to `index_sequence<0, 1, ..., N-1>`, +// and is designed to be a drop-in replacement for C++14's +// `std::make_index_sequence`. +template +using make_index_sequence = make_integer_sequence; + +// index_sequence_for +// +// Converts a typename pack into an index sequence of the same length, and +// is designed to be a drop-in replacement for C++14's +// `std::index_sequence_for()` +template +using index_sequence_for = make_index_sequence; + +//// END OF CODE FROM GOOGLE ABSEIL + +#endif + +// dispatch utility (taken from ranges-v3) +template struct priority_tag : priority_tag < N - 1 > {}; +template<> struct priority_tag<0> {}; + +// taken from ranges-v3 +template +struct static_const +{ + static JSON_INLINE_VARIABLE constexpr T value{}; +}; + +#ifndef JSON_HAS_CPP_17 + template + constexpr T static_const::value; +#endif + +template +inline constexpr std::array make_array(Args&& ... args) +{ + return std::array {{static_cast(std::forward(args))...}}; +} + +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END + +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.11.3 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +#include // numeric_limits +#include // false_type, is_constructible, is_integral, is_same, true_type +#include // declval +#include // tuple +#include // char_traits + +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.11.3 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +#include // random_access_iterator_tag + +// #include + +// #include + +// #include + + +NLOHMANN_JSON_NAMESPACE_BEGIN +namespace detail +{ + +template +struct iterator_types {}; + +template +struct iterator_types < + It, + void_t> +{ + using difference_type = typename It::difference_type; + using value_type = typename It::value_type; + using pointer = typename It::pointer; + using reference = typename It::reference; + using iterator_category = typename It::iterator_category; +}; + +// This is required as some compilers implement std::iterator_traits in a way that +// doesn't work with SFINAE. See https://github.com/nlohmann/json/issues/1341. +template +struct iterator_traits +{ +}; + +template +struct iterator_traits < T, enable_if_t < !std::is_pointer::value >> + : iterator_types +{ +}; + +template +struct iterator_traits::value>> +{ + using iterator_category = std::random_access_iterator_tag; + using value_type = T; + using difference_type = ptrdiff_t; + using pointer = T*; + using reference = T&; +}; + +} // namespace detail +NLOHMANN_JSON_NAMESPACE_END + +// #include + +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.11.3 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +// #include + + +NLOHMANN_JSON_NAMESPACE_BEGIN + +NLOHMANN_CAN_CALL_STD_FUNC_IMPL(begin); + +NLOHMANN_JSON_NAMESPACE_END + +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.11.3 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann +// SPDX-License-Identifier: MIT + + + +// #include + + +NLOHMANN_JSON_NAMESPACE_BEGIN + +NLOHMANN_CAN_CALL_STD_FUNC_IMPL(end); + +NLOHMANN_JSON_NAMESPACE_END + +// #include + +// #include + +// #include +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ +// | | |__ | | | | | | version 3.11.3 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2023 Niels Lohmann +// SPDX-License-Identifier: MIT + +#ifndef INCLUDE_NLOHMANN_JSON_FWD_HPP_ + #define INCLUDE_NLOHMANN_JSON_FWD_HPP_ + + #include // int64_t, uint64_t + #include // map + #include // allocator + #include // string + #include // vector + + // #include + + + /*! + @brief namespace for Niels Lohmann + @see https://github.com/nlohmann + @since version 1.0.0 + */ + NLOHMANN_JSON_NAMESPACE_BEGIN + + /*! + @brief default JSONSerializer template argument + + This serializer ignores the template arguments and uses ADL + ([argument-dependent lookup](https://en.cppreference.com/w/cpp/language/adl)) + for serialization. + */ + template + struct adl_serializer; + + /// a class to store JSON values + /// @sa https://json.nlohmann.me/api/basic_json/ + template class ObjectType = + std::map, + template class ArrayType = std::vector, + class StringType = std::string, class BooleanType = bool, + class NumberIntegerType = std::int64_t, + class NumberUnsignedType = std::uint64_t, + class NumberFloatType = double, + template class AllocatorType = std::allocator, + template class JSONSerializer = + adl_serializer, + class BinaryType = std::vector, // cppcheck-suppress syntaxError + class CustomBaseClass = void> + class basic_json; + + /// @brief JSON Pointer defines a string syntax for identifying a specific value within a JSON document + /// @sa https://json.nlohmann.me/api/json_pointer/ + template + class json_pointer; + + /*! + @brief default specialization + @sa https://json.nlohmann.me/api/json/ + */ + using json = basic_json<>; + + /// @brief a minimal map-like container that preserves insertion order + /// @sa https://json.nlohmann.me/api/ordered_map/ + template + struct ordered_map; + + /// @brief specialization that maintains the insertion order of object keys + /// @sa https://json.nlohmann.me/api/ordered_json/ + using ordered_json = basic_json; + + NLOHMANN_JSON_NAMESPACE_END + +#endif // INCLUDE_NLOHMANN_JSON_FWD_HPP_ + + +NLOHMANN_JSON_NAMESPACE_BEGIN +/*! +@brief detail namespace with internal helper functions + +This namespace collects functions that should not be exposed, +implementations of some @ref basic_json methods, and meta-programming helpers. + +@since version 2.1.0 +*/ +namespace detail +{ + +///////////// +// helpers // +///////////// + +// Note to maintainers: +// +// Every trait in this file expects a non CV-qualified type. +// The only exceptions are in the 'aliases for detected' section +// (i.e. those of the form: decltype(T::member_function(std::declval()))) +// +// In this case, T has to be properly CV-qualified to constraint the function arguments +// (e.g. to_json(BasicJsonType&, const T&)) + +template struct is_basic_json : std::false_type {}; + +NLOHMANN_BASIC_JSON_TPL_DECLARATION +struct is_basic_json : std::true_type {}; + +// used by exceptions create() member functions +// true_type for pointer to possibly cv-qualified basic_json or std::nullptr_t +// false_type otherwise +template +struct is_basic_json_context : + std::integral_constant < bool, + is_basic_json::type>::type>::value + || std::is_same::value > +{}; + +////////////////////// +// json_ref helpers // +////////////////////// + +template +class json_ref; + +template +struct is_json_ref : std::false_type {}; + +template +struct is_json_ref> : std::true_type {}; + +////////////////////////// +// aliases for detected // +////////////////////////// + +template +using mapped_type_t = typename T::mapped_type; + +template +using key_type_t = typename T::key_type; + +template +using value_type_t = typename T::value_type; + +template +using difference_type_t = typename T::difference_type; + +template +using pointer_t = typename T::pointer; + +template +using reference_t = typename T::reference; + +template +using iterator_category_t = typename T::iterator_category; + +template +using to_json_function = decltype(T::to_json(std::declval()...)); + +template +using from_json_function = decltype(T::from_json(std::declval()...)); + +template +using get_template_function = decltype(std::declval().template get()); + +// trait checking if JSONSerializer::from_json(json const&, udt&) exists +template +struct has_from_json : std::false_type {}; + +// trait checking if j.get is valid +// use this trait instead of std::is_constructible or std::is_convertible, +// both rely on, or make use of implicit conversions, and thus fail when T +// has several constructors/operator= (see https://github.com/nlohmann/json/issues/958) +template +struct is_getable +{ + static constexpr bool value = is_detected::value; +}; + +template +struct has_from_json < BasicJsonType, T, enable_if_t < !is_basic_json::value >> +{ + using serializer = typename BasicJsonType::template json_serializer; + + static constexpr bool value = + is_detected_exact::value; +}; + +// This trait checks if JSONSerializer::from_json(json const&) exists +// this overload is used for non-default-constructible user-defined-types +template +struct has_non_default_from_json : std::false_type {}; + +template +struct has_non_default_from_json < BasicJsonType, T, enable_if_t < !is_basic_json::value >> +{ + using serializer = typename BasicJsonType::template json_serializer; + + static constexpr bool value = + is_detected_exact::value; +}; + +// This trait checks if BasicJsonType::json_serializer::to_json exists +// Do not evaluate the trait when T is a basic_json type, to avoid template instantiation infinite recursion. +template +struct has_to_json : std::false_type {}; + +template +struct has_to_json < BasicJsonType, T, enable_if_t < !is_basic_json::value >> +{ + using serializer = typename BasicJsonType::template json_serializer; + + static constexpr bool value = + is_detected_exact::value; +}; + +template +using detect_key_compare = typename T::key_compare; + +template +struct has_key_compare : std::integral_constant::value> {}; + +// obtains the actual object key comparator +template +struct actual_object_comparator +{ + using object_t = typename BasicJsonType::object_t; + using object_comparator_t = typename BasicJsonType::default_object_comparator_t; + using type = typename std::conditional < has_key_compare::value, + typename object_t::key_compare, object_comparator_t>::type; +}; + +template +using actual_object_comparator_t = typename actual_object_comparator::type; + +///////////////// +// char_traits // +///////////////// + +// Primary template of char_traits calls std char_traits +template +struct char_traits : std::char_traits +{}; + +// Explicitly define char traits for unsigned char since it is not standard +template<> +struct char_traits : std::char_traits +{ + using char_type = unsigned char; + using int_type = uint64_t; + + // Redefine to_int_type function + static int_type to_int_type(char_type c) noexcept + { + return static_cast(c); + } + + static char_type to_char_type(int_type i) noexcept + { + return static_cast(i); + } + + static constexpr int_type eof() noexcept + { + return static_cast(EOF); + } +}; + +// Explicitly define char traits for signed char since it is not standard +template<> +struct char_traits : std::char_traits +{ + using char_type = signed char; + using int_type = uint64_t; + + // Redefine to_int_type function + static int_type to_int_type(char_type c) noexcept + { + return static_cast(c); + } + + static char_type to_char_type(int_type i) noexcept + { + return static_cast(i); + } + + static constexpr int_type eof() noexcept + { + return static_cast(EOF); + } +}; + +/////////////////// +// is_ functions // +/////////////////// + +// https://en.cppreference.com/w/cpp/types/conjunction +template struct conjunction : std::true_type { }; +template struct conjunction : B { }; +template +struct conjunction +: std::conditional(B::value), conjunction, B>::type {}; + +// https://en.cppreference.com/w/cpp/types/negation +template struct negation : std::integral_constant < bool, !B::value > { }; + +// Reimplementation of is_constructible and is_default_constructible, due to them being broken for +// std::pair and std::tuple until LWG 2367 fix (see https://cplusplus.github.io/LWG/lwg-defects.html#2367). +// This causes compile errors in e.g. clang 3.5 or gcc 4.9. +template +struct is_default_constructible : std::is_default_constructible {}; + +template +struct is_default_constructible> + : conjunction, is_default_constructible> {}; + +template +struct is_default_constructible> + : conjunction, is_default_constructible> {}; + +template +struct is_default_constructible> + : conjunction...> {}; + +template +struct is_default_constructible> + : conjunction...> {}; + +template +struct is_constructible : std::is_constructible {}; + +template +struct is_constructible> : is_default_constructible> {}; + +template +struct is_constructible> : is_default_constructible> {}; + +template +struct is_constructible> : is_default_constructible> {}; + +template +struct is_constructible> : is_default_constructible> {}; + +template +struct is_iterator_traits : std::false_type {}; + +template +struct is_iterator_traits> +{ + private: + using traits = iterator_traits; + + public: + static constexpr auto value = + is_detected::value && + is_detected::value && + is_detected::value && + is_detected::value && + is_detected::value; +}; + +template +struct is_range +{ + private: + using t_ref = typename std::add_lvalue_reference::type; + + using iterator = detected_t; + using sentinel = detected_t; + + // to be 100% correct, it should use https://en.cppreference.com/w/cpp/iterator/input_or_output_iterator + // and https://en.cppreference.com/w/cpp/iterator/sentinel_for + // but reimplementing these would be too much work, as a lot of other concepts are used underneath + static constexpr auto is_iterator_begin = + is_iterator_traits>::value; + + public: + static constexpr bool value = !std::is_same::value && !std::is_same::value && is_iterator_begin; +}; + +template +using iterator_t = enable_if_t::value, result_of_begin())>>; + +template +using range_value_t = value_type_t>>; + +// The following implementation of is_complete_type is taken from +// https://blogs.msdn.microsoft.com/vcblog/2015/12/02/partial-support-for-expression-sfinae-in-vs-2015-update-1/ +// and is written by Xiang Fan who agreed to using it in this library. + +template +struct is_complete_type : std::false_type {}; + +template +struct is_complete_type : std::true_type {}; + +template +struct is_compatible_object_type_impl : std::false_type {}; + +template +struct is_compatible_object_type_impl < + BasicJsonType, CompatibleObjectType, + enable_if_t < is_detected::value&& + is_detected::value >> +{ + using object_t = typename BasicJsonType::object_t; + + // macOS's is_constructible does not play well with nonesuch... + static constexpr bool value = + is_constructible::value && + is_constructible::value; +}; + +template +struct is_compatible_object_type + : is_compatible_object_type_impl {}; + +template +struct is_constructible_object_type_impl : std::false_type {}; + +template +struct is_constructible_object_type_impl < + BasicJsonType, ConstructibleObjectType, + enable_if_t < is_detected::value&& + is_detected::value >> +{ + using object_t = typename BasicJsonType::object_t; + + static constexpr bool value = + (is_default_constructible::value && + (std::is_move_assignable::value || + std::is_copy_assignable::value) && + (is_constructible::value && + std::is_same < + typename object_t::mapped_type, + typename ConstructibleObjectType::mapped_type >::value)) || + (has_from_json::value || + has_non_default_from_json < + BasicJsonType, + typename ConstructibleObjectType::mapped_type >::value); +}; + +template +struct is_constructible_object_type + : is_constructible_object_type_impl {}; + +template +struct is_compatible_string_type +{ + static constexpr auto value = + is_constructible::value; +}; + +template +struct is_constructible_string_type +{ + // launder type through decltype() to fix compilation failure on ICPC +#ifdef __INTEL_COMPILER + using laundered_type = decltype(std::declval()); +#else + using laundered_type = ConstructibleStringType; +#endif + + static constexpr auto value = + conjunction < + is_constructible, + is_detected_exact>::value; +}; + +template +struct is_compatible_array_type_impl : std::false_type {}; + +template +struct is_compatible_array_type_impl < + BasicJsonType, CompatibleArrayType, + enable_if_t < + is_detected::value&& + is_iterator_traits>>::value&& +// special case for types like std::filesystem::path whose iterator's value_type are themselves +// c.f. https://github.com/nlohmann/json/pull/3073 + !std::is_same>::value >> +{ + static constexpr bool value = + is_constructible>::value; +}; + +template +struct is_compatible_array_type + : is_compatible_array_type_impl {}; + +template +struct is_constructible_array_type_impl : std::false_type {}; + +template +struct is_constructible_array_type_impl < + BasicJsonType, ConstructibleArrayType, + enable_if_t::value >> + : std::true_type {}; + +template +struct is_constructible_array_type_impl < + BasicJsonType, ConstructibleArrayType, + enable_if_t < !std::is_same::value&& + !is_compatible_string_type::value&& + is_default_constructible::value&& +(std::is_move_assignable::value || + std::is_copy_assignable::value)&& +is_detected::value&& +is_iterator_traits>>::value&& +is_detected::value&& +// special case for types like std::filesystem::path whose iterator's value_type are themselves +// c.f. https://github.com/nlohmann/json/pull/3073 +!std::is_same>::value&& + is_complete_type < + detected_t>::value >> +{ + using value_type = range_value_t; + + static constexpr bool value = + std::is_same::value || + has_from_json::value || + has_non_default_from_json < + BasicJsonType, + value_type >::value; +}; + +template +struct is_constructible_array_type + : is_constructible_array_type_impl {}; + +template +struct is_compatible_integer_type_impl : std::false_type {}; + +template +struct is_compatible_integer_type_impl < + RealIntegerType, CompatibleNumberIntegerType, + enable_if_t < std::is_integral::value&& + std::is_integral::value&& + !std::is_same::value >> +{ + // is there an assert somewhere on overflows? + using RealLimits = std::numeric_limits; + using CompatibleLimits = std::numeric_limits; + + static constexpr auto value = + is_constructible::value && + CompatibleLimits::is_integer && + RealLimits::is_signed == CompatibleLimits::is_signed; +}; + +template +struct is_compatible_integer_type + : is_compatible_integer_type_impl {}; + +template +struct is_compatible_type_impl: std::false_type {}; + +template +struct is_compatible_type_impl < + BasicJsonType, CompatibleType, + enable_if_t::value >> +{ + static constexpr bool value = + has_to_json::value; +}; + +template +struct is_compatible_type + : is_compatible_type_impl {}; + +template +struct is_constructible_tuple : std::false_type {}; + +template +struct is_constructible_tuple> : conjunction...> {}; + +template +struct is_json_iterator_of : std::false_type {}; + +template +struct is_json_iterator_of : std::true_type {}; + +template +struct is_json_iterator_of : std::true_type +{}; + +// checks if a given type T is a template specialization of Primary +template