diff --git a/.changes/esp-idf-6-migration.md b/.changes/esp-idf-6-migration.md new file mode 100644 index 00000000..b36723f0 --- /dev/null +++ b/.changes/esp-idf-6-migration.md @@ -0,0 +1,14 @@ +--- +kind: changed +breaking: true +--- +Rebuild the firmware on the native ESP-IDF platform + +## Release Note +This is one of the biggest updates the firmware has ever had: the whole thing has been rebuilt from the ground up on Espressif's native platform, leaving the old Arduino foundation behind. + +You won't see a pile of new buttons from this release — most of the work is under the hood. What you get is a firmware that's faster to start up, lighter on memory, and far more stable, with a much cleaner codebase that makes future features quicker and safer to add. It also brings a real security upgrade: your hub now checks that it's genuinely talking to OpenShock's servers before it trusts them, so the connection between your device and the network is properly protected. + +Just about every part of the firmware was touched along the way — WiFi and networking, the way settings are stored, the radio that talks to your shockers, the status LEDs, and the serial console. Everything has been retested and put back together on the new foundation. + +Because this rework runs so deep, we're treating it as a major release. It flashes and runs like any other update, and your existing setup carries over. diff --git a/.claude/hooks/clang-format.sh b/.claude/hooks/clang-format.sh deleted file mode 100755 index 8323c4a7..00000000 --- a/.claude/hooks/clang-format.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash -FILE_PATH=$(jq -r '.tool_input.file_path // empty') - -if [[ "$FILE_PATH" =~ \.(cpp|c|h|hpp|cc|cxx)$ ]] && [[ "$FILE_PATH" != */_fbs/* ]]; then - npx clang-format -i "$FILE_PATH" -fi - -exit 0 diff --git a/.claude/settings.json b/.claude/settings.json deleted file mode 100644 index 79e67f80..00000000 --- a/.claude/settings.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "hooks": { - "PostToolUse": [ - { - "matcher": "Edit|Write", - "hooks": [ - { - "type": "command", - "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/clang-format.sh" - } - ] - } - ] - } -} diff --git a/.env b/.env deleted file mode 100644 index b9c8220a..00000000 --- a/.env +++ /dev/null @@ -1,5 +0,0 @@ -OPENSHOCK_API_DOMAIN=api.openshock.app -OPENSHOCK_FW_CDN_DOMAIN=firmware.openshock.org -OPENSHOCK_FW_HOSTNAME=OpenShock -OPENSHOCK_FW_AP_PREFIX=OpenShock- -OPENSHOCK_URI_BUFFER_SIZE=256 diff --git a/.env.development b/.env.development deleted file mode 100644 index 2048dfea..00000000 --- a/.env.development +++ /dev/null @@ -1,3 +0,0 @@ -# Included by frontend/ (DO NOT RENAME THIS FILE!) -# Intended for development builds (locally). -LOG_LEVEL=VERBOSE diff --git a/.env.production b/.env.production deleted file mode 100644 index 019df3bf..00000000 --- a/.env.production +++ /dev/null @@ -1,3 +0,0 @@ -# Included by frontend/ (DO NOT RENAME THIS FILE!) -# Intended for all CI firmware builds. -LOG_LEVEL=INFO diff --git a/.gitattributes b/.gitattributes index c4c1ab34..c963241f 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,4 @@ * text=auto eol=lf *.txt text eol=lf +*.exe filter=lfs diff=lfs merge=lfs -text +scripts/flatc filter=lfs diff=lfs merge=lfs -text \ No newline at end of file diff --git a/.github/actions/build-compilationdb/action.yml b/.github/actions/build-compilationdb/action.yml index a56274f2..c809318f 100644 --- a/.github/actions/build-compilationdb/action.yml +++ b/.github/actions/build-compilationdb/action.yml @@ -1,5 +1,8 @@ name: build-compilationdb -description: Builds the compilation database for the firmware for code analysis +description: >- + Build a representative board with native ESP-IDF to emit + build/compile_commands.json (for cpp-linter) and a real compile pass (for + CodeQL C/C++). Uses OpenShock-Core-V2 / esp32s3 as the canonical config. inputs: version: required: true @@ -8,25 +11,20 @@ inputs: runs: using: composite steps: - - uses: ./.github/actions/pio-cache - with: - env: ci-build + - uses: ./.github/actions/setup-esp-idf - - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 - with: - cache: 'pip' - - - name: Install python dependencies - shell: bash - run: pip install -r requirements.txt - - - name: Build firmware - working-directory: . + # Build only the `app` target: this configures the project (writing + # build/OpenShock-Core-V2/compile_commands.json for cpp-linter) and compiles + # the application (so CodeQL traces the C/C++). It skips the static0 image, so + # no frontend / littlefs is needed. ccache is left off so CodeQL sees every + # compiler invocation. + - name: Build firmware (OpenShock-Core-V2 / esp32s3) shell: bash run: | + set -euo pipefail export OPENSHOCK_FW_BUILD_DATE="$(git show -s --format=%cI HEAD 2>/dev/null || date -u +%Y-%m-%dT%H:%M:%SZ)" - pio run -e ci-build -t compiledb + python3 scripts/build.py OpenShock-Core-V2 app env: OPENSHOCK_FW_VERSION: ${{ inputs.version }} OPENSHOCK_FW_GIT_REF: ${{ github.ref }} - OPENSHOCK_FW_GIT_COMMIT: ${{ github.sha }} \ No newline at end of file + OPENSHOCK_FW_GIT_COMMIT: ${{ github.sha }} diff --git a/.github/actions/build-firmware/action.yml b/.github/actions/build-firmware/action.yml index e81af433..2c7ce377 100644 --- a/.github/actions/build-firmware/action.yml +++ b/.github/actions/build-firmware/action.yml @@ -1,9 +1,13 @@ name: build-firmware -description: Builds the firmware partitions and uploads them as an artifact +description: >- + Compile one board's C++ firmware with native ESP-IDF and upload its discrete + partition images (app, bootloader, partition-table). Builds only the code + targets - not the static0 web-assets image - so it runs in parallel with + build-staticfs; the merge-partitions job links them together. inputs: board: required: true - description: 'Board name to build' + description: 'Board name to build (matches boards/.defaults)' version: required: true description: 'Current firmware version' @@ -11,36 +15,51 @@ inputs: runs: using: composite steps: - - uses: ./.github/actions/pio-cache - with: - env: ${{ inputs.board }} + - uses: ./.github/actions/setup-esp-idf - - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + # ccache keyed per board's chip (compiler/framework artifacts track the chip). + - name: Cache ccache + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: - cache: 'pip' - - - name: Install python dependencies - shell: bash - run: pip install -r requirements.txt + path: ~/.cache/ccache + key: ccache-${{ runner.os }}-${{ inputs.board }}-${{ github.sha }} + restore-keys: | + ccache-${{ runner.os }}-${{ inputs.board }}- + # Build only the code targets (app + bootloader + partition-table). This + # skips the static0 image (built separately by build-staticfs), so no + # frontend is needed here. scripts/build.py resolves the target from + # boards/.defaults, sets OPENSHOCK_BOARD, and layers the sdkconfig. - name: Build firmware - working-directory: . shell: bash - # head_commit.timestamp only exists on push events, so derive the build - # date from the checked-out commit (falling back to now) - otherwise - # schedule/dispatch builds bake in an empty date. run: | + set -euo pipefail export OPENSHOCK_FW_BUILD_DATE="$(git show -s --format=%cI HEAD 2>/dev/null || date -u +%Y-%m-%dT%H:%M:%SZ)" - pio run -e ${{ inputs.board }} + export IDF_CCACHE_ENABLE=1 + python3 scripts/build.py "${{ inputs.board }}" app bootloader partition-table env: OPENSHOCK_FW_VERSION: ${{ inputs.version }} OPENSHOCK_FW_GIT_REF: ${{ github.ref }} OPENSHOCK_FW_GIT_COMMIT: ${{ github.sha }} - - name: Upload build artifacts + # Flat, canonically-named images. app.bin/staticfs.bin are the names the + # device OTA pulls (OtaUpdateManager.cpp); merge_image.py and the CDN upload + # consume these too. `project(firmware)` names the app image firmware.bin. + - name: Stage partition images + shell: bash + env: + B: build/${{ inputs.board }} + run: | + set -euo pipefail + mkdir -p dist + cp "$B/firmware.bin" dist/app.bin + cp "$B/bootloader/bootloader.bin" dist/bootloader.bin + cp "$B/partition_table/partition-table.bin" dist/partition-table.bin + + - name: Upload partition images uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: firmware_build_${{ inputs.board }} - path: .pio/build/${{ inputs.board }}/*.bin - retention-days: 1 + path: dist/*.bin + retention-days: 7 if-no-files-found: error diff --git a/.github/actions/build-staticfs/action.yml b/.github/actions/build-staticfs/action.yml index 955ac291..8c0e5d51 100644 --- a/.github/actions/build-staticfs/action.yml +++ b/.github/actions/build-staticfs/action.yml @@ -1,51 +1,52 @@ name: build-staticfs -description: Builds the static filesystem partition and uploads it as an artifact +description: >- + Pack the captive-portal web assets into the static0 littlefs image once + (board-independent) via scripts/gen_staticfs.py. Runs in parallel with the + per-board C++ build; the merge job combines the two. inputs: version: - required: true - description: 'Current firmware version' + description: 'Current firmware version (unused in the image, kept for parity)' + required: false + default: '' runs: using: composite steps: - - uses: ./.github/actions/pio-cache - with: - env: fs - - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: cache: 'pip' - - name: Install dependencies + - name: Install Python dependencies shell: bash run: pip install -r requirements.txt - - name: Download built frontend + # The frontend is built once in build-frontend and downloaded here so + # gen_staticfs.py packs the prebuilt assets rather than rebuilding them. + - name: Download frontend uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: frontend - path: frontend/build/ + path: frontend/build - - name: Build filesystem partition + # static0 is 0x09D000 bytes in the 4MB OTA table (partitions/ota_4mb.csv); + # the image is board-independent, so one build serves every board. Geometry + # (4 KiB blocks, 128-byte read/prog) matches the runtime mount in + # components/fs/src/LfsPartition.cpp and the CMake build. + - name: Build static filesystem image shell: bash - # head_commit.timestamp only exists on push events; derive from the commit - # (falling back to now) so schedule/dispatch builds don't get an empty date. run: | - export OPENSHOCK_FW_BUILD_DATE="$(git show -s --format=%cI HEAD 2>/dev/null || date -u +%Y-%m-%dT%H:%M:%SZ)" - pio run --target buildfs -e fs - env: - OPENSHOCK_FW_VERSION: ${{ inputs.version }} - OPENSHOCK_FW_GIT_REF: ${{ github.ref }} - OPENSHOCK_FW_GIT_COMMIT: ${{ github.sha }} - - - name: Rename partition binary - shell: bash - run: mv .pio/build/fs/littlefs.bin staticfs.bin + python scripts/gen_staticfs.py \ + --frontend-dir frontend \ + --staging-dir staticfs \ + --output staticfs.bin \ + --partition-size 0x09D000 \ + --block-size 4096 --read-size 128 --prog-size 128 \ + --skip-frontend-build - - name: Upload internal filesystem artifact + - name: Upload static filesystem artifact uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: firmware_staticfs path: staticfs.bin - retention-days: 1 + retention-days: 7 if-no-files-found: error diff --git a/.github/actions/cdn-upload-firmware/action.yml b/.github/actions/cdn-upload-firmware/action.yml index bf7804b2..88fb67f8 100644 --- a/.github/actions/cdn-upload-firmware/action.yml +++ b/.github/actions/cdn-upload-firmware/action.yml @@ -30,9 +30,10 @@ runs: pattern: firmware_* # Assemble the full // tree locally, one directory per board, so it - # can be pushed in a single mirror. Each board directory keeps the previous - # per-board layout: static + build partitions, the merged firmware.bin, and - # md5/sha256 checksums over every .bin. + # can be pushed in a single mirror. Each board directory keeps the per-board + # layout the device OTA expects: the shared staticfs.bin, the board's app.bin + # (+ bootloader / partition-table) from firmware_build_, the merged + # firmware.bin, and md5/sha256 checksums over every .bin. - name: Assemble upload tree shell: bash env: @@ -44,7 +45,7 @@ runs: [ -z "$board" ] && continue dest="upload/$board" mkdir -p "$dest" - cp artifacts/firmware_staticfs/*.bin "$dest"/ + cp artifacts/firmware_staticfs/staticfs.bin "$dest"/ cp "artifacts/firmware_build_$board"/*.bin "$dest"/ cp "artifacts/firmware_merged_$board"/OpenShock_*.bin "$dest"/firmware.bin ( diff --git a/.github/actions/merge-partitions/action.yml b/.github/actions/merge-partitions/action.yml index 2ad9fd55..1d282162 100644 --- a/.github/actions/merge-partitions/action.yml +++ b/.github/actions/merge-partitions/action.yml @@ -1,8 +1,11 @@ name: merge-partitions -description: Merges multiple partitions into a single flashable binary +description: >- + Link a board's C++ partition images (from build-firmware) with the shared + static0 image (from build-staticfs) into a single flashable binary via + scripts/merge_image.py. Needs esptool only - no full ESP-IDF install. inputs: version: - description: 'Version of the firmware' + description: 'Firmware version' required: true board: description: 'Board name to merge partitions for' @@ -15,25 +18,31 @@ runs: with: cache: 'pip' - - name: Install dependencies + - name: Install Python dependencies shell: bash run: pip install -r requirements.txt - - name: Download static filesystem partition + - name: Download C++ partition images uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: firmware_staticfs + name: firmware_build_${{ inputs.board }} + path: merge - - name: Download firmware partitions + - name: Download static filesystem image uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: firmware_build_${{ inputs.board }} + name: firmware_staticfs + path: merge - name: Merge partitions shell: bash run: | - python scripts/merge_image.py ${{ inputs.board }} - mv merged.bin OpenShock_${{ inputs.board }}_${{ inputs.version }}.bin + set -euo pipefail + python scripts/merge_image.py \ + --board "${{ inputs.board }}" \ + --bindir merge \ + --staticfs merge/staticfs.bin \ + --output "OpenShock_${{ inputs.board }}_${{ inputs.version }}.bin" - name: Upload merged firmware binary uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 diff --git a/.github/actions/pio-cache/action.yml b/.github/actions/pio-cache/action.yml deleted file mode 100644 index b05016aa..00000000 --- a/.github/actions/pio-cache/action.yml +++ /dev/null @@ -1,56 +0,0 @@ -name: pio-cache -description: >- - Restore/save PlatformIO caches, split by what actually varies per board so the - matrix does not blow past the Actions cache storage limit. -inputs: - env: - required: true - description: 'PlatformIO environment (the env:) being built' - -runs: - using: composite - steps: - # The only thing that differs across boards in ~/.platformio is the toolchain - # + precompiled framework libs, and those track the *chip*, not the board. - # Resolve the chip for this env so packages can be keyed per-chip (shared by - # every board of that chip) instead of per-board. Uses the runner's system - # python3 so it can run before setup-python. - - name: Resolve chip for cache key - id: chip - shell: bash - env: - PIO_ENV: ${{ inputs.env }} - run: | - chip=$(python3 - <<'PY' - import configparser, os - cp = configparser.ConfigParser(interpolation=None, inline_comment_prefixes=(';',)) - cp.read('platformio.ini') - print(cp.get(f"env:{os.environ['PIO_ENV']}", 'custom_openshock.chip', fallback='unknown').strip()) - PY - ) - echo "chip=$chip" >> "$GITHUB_OUTPUT" - echo "env ${PIO_ENV} -> chip ${chip}" - - # Platform is identical for every board; packages (toolchain + framework libs) - # vary per chip. Keying by chip means N boards on one chip share a single - # entry instead of each storing its own copy. ~/.platformio/.cache (the - # downloaded archives) is deliberately NOT cached: the extracted packages - # cache already avoids re-downloads, and caching it roughly doubled the size. - - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 - with: - path: | - ~/.platformio/platforms - ~/.platformio/packages - key: pio-pkgs-${{ runner.os }}-${{ steps.chip.outputs.chip }}-${{ hashFiles('platformio.ini', 'requirements.txt') }} - restore-keys: | - pio-pkgs-${{ runner.os }}-${{ steps.chip.outputs.chip }}- - - # Library sources live in per-env directories. lib_deps is global so the - # contents are identical across boards, but PlatformIO stores them under - # .pio/libdeps/, so key them per-env. This slice is small. - - uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 - with: - path: .pio/libdeps - key: pio-libdeps-${{ runner.os }}-${{ inputs.env }}-${{ hashFiles('platformio.ini') }} - restore-keys: | - pio-libdeps-${{ runner.os }}-${{ inputs.env }}- diff --git a/.github/actions/setup-esp-idf/action.yml b/.github/actions/setup-esp-idf/action.yml new file mode 100644 index 00000000..87c3f5ea --- /dev/null +++ b/.github/actions/setup-esp-idf/action.yml @@ -0,0 +1,37 @@ +name: setup-esp-idf +description: >- + Install a pinned native ESP-IDF toolchain and put its `idf.py` wrapper on PATH. + scripts/build.py invokes that wrapper, which activates the tool + venv + environment itself. Shared by every job that runs idf.py; replaces pio-cache. +inputs: + idf-version: + description: 'ESP-IDF version to install (EIM version tag).' + required: false + default: 'v6.0.2' + +runs: + using: composite + steps: + # Cache the whole ESP-IDF install so a warm run skips the multi-minute IDF + # source clone + submodule fetch + toolchain download. On Linux this action + # installs the source (and its submodules) under /tmp/esp/idf and the tools / + # Python venv under /tmp/esp (older layouts used /opt/esp or ~/.espressif, so + # cache all three - missing paths are skipped). Keyed by version. Bump the + # trailing number to invalidate a stale cache (keys are immutable once saved). + - name: Cache ESP-IDF install + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: | + /tmp/esp + /opt/esp + ~/.espressif + key: esp-idf-${{ runner.os }}-${{ inputs.idf-version }}-2 + restore-keys: | + esp-idf-${{ runner.os }}-${{ inputs.idf-version }}- + + # Installs ESP-IDF, sets IDF_PATH, and puts the `idf.py` wrapper on PATH for + # later steps. v1 is a moving branch (no release tags), pinned by SHA. + - name: Install ESP-IDF + uses: espressif/install-esp-idf-action@8fc05d1470d5591417e7a3707a1f2bec178db4ae # v1 + with: + version: ${{ inputs.idf-version }} diff --git a/.github/scripts/.gitignore b/.github/scripts/.gitignore deleted file mode 100644 index 3c3629e6..00000000 --- a/.github/scripts/.gitignore +++ /dev/null @@ -1 +0,0 @@ -node_modules diff --git a/.github/scripts/get-vars.js b/.github/scripts/get-vars.js deleted file mode 100644 index 0f567659..00000000 --- a/.github/scripts/get-vars.js +++ /dev/null @@ -1,165 +0,0 @@ -import { setFailed, setOutput } from '@actions/core'; -import ini from 'ini'; -import child_process from 'node:child_process'; -import fs from 'node:fs'; -import semver from 'semver'; - -// Get branch name -const gitRef = process.env.GITHUB_REF; -if (gitRef === undefined) { - setFailed('Environment variable "GITHUB_REF" not found'); - process.exit(); -} - -const isGitTag = gitRef.startsWith('refs/tags/'); -const isGitBranch = gitRef.startsWith('refs/heads/'); -const isGitPullRequest = gitRef.startsWith('refs/pull/') && gitRef.endsWith('/merge'); - -if (!isGitTag && !isGitBranch && !isGitPullRequest) { - setFailed(`Git ref "${gitRef}" is not a valid branch, tag or pull request`); - process.exit(); -} - -const gitCommitHash = process.env.GITHUB_SHA; -if (gitCommitHash === undefined) { - setFailed('Environment variable "GITHUB_SHA" not found'); - process.exit(); -} -const gitShortCommitHash = gitCommitHash.substring(0, 8); - -const gitHeadRefName = isGitPullRequest ? process.env.GITHUB_HEAD_REF : gitRef.split('/')[2]; -if (gitHeadRefName === undefined) { - setFailed('Failed to get git head ref name'); - process.exit(); -} - -const gitTagsList = child_process - .execSync('git for-each-ref --sort=-creatordate --format "%(refname:short)" refs/tags') - .toString() - .trim(); - -// The current tag on a tag build is authoritative: an unparseable value here is -// a hard failure, since we cannot derive a version from it. -function parseCurrentTag(tag) { - const parsed = semver.parse(tag); - if (parsed === null || parsed.loose) { - setFailed(`Git tag "${tag}" is not a valid semver version`); - process.exit(); - } - - return parsed; -} - -const gitTagsArray = gitTagsList - .split('\n') - .map((tag) => tag.trim()) - .filter((tag) => tag !== ''); - -// Existing repo tags are best-effort: silently skip any that are not strict -// semver (legacy or malformed tags) rather than failing the entire build, which -// convertGitTagToSemver used to do on the first bad tag it mapped over. -const releasesArray = gitTagsArray - .map((tag) => semver.parse(tag)) - .filter((parsed) => parsed !== null && !parsed.loose); - -// Fall back to 0.0.0 when there are no valid tags yet (fresh repo / all tags -// filtered out). -const latestRelease = isGitTag - ? parseCurrentTag(gitRef.split('/')[2]) - : (releasesArray[0] ?? semver.parse('0.0.0')); - -function isStableRelease(release) { - return release.prerelease.length === 0 || release.prerelease[0] === 'stable'; -} -function isBetaRelease(release) { - return release.prerelease.length > 0 && ['rc', 'beta'].includes(release.prerelease[0]); -} -function isDevRelease(release) { - return release.prerelease.length > 0 && ['dev', 'develop'].includes(release.prerelease[0]); -} - -// Build version string. -// -// The base MAJOR.MINOR.PATCH comes from release-tool (status mode), which is the -// single source of truth for the version bump: it reads .changes/ and returns -// the *next* version. For branch/PR builds we label the artifact as a -// pre-release of that upcoming version (e.g. 1.6.0-develop+sha) rather than the -// last released tag. We fall back to the latest tag when there are no pending -// changes (RELEASE_SKIP=true) or the value is absent, and always for tag builds -// (the tag itself is authoritative there). -const nextVersion = process.env.RELEASE_NEXT_VERSION; -const releaseSkip = process.env.RELEASE_SKIP === 'true'; -const latestBase = `${latestRelease.major}.${latestRelease.minor}.${latestRelease.patch}`; - -let currentVersion = - !isGitTag && nextVersion && !releaseSkip ? nextVersion : latestBase; -if (!isGitTag) { - // Get last part of branch name and replace all non-alphanumeric characters with dashes - let sanitizedGitHeadRefName = gitHeadRefName - .split('/') - .pop() - .replace(/[^a-zA-Z0-9-]/g, '-'); - - // Remove leading and trailing dashes - sanitizedGitHeadRefName = sanitizedGitHeadRefName.replace(/^\-+|\-+$/g, ''); - - if (sanitizedGitHeadRefName.length > 0) { - currentVersion += `-${sanitizedGitHeadRefName}`; - } - - // Add the git commit hash to the version string - currentVersion += `+${gitShortCommitHash}`; -} else { - if (latestRelease.prerelease.length > 0) { - currentVersion += `-${latestRelease.prerelease.join('.')}`; - } - if (latestRelease.build.length > 0) { - currentVersion += `+${latestRelease.build.join('.')}`; - } -} - -// Get the channel to deploy to -let currentChannel; -if (gitHeadRefName === 'master' || (isGitTag && isStableRelease(latestRelease))) { - currentChannel = 'stable'; -} else if (gitHeadRefName === 'beta' || (isGitTag && isBetaRelease(latestRelease))) { - currentChannel = 'beta'; -} else if (gitHeadRefName === 'develop' || (isGitTag && isDevRelease(latestRelease))) { - currentChannel = 'develop'; -} else { - currentChannel = gitHeadRefName.replace(/[^a-zA-Z0-9-]/g, '-').replace(/^\-+|\-+$/g, ''); -} - -// CHANGELOG.md validation lives in release-tool (mode: check, see -// check-changes.yml); get-vars only derives the version, channel and boards. - -// Make sure we have the files we need -if (!fs.existsSync('platformio.ini')) { - setFailed('File "platformio.ini" not found'); - process.exit(); -} - -// Read files -const platformioIniStr = fs.readFileSync('platformio.ini', 'utf8').trim(); - -// Parse platformio.ini and extract the different boards -const platformioIni = ini.parse(platformioIniStr); - -// Get every key that starts with "env:", and that isnt "env:fs" (which is the filesystem) or "env:ci-build" (which is for CI CodeQL and cppcheck) -const boards = Object.keys(platformioIni) - .filter((key) => key.startsWith('env:') && key !== 'env:fs' && key !== 'env:ci-build') - .reduce((arr, key) => { - arr.push(key.substring(4)); - return arr; - }, []); - -console.log('Version: ' + currentVersion); -console.log('Channel: ' + currentChannel); -console.log('Boards: ' + boards.join(', ')); -console.log('Tags: ' + gitTagsArray.join(', ')); - -// Set outputs -setOutput('version', currentVersion); -setOutput('release-channel', currentChannel); -setOutput('board-list', boards.join('\n')); -setOutput('board-matrix', JSON.stringify({ board: boards })); diff --git a/.github/scripts/get-vars.py b/.github/scripts/get-vars.py new file mode 100644 index 00000000..d87986d9 --- /dev/null +++ b/.github/scripts/get-vars.py @@ -0,0 +1,177 @@ +#!/usr/bin/env python3 +"""Derive the CI build variables: firmware version, release channel, board matrix. + +Python port of the former get-vars.js (stdlib only, no node/pnpm toolchain). +Reads the git ref/sha from the GITHUB_* env, the pending version from +release-tool (RELEASE_NEXT_VERSION / RELEASE_SKIP), the repo tags via git, and +the buildable boards from boards/*.defaults, then writes GitHub Action outputs. +""" + +import json +import os +import re +import subprocess +import sys +from pathlib import Path +from typing import NoReturn + +MIN_PYTHON = (3, 12, 3) +if sys.version_info < MIN_PYTHON: + raise SystemExit( + 'get-vars requires Python >= %s, got %s' + % ('.'.join(map(str, MIN_PYTHON)), sys.version.split()[0]) + ) + +SEMVER_RE = re.compile( + r'^v?(?P\d+)\.(?P\d+)\.(?P\d+)' + r'(?:-(?P[0-9A-Za-z.-]+))?(?:\+(?P[0-9A-Za-z.-]+))?$' +) + + +def set_failed(msg: str) -> NoReturn: + print(f'::error::{msg}') + sys.exit(1) + + +def set_output(name: str, value: str): + gh_output = os.environ.get('GITHUB_OUTPUT') + if not gh_output: + print(f'[output] {name}={value}') + return + with open(gh_output, 'a', encoding='utf-8') as f: + if '\n' in value: + delim = f'__EOF_{name}__' + f.write(f'{name}<<{delim}\n{value}\n{delim}\n') + else: + f.write(f'{name}={value}\n') + + +class SemVer: + def __init__(self, major, minor, patch, prerelease, build): + self.major, self.minor, self.patch = major, minor, patch + # prerelease/build as dot-separated identifier lists (like node-semver). + self.prerelease = prerelease.split('.') if prerelease else [] + self.build = build.split('.') if build else [] + + @classmethod + def parse(cls, tag: str): + m = SEMVER_RE.match(tag.strip()) + if not m: + return None + return cls(int(m['major']), int(m['minor']), int(m['patch']), + m['prerelease'], m['build']) + + @property + def base(self) -> str: + return f'{self.major}.{self.minor}.{self.patch}' + + def is_stable(self) -> bool: + return len(self.prerelease) == 0 or self.prerelease[0] == 'stable' + + def is_beta(self) -> bool: + return len(self.prerelease) > 0 and self.prerelease[0] in ('rc', 'beta') + + def is_dev(self) -> bool: + return len(self.prerelease) > 0 and self.prerelease[0] in ('dev', 'develop') + + +def sanitize(name: str) -> str: + return re.sub(r'^-+|-+$', '', re.sub(r'-+', '-', re.sub(r'[^a-zA-Z0-9-]', '-', name))) + + +def main() -> int: + git_ref = os.environ.get('GITHUB_REF') + if git_ref is None: + set_failed('Environment variable "GITHUB_REF" not found') + + is_git_tag = git_ref.startswith('refs/tags/') + is_git_branch = git_ref.startswith('refs/heads/') + is_git_pr = git_ref.startswith('refs/pull/') and git_ref.endswith('/merge') + if not (is_git_tag or is_git_branch or is_git_pr): + set_failed(f'Git ref "{git_ref}" is not a valid branch, tag or pull request') + + git_sha = os.environ.get('GITHUB_SHA') + if git_sha is None: + set_failed('Environment variable "GITHUB_SHA" not found') + short_sha = git_sha[:8] + + head_ref = os.environ.get('GITHUB_HEAD_REF') if is_git_pr else git_ref.split('/', 2)[2] + if not head_ref: + set_failed('Failed to get git head ref name') + + try: + tags_raw = subprocess.check_output( + ['git', 'for-each-ref', '--sort=-creatordate', + '--format=%(refname:short)', 'refs/tags'], + text=True, + ).strip() + except (subprocess.CalledProcessError, FileNotFoundError) as e: + set_failed(f'Failed to read git tags: {e}') + tags = [t.strip() for t in tags_raw.split('\n') if t.strip()] + + # Existing repo tags are best-effort: skip any that aren't strict semver. + releases = [s for s in (SemVer.parse(t) for t in tags) if s is not None] + + if is_git_tag: + # A tag build's own tag is authoritative; an unparseable value is fatal. + latest = SemVer.parse(git_ref.split('/')[2]) + if latest is None: + set_failed(f'Git tag "{git_ref.split("/")[2]}" is not a valid semver version') + else: + latest = releases[0] if releases else SemVer.parse('0.0.0') + + # Version: release-tool's next version for branch/PR builds (labelled as a + # pre-release of the upcoming version), else the latest tag base. + next_version = os.environ.get('RELEASE_NEXT_VERSION') + release_skip = os.environ.get('RELEASE_SKIP') == 'true' + + if not is_git_tag and next_version and not release_skip: + version = next_version + else: + version = latest.base + + if not is_git_tag: + suffix = sanitize(head_ref.split('/')[-1]) + if suffix: + version += f'-{suffix}' + version += f'+{short_sha}' + else: + if latest.prerelease: + version += '-' + '.'.join(latest.prerelease) + if latest.build: + version += '+' + '.'.join(latest.build) + + # Release channel. + if head_ref == 'master' or (is_git_tag and latest.is_stable()): + channel = 'stable' + elif head_ref == 'beta' or (is_git_tag and latest.is_beta()): + channel = 'beta' + elif head_ref == 'develop' or (is_git_tag and latest.is_dev()): + channel = 'develop' + elif is_git_tag: + set_failed(f'Tag "{head_ref}" has an unrecognized prerelease channel') + else: + channel = sanitize(head_ref) + + # Board matrix: one entry per boards/.defaults fragment. + boards_dir = Path('boards') + if not boards_dir.is_dir(): + set_failed('Directory "boards" not found') + boards = sorted(p.stem for p in boards_dir.glob('*.defaults')) + if not boards: + set_failed('No boards/*.defaults files found in "boards"') + + print('Version: ' + version) + print('Channel: ' + channel) + print('Boards: ' + ', '.join(boards)) + print('Tags: ' + ', '.join(tags)) + + set_output('version', version) + set_output('release-channel', channel) + set_output('board-list', '\n'.join(boards)) + set_output('board-matrix', json.dumps({'board': boards})) + return 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/.github/scripts/package.json b/.github/scripts/package.json deleted file mode 100644 index dab7d5b4..00000000 --- a/.github/scripts/package.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "name": "get-variables", - "version": "1.0.0", - "private": true, - "type": "module", - "main": "src/index.ts", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "dependencies": { - "@actions/core": "^3.0.1", - "ini": "^7.0.0", - "semver": "^7.8.5" - }, - "engines": { - "node": "^24.13.0", - "pnpm": "^11.10.0" - }, - "packageManager": "pnpm@11.10.0" -} diff --git a/.github/scripts/pnpm-lock.yaml b/.github/scripts/pnpm-lock.yaml deleted file mode 100644 index 9085b51b..00000000 --- a/.github/scripts/pnpm-lock.yaml +++ /dev/null @@ -1,76 +0,0 @@ -lockfileVersion: '9.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -importers: - - .: - dependencies: - '@actions/core': - specifier: ^3.0.1 - version: 3.0.1 - ini: - specifier: ^7.0.0 - version: 7.0.0 - semver: - specifier: ^7.8.5 - version: 7.8.5 - -packages: - - '@actions/core@3.0.1': - resolution: {integrity: sha512-a6d/Nwahm9fliVGRhdhofo40HjHQasUPusmc7vBfyky+7Z+P2A1J68zyFVaNcEclc/Se+eO595oAr5nwEIoIUA==} - - '@actions/exec@3.0.0': - resolution: {integrity: sha512-6xH/puSoNBXb72VPlZVm7vQ+svQpFyA96qdDBvhB8eNZOE8LtPf9L4oAsfzK/crCL8YZ+19fKYVnM63Sl+Xzlw==} - - '@actions/http-client@4.0.1': - resolution: {integrity: sha512-+Nvd1ImaOZBSoPbsUtEhv+1z99H12xzncCkz0a3RuehINE81FZSe2QTj3uvAPTcJX/SCzUQHQ0D1GrPMbrPitg==} - - '@actions/io@3.0.2': - resolution: {integrity: sha512-nRBchcMM+QK1pdjO7/idu86rbJI5YHUKCvKs0KxnSYbVe3F51UfGxuZX4Qy/fWlp6l7gWFwIkrOzN+oUK03kfw==} - - ini@7.0.0: - resolution: {integrity: sha512-ifK0CgjALofS5bkrcTy4RaQ9Vx2Knf/eLeIO+NaswQEpH1UblrtTSCIvN71qQDMq0PeQ/SSPojvEJp9vvvfr+w==} - engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0} - - semver@7.8.5: - resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} - engines: {node: '>=10'} - hasBin: true - - tunnel@0.0.6: - resolution: {integrity: sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==} - engines: {node: '>=0.6.11 <=0.7.0 || >=0.7.3'} - - undici@6.27.0: - resolution: {integrity: sha512-YmfV3YnEDzXRC5lZ2jWtWWHKGUm1zIt8AhesR1tens+HTNv+YZlN/dp6G727LOvMJ8xjP9Be7Y2Sdr96LDm+pg==} - engines: {node: '>=18.17'} - -snapshots: - - '@actions/core@3.0.1': - dependencies: - '@actions/exec': 3.0.0 - '@actions/http-client': 4.0.1 - - '@actions/exec@3.0.0': - dependencies: - '@actions/io': 3.0.2 - - '@actions/http-client@4.0.1': - dependencies: - tunnel: 0.0.6 - undici: 6.27.0 - - '@actions/io@3.0.2': {} - - ini@7.0.0: {} - - semver@7.8.5: {} - - tunnel@0.0.6: {} - - undici@6.27.0: {} diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index bae9d2e9..64b310e1 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -176,6 +176,8 @@ jobs: - uses: ./.github/actions/build-frontend + # Pack the captive-portal assets into the static0 image once (board-independent). + # Runs in parallel with the per-board C++ build below; merge-partitions links them. build-staticfs: runs-on: ubuntu-latest needs: [getvars, build-frontend] @@ -183,27 +185,35 @@ jobs: steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - uses: ./.github/actions/build-staticfs with: version: ${{ needs.getvars.outputs.version }} + # Compile each board's C++ firmware. Builds only the code partitions (no static0 + # image, no frontend), so it runs in parallel with build-staticfs. build-firmware: runs-on: ubuntu-latest needs: [gate, getvars] if: ${{ needs.gate.outputs.build == 'true' }} - timeout-minutes: 10 + timeout-minutes: 30 strategy: fail-fast: false matrix: ${{ fromJSON(needs.getvars.outputs.board-matrix) }} steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + # flatbuffers runtime lives in a submodule (components/flatbuffers). + submodules: recursive - uses: ./.github/actions/build-firmware with: board: ${{ matrix.board }} version: ${{ needs.getvars.outputs.version }} + # Link each board's C++ partitions with the shared static0 image into one + # flashable binary. Waits on both parallel build legs. merge-partitions: runs-on: ubuntu-latest needs: [getvars, build-staticfs, build-firmware] @@ -219,7 +229,6 @@ jobs: .github scripts boards - chips - uses: ./.github/actions/merge-partitions with: diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index ffe9f901..b461788a 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -78,15 +78,17 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + # flatbuffers runtime lives in a submodule (components/flatbuffers). + submodules: recursive - name: Initialize CodeQL uses: github/codeql-action/init@c35d1b164463ee62a100735382aaaa525c5d3496 # codeql-bundle-v2.25.6 with: languages: ${{ env.language }} - - uses: ./.github/actions/build-firmware + - uses: ./.github/actions/build-compilationdb with: - board: ci-build version: 0.0.0-codeql+build - name: Perform CodeQL Analysis diff --git a/.github/workflows/cpp-linter.yml b/.github/workflows/cpp-linter.yml index 98b0950a..a1ba25ae 100644 --- a/.github/workflows/cpp-linter.yml +++ b/.github/workflows/cpp-linter.yml @@ -28,11 +28,14 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + # flatbuffers runtime lives in a submodule (components/flatbuffers). + submodules: recursive - uses: ./.github/actions/build-compilationdb with: version: 0.0.0-test+build # Doesn't matter, just need the compilation database - + - uses: cpp-linter/cpp-linter-action@8e85cd02c8c3fe3ae527c94b5683fe2366b144ed # v2.20.0 id: linter env: @@ -40,9 +43,27 @@ jobs: with: style: file version: 21 + # build.py writes compile_commands.json into build//. + database: build/OpenShock-Core-V2 + # clang-tidy is disabled: the native ESP-IDF compile DB is generated by + # xtensa-esp32s3-elf-gcc, whose flags (-mlongcalls, -fstrict-volatile-bitfields, + # ...) LLVM clang-tidy rejects, and standalone headers have no compile command + # so their ESP-IDF includes don't resolve - both surface as spurious + # clang-diagnostic-errors. clang-format still gates style, and CodeQL provides + # deep static analysis. Re-enable once the DB is clang-toolchain compatible. + tidy-checks: '-*' + # Walk the source tree instead of the PR diff. GitHub returns 406 for very + # large diffs (the Arduino migration PR is ~27k lines), which drops cpp-linter + # into a paginated fallback that crashes on files GitHub omits a patch for. + # Scanning the tree sidesteps the diff-size limit entirely. + files-changed-only: false ignore: | .github + build include/serialization/_fbs + components/littlefs + managed_components + components/*/host_test # Commit comments (push events) need contents:write, which this job # deliberately does not grant; only post thread comments on PRs, where # pull-requests:write applies. Otherwise the action 403s and fails the diff --git a/.github/workflows/get-vars.yml b/.github/workflows/get-vars.yml index b3a2f8f0..2e97b616 100644 --- a/.github/workflows/get-vars.yml +++ b/.github/workflows/get-vars.yml @@ -40,22 +40,10 @@ jobs: sparse-checkout: | .github .changes + boards - # Pin Node to the range declared in the scripts package (engines.node), - # rather than relying on whatever the runner image happens to ship. - - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 - with: - node-version-file: .github/scripts/package.json - - - uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9 - name: Install pnpm - with: - cache: true - cache_dependency_path: .github/scripts/pnpm-lock.yaml - package_json_file: .github/scripts/package.json - run_install: | - - cwd: .github/scripts - args: [--frozen-lockfile, --strict-peer-dependencies] + # No setup-python: the runner ships Python 3.12.3, which get-vars.py needs + # (it self-checks the floor via MIN_PYTHON and fails clearly otherwise). # Single source of truth for the version bump: release-tool computes the # next version from .changes/. status mode works on every branch (no @@ -73,4 +61,4 @@ jobs: env: RELEASE_NEXT_VERSION: ${{ steps.relmeta.outputs.next-version }} RELEASE_SKIP: ${{ steps.relmeta.outputs.skip }} - run: node .github/scripts/get-vars.js + run: python3 .github/scripts/get-vars.py diff --git a/.github/workflows/pinned-cert-audit.yml b/.github/workflows/pinned-cert-audit.yml new file mode 100644 index 00000000..d3b371b1 --- /dev/null +++ b/.github/workflows/pinned-cert-audit.yml @@ -0,0 +1,80 @@ +name: pinned-cert-audit + +# Audits certificates/pinned_certs/*.pem against reality (certificates/cert_bundle.py audit): +# - probes api.openshock.app / api.openshock.dev and retires any pinned root no live +# endpoint chains through anymore (opens a PR that deletes it + regenerates cacert-merged.pem); +# - fails the job (→ notifies) when a still-needed pinned root is expired or nearing expiry. +on: + schedule: + - cron: '17 6 * * 1' # Weekly, Monday 06:17 UTC + workflow_dispatch: # Manually invoked. + push: + branches: [develop, beta, master] + paths: ['certificates/pinned_certs/**', 'certificates/cert_bundle.py', '.github/workflows/pinned-cert-audit.yml'] + +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: false + +permissions: + contents: write # push the retire-pin branch + pull-requests: write # open the retire-pin PR + +jobs: + audit: + name: Audit pinned certs + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: Checkout repository + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + + - name: Install cryptography + run: python3 -m pip install --user cryptography + + # Probes the live endpoints + checks expiry. Never fails the step itself (so the + # follow-up steps always see its outputs); the expiry alert is raised at the end. + - name: Audit + id: audit + continue-on-error: true + working-directory: certificates + run: python3 cert_bundle.py audit + + # A pinned root that no monitored endpoint chains through anymore: delete it, + # regenerate cacert-merged.pem, and open a PR. Runs only when the audit positively found + # removable pins (empty when any probe failed — fail-safe). + - name: Open PR to retire unneeded pinned root(s) + if: steps.audit.outputs.removable != '' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + branch="chore/retire-pinned-certs-${{ github.run_id }}" + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git checkout -b "$branch" + for f in ${{ steps.audit.outputs.removable }}; do + echo "Retiring certificates/$f" + git rm "certificates/$f" + done + # Regenerate the base + packed bundle (verified curl base + remaining pinned certs). + ( cd certificates && python3 cert_bundle.py generate ) + git add certificates/cacert-curl.pem certificates/cacert-merged.pem + git commit -m "chore(certs): retire pinned root(s) no longer needed by live endpoints" + git push origin "$branch" + gh pr create \ + --base "${{ github.ref_name }}" \ + --head "$branch" \ + --title "chore(certs): retire unneeded pinned root(s)" \ + --body "Automated by \`pinned-cert-audit\`: \`cert_bundle.py audit\` found pinned root(s) that no monitored endpoint (api.openshock.app / api.openshock.dev) chains through anymore. Deleted them from \`certificates/pinned_certs/\` and regenerated \`certificates/cacert-merged.pem\`. + + Merge only after confirming the endpoints still verify against the base trust store (i.e. the server chain was intentionally trimmed)." + + # Fail last so the retire-PR step above still runs. A still-needed pinned root that + # is expired / nearing expiry means we must rotate it. + - name: Fail on expiry alert + if: steps.audit.outputs.alert == 'true' + run: | + echo "::error::A still-needed pinned certificate is expired or nearing expiry — rotate it in certificates/pinned_certs/ and regenerate cacert-merged.pem." + exit 1 diff --git a/.gitignore b/.gitignore index bd8b6bd7..d2e1bb88 100644 --- a/.gitignore +++ b/.gitignore @@ -1,23 +1,30 @@ -.pio -.vscode/.browse.c_cpp.db* -.vscode/c_cpp_properties.json -.vscode/extensions.json -.vscode/launch.json -.vscode/ipch +# ESP-IDF build output +/build *.bin -# data/www is autogenerated by the build process, ignore it: -data/www -!data/**/*.bin +.vscode/ -# ignore local config files +# staticfs/www is autogenerated by the frontend build (scripts/build_frontend.py) +# and packed into the static0 partition image at build time. +/staticfs/www + +# local config files *.local -# ignore generated files +# generated files .vs /packages *.sln compile_commands.json +managed_components +dependencies.lock + +# ESP-IDF: sdkconfig and sdkconfig.old are generated ("DO NOT EDIT") from +# sdkconfig.defaults[.]. Track only the editable defaults. +/sdkconfig +/sdkconfig.* +!/sdkconfig.defaults +!/sdkconfig.defaults.* # Claude Code .claude/settings.local.json \ No newline at end of file diff --git a/.gitmodules b/.gitmodules index 1811e74e..153edf04 100644 --- a/.gitmodules +++ b/.gitmodules @@ -5,3 +5,6 @@ [submodule "frontend/packages/svelte-core"] path = frontend/packages/svelte-core url = git@github.com:OpenShock/svelte-core.git +[submodule "components/flatbuffers/flatbuffers"] + path = components/flatbuffers/flatbuffers + url = https://github.com/google/flatbuffers.git diff --git a/.mcp.json b/.mcp.json deleted file mode 100644 index 209568c4..00000000 --- a/.mcp.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "mcpServers": { - "github": { - "type": "http", - "url": "https://api.githubcopilot.com/mcp/" - } - } -} diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 91960c15..00000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,102 +0,0 @@ -{ - "C_Cpp.errorSquiggles": "disabled", - "files.associations": { - "array": "cpp", - "atomic": "cpp", - "*.tcc": "cpp", - "bitset": "cpp", - "cctype": "cpp", - "clocale": "cpp", - "cmath": "cpp", - "cstdarg": "cpp", - "cstddef": "cpp", - "cstdint": "cpp", - "cstdio": "cpp", - "cstdlib": "cpp", - "cstring": "cpp", - "ctime": "cpp", - "cwchar": "cpp", - "cwctype": "cpp", - "deque": "cpp", - "unordered_map": "cpp", - "unordered_set": "cpp", - "vector": "cpp", - "exception": "cpp", - "algorithm": "cpp", - "functional": "cpp", - "iterator": "cpp", - "map": "cpp", - "memory": "cpp", - "memory_resource": "cpp", - "numeric": "cpp", - "optional": "cpp", - "random": "cpp", - "string": "cpp", - "string_view": "cpp", - "system_error": "cpp", - "tuple": "cpp", - "type_traits": "cpp", - "utility": "cpp", - "fstream": "cpp", - "initializer_list": "cpp", - "iomanip": "cpp", - "iosfwd": "cpp", - "istream": "cpp", - "limits": "cpp", - "new": "cpp", - "ostream": "cpp", - "sstream": "cpp", - "stdexcept": "cpp", - "streambuf": "cpp", - "cinttypes": "cpp", - "typeinfo": "cpp", - "networks.c": "cpp", - "regex": "cpp", - "iostream": "cpp", - "chrono": "cpp", - "codecvt": "cpp", - "condition_variable": "cpp", - "list": "cpp", - "ratio": "cpp", - "set": "cpp", - "thread": "cpp" - }, - "[cpp]": { - "editor.defaultFormatter": "xaver.clang-format", - "editor.formatOnSave": true - }, - "[html]": { - "editor.defaultFormatter": "esbenp.prettier-vscode", - "editor.formatOnSave": true - }, - "[css]": { - "editor.defaultFormatter": "esbenp.prettier-vscode", - "editor.formatOnSave": true - }, - "[javascript]": { - "editor.defaultFormatter": "esbenp.prettier-vscode", - "editor.formatOnSave": true - }, - "[typescript]": { - "editor.defaultFormatter": "esbenp.prettier-vscode", - "editor.formatOnSave": true - }, - "[svelte]": { - "editor.defaultFormatter": "svelte.svelte-vscode", - "editor.formatOnSave": true - }, - "[json]": { - "editor.defaultFormatter": "esbenp.prettier-vscode", - "editor.formatOnSave": true - }, - "[python]": { - "editor.defaultFormatter": "ms-python.black-formatter", - "editor.formatOnSave": true - }, - "[markdown]": { - "editor.defaultFormatter": "esbenp.prettier-vscode", - "editor.formatOnSave": true - }, - "python.analysis.typeCheckingMode": "basic", - "black-formatter.args": ["--line-length", "120", "--skip-string-normalization"] -} diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 00000000..62a11df1 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,94 @@ +cmake_minimum_required(VERSION 3.16.0) + +# --- OpenShock native ESP-IDF environment header ----------------------------- +# When a board is selected via OPENSHOCK_BOARD (set by the idf.py build driver / +# CI), generate the OpenShock environment header (firmware version, API/CDN +# domains, board/chip identity, log level) and force-include it into every +# translation unit. This replaces the PlatformIO embed_env_vars.py extra_script. +# +# The legacy PlatformIO build leaves OPENSHOCK_BOARD unset and supplies the same +# macros via -D, so everything below is skipped there and the two builds don't +# fight over the definitions. +if(NOT DEFINED OPENSHOCK_BOARD AND DEFINED ENV{OPENSHOCK_BOARD}) + set(OPENSHOCK_BOARD $ENV{OPENSHOCK_BOARD}) +endif() + +if(DEFINED OPENSHOCK_BOARD) + # Map the IDF target to an OpenShock chip label for OPENSHOCK_FW_CHIP. + if(DEFINED IDF_TARGET) + set(_os_target ${IDF_TARGET}) + elseif(DEFINED ENV{IDF_TARGET}) + set(_os_target $ENV{IDF_TARGET}) + else() + set(_os_target "esp32") + endif() + if(_os_target STREQUAL "esp32s3") + set(_os_chip "ESP32-S3") + elseif(_os_target STREQUAL "esp32s2") + set(_os_chip "ESP32-S2") + elseif(_os_target STREQUAL "esp32c3") + set(_os_chip "ESP32-C3") + else() + set(_os_chip "ESP32") + endif() + + set(OPENSHOCK_ENV_HEADER ${CMAKE_BINARY_DIR}/config/openshock_env.h) + execute_process( + COMMAND ${CMAKE_COMMAND} -E env + python ${CMAKE_CURRENT_LIST_DIR}/scripts/gen_env_header.py + --output ${OPENSHOCK_ENV_HEADER} + --board ${OPENSHOCK_BOARD} + --chip ${_os_chip} + RESULT_VARIABLE _os_env_rc + ) + if(NOT _os_env_rc EQUAL 0) + message(FATAL_ERROR "gen_env_header.py failed (rc=${_os_env_rc})") + endif() + set(OPENSHOCK_NATIVE_ENV TRUE) +endif() + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) + +if(OPENSHOCK_NATIVE_ENV) + idf_build_set_property(COMPILE_OPTIONS "-include${OPENSHOCK_ENV_HEADER}" APPEND) +endif() + +project(firmware) + +# --- Static filesystem image (captive-portal web assets) --------------------- +# Build the SvelteKit frontend, gzip it, and pack it into a littlefs image whose +# geometry matches the runtime mount in components/fs/src/LfsPartition.cpp (4 KiB +# blocks, 128-byte read/prog). `idf.py flash` writes it to the `static0` +# partition. Replaces PlatformIO's build_frontend.py post-action + mklittlefs. +# +# The partition only exists in the OpenShock custom partition CSV; if the active +# partition table has no `static0` (e.g. a bare default-table build), skip the +# image instead of failing the whole build. +partition_table_get_partition_info(_staticfs_offset "--partition-name static0" "offset") +partition_table_get_partition_info(_staticfs_size "--partition-name static0" "size") + +if(_staticfs_offset STREQUAL "" OR _staticfs_size STREQUAL "") + message(WARNING "No `static0` partition in the active partition table — " + "skipping static filesystem image generation.") +else() + set(_staticfs_image ${CMAKE_BINARY_DIR}/staticfs.bin) + + # ALL target so a normal `idf.py build` regenerates the image. The generator + # skips the pnpm rebuild when frontend/build already exists (or in CI, where the + # frontend is downloaded as an artifact), so this stays cheap on incremental builds. + add_custom_target(staticfs ALL + BYPRODUCTS ${_staticfs_image} + COMMAND ${CMAKE_COMMAND} -E env + python ${CMAKE_CURRENT_LIST_DIR}/scripts/gen_staticfs.py + --frontend-dir ${CMAKE_CURRENT_LIST_DIR}/frontend + --staging-dir ${CMAKE_CURRENT_LIST_DIR}/staticfs + --output ${_staticfs_image} + --partition-size ${_staticfs_size} + --block-size 4096 --read-size 128 --prog-size 128 + COMMENT "Building static filesystem image (staticfs.bin) for static0" + VERBATIM USES_TERMINAL) + + # Register the image with the flasher so `idf.py flash` writes it to the + # static0 offset alongside the app and partition table. + esptool_py_flash_target_image(flash "static0" "${_staticfs_offset}" "${_staticfs_image}") +endif() diff --git a/FLATBUFFERS_CHANGES.md b/FLATBUFFERS_CHANGES.md deleted file mode 100644 index d7bf3ce0..00000000 --- a/FLATBUFFERS_CHANGES.md +++ /dev/null @@ -1,32 +0,0 @@ -# FlatBuffers Schema Changes - -**Repository:** https://github.com/OpenShock/flatbuffers-schemas -**Branch:** `local-comms-revamp` - -These schema changes have been applied to the submodule and are tracked by this branch. They are breaking changes that accompany the WS-to-REST migration. - -## Applied Changes - -### `HubToLocalMessage.fbs` -- Removed all command result types (`AccountLinkCommandResult`, `SetRfTxPinCommandResult`, `SetEstopPinCommandResult`, `SetEstopEnabledCommandResult`, `SetGPIOResultCode`, `AccountLinkResultCode`) -- Removed all WS command types from `HubToLocalMessagePayload` union that were migrated to REST -- Added `AccountLinkStatusEvent` — broadcast when auth token is validated on gateway connect -- Added `has_standardized_pins` field to `ReadyMessage` - -### `LocalToHubMessage.fbs` -- Removed all command types except `Common_ShockerCommandList` (WiFi, OTA, Account, GPIO commands moved to REST) -- `ShockerCommandList` moved to `Common` namespace, shared between gateway and local - -### `HubConfig.fbs` -- Added `MacAddress` struct (6-byte fixed-size array) -- Added `auth_mode` (WifiAuthMode enum) and `bssid` (MacAddress) fields to `WiFiCredentials` for security pinning - -### `Common/ShockerCommand.fbs` (new) -- Extracted `ShockerCommand` and `ShockerCommandList` tables into shared Common namespace - -### `GatewayToHubMessage.fbs` -- Updated `ShockerCommandList` reference to `Common_ShockerCommandList` - -## Pending -- Push `local-comms-revamp` branch to schemas repo and create PR -- After merge, update submodule pointer in firmware repo diff --git a/boards/DFRobot-Firebeetle2-ESP32E.defaults b/boards/DFRobot-Firebeetle2-ESP32E.defaults new file mode 100644 index 00000000..38996f1a --- /dev/null +++ b/boards/DFRobot-Firebeetle2-ESP32E.defaults @@ -0,0 +1,14 @@ +# DFRobot-Firebeetle2-ESP32E — auto-ported from platformio.ini. Board-specific deltas only; +# shared config lives in sdkconfig.defaults. + +CONFIG_IDF_TARGET="esp32" +CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y + +# OpenShock uses a single 4 MB OTA partition layout (fits every board's flash). +CONFIG_PARTITION_TABLE_CUSTOM=y +CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions/ota_4mb.csv" + +# Hardware pins (see src/Kconfig.projbuild) +CONFIG_OPENSHOCK_RF_TX_GPIO=13 +CONFIG_OPENSHOCK_LED_WS2812B=5 +CONFIG_OPENSHOCK_LED_GPIO=2 diff --git a/boards/NodeMCU-32S.defaults b/boards/NodeMCU-32S.defaults new file mode 100644 index 00000000..2dee30df --- /dev/null +++ b/boards/NodeMCU-32S.defaults @@ -0,0 +1,12 @@ +# NodeMCU-32S — auto-ported from platformio.ini. Board-specific deltas only; +# shared config lives in sdkconfig.defaults. + +CONFIG_IDF_TARGET="esp32" +CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y + +# OpenShock uses a single 4 MB OTA partition layout (fits every board's flash). +CONFIG_PARTITION_TABLE_CUSTOM=y +CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions/ota_4mb.csv" + +# Hardware pins (see src/Kconfig.projbuild) +CONFIG_OPENSHOCK_LED_GPIO=2 diff --git a/boards/OpenShock-Core-V1.defaults b/boards/OpenShock-Core-V1.defaults new file mode 100644 index 00000000..61089a3a --- /dev/null +++ b/boards/OpenShock-Core-V1.defaults @@ -0,0 +1,15 @@ +# OpenShock-Core-V1 — auto-ported from platformio.ini. Board-specific deltas only; +# shared config lives in sdkconfig.defaults. + +CONFIG_IDF_TARGET="esp32s3" +CONFIG_ESPTOOLPY_FLASHSIZE_8MB=y + +# OpenShock uses a single 4 MB OTA partition layout (fits every board's flash). +CONFIG_PARTITION_TABLE_CUSTOM=y +CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions/ota_4mb.csv" + +# Hardware pins (see src/Kconfig.projbuild) +CONFIG_OPENSHOCK_LED_WS2812B=48 +CONFIG_OPENSHOCK_LED_GPIO=35 +CONFIG_OPENSHOCK_RF_TX_GPIO=15 +CONFIG_OPENSHOCK_ESTOP_PIN=13 diff --git a/boards/OpenShock-Core-V2.defaults b/boards/OpenShock-Core-V2.defaults new file mode 100644 index 00000000..e17eddf9 --- /dev/null +++ b/boards/OpenShock-Core-V2.defaults @@ -0,0 +1,16 @@ +# OpenShock Core V2 — ESP32-S3, 8 MB flash, no PSRAM. +# Loaded on top of the shared sdkconfig.defaults; only board-specific deltas here. + +CONFIG_IDF_TARGET="esp32s3" +CONFIG_ESPTOOLPY_FLASHSIZE_8MB=y + +# OpenShock custom OTA partition layout. There is no dedicated 8 MB table yet, so +# reuse the 4 MB OTA layout — it fits within 8 MB (upper flash currently unused). +CONFIG_PARTITION_TABLE_CUSTOM=y +CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions/ota_4mb.csv" + +# Hardware pins (see src/Kconfig.projbuild) +CONFIG_OPENSHOCK_LED_WS2812B=14 +CONFIG_OPENSHOCK_LED_GPIO=13 +CONFIG_OPENSHOCK_RF_TX_GPIO=1 +CONFIG_OPENSHOCK_ESTOP_PIN=38 diff --git a/boards/Pishock-2023.defaults b/boards/Pishock-2023.defaults new file mode 100644 index 00000000..0c41404a --- /dev/null +++ b/boards/Pishock-2023.defaults @@ -0,0 +1,13 @@ +# Pishock-2023 — auto-ported from platformio.ini. Board-specific deltas only; +# shared config lives in sdkconfig.defaults. + +CONFIG_IDF_TARGET="esp32" +CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y + +# OpenShock uses a single 4 MB OTA partition layout (fits every board's flash). +CONFIG_PARTITION_TABLE_CUSTOM=y +CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions/ota_4mb.csv" + +# Hardware pins (see src/Kconfig.projbuild) +CONFIG_OPENSHOCK_LED_GPIO=2 +CONFIG_OPENSHOCK_RF_TX_GPIO=12 diff --git a/boards/Pishock-Lite-2021.defaults b/boards/Pishock-Lite-2021.defaults new file mode 100644 index 00000000..eef0d58f --- /dev/null +++ b/boards/Pishock-Lite-2021.defaults @@ -0,0 +1,13 @@ +# Pishock-Lite-2021 — auto-ported from platformio.ini. Board-specific deltas only; +# shared config lives in sdkconfig.defaults. + +CONFIG_IDF_TARGET="esp32" +CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y + +# OpenShock uses a single 4 MB OTA partition layout (fits every board's flash). +CONFIG_PARTITION_TABLE_CUSTOM=y +CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions/ota_4mb.csv" + +# Hardware pins (see src/Kconfig.projbuild) +CONFIG_OPENSHOCK_LED_GPIO=2 +CONFIG_OPENSHOCK_RF_TX_GPIO=15 diff --git a/boards/Seeed-Xiao-ESP32C3.defaults b/boards/Seeed-Xiao-ESP32C3.defaults new file mode 100644 index 00000000..bdf781df --- /dev/null +++ b/boards/Seeed-Xiao-ESP32C3.defaults @@ -0,0 +1,9 @@ +# Seeed-Xiao-ESP32C3 — auto-ported from platformio.ini. Board-specific deltas only; +# shared config lives in sdkconfig.defaults. + +CONFIG_IDF_TARGET="esp32c3" +CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y + +# OpenShock uses a single 4 MB OTA partition layout (fits every board's flash). +CONFIG_PARTITION_TABLE_CUSTOM=y +CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions/ota_4mb.csv" diff --git a/boards/Seeed-Xiao-ESP32S3.defaults b/boards/Seeed-Xiao-ESP32S3.defaults new file mode 100644 index 00000000..bb6f42ea --- /dev/null +++ b/boards/Seeed-Xiao-ESP32S3.defaults @@ -0,0 +1,12 @@ +# Seeed-Xiao-ESP32S3 — auto-ported from platformio.ini. Board-specific deltas only; +# shared config lives in sdkconfig.defaults. + +CONFIG_IDF_TARGET="esp32s3" +CONFIG_ESPTOOLPY_FLASHSIZE_8MB=y + +# OpenShock uses a single 4 MB OTA partition layout (fits every board's flash). +CONFIG_PARTITION_TABLE_CUSTOM=y +CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions/ota_4mb.csv" + +# Hardware pins (see src/Kconfig.projbuild) +CONFIG_OPENSHOCK_LED_GPIO=21 diff --git a/boards/Waveshare_esp32_s3_zero.defaults b/boards/Waveshare_esp32_s3_zero.defaults new file mode 100644 index 00000000..e2912f1b --- /dev/null +++ b/boards/Waveshare_esp32_s3_zero.defaults @@ -0,0 +1,14 @@ +# Waveshare_esp32_s3_zero — auto-ported from platformio.ini. Board-specific deltas only; +# shared config lives in sdkconfig.defaults. + +CONFIG_IDF_TARGET="esp32s3" +CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y + +# OpenShock uses a single 4 MB OTA partition layout (fits every board's flash). +CONFIG_PARTITION_TABLE_CUSTOM=y +CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions/ota_4mb.csv" + +# Hardware pins (see src/Kconfig.projbuild) +CONFIG_OPENSHOCK_RF_TX_GPIO=1 +CONFIG_OPENSHOCK_LED_WS2812B=21 +CONFIG_OPENSHOCK_LED_SWAP_RG_CHANNELS=y diff --git a/boards/Wemos-D1-Mini-ESP32.defaults b/boards/Wemos-D1-Mini-ESP32.defaults new file mode 100644 index 00000000..ad8d9c64 --- /dev/null +++ b/boards/Wemos-D1-Mini-ESP32.defaults @@ -0,0 +1,13 @@ +# Wemos-D1-Mini-ESP32 — auto-ported from platformio.ini. Board-specific deltas only; +# shared config lives in sdkconfig.defaults. + +CONFIG_IDF_TARGET="esp32" +CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y + +# OpenShock uses a single 4 MB OTA partition layout (fits every board's flash). +CONFIG_PARTITION_TABLE_CUSTOM=y +CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions/ota_4mb.csv" + +# Hardware pins (see src/Kconfig.projbuild) +CONFIG_OPENSHOCK_LED_GPIO=2 +CONFIG_OPENSHOCK_RF_TX_GPIO=15 diff --git a/boards/Wemos-D1-Mini-ESP32.json b/boards/Wemos-D1-Mini-ESP32.json deleted file mode 100644 index 66cbfbf3..00000000 --- a/boards/Wemos-D1-Mini-ESP32.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "build": { - "arduino": { - "ldscript": "esp32_out.ld" - }, - "core": "esp32", - "extra_flags": "-DARDUINO_D1_MINI32", - "f_cpu": "240000000L", - "f_flash": "40000000L", - "flash_mode": "dio", - "mcu": "esp32", - "variant": "d1_mini32" - }, - "connectivity": [ - "wifi", - "bluetooth", - "ethernet", - "can" - ], - "debug": { - "openocd_board": "esp-wroom-32.cfg" - }, - "frameworks": [ - "arduino", - "espidf" - ], - "name": "WEMOS D1 MINI ESP32", - "upload": { - "flash_size": "4MB", - "maximum_ram_size": 327680, - "maximum_size": 4194304, - "require_upload_port": true, - "speed": 460800 - }, - "url": "https://www.wemos.cc", - "vendor": "WEMOS" -} \ No newline at end of file diff --git a/boards/Wemos-Lolin-S2-Mini.defaults b/boards/Wemos-Lolin-S2-Mini.defaults new file mode 100644 index 00000000..c67cd6ca --- /dev/null +++ b/boards/Wemos-Lolin-S2-Mini.defaults @@ -0,0 +1,12 @@ +# Wemos-Lolin-S2-Mini — auto-ported from platformio.ini. Board-specific deltas only; +# shared config lives in sdkconfig.defaults. + +CONFIG_IDF_TARGET="esp32s2" +CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y + +# OpenShock uses a single 4 MB OTA partition layout (fits every board's flash). +CONFIG_PARTITION_TABLE_CUSTOM=y +CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions/ota_4mb.csv" + +# Hardware pins (see src/Kconfig.projbuild) +CONFIG_OPENSHOCK_LED_GPIO=15 diff --git a/boards/Wemos-Lolin-S2-Mini.json b/boards/Wemos-Lolin-S2-Mini.json deleted file mode 100644 index 11cd46f4..00000000 --- a/boards/Wemos-Lolin-S2-Mini.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "build": { - "arduino": { - "ldscript": "esp32s2_out.ld" - }, - "core": "esp32", - "extra_flags": [ - "-DARDUINO_LOLIN_S2_MINI", - "-DBOARD_HAS_PSRAM", - "-DARDUINO_USB_CDC_ON_BOOT=1" - ], - "f_cpu": "240000000L", - "f_flash": "80000000L", - "flash_mode": "dio", - "hwids": [ - [ - "0X303A", - "0x80C2" - ] - ], - "mcu": "esp32s2", - "variant": "lolin_s2_mini" - }, - "connectivity": [ - "wifi" - ], - "debug": { - "openocd_target": "esp32s2.cfg" - }, - "frameworks": [ - "arduino", - "espidf" - ], - "name": "WEMOS LOLIN S2 Mini", - "upload": { - "flash_size": "4MB", - "maximum_ram_size": 327680, - "maximum_size": 4194304, - "use_1200bps_touch": true, - "wait_for_upload_port": true, - "require_upload_port": true, - "speed": 921600 - }, - "url": "https://www.wemos.cc/en/latest/s2/s2_mini.html", - "vendor": "WEMOS" -} \ No newline at end of file diff --git a/boards/Wemos-Lolin-S3-Mini.defaults b/boards/Wemos-Lolin-S3-Mini.defaults new file mode 100644 index 00000000..53904ed1 --- /dev/null +++ b/boards/Wemos-Lolin-S3-Mini.defaults @@ -0,0 +1,13 @@ +# Wemos-Lolin-S3-Mini — auto-ported from platformio.ini. Board-specific deltas only; +# shared config lives in sdkconfig.defaults. + +CONFIG_IDF_TARGET="esp32s3" +CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y + +# OpenShock uses a single 4 MB OTA partition layout (fits every board's flash). +CONFIG_PARTITION_TABLE_CUSTOM=y +CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions/ota_4mb.csv" + +# Hardware pins (see src/Kconfig.projbuild) +CONFIG_OPENSHOCK_LED_WS2812B=47 +CONFIG_OPENSHOCK_LED_SWAP_RG_CHANNELS=y diff --git a/boards/Wemos-Lolin-S3.defaults b/boards/Wemos-Lolin-S3.defaults new file mode 100644 index 00000000..04667891 --- /dev/null +++ b/boards/Wemos-Lolin-S3.defaults @@ -0,0 +1,12 @@ +# Wemos-Lolin-S3 — auto-ported from platformio.ini. Board-specific deltas only; +# shared config lives in sdkconfig.defaults. + +CONFIG_IDF_TARGET="esp32s3" +CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y + +# OpenShock uses a single 4 MB OTA partition layout (fits every board's flash). +CONFIG_PARTITION_TABLE_CUSTOM=y +CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions/ota_4mb.csv" + +# Hardware pins (see src/Kconfig.projbuild) +CONFIG_OPENSHOCK_LED_WS2812B=38 diff --git a/boards/Wemos-Lolin-S3.json b/boards/Wemos-Lolin-S3.json deleted file mode 100644 index 82b81be5..00000000 --- a/boards/Wemos-Lolin-S3.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "build": { - "arduino": { - "ldscript": "esp32s3_out.ld", - "partitions": "default_16MB.csv", - "memory_type": "qio_opi" - }, - "core": "esp32", - "extra_flags": ["-DBOARD_HAS_PSRAM", "-DARDUINO_LOLIN_S3", "-DARDUINO_USB_MODE=1"], - "f_cpu": "240000000L", - "f_flash": "80000000L", - "flash_mode": "qio", - "hwids": [["0x303A", "0x1001"]], - "mcu": "esp32s3", - "variant": "lolin_s3" - }, - "connectivity": ["wifi", "bluetooth"], - "debug": { - "openocd_target": "esp32s3.cfg" - }, - "frameworks": ["arduino", "espidf"], - "name": "WEMOS LOLIN S3", - "upload": { - "flash_size": "16MB", - "maximum_ram_size": 327680, - "maximum_size": 16777216, - "use_1200bps_touch": true, - "wait_for_upload_port": true, - "require_upload_port": true, - "speed": 460800 - }, - "url": "https://www.wemos.cc/en/latest/s3/index.html", - "vendor": "WEMOS" -} diff --git a/certificates/.gitignore b/certificates/.gitignore index 15c59972..116c6b4b 100644 --- a/certificates/.gitignore +++ b/certificates/.gitignore @@ -10,3 +10,7 @@ custom_certs/ *.pem.key *.p12 *.pfx + +# Python bytecode cache from cert_bundle.py +__pycache__/ +*.pyc diff --git a/certificates/README.md b/certificates/README.md index 67b4069e..386de439 100644 --- a/certificates/README.md +++ b/certificates/README.md @@ -1,106 +1,154 @@ # Certificates used for HTTPS -This project uses a curated CA certificate bundle for TLS/SSL verification on the ESP32. +This project compiles a curated CA trust store into the firmware for TLS/SSL verification on +the ESP32. The build packs `cacert-merged.pem` into the flash image via ESP-IDF's +certificate-bundle step, and the ESP32 verifies every TLS server (gateway WebSocket, HTTP/OTA) +against it. -The bundle is sourced from **curl’s official CA extract** and can optionally be extended with **user-provided custom CA certificates**. +`certificates/cert_bundle.py` manages the store. It has two subcommands: + +```sh +python3 cert_bundle.py generate # (default) rebuild cacert-curl.pem + cacert-merged.pem +python3 cert_bundle.py audit # audit expiry + live-endpoint relevance (used by CI) +``` + +## The two files + +| File | What it is | Packed into firmware? | +|---|---|---| +| `cacert-curl.pem` | **Verbatim** curl/Mozilla CA extract, exactly as downloaded. Its `sha256sum` matches curl's published hash. | No — reference/base only | +| `cacert-merged.pem` | Generated: the base **plus** `pinned_certs/*.pem` (plus any local `custom_certs/*.pem`), de-duplicated, with an OpenShock header banner instead of curl's. | **Yes** | + +Both are tracked and committed. `cacert-merged.pem` is what actually ships; `cacert-curl.pem` +exists so the base can be independently checksum-verified against curl. + +### Why pinned roots exist + +esp_crt_bundle does **not** do certificate-path building. It trusts the chain exactly as a +server presents it and looks up the **issuer of the topmost presented cert** among the trusted +roots. If a server sends a legacy cross-signed root at the top of its chain, the anchor it +"requires" is that cross-signer — which a modern (Mozilla-derived) store may no longer include. + +Concretely: `api.openshock.app` presents `leaf → GTS WE1 → GTS Root R4`, where R4 is the +**cross-signed** copy issued by `GlobalSign Root CA` (R1). Mozilla/curl retired R1, so without +it esp_crt_bundle reports *"No matching trusted root certificate found"*. `pinned_certs/` carries +R1 so that chain verifies. (The cleaner long-term fix is trimming the server chain so it +validates against `GTS Root R4`, which every store already ships — see **Auditing** below.) ## Source of Trust -- The base CA bundle comes from: - [https://curl.se/docs/caextract.html](https://curl.se/docs/caextract.html) -- `cacert.pem` is downloaded directly from curl.se -- A SHA-256 checksum is computed locally and compared against curl’s published checksum -- All certificates are parsed and validated using `cryptography` -- Any parsing error, mismatch, or invalid certificate **aborts generation** +- The base CA bundle comes from: [https://curl.se/docs/caextract.html](https://curl.se/docs/caextract.html) +- `cacert-curl.pem` is the base PEM downloaded from curl.se; its SHA-256 is compared against + curl's published checksum +- Every certificate (base, pinned, custom) is parsed and validated with `cryptography` +- Pinned/custom certs must be CA certificates (BasicConstraints CA=TRUE) +- The merged output is **de-duplicated** by certificate DER, and duplicate subject names are + rejected (they would break esp_crt_bundle's subject-name lookup) +- Any parsing error, checksum mismatch, or an already-expired pinned root **aborts generation** > ⚠️ **Trust warning** -> The PEM file and its checksum are fetched from the same domain. -> If curl.se were compromised or TLS trust failed, both could be malicious but internally consistent. -> **Manual verification against curl’s website is therefore mandatory.** +> The base PEM and its checksum are fetched from the same domain. If curl.se were compromised +> or TLS trust failed, both could be malicious but internally consistent. +> **Manual verification against curl's website is therefore mandatory.** ## Updating the Bundle ### 1. Generate -Run: - ```sh -python3 gen_crt_bundle.py +python3 cert_bundle.py generate ``` The script will: -- Download `cacert.pem` -- Compute its SHA-256 hash -- Download curl’s published checksum -- Verify the PEM matches the computed checksum -- Optionally include custom certificates (see below) -- Abort on any error or inconsistency +- Download the curl base PEM and compute its SHA-256 +- Download curl's published checksum and verify the download matches it +- Parse/validate the base + `pinned_certs/*.pem` (+ any `custom_certs/*.pem`) +- Reject non-CA pins, duplicate subjects, and already-expired pins; de-dup identical certs +- Write `cacert-curl.pem` (verbatim base) and `cacert-merged.pem` (base + pinned + custom) ### 2. **Mandatory Manual Verification** ⚠️ **Do not commit anything before completing the steps below.** +`cacert-curl.pem` is the verbatim curl extract, so verify it directly: + 1. Open: [https://curl.se/docs/caextract.html](https://curl.se/docs/caextract.html) 2. Locate the latest published SHA-256 checksum -3. Manually compare it against: +3. Compare it against: ```sh -sha256sum cacert.pem +sha256sum cacert-curl.pem ``` -Both values **must match exactly**. - -If they do not: - -- Do not commit -- Treat the bundle as untrusted - -> Automatic checks ensure internal consistency. -> Manual verification establishes the external trust boundary. +Both values **must match exactly**. If they do not: do not commit; treat the bundle as untrusted. ### 3. Commit After successful manual verification, commit: -- `cacert.pem` -- `x509_crt_bundle` +- `cacert-curl.pem` +- `cacert-merged.pem` +- any change under `pinned_certs/` -❌ **Never commit anything under `custom_certs/`.** +`cacert-merged.pem` is the packed artifact. The script does **not** emit a packed +`x509_crt_bundle` — the firmware build converts and packs `cacert-merged.pem` into flash itself +via ESP-IDF's certificate-bundle step (`CONFIG_MBEDTLS_CUSTOM_CERTIFICATE_BUNDLE_PATH` in +`sdkconfig.defaults`). -## Custom Certificates (Optional) +> ⚠️ **Build gotcha:** the ESP-IDF cert-bundle step does not always detect a changed +> `cacert-merged.pem`. After regenerating, delete `.pio/build//x509_crt_bundle*` (or do a +> clean build) or the firmware may silently ship the previous bundle. -Self-hosted setups may require trusting additional Certificate Authorities (e.g. internal PKI or private reverse proxies). +## Pinned Certificates (`pinned_certs/`) -Custom CA certificates can be added locally and merged into the bundle. +Roots we intentionally ship on top of the Mozilla base. **Tracked and committed.** Each file +must contain exactly one PEM certificate block (an optional leading label/comment line, as in +curl's bundle, is allowed) and must be a CA certificate. -### How to add +Before adding a pinned root, fingerprint-verify it out-of-band against at least one independent +source (e.g. the CA's own repository plus Cloudflare's `cfssl_trust`), then drop the PEM into +`pinned_certs/` and run `cert_bundle.py generate`. -Place certificates in: +### Auditing -```text -custom_certs/ -``` +`cert_bundle.py audit` (run weekly by the `pinned-cert-audit` CI workflow): -Only files ending in `.pem` are considered. +- **Expiry** — checks every cert in `cacert-merged.pem`. A still-needed pinned root that is + expired or within the warn window (`PINNED_CERT_EXPIRY_WARN_DAYS`, default 90) fails the job + so we rotate it in time. +- **Relevance** — probes `api.openshock.app` / `api.openshock.dev` and works out which anchor + each requires. A pinned root that **no** reachable endpoint chains through anymore is dead + weight; the workflow opens a PR that deletes it and regenerates the bundle. (Fail-safe: if a + probe is unreachable, nothing is retired.) -### Requirements +This is what makes the pins self-cleaning: once the servers stop sending a legacy cross-signed +root, the corresponding pin is automatically proposed for removal. + +## Custom Certificates (Optional) -Each custom certificate file must contain exactly one PEM certificate block and nothing else. +Self-hosted setups may need to trust additional CAs (internal PKI, private reverse proxies). +Drop them in `custom_certs/` locally — this directory is **git-ignored** and never committed. -The script will parse and validate them as CA certificates and reject anything invalid or duplicated. +- Only `*.pem` files are considered; each must contain exactly one CA certificate (an optional + leading label/comment line is allowed, but no trailing data or private keys). +- The script validates them and rejects anything invalid or duplicated. -> ⚠️ Adding custom CAs expands the ESP32’s trust boundary. -> Only add certificates you fully trust and control. +> ⚠️ Adding custom CAs expands the ESP32's trust boundary. Only add certificates you fully +> trust and control. -If `custom_certs/` does not exist or doesn't contain any .pem certificates, only curl’s CA bundle is used. +> **Note:** custom-cert support is currently **dormant** in shipped builds — the committed +> `cacert-merged.pem` is generated without any `custom_certs/`, so custom certs only reach the +> trust store in a local rebuild. ## Trust Model Summary - Automatic verification protects against corruption and mismatched downloads -- Manual verification anchors trust outside the update mechanism -- Custom certificates explicitly extend trust and are opt-in only -- The generated bundle is deterministic for a given input set +- Manual verification (`sha256sum cacert-curl.pem` vs curl) anchors trust outside the update mechanism +- Pinned roots are explicit, fingerprint-verified additions, and are expiry/relevance-audited +- The merged store is de-duplicated and cannot contain the same certificate twice +- Custom certificates explicitly extend trust and are opt-in, local-only +- `cacert-merged.pem` = verified curl base + `pinned_certs/` (+ local `custom_certs/`) ## References diff --git a/certificates/cacert.pem b/certificates/cacert-curl.pem similarity index 83% rename from certificates/cacert.pem rename to certificates/cacert-curl.pem index 65be891e..3e158b86 100644 --- a/certificates/cacert.pem +++ b/certificates/cacert-curl.pem @@ -1,7 +1,7 @@ ## ## Bundle of CA Root Certificates ## -## Certificate data from Mozilla as of: Tue Dec 2 04:12:02 2025 GMT +## Certificate data from Mozilla as of: Thu May 14 03:12:02 2026 GMT ## ## Find updated versions here: https://curl.se/docs/caextract.html ## @@ -15,8 +15,8 @@ ## an Apache+mod_ssl webserver for SSL client authentication. ## Just configure this file as the SSLCACertificateFile. ## -## Conversion done with mk-ca-bundle.pl version 1.30. -## SHA256: a903b3cd05231e39332515ef7ebe37e697262f39515a52015c23c62805b73cd0 +## Conversion done with mk-ca-bundle.pl version 1.33. +## SHA256: 77130ef91213772844561fbd3aa31d413b25c2ac7f576fea3bc3bbff7ef93489 ## @@ -46,237 +46,6 @@ W3iDVuycNsMm4hH2Z0kdkquM++v/eu6FSqdQgPCnXEqULl8FmTxSQeDNtGPPAUO6nIPcj2A781q0 tHuu2guQOHXvgR1m0vdXcDazv/wor3ElhVsT/h5/WrQ8 -----END CERTIFICATE----- -QuoVadis Root CA 2 -================== ------BEGIN CERTIFICATE----- -MIIFtzCCA5+gAwIBAgICBQkwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0xGTAXBgNVBAoT -EFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJvb3QgQ0EgMjAeFw0wNjExMjQx -ODI3MDBaFw0zMTExMjQxODIzMzNaMEUxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM -aW1pdGVkMRswGQYDVQQDExJRdW9WYWRpcyBSb290IENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4IC -DwAwggIKAoICAQCaGMpLlA0ALa8DKYrwD4HIrkwZhR0In6spRIXzL4GtMh6QRr+jhiYaHv5+HBg6 -XJxgFyo6dIMzMH1hVBHL7avg5tKifvVrbxi3Cgst/ek+7wrGsxDp3MJGF/hd/aTa/55JWpzmM+Yk -lvc/ulsrHHo1wtZn/qtmUIttKGAr79dgw8eTvI02kfN/+NsRE8Scd3bBrrcCaoF6qUWD4gXmuVbB -lDePSHFjIuwXZQeVikvfj8ZaCuWw419eaxGrDPmF60Tp+ARz8un+XJiM9XOva7R+zdRcAitMOeGy -lZUtQofX1bOQQ7dsE/He3fbE+Ik/0XX1ksOR1YqI0JDs3G3eicJlcZaLDQP9nL9bFqyS2+r+eXyt -66/3FsvbzSUr5R/7mp/iUcw6UwxI5g69ybR2BlLmEROFcmMDBOAENisgGQLodKcftslWZvB1Jdxn -wQ5hYIizPtGo/KPaHbDRsSNU30R2be1B2MGyIrZTHN81Hdyhdyox5C315eXbyOD/5YDXC2Og/zOh -D7osFRXql7PSorW+8oyWHhqPHWykYTe5hnMz15eWniN9gqRMgeKh0bpnX5UHoycR7hYQe7xFSkyy -BNKr79X9DFHOUGoIMfmR2gyPZFwDwzqLID9ujWc9Otb+fVuIyV77zGHcizN300QyNQliBJIWENie -J0f7OyHj+OsdWwIDAQABo4GwMIGtMA8GA1UdEwEB/wQFMAMBAf8wCwYDVR0PBAQDAgEGMB0GA1Ud -DgQWBBQahGK8SEwzJQTU7tD2A8QZRtGUazBuBgNVHSMEZzBlgBQahGK8SEwzJQTU7tD2A8QZRtGU -a6FJpEcwRTELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMT -ElF1b1ZhZGlzIFJvb3QgQ0EgMoICBQkwDQYJKoZIhvcNAQEFBQADggIBAD4KFk2fBluornFdLwUv -Z+YTRYPENvbzwCYMDbVHZF34tHLJRqUDGCdViXh9duqWNIAXINzng/iN/Ae42l9NLmeyhP3ZRPx3 -UIHmfLTJDQtyU/h2BwdBR5YM++CCJpNVjP4iH2BlfF/nJrP3MpCYUNQ3cVX2kiF495V5+vgtJodm -VjB3pjd4M1IQWK4/YY7yarHvGH5KWWPKjaJW1acvvFYfzznB4vsKqBUsfU16Y8Zsl0Q80m/DShcK -+JDSV6IZUaUtl0HaB0+pUNqQjZRG4T7wlP0QADj1O+hA4bRuVhogzG9Yje0uRY/W6ZM/57Es3zrW -IozchLsib9D45MY56QSIPMO661V6bYCZJPVsAfv4l7CUW+v90m/xd2gNNWQjrLhVoQPRTUIZ3Ph1 -WVaj+ahJefivDrkRoHy3au000LYmYjgahwz46P0u05B/B5EqHdZ+XIWDmbA4CD/pXvk1B+TJYm5X -f6dQlfe6yJvmjqIBxdZmv3lh8zwc4bmCXF2gw+nYSL0ZohEUGW6yhhtoPkg3Goi3XZZenMfvJ2II -4pEZXNLxId26F0KCl3GBUzGpn/Z9Yr9y4aOTHcyKJloJONDO1w2AFrR4pTqHTI2KpdVGl/IsELm8 -VCLAAVBpQ570su9t+Oza8eOx79+Rj1QqCyXBJhnEUhAFZdWCEOrCMc0u ------END CERTIFICATE----- - -QuoVadis Root CA 3 -================== ------BEGIN CERTIFICATE----- -MIIGnTCCBIWgAwIBAgICBcYwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0xGTAXBgNVBAoT -EFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJvb3QgQ0EgMzAeFw0wNjExMjQx -OTExMjNaFw0zMTExMjQxOTA2NDRaMEUxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM -aW1pdGVkMRswGQYDVQQDExJRdW9WYWRpcyBSb290IENBIDMwggIiMA0GCSqGSIb3DQEBAQUAA4IC -DwAwggIKAoICAQDMV0IWVJzmmNPTTe7+7cefQzlKZbPoFog02w1ZkXTPkrgEQK0CSzGrvI2RaNgg -DhoB4hp7Thdd4oq3P5kazethq8Jlph+3t723j/z9cI8LoGe+AaJZz3HmDyl2/7FWeUUrH556VOij -KTVopAFPD6QuN+8bv+OPEKhyq1hX51SGyMnzW9os2l2ObjyjPtr7guXd8lyyBTNvijbO0BNO/79K -DDRMpsMhvVAEVeuxu537RR5kFd5VAYwCdrXLoT9CabwvvWhDFlaJKjdhkf2mrk7AyxRllDdLkgbv -BNDInIjbC3uBr7E9KsRlOni27tyAsdLTmZw67mtaa7ONt9XOnMK+pUsvFrGeaDsGb659n/je7Mwp -p5ijJUMv7/FfJuGITfhebtfZFG4ZM2mnO4SJk8RTVROhUXhA+LjJou57ulJCg54U7QVSWllWp5f8 -nT8KKdjcT5EOE7zelaTfi5m+rJsziO+1ga8bxiJTyPbH7pcUsMV8eFLI8M5ud2CEpukqdiDtWAEX -MJPpGovgc2PZapKUSU60rUqFxKMiMPwJ7Wgic6aIDFUhWMXhOp8q3crhkODZc6tsgLjoC2SToJyM -Gf+z0gzskSaHirOi4XCPLArlzW1oUevaPwV/izLmE1xr/l9A4iLItLRkT9a6fUg+qGkM17uGcclz -uD87nSVL2v9A6wIDAQABo4IBlTCCAZEwDwYDVR0TAQH/BAUwAwEB/zCB4QYDVR0gBIHZMIHWMIHT -BgkrBgEEAb5YAAMwgcUwgZMGCCsGAQUFBwICMIGGGoGDQW55IHVzZSBvZiB0aGlzIENlcnRpZmlj -YXRlIGNvbnN0aXR1dGVzIGFjY2VwdGFuY2Ugb2YgdGhlIFF1b1ZhZGlzIFJvb3QgQ0EgMyBDZXJ0 -aWZpY2F0ZSBQb2xpY3kgLyBDZXJ0aWZpY2F0aW9uIFByYWN0aWNlIFN0YXRlbWVudC4wLQYIKwYB -BQUHAgEWIWh0dHA6Ly93d3cucXVvdmFkaXNnbG9iYWwuY29tL2NwczALBgNVHQ8EBAMCAQYwHQYD -VR0OBBYEFPLAE+CCQz777i9nMpY1XNu4ywLQMG4GA1UdIwRnMGWAFPLAE+CCQz777i9nMpY1XNu4 -ywLQoUmkRzBFMQswCQYDVQQGEwJCTTEZMBcGA1UEChMQUXVvVmFkaXMgTGltaXRlZDEbMBkGA1UE -AxMSUXVvVmFkaXMgUm9vdCBDQSAzggIFxjANBgkqhkiG9w0BAQUFAAOCAgEAT62gLEz6wPJv92ZV -qyM07ucp2sNbtrCD2dDQ4iH782CnO11gUyeim/YIIirnv6By5ZwkajGxkHon24QRiSemd1o417+s -hvzuXYO8BsbRd2sPbSQvS3pspweWyuOEn62Iix2rFo1bZhfZFvSLgNLd+LJ2w/w4E6oM3kJpK27z -POuAJ9v1pkQNn1pVWQvVDVJIxa6f8i+AxeoyUDUSly7B4f/xI4hROJ/yZlZ25w9Rl6VSDE1JUZU2 -Pb+iSwwQHYaZTKrzchGT5Or2m9qoXadNt54CrnMAyNojA+j56hl0YgCUyyIgvpSnWbWCar6ZeXqp -8kokUvd0/bpO5qgdAm6xDYBEwa7TIzdfu4V8K5Iu6H6li92Z4b8nby1dqnuH/grdS/yO9SbkbnBC -bjPsMZ57k8HkyWkaPcBrTiJt7qtYTcbQQcEr6k8Sh17rRdhs9ZgC06DYVYoGmRmioHfRMJ6szHXu -g/WwYjnPbFfiTNKRCw51KBuav/0aQ/HKd/s7j2G4aSgWQgRecCocIdiP4b0jWy10QJLZYxkNc91p -vGJHvOB0K7Lrfb5BG7XARsWhIstfTsEokt4YutUqKLsRixeTmJlglFwjz1onl14LBQaTNx47aTbr -qZ5hHY8y2o4M1nQ+ewkk2gF3R8Q7zTSMmfXK4SVhM7JZG+Ju1zdXtg2pEto= ------END CERTIFICATE----- - -DigiCert Assured ID Root CA -=========================== ------BEGIN CERTIFICATE----- -MIIDtzCCAp+gAwIBAgIQDOfg5RfYRv6P5WD8G/AwOTANBgkqhkiG9w0BAQUFADBlMQswCQYDVQQG -EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSQw -IgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0EwHhcNMDYxMTEwMDAwMDAwWhcNMzEx -MTEwMDAwMDAwWjBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQL -ExB3d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0Ew -ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCtDhXO5EOAXLGH87dg+XESpa7cJpSIqvTO -9SA5KFhgDPiA2qkVlTJhPLWxKISKityfCgyDF3qPkKyK53lTXDGEKvYPmDI2dsze3Tyoou9q+yHy -UmHfnyDXH+Kx2f4YZNISW1/5WBg1vEfNoTb5a3/UsDg+wRvDjDPZ2C8Y/igPs6eD1sNuRMBhNZYW -/lmci3Zt1/GiSw0r/wty2p5g0I6QNcZ4VYcgoc/lbQrISXwxmDNsIumH0DJaoroTghHtORedmTpy -oeb6pNnVFzF1roV9Iq4/AUaG9ih5yLHa5FcXxH4cDrC0kqZWs72yl+2qp/C3xag/lRbQ/6GW6whf -GHdPAgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRF -66Kv9JLLgjEtUYunpyGd823IDzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYunpyGd823IDzANBgkq -hkiG9w0BAQUFAAOCAQEAog683+Lt8ONyc3pklL/3cmbYMuRCdWKuh+vy1dneVrOfzM4UKLkNl2Bc -EkxY5NM9g0lFWJc1aRqoR+pWxnmrEthngYTffwk8lOa4JiwgvT2zKIn3X/8i4peEH+ll74fg38Fn -SbNd67IJKusm7Xi+fT8r87cmNW1fiQG2SVufAQWbqz0lwcy2f8Lxb4bG+mRo64EtlOtCt/qMHt1i -8b5QZ7dsvfPxH2sMNgcWfzd8qVttevESRmCD1ycEvkvOl77DZypoEd+A5wwzZr8TDRRu838fYxAe -+o0bJW1sj6W3YQGx0qMmoRBxna3iw/nDmVG3KwcIzi7mULKn+gpFL6Lw8g== ------END CERTIFICATE----- - -DigiCert Global Root CA -======================= ------BEGIN CERTIFICATE----- -MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBhMQswCQYDVQQG -EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSAw -HgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBDQTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAw -MDAwMDBaMGExCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3 -dy5kaWdpY2VydC5jb20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkq -hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsBCSDMAZOn -TjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97nh6Vfe63SKMI2tavegw5 -BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt43C/dxC//AH2hdmoRBBYMql1GNXRor5H -4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7PT19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y -7vrTC0LUq7dBMtoM1O/4gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQAB -o2MwYTAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbRTLtm -8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUwDQYJKoZIhvcNAQEF -BQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/EsrhMAtudXH/vTBH1jLuG2cenTnmCmr -EbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIt -tep3Sp+dWOIrWcBAI+0tKIJFPnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886 -UAb3LujEV0lsYSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk -CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4= ------END CERTIFICATE----- - -DigiCert High Assurance EV Root CA -================================== ------BEGIN CERTIFICATE----- -MIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBsMQswCQYDVQQG -EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSsw -KQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5jZSBFViBSb290IENBMB4XDTA2MTExMDAwMDAw -MFoXDTMxMTExMDAwMDAwMFowbDELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZ -MBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNlcnQgSGlnaCBBc3N1cmFu -Y2UgRVYgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMbM5XPm+9S75S0t -Mqbf5YE/yc0lSbZxKsPVlDRnogocsF9ppkCxxLeyj9CYpKlBWTrT3JTWPNt0OKRKzE0lgvdKpVMS -OO7zSW1xkX5jtqumX8OkhPhPYlG++MXs2ziS4wblCJEMxChBVfvLWokVfnHoNb9Ncgk9vjo4UFt3 -MRuNs8ckRZqnrG0AFFoEt7oT61EKmEFBIk5lYYeBQVCmeVyJ3hlKV9Uu5l0cUyx+mM0aBhakaHPQ -NAQTXKFx01p8VdteZOE3hzBWBOURtCmAEvF5OYiiAhF8J2a3iLd48soKqDirCmTCv2ZdlYTBoSUe -h10aUAsgEsxBu24LUTi4S8sCAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMB -Af8wHQYDVR0OBBYEFLE+w2kD+L9HAdSYJhoIAu9jZCvDMB8GA1UdIwQYMBaAFLE+w2kD+L9HAdSY -JhoIAu9jZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQAcGgaX3NecnzyIZgYIVyHbIUf4KmeqvxgydkAQ -V8GK83rZEWWONfqe/EW1ntlMMUu4kehDLI6zeM7b41N5cdblIZQB2lWHmiRk9opmzN6cN82oNLFp -myPInngiK3BD41VHMWEZ71jFhS9OMPagMRYjyOfiZRYzy78aG6A9+MpeizGLYAiJLQwGXFK3xPkK -mNEVX58Svnw2Yzi9RKR/5CYrCsSXaQ3pjOLAEFe4yHYSkVXySGnYvCoCWw9E1CAx2/S6cCZdkGCe -vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep+OkuE6N36B9K ------END CERTIFICATE----- - -SwissSign Gold CA - G2 -====================== ------BEGIN CERTIFICATE----- -MIIFujCCA6KgAwIBAgIJALtAHEP1Xk+wMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNVBAYTAkNIMRUw -EwYDVQQKEwxTd2lzc1NpZ24gQUcxHzAdBgNVBAMTFlN3aXNzU2lnbiBHb2xkIENBIC0gRzIwHhcN -MDYxMDI1MDgzMDM1WhcNMzYxMDI1MDgzMDM1WjBFMQswCQYDVQQGEwJDSDEVMBMGA1UEChMMU3dp -c3NTaWduIEFHMR8wHQYDVQQDExZTd2lzc1NpZ24gR29sZCBDQSAtIEcyMIICIjANBgkqhkiG9w0B -AQEFAAOCAg8AMIICCgKCAgEAr+TufoskDhJuqVAtFkQ7kpJcyrhdhJJCEyq8ZVeCQD5XJM1QiyUq -t2/876LQwB8CJEoTlo8jE+YoWACjR8cGp4QjK7u9lit/VcyLwVcfDmJlD909Vopz2q5+bbqBHH5C -jCA12UNNhPqE21Is8w4ndwtrvxEvcnifLtg+5hg3Wipy+dpikJKVyh+c6bM8K8vzARO/Ws/BtQpg -vd21mWRTuKCWs2/iJneRjOBiEAKfNA+k1ZIzUd6+jbqEemA8atufK+ze3gE/bk3lUIbLtK/tREDF -ylqM2tIrfKjuvqblCqoOpd8FUrdVxyJdMmqXl2MT28nbeTZ7hTpKxVKJ+STnnXepgv9VHKVxaSvR -AiTysybUa9oEVeXBCsdtMDeQKuSeFDNeFhdVxVu1yzSJkvGdJo+hB9TGsnhQ2wwMC3wLjEHXuend -jIj3o02yMszYF9rNt85mndT9Xv+9lz4pded+p2JYryU0pUHHPbwNUMoDAw8IWh+Vc3hiv69yFGkO -peUDDniOJihC8AcLYiAQZzlG+qkDzAQ4embvIIO1jEpWjpEA/I5cgt6IoMPiaG59je883WX0XaxR -7ySArqpWl2/5rX3aYT+YdzylkbYcjCbaZaIJbcHiVOO5ykxMgI93e2CaHt+28kgeDrpOVG2Y4OGi -GqJ3UM/EY5LsRxmd6+ZrzsECAwEAAaOBrDCBqTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUw -AwEB/zAdBgNVHQ4EFgQUWyV7lqRlUX64OfPAeGZe6Drn8O4wHwYDVR0jBBgwFoAUWyV7lqRlUX64 -OfPAeGZe6Drn8O4wRgYDVR0gBD8wPTA7BglghXQBWQECAQEwLjAsBggrBgEFBQcCARYgaHR0cDov -L3JlcG9zaXRvcnkuc3dpc3NzaWduLmNvbS8wDQYJKoZIhvcNAQEFBQADggIBACe645R88a7A3hfm -5djV9VSwg/S7zV4Fe0+fdWavPOhWfvxyeDgD2StiGwC5+OlgzczOUYrHUDFu4Up+GC9pWbY9ZIEr -44OE5iKHjn3g7gKZYbge9LgriBIWhMIxkziWMaa5O1M/wySTVltpkuzFwbs4AOPsF6m43Md8AYOf -Mke6UiI0HTJ6CVanfCU2qT1L2sCCbwq7EsiHSycR+R4tx5M/nttfJmtS2S6K8RTGRI0Vqbe/vd6m -Gu6uLftIdxf+u+yvGPUqUfA5hJeVbG4bwyvEdGB5JbAKJ9/fXtI5z0V9QkvfsywexcZdylU6oJxp -mo/a77KwPJ+HbBIrZXAVUjEaJM9vMSNQH4xPjyPDdEFjHFWoFN0+4FFQz/EbMFYOkrCChdiDyyJk -vC24JdVUorgG6q2SpCSgwYa1ShNqR88uC1aVVMvOmttqtKay20EIhid392qgQmwLOM7XdVAyksLf -KzAiSNDVQTglXaTpXZ/GlHXQRf0wl0OPkKsKx4ZzYEppLd6leNcG2mqeSz53OiATIgHQv2ieY2Br -NU0LbbqhPcCT4H8js1WtciVORvnSFu+wZMEBnunKoGqYDs/YYPIvSbjkQuE4NRb0yG5P94FW6Lqj -viOvrv1vA+ACOzB2+httQc8Bsem4yWb02ybzOqR08kkkW8mw0FfB+j564ZfJ ------END CERTIFICATE----- - -SecureTrust CA -============== ------BEGIN CERTIFICATE----- -MIIDuDCCAqCgAwIBAgIQDPCOXAgWpa1Cf/DrJxhZ0DANBgkqhkiG9w0BAQUFADBIMQswCQYDVQQG -EwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24xFzAVBgNVBAMTDlNlY3VyZVRy -dXN0IENBMB4XDTA2MTEwNzE5MzExOFoXDTI5MTIzMTE5NDA1NVowSDELMAkGA1UEBhMCVVMxIDAe -BgNVBAoTF1NlY3VyZVRydXN0IENvcnBvcmF0aW9uMRcwFQYDVQQDEw5TZWN1cmVUcnVzdCBDQTCC -ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKukgeWVzfX2FI7CT8rU4niVWJxB4Q2ZQCQX -OZEzZum+4YOvYlyJ0fwkW2Gz4BERQRwdbvC4u/jep4G6pkjGnx29vo6pQT64lO0pGtSO0gMdA+9t -DWccV9cGrcrI9f4Or2YlSASWC12juhbDCE/RRvgUXPLIXgGZbf2IzIaowW8xQmxSPmjL8xk037uH -GFaAJsTQ3MBv396gwpEWoGQRS0S8Hvbn+mPeZqx2pHGj7DaUaHp3pLHnDi+BeuK1cobvomuL8A/b -01k/unK8RCSc43Oz969XL0Imnal0ugBS8kvNU3xHCzaFDmapCJcWNFfBZveA4+1wVMeT4C4oFVmH -ursCAwEAAaOBnTCBmjATBgkrBgEEAYI3FAIEBh4EAEMAQTALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/ -BAUwAwEB/zAdBgNVHQ4EFgQUQjK2FvoE/f5dS3rD/fdMQB1aQ68wNAYDVR0fBC0wKzApoCegJYYj -aHR0cDovL2NybC5zZWN1cmV0cnVzdC5jb20vU1RDQS5jcmwwEAYJKwYBBAGCNxUBBAMCAQAwDQYJ -KoZIhvcNAQEFBQADggEBADDtT0rhWDpSclu1pqNlGKa7UTt36Z3q059c4EVlew3KW+JwULKUBRSu -SceNQQcSc5R+DCMh/bwQf2AQWnL1mA6s7Ll/3XpvXdMc9P+IBWlCqQVxyLesJugutIxq/3HcuLHf -mbx8IVQr5Fiiu1cprp6poxkmD5kuCLDv/WnPmRoJjeOnnyvJNjR7JLN4TJUXpAYmHrZkUjZfYGfZ -nMUFdAvnZyPSCPyI6a6Lf+Ew9Dd+/cYy2i2eRDAwbO4H3tI0/NL/QPZL9GZGBlSm8jIKYyYwa5vR -3ItHuuG51WLQoqD0ZwV4KWMabwTW+MZMo5qxN7SN5ShLHZ4swrhovO0C7jE= ------END CERTIFICATE----- - -Secure Global CA -================ ------BEGIN CERTIFICATE----- -MIIDvDCCAqSgAwIBAgIQB1YipOjUiolN9BPI8PjqpTANBgkqhkiG9w0BAQUFADBKMQswCQYDVQQG -EwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24xGTAXBgNVBAMTEFNlY3VyZSBH -bG9iYWwgQ0EwHhcNMDYxMTA3MTk0MjI4WhcNMjkxMjMxMTk1MjA2WjBKMQswCQYDVQQGEwJVUzEg -MB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24xGTAXBgNVBAMTEFNlY3VyZSBHbG9iYWwg -Q0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvNS7YrGxVaQZx5RNoJLNP2MwhR/jx -YDiJiQPpvepeRlMJ3Fz1Wuj3RSoC6zFh1ykzTM7HfAo3fg+6MpjhHZevj8fcyTiW89sa/FHtaMbQ -bqR8JNGuQsiWUGMu4P51/pinX0kuleM5M2SOHqRfkNJnPLLZ/kG5VacJjnIFHovdRIWCQtBJwB1g -8NEXLJXr9qXBkqPFwqcIYA1gBBCWeZ4WNOaptvolRTnIHmX5k/Wq8VLcmZg9pYYaDDUz+kulBAYV -HDGA76oYa8J719rO+TMg1fW9ajMtgQT7sFzUnKPiXB3jqUJ1XnvUd+85VLrJChgbEplJL4hL/VBi -0XPnj3pDAgMBAAGjgZ0wgZowEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0PBAQDAgGGMA8GA1Ud -EwEB/wQFMAMBAf8wHQYDVR0OBBYEFK9EBMJBfkiD2045AuzshHrmzsmkMDQGA1UdHwQtMCswKaAn -oCWGI2h0dHA6Ly9jcmwuc2VjdXJldHJ1c3QuY29tL1NHQ0EuY3JsMBAGCSsGAQQBgjcVAQQDAgEA -MA0GCSqGSIb3DQEBBQUAA4IBAQBjGghAfaReUw132HquHw0LURYD7xh8yOOvaliTFGCRsoTciE6+ -OYo68+aCiV0BN7OrJKQVDpI1WkpEXk5X+nXOH0jOZvQ8QCaSmGwb7iRGDBezUqXbpZGRzzfTb+cn -CDpOGR86p1hcF895P4vkp9MmI50mD1hp/Ed+stCNi5O/KU9DaXR2Z0vPB4zmAve14bRDtUstFJ/5 -3CYNv6ZHdAbYiNE6KTCEztI5gGIbqMdXSbxqVVFnFUq+NQfk1XWYN3kwFNspnWzFacxHVaIw98xc -f8LDmBxrThaA63p4ZUWiABqvDA1VZDRIuJK58bRQKfJPIx/abKwfROHdI3hRW8cW ------END CERTIFICATE----- - -COMODO Certification Authority -============================== ------BEGIN CERTIFICATE----- -MIIEHTCCAwWgAwIBAgIQToEtioJl4AsC7j41AkblPTANBgkqhkiG9w0BAQUFADCBgTELMAkGA1UE -BhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgG -A1UEChMRQ09NT0RPIENBIExpbWl0ZWQxJzAlBgNVBAMTHkNPTU9ETyBDZXJ0aWZpY2F0aW9uIEF1 -dGhvcml0eTAeFw0wNjEyMDEwMDAwMDBaFw0yOTEyMzEyMzU5NTlaMIGBMQswCQYDVQQGEwJHQjEb -MBkGA1UECBMSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHEwdTYWxmb3JkMRowGAYDVQQKExFD -T01PRE8gQ0EgTGltaXRlZDEnMCUGA1UEAxMeQ09NT0RPIENlcnRpZmljYXRpb24gQXV0aG9yaXR5 -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0ECLi3LjkRv3UcEbVASY06m/weaKXTuH -+7uIzg3jLz8GlvCiKVCZrts7oVewdFFxze1CkU1B/qnI2GqGd0S7WWaXUF601CxwRM/aN5VCaTww -xHGzUvAhTaHYujl8HJ6jJJ3ygxaYqhZ8Q5sVW7euNJH+1GImGEaaP+vB+fGQV+useg2L23IwambV -4EajcNxo2f8ESIl33rXp+2dtQem8Ob0y2WIC8bGoPW43nOIv4tOiJovGuFVDiOEjPqXSJDlqR6sA -1KGzqSX+DT+nHbrTUcELpNqsOO9VUCQFZUaTNE8tja3G1CEZ0o7KBWFxB3NH5YoZEr0ETc5OnKVI -rLsm9wIDAQABo4GOMIGLMB0GA1UdDgQWBBQLWOWLxkwVN6RAqTCpIb5HNlpW/zAOBgNVHQ8BAf8E -BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zBJBgNVHR8EQjBAMD6gPKA6hjhodHRwOi8vY3JsLmNvbW9k -b2NhLmNvbS9DT01PRE9DZXJ0aWZpY2F0aW9uQXV0aG9yaXR5LmNybDANBgkqhkiG9w0BAQUFAAOC -AQEAPpiem/Yb6dc5t3iuHXIYSdOH5EOC6z/JqvWote9VfCFSZfnVDeFs9D6Mk3ORLgLETgdxb8CP -OGEIqB6BCsAvIC9Bi5HcSEW88cbeunZrM8gALTFGTO3nnc+IlP8zwFboJIYmuNg4ON8qa90SzMc/ -RxdMosIGlgnW2/4/PEZB31jiVg88O8EckzXZOFKs7sjsLjBOlDW0JB9LeGna8gI4zJVSk/BwJVmc -IGfE7vmLV2H0knZ9P4SNVbfo5azV8fUZVqZa+5Acr5Pr5RzUZ5ddBA6+C4OmF4O5MBKgxTMVBbkN -+8cFduPYSo38NBejxiEovjBFMR7HeL5YYTisO+IBZQ== ------END CERTIFICATE----- - COMODO ECC Certification Authority ================================== -----BEGIN CERTIFICATE----- @@ -294,28 +63,6 @@ FAkK+qDmfQjGGoe9GKhzvSbKYAydzpmfz1wPMOG+FDHqAjAU9JM8SaczepBGR7NjfRObTrdvGDeA U/7dIOA1mjbRxwG55tzd8/8dLDoWV9mSOdY= -----END CERTIFICATE----- -Certigna -======== ------BEGIN CERTIFICATE----- -MIIDqDCCApCgAwIBAgIJAP7c4wEPyUj/MA0GCSqGSIb3DQEBBQUAMDQxCzAJBgNVBAYTAkZSMRIw -EAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hMB4XDTA3MDYyOTE1MTMwNVoXDTI3 -MDYyOTE1MTMwNVowNDELMAkGA1UEBhMCRlIxEjAQBgNVBAoMCURoaW15b3RpczERMA8GA1UEAwwI -Q2VydGlnbmEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDIaPHJ1tazNHUmgh7stL7q -XOEm7RFHYeGifBZ4QCHkYJ5ayGPhxLGWkv8YbWkj4Sti993iNi+RB7lIzw7sebYs5zRLcAglozyH -GxnygQcPOJAZ0xH+hrTy0V4eHpbNgGzOOzGTtvKg0KmVEn2lmsxryIRWijOp5yIVUxbwzBfsV1/p -ogqYCd7jX5xv3EjjhQsVWqa6n6xI4wmy9/Qy3l40vhx4XUJbzg4ij02Q130yGLMLLGq/jj8UEYkg -DncUtT2UCIf3JR7VsmAA7G8qKCVuKj4YYxclPz5EIBb2JsglrgVKtOdjLPOMFlN+XPsRGgjBRmKf -Irjxwo1p3Po6WAbfAgMBAAGjgbwwgbkwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUGu3+QTmQ -tCRZvgHyUtVF9lo53BEwZAYDVR0jBF0wW4AUGu3+QTmQtCRZvgHyUtVF9lo53BGhOKQ2MDQxCzAJ -BgNVBAYTAkZSMRIwEAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hggkA/tzjAQ/J -SP8wDgYDVR0PAQH/BAQDAgEGMBEGCWCGSAGG+EIBAQQEAwIABzANBgkqhkiG9w0BAQUFAAOCAQEA -hQMeknH2Qq/ho2Ge6/PAD/Kl1NqV5ta+aDY9fm4fTIrv0Q8hbV6lUmPOEvjvKtpv6zf+EwLHyzs+ -ImvaYS5/1HI93TDhHkxAGYwP15zRgzB7mFncfca5DClMoTOi62c6ZYTTluLtdkVwj7Ur3vkj1klu -PBS1xp81HlDQwY9qcEQCYsuuHWhBp6pX6FOqB9IG9tUUBguRA3UsbHK1YZWaDYu5Def131TN3ubY -1gkIl2PlwS6wt0QmwCbAr1UwnjvVNioZBPRcHv/PLLf/0P2HQBHVESO7SMAhqaQoLf0V+LBOK/Qw -WyH8EZE0vkHve52Xdf+XlcCWWC/qu0bXu+TZLg== ------END CERTIFICATE----- - ePKI Root Certification Authority ================================= -----BEGIN CERTIFICATE----- @@ -347,26 +94,6 @@ sP6SHhYKGvzZ8/gntsm+HbRsZJB/9OTEW9c3rkIO3aQab3yIVMUWbuF6aC74Or8NpDyJO3inTmOD BCEIZ43ygknQW/2xzQ+DhNQ+IIX3Sj0rnP0qCglN6oH4EZw= -----END CERTIFICATE----- -certSIGN ROOT CA -================ ------BEGIN CERTIFICATE----- -MIIDODCCAiCgAwIBAgIGIAYFFnACMA0GCSqGSIb3DQEBBQUAMDsxCzAJBgNVBAYTAlJPMREwDwYD -VQQKEwhjZXJ0U0lHTjEZMBcGA1UECxMQY2VydFNJR04gUk9PVCBDQTAeFw0wNjA3MDQxNzIwMDRa -Fw0zMTA3MDQxNzIwMDRaMDsxCzAJBgNVBAYTAlJPMREwDwYDVQQKEwhjZXJ0U0lHTjEZMBcGA1UE -CxMQY2VydFNJR04gUk9PVCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALczuX7I -JUqOtdu0KBuqV5Do0SLTZLrTk+jUrIZhQGpgV2hUhE28alQCBf/fm5oqrl0Hj0rDKH/v+yv6efHH -rfAQUySQi2bJqIirr1qjAOm+ukbuW3N7LBeCgV5iLKECZbO9xSsAfsT8AzNXDe3i+s5dRdY4zTW2 -ssHQnIFKquSyAVwdj1+ZxLGt24gh65AIgoDzMKND5pCCrlUoSe1b16kQOA7+j0xbm0bqQfWwCHTD -0IgztnzXdN/chNFDDnU5oSVAKOp4yw4sLjmdjItuFhwvJoIQ4uNllAoEwF73XVv4EOLQunpL+943 -AAAaWyjj0pxzPjKHmKHJUS/X3qwzs08CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8B -Af8EBAMCAcYwHQYDVR0OBBYEFOCMm9slSbPxfIbWskKHC9BroNnkMA0GCSqGSIb3DQEBBQUAA4IB -AQA+0hyJLjX8+HXd5n9liPRyTMks1zJO890ZeUe9jjtbkw9QSSQTaxQGcu8J06Gh40CEyecYMnQ8 -SG4Pn0vU9x7Tk4ZkVJdjclDVVc/6IJMCopvDI5NOFlV2oHB5bc0hH88vLbwZ44gx+FkagQnIl6Z0 -x2DEW8xXjrJ1/RsCCdtZb3KTafcxQdaIOL+Hsr0Wefmq5L6IJd1hJyMctTEHBDa0GpC9oHRxUIlt -vBTjD4au8as+x6AJzKNI0eDbZOeStc+vckNwi/nDhDwTqn6Sm1dTk/pwwpEOMfmbZ13pljheX7Nz -TogVZ96edhBiIL5VaZVDADlN9u6wWk5JRFRYX0KD ------END CERTIFICATE----- - NetLock Arany (Class Gold) Főtanúsítvány ======================================== -----BEGIN CERTIFICATE----- @@ -536,90 +263,6 @@ iEDPfUYd/x7H4c7/I9vG+o1VTqkC50cRRj70/b17KSa7qWFiNyi2LSr2EIZkyXCn0q23KXB56jza YyWf/Wi3MOxw+3WKt21gZ7IeyLnp2KhvAotnDU0mV3HaIPzBSlCNsSi6 -----END CERTIFICATE----- -AffirmTrust Commercial -====================== ------BEGIN CERTIFICATE----- -MIIDTDCCAjSgAwIBAgIId3cGJyapsXwwDQYJKoZIhvcNAQELBQAwRDELMAkGA1UEBhMCVVMxFDAS -BgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVzdCBDb21tZXJjaWFsMB4XDTEw -MDEyOTE0MDYwNloXDTMwMTIzMTE0MDYwNlowRDELMAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmly -bVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVzdCBDb21tZXJjaWFsMIIBIjANBgkqhkiG9w0BAQEF -AAOCAQ8AMIIBCgKCAQEA9htPZwcroRX1BiLLHwGy43NFBkRJLLtJJRTWzsO3qyxPxkEylFf6Eqdb -DuKPHx6GGaeqtS25Xw2Kwq+FNXkyLbscYjfysVtKPcrNcV/pQr6U6Mje+SJIZMblq8Yrba0F8PrV -C8+a5fBQpIs7R6UjW3p6+DM/uO+Zl+MgwdYoic+U+7lF7eNAFxHUdPALMeIrJmqbTFeurCA+ukV6 -BfO9m2kVrn1OIGPENXY6BwLJN/3HR+7o8XYdcxXyl6S1yHp52UKqK39c/s4mT6NmgTWvRLpUHhww -MmWd5jyTXlBOeuM61G7MGvv50jeuJCqrVwMiKA1JdX+3KNp1v47j3A55MQIDAQABo0IwQDAdBgNV -HQ4EFgQUnZPGU4teyq8/nx4P5ZmVvCT2lI8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC -AQYwDQYJKoZIhvcNAQELBQADggEBAFis9AQOzcAN/wr91LoWXym9e2iZWEnStB03TX8nfUYGXUPG -hi4+c7ImfU+TqbbEKpqrIZcUsd6M06uJFdhrJNTxFq7YpFzUf1GO7RgBsZNjvbz4YYCanrHOQnDi -qX0GJX0nof5v7LMeJNrjS1UaADs1tDvZ110w/YETifLCBivtZ8SOyUOyXGsViQK8YvxO8rUzqrJv -0wqiUOP2O+guRMLbZjipM1ZI8W0bM40NjD9gN53Tym1+NH4Nn3J2ixufcv1SNUFFApYvHLKac0kh -sUlHRUe072o0EclNmsxZt9YCnlpOZbWUrhvfKbAW8b8Angc6F2S1BLUjIZkKlTuXfO8= ------END CERTIFICATE----- - -AffirmTrust Networking -====================== ------BEGIN CERTIFICATE----- -MIIDTDCCAjSgAwIBAgIIfE8EORzUmS0wDQYJKoZIhvcNAQEFBQAwRDELMAkGA1UEBhMCVVMxFDAS -BgNVBAoMC0FmZmlybVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVzdCBOZXR3b3JraW5nMB4XDTEw -MDEyOTE0MDgyNFoXDTMwMTIzMTE0MDgyNFowRDELMAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmly -bVRydXN0MR8wHQYDVQQDDBZBZmZpcm1UcnVzdCBOZXR3b3JraW5nMIIBIjANBgkqhkiG9w0BAQEF -AAOCAQ8AMIIBCgKCAQEAtITMMxcua5Rsa2FSoOujz3mUTOWUgJnLVWREZY9nZOIG41w3SfYvm4SE -Hi3yYJ0wTsyEheIszx6e/jarM3c1RNg1lho9Nuh6DtjVR6FqaYvZ/Ls6rnla1fTWcbuakCNrmreI -dIcMHl+5ni36q1Mr3Lt2PpNMCAiMHqIjHNRqrSK6mQEubWXLviRmVSRLQESxG9fhwoXA3hA/Pe24 -/PHxI1Pcv2WXb9n5QHGNfb2V1M6+oF4nI979ptAmDgAp6zxG8D1gvz9Q0twmQVGeFDdCBKNwV6gb -h+0t+nvujArjqWaJGctB+d1ENmHP4ndGyH329JKBNv3bNPFyfvMMFr20FQIDAQABo0IwQDAdBgNV -HQ4EFgQUBx/S55zawm6iQLSwelAQUHTEyL0wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC -AQYwDQYJKoZIhvcNAQEFBQADggEBAIlXshZ6qML91tmbmzTCnLQyFE2npN/svqe++EPbkTfOtDIu -UFUaNU52Q3Eg75N3ThVwLofDwR1t3Mu1J9QsVtFSUzpE0nPIxBsFZVpikpzuQY0x2+c06lkh1QF6 -12S4ZDnNye2v7UsDSKegmQGA3GWjNq5lWUhPgkvIZfFXHeVZLgo/bNjR9eUJtGxUAArgFU2HdW23 -WJZa3W3SAKD0m0i+wzekujbgfIeFlxoVot4uolu9rxj5kFDNcFn4J2dHy8egBzp90SxdbBk6ZrV9 -/ZFvgrG+CJPbFEfxojfHRZ48x3evZKiT3/Zpg4Jg8klCNO1aAFSFHBY2kgxc+qatv9s= ------END CERTIFICATE----- - -AffirmTrust Premium -=================== ------BEGIN CERTIFICATE----- -MIIFRjCCAy6gAwIBAgIIbYwURrGmCu4wDQYJKoZIhvcNAQEMBQAwQTELMAkGA1UEBhMCVVMxFDAS -BgNVBAoMC0FmZmlybVRydXN0MRwwGgYDVQQDDBNBZmZpcm1UcnVzdCBQcmVtaXVtMB4XDTEwMDEy -OTE0MTAzNloXDTQwMTIzMTE0MTAzNlowQTELMAkGA1UEBhMCVVMxFDASBgNVBAoMC0FmZmlybVRy -dXN0MRwwGgYDVQQDDBNBZmZpcm1UcnVzdCBQcmVtaXVtMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A -MIICCgKCAgEAxBLfqV/+Qd3d9Z+K4/as4Tx4mrzY8H96oDMq3I0gW64tb+eT2TZwamjPjlGjhVtn -BKAQJG9dKILBl1fYSCkTtuG+kU3fhQxTGJoeJKJPj/CihQvL9Cl/0qRY7iZNyaqoe5rZ+jjeRFcV -5fiMyNlI4g0WJx0eyIOFJbe6qlVBzAMiSy2RjYvmia9mx+n/K+k8rNrSs8PhaJyJ+HoAVt70VZVs -+7pk3WKL3wt3MutizCaam7uqYoNMtAZ6MMgpv+0GTZe5HMQxK9VfvFMSF5yZVylmd2EhMQcuJUmd -GPLu8ytxjLW6OQdJd/zvLpKQBY0tL3d770O/Nbua2Plzpyzy0FfuKE4mX4+QaAkvuPjcBukumj5R -p9EixAqnOEhss/n/fauGV+O61oV4d7pD6kh/9ti+I20ev9E2bFhc8e6kGVQa9QPSdubhjL08s9NI -S+LI+H+SqHZGnEJlPqQewQcDWkYtuJfzt9WyVSHvutxMAJf7FJUnM7/oQ0dG0giZFmA7mn7S5u04 -6uwBHjxIVkkJx0w3AJ6IDsBz4W9m6XJHMD4Q5QsDyZpCAGzFlH5hxIrff4IaC1nEWTJ3s7xgaVY5 -/bQGeyzWZDbZvUjthB9+pSKPKrhC9IK31FOQeE4tGv2Bb0TXOwF0lkLgAOIua+rF7nKsu7/+6qqo -+Nz2snmKtmcCAwEAAaNCMEAwHQYDVR0OBBYEFJ3AZ6YMItkm9UWrpmVSESfYRaxjMA8GA1UdEwEB -/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBDAUAA4ICAQCzV00QYk465KzquByv -MiPIs0laUZx2KI15qldGF9X1Uva3ROgIRL8YhNILgM3FEv0AVQVhh0HctSSePMTYyPtwni94loMg -Nt58D2kTiKV1NpgIpsbfrM7jWNa3Pt668+s0QNiigfV4Py/VpfzZotReBA4Xrf5B8OWycvpEgjNC -6C1Y91aMYj+6QrCcDFx+LmUmXFNPALJ4fqENmS2NuB2OosSw/WDQMKSOyARiqcTtNd56l+0OOF6S -L5Nwpamcb6d9Ex1+xghIsV5n61EIJenmJWtSKZGc0jlzCFfemQa0W50QBuHCAKi4HEoCChTQwUHK -+4w1IX2COPKpVJEZNZOUbWo6xbLQu4mGk+ibyQ86p3q4ofB4Rvr8Ny/lioTz3/4E2aFooC8k4gmV -BtWVyuEklut89pMFu+1z6S3RdTnX5yTb2E5fQ4+e0BQ5v1VwSJlXMbSc7kqYA5YwH2AG7hsj/oFg -IxpHYoWlzBk0gG+zrBrjn/B7SK3VAdlntqlyk+otZrWyuOQ9PLLvTIzq6we/qzWaVYa8GKa1qF60 -g2xraUDTn9zxw2lrueFtCfTxqlB2Cnp9ehehVZZCmTEJ3WARjQUwfuaORtGdFNrHF+QFlozEJLUb -zxQHskD4o55BhrwE0GuWyCqANP2/7waj3VjFhT0+j/6eKeC2uAloGRwYQw== ------END CERTIFICATE----- - -AffirmTrust Premium ECC -======================= ------BEGIN CERTIFICATE----- -MIIB/jCCAYWgAwIBAgIIdJclisc/elQwCgYIKoZIzj0EAwMwRTELMAkGA1UEBhMCVVMxFDASBgNV -BAoMC0FmZmlybVRydXN0MSAwHgYDVQQDDBdBZmZpcm1UcnVzdCBQcmVtaXVtIEVDQzAeFw0xMDAx -MjkxNDIwMjRaFw00MDEyMzExNDIwMjRaMEUxCzAJBgNVBAYTAlVTMRQwEgYDVQQKDAtBZmZpcm1U -cnVzdDEgMB4GA1UEAwwXQWZmaXJtVHJ1c3QgUHJlbWl1bSBFQ0MwdjAQBgcqhkjOPQIBBgUrgQQA -IgNiAAQNMF4bFZ0D0KF5Nbc6PJJ6yhUczWLznCZcBz3lVPqj1swS6vQUX+iOGasvLkjmrBhDeKzQ -N8O9ss0s5kfiGuZjuD0uL3jET9v0D6RoTFVya5UdThhClXjMNzyR4ptlKymjQjBAMB0GA1UdDgQW -BBSaryl6wBE1NSZRMADDav5A1a7WPDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAK -BggqhkjOPQQDAwNnADBkAjAXCfOHiFBar8jAQr9HX/VsaobgxCd05DhT1wV/GzTjxi+zygk8N53X -57hG8f2h4nECMEJZh0PUUd+60wkyWs6Iflc9nF9Ca/UHLbXwgpP5WW+uZPpY5Yse42O+tYHNbwKM -eQ== ------END CERTIFICATE----- - Certum Trusted Network CA ========================= -----BEGIN CERTIFICATE----- @@ -946,35 +589,6 @@ EHiGG3njxPPyBJUgriOCxLM6AGK/5jYk4Ve6xx6QddVfP5VhK8E7zeWzaGHQRiapIVJpLesux+t3 zqY6tQMzT3bR51xUAV3LePTJDL/PEo4XLSNolOer/qmyKwbQBM0= -----END CERTIFICATE----- -TeliaSonera Root CA v1 -====================== ------BEGIN CERTIFICATE----- -MIIFODCCAyCgAwIBAgIRAJW+FqD3LkbxezmCcvqLzZYwDQYJKoZIhvcNAQEFBQAwNzEUMBIGA1UE -CgwLVGVsaWFTb25lcmExHzAdBgNVBAMMFlRlbGlhU29uZXJhIFJvb3QgQ0EgdjEwHhcNMDcxMDE4 -MTIwMDUwWhcNMzIxMDE4MTIwMDUwWjA3MRQwEgYDVQQKDAtUZWxpYVNvbmVyYTEfMB0GA1UEAwwW -VGVsaWFTb25lcmEgUm9vdCBDQSB2MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMK+ -6yfwIaPzaSZVfp3FVRaRXP3vIb9TgHot0pGMYzHw7CTww6XScnwQbfQ3t+XmfHnqjLWCi65ItqwA -3GV17CpNX8GH9SBlK4GoRz6JI5UwFpB/6FcHSOcZrr9FZ7E3GwYq/t75rH2D+1665I+XZ75Ljo1k -B1c4VWk0Nj0TSO9P4tNmHqTPGrdeNjPUtAa9GAH9d4RQAEX1jF3oI7x+/jXh7VB7qTCNGdMJjmhn -Xb88lxhTuylixcpecsHHltTbLaC0H2kD7OriUPEMPPCs81Mt8Bz17Ww5OXOAFshSsCPN4D7c3TxH -oLs1iuKYaIu+5b9y7tL6pe0S7fyYGKkmdtwoSxAgHNN/Fnct7W+A90m7UwW7XWjH1Mh1Fj+JWov3 -F0fUTPHSiXk+TT2YqGHeOh7S+F4D4MHJHIzTjU3TlTazN19jY5szFPAtJmtTfImMMsJu7D0hADnJ -oWjiUIMusDor8zagrC/kb2HCUQk5PotTubtn2txTuXZZNp1D5SDgPTJghSJRt8czu90VL6R4pgd7 -gUY2BIbdeTXHlSw7sKMXNeVzH7RcWe/a6hBle3rQf5+ztCo3O3CLm1u5K7fsslESl1MpWtTwEhDc -TwK7EpIvYtQ/aUN8Ddb8WHUBiJ1YFkveupD/RwGJBmr2X7KQarMCpgKIv7NHfirZ1fpoeDVNAgMB -AAGjPzA9MA8GA1UdEwEB/wQFMAMBAf8wCwYDVR0PBAQDAgEGMB0GA1UdDgQWBBTwj1k4ALP1j5qW -DNXr+nuqF+gTEjANBgkqhkiG9w0BAQUFAAOCAgEAvuRcYk4k9AwI//DTDGjkk0kiP0Qnb7tt3oNm -zqjMDfz1mgbldxSR651Be5kqhOX//CHBXfDkH1e3damhXwIm/9fH907eT/j3HEbAek9ALCI18Bmx -0GtnLLCo4MBANzX2hFxc469CeP6nyQ1Q6g2EdvZR74NTxnr/DlZJLo961gzmJ1TjTQpgcmLNkQfW -pb/ImWvtxBnmq0wROMVvMeJuScg/doAmAyYp4Db29iBT4xdwNBedY2gea+zDTYa4EzAvXUYNR0PV -G6pZDrlcjQZIrXSHX8f8MVRBE+LHIQ6e4B4N4cB7Q4WQxYpYxmUKeFfyxiMPAdkgS94P+5KFdSpc -c41teyWRyu5FrgZLAMzTsVlQ2jqIOylDRl6XK1TOU2+NSueW+r9xDkKLfP0ooNBIytrEgUy7onOT -JsjrDNYmiLbAJM+7vVvrdX3pCI6GMyx5dwlppYn8s3CQh3aP0yK7Qs69cwsgJirQmz1wHiRszYd2 -qReWt88NkvuOGKmYSdGe/mBEciG5Ge3C9THxOUiIkCR1VBatzvT4aRRkOfujuLpwQMcnHL/EVlP6 -Y2XQ8xwOFvVrhlhNGNTkDY6lnVuR3HYkUD/GKvvZt5y11ubQ2egZixVxSK236thZiNSQvxaz2ems -WWFUyBy6ysHK4bkgTI86k4mloMy/0/Z1pHWWbVY= ------END CERTIFICATE----- - T-TeleSec GlobalRoot Class 2 ============================ -----BEGIN CERTIFICATE----- @@ -1371,50 +985,6 @@ ZHTetBXZ9FRUGi8c15dxVJCO2SCdUyt/q4/i6jC8UDfv8Ue1fXwsBOxonbRJRBD0ckscZOf85muQ 3Wl9af0AVqW3rLatt8o+Ae+c -----END CERTIFICATE----- -Entrust Root Certification Authority - G2 -========================================= ------BEGIN CERTIFICATE----- -MIIEPjCCAyagAwIBAgIESlOMKDANBgkqhkiG9w0BAQsFADCBvjELMAkGA1UEBhMCVVMxFjAUBgNV -BAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50cnVzdC5uZXQvbGVnYWwtdGVy -bXMxOTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3QsIEluYy4gLSBmb3IgYXV0aG9yaXplZCB1c2Ug -b25seTEyMDAGA1UEAxMpRW50cnVzdCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRzIw -HhcNMDkwNzA3MTcyNTU0WhcNMzAxMjA3MTc1NTU0WjCBvjELMAkGA1UEBhMCVVMxFjAUBgNVBAoT -DUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50cnVzdC5uZXQvbGVnYWwtdGVybXMx -OTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3QsIEluYy4gLSBmb3IgYXV0aG9yaXplZCB1c2Ugb25s -eTEyMDAGA1UEAxMpRW50cnVzdCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRzIwggEi -MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC6hLZy254Ma+KZ6TABp3bqMriVQRrJ2mFOWHLP -/vaCeb9zYQYKpSfYs1/TRU4cctZOMvJyig/3gxnQaoCAAEUesMfnmr8SVycco2gvCoe9amsOXmXz -HHfV1IWNcCG0szLni6LVhjkCsbjSR87kyUnEO6fe+1R9V77w6G7CebI6C1XiUJgWMhNcL3hWwcKU -s/Ja5CeanyTXxuzQmyWC48zCxEXFjJd6BmsqEZ+pCm5IO2/b1BEZQvePB7/1U1+cPvQXLOZprE4y -TGJ36rfo5bs0vBmLrpxR57d+tVOxMyLlbc9wPBr64ptntoP0jaWvYkxN4FisZDQSA/i2jZRjJKRx -AgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqciZ6 -0B7vfec7aVHUbI2fkBJmqzANBgkqhkiG9w0BAQsFAAOCAQEAeZ8dlsa2eT8ijYfThwMEYGprmi5Z -iXMRrEPR9RP/jTkrwPK9T3CMqS/qF8QLVJ7UG5aYMzyorWKiAHarWWluBh1+xLlEjZivEtRh2woZ -Rkfz6/djwUAFQKXSt/S1mja/qYh2iARVBCuch38aNzx+LaUa2NSJXsq9rD1s2G2v1fN2D807iDgi -nWyTmsQ9v4IbZT+mD12q/OWyFcq1rca8PdCE6OoGcrBNOTJ4vz4RnAuknZoh8/CbCzB428Hch0P+ -vGOaysXCHMnHjf87ElgI5rY97HosTvuDls4MPGmHVHOkc8KT/1EQrBVUAdj8BbGJoX90g5pJ19xO -e4pIb4tF9g== ------END CERTIFICATE----- - -Entrust Root Certification Authority - EC1 -========================================== ------BEGIN CERTIFICATE----- -MIIC+TCCAoCgAwIBAgINAKaLeSkAAAAAUNCR+TAKBggqhkjOPQQDAzCBvzELMAkGA1UEBhMCVVMx -FjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50cnVzdC5uZXQvbGVn -YWwtdGVybXMxOTA3BgNVBAsTMChjKSAyMDEyIEVudHJ1c3QsIEluYy4gLSBmb3IgYXV0aG9yaXpl -ZCB1c2Ugb25seTEzMDEGA1UEAxMqRW50cnVzdCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5 -IC0gRUMxMB4XDTEyMTIxODE1MjUzNloXDTM3MTIxODE1NTUzNlowgb8xCzAJBgNVBAYTAlVTMRYw -FAYDVQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQLEx9TZWUgd3d3LmVudHJ1c3QubmV0L2xlZ2Fs -LXRlcm1zMTkwNwYDVQQLEzAoYykgMjAxMiBFbnRydXN0LCBJbmMuIC0gZm9yIGF1dGhvcml6ZWQg -dXNlIG9ubHkxMzAxBgNVBAMTKkVudHJ1c3QgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAt -IEVDMTB2MBAGByqGSM49AgEGBSuBBAAiA2IABIQTydC6bUF74mzQ61VfZgIaJPRbiWlH47jCffHy -AsWfoPZb1YsGGYZPUxBtByQnoaD41UcZYUx9ypMn6nQM72+WCf5j7HBdNq1nd67JnXxVRDqiY1Ef -9eNi1KlHBz7MIKNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYE -FLdj5xrdjekIplWDpOBqUEFlEUJJMAoGCCqGSM49BAMDA2cAMGQCMGF52OVCR98crlOZF7ZvHH3h -vxGU0QOIdeSNiaSKd0bebWHvAvX7td/M/k7//qnmpwIwW5nXhTcGtXsI/esni0qU+eH6p44mCOh8 -kmhtc9hvJqwhAriZtyZBWyVgrtBIGu4G ------END CERTIFICATE----- - CFCA EV ROOT ============ -----BEGIN CERTIFICATE----- @@ -2197,71 +1767,6 @@ NMn5X7azKFGnpyuqSfqNZSlO42sTp5SjLVFteAxEy9/eCG/Oo2Sr05WE1LlSVHJ7liXMvGnjSG4N 0MedJ5qq+BOS3R7fY581qRY27Iy4g/Q9iY/NtBde17MXQRBdJ3NghVdJIgc= -----END CERTIFICATE----- -Trustwave Global Certification Authority -======================================== ------BEGIN CERTIFICATE----- -MIIF2jCCA8KgAwIBAgIMBfcOhtpJ80Y1LrqyMA0GCSqGSIb3DQEBCwUAMIGIMQswCQYDVQQGEwJV -UzERMA8GA1UECAwISWxsaW5vaXMxEDAOBgNVBAcMB0NoaWNhZ28xITAfBgNVBAoMGFRydXN0d2F2 -ZSBIb2xkaW5ncywgSW5jLjExMC8GA1UEAwwoVHJ1c3R3YXZlIEdsb2JhbCBDZXJ0aWZpY2F0aW9u -IEF1dGhvcml0eTAeFw0xNzA4MjMxOTM0MTJaFw00MjA4MjMxOTM0MTJaMIGIMQswCQYDVQQGEwJV -UzERMA8GA1UECAwISWxsaW5vaXMxEDAOBgNVBAcMB0NoaWNhZ28xITAfBgNVBAoMGFRydXN0d2F2 -ZSBIb2xkaW5ncywgSW5jLjExMC8GA1UEAwwoVHJ1c3R3YXZlIEdsb2JhbCBDZXJ0aWZpY2F0aW9u -IEF1dGhvcml0eTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALldUShLPDeS0YLOvR29 -zd24q88KPuFd5dyqCblXAj7mY2Hf8g+CY66j96xz0XznswuvCAAJWX/NKSqIk4cXGIDtiLK0thAf -LdZfVaITXdHG6wZWiYj+rDKd/VzDBcdu7oaJuogDnXIhhpCujwOl3J+IKMujkkkP7NAP4m1ET4Bq -stTnoApTAbqOl5F2brz81Ws25kCI1nsvXwXoLG0R8+eyvpJETNKXpP7ScoFDB5zpET71ixpZfR9o -WN0EACyW80OzfpgZdNmcc9kYvkHHNHnZ9GLCQ7mzJ7Aiy/k9UscwR7PJPrhq4ufogXBeQotPJqX+ -OsIgbrv4Fo7NDKm0G2x2EOFYeUY+VM6AqFcJNykbmROPDMjWLBz7BegIlT1lRtzuzWniTY+HKE40 -Cz7PFNm73bZQmq131BnW2hqIyE4bJ3XYsgjxroMwuREOzYfwhI0Vcnyh78zyiGG69Gm7DIwLdVcE -uE4qFC49DxweMqZiNu5m4iK4BUBjECLzMx10coos9TkpoNPnG4CELcU9402x/RpvumUHO1jsQkUm -+9jaJXLE9gCxInm943xZYkqcBW89zubWR2OZxiRvchLIrH+QtAuRcOi35hYQcRfO3gZPSEF9NUqj -ifLJS3tBEW1ntwiYTOURGa5CgNz7kAXU+FDKvuStx8KU1xad5hePrzb7AgMBAAGjQjBAMA8GA1Ud -EwEB/wQFMAMBAf8wHQYDVR0OBBYEFJngGWcNYtt2s9o9uFvo/ULSMQ6HMA4GA1UdDwEB/wQEAwIB -BjANBgkqhkiG9w0BAQsFAAOCAgEAmHNw4rDT7TnsTGDZqRKGFx6W0OhUKDtkLSGm+J1WE2pIPU/H -PinbbViDVD2HfSMF1OQc3Og4ZYbFdada2zUFvXfeuyk3QAUHw5RSn8pk3fEbK9xGChACMf1KaA0H -ZJDmHvUqoai7PF35owgLEQzxPy0QlG/+4jSHg9bP5Rs1bdID4bANqKCqRieCNqcVtgimQlRXtpla -4gt5kNdXElE1GYhBaCXUNxeEFfsBctyV3lImIJgm4nb1J2/6ADtKYdkNy1GTKv0WBpanI5ojSP5R -vbbEsLFUzt5sQa0WZ37b/TjNuThOssFgy50X31ieemKyJo90lZvkWx3SD92YHJtZuSPTMaCm/zjd -zyBP6VhWOmfD0faZmZ26NraAL4hHT4a/RDqA5Dccprrql5gR0IRiR2Qequ5AvzSxnI9O4fKSTx+O -856X3vOmeWqJcU9LJxdI/uz0UA9PSX3MReO9ekDFQdxhVicGaeVyQYHTtgGJoC86cnn+OjC/QezH -Yj6RS8fZMXZC+fc8Y+wmjHMMfRod6qh8h6jCJ3zhM0EPz8/8AKAigJ5Kp28AsEFFtyLKaEjFQqKu -3R3y4G5OBVixwJAWKqQ9EEC+j2Jjg6mcgn0tAumDMHzLJ8n9HmYAsC7TIS+OMxZsmO0QqAfWzJPP -29FpHOTKyeC2nOnOcXHebD8WpHk= ------END CERTIFICATE----- - -Trustwave Global ECC P256 Certification Authority -================================================= ------BEGIN CERTIFICATE----- -MIICYDCCAgegAwIBAgIMDWpfCD8oXD5Rld9dMAoGCCqGSM49BAMCMIGRMQswCQYDVQQGEwJVUzER -MA8GA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAfBgNVBAoTGFRydXN0d2F2ZSBI -b2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3YXZlIEdsb2JhbCBFQ0MgUDI1NiBDZXJ0aWZp -Y2F0aW9uIEF1dGhvcml0eTAeFw0xNzA4MjMxOTM1MTBaFw00MjA4MjMxOTM1MTBaMIGRMQswCQYD -VQQGEwJVUzERMA8GA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAfBgNVBAoTGFRy -dXN0d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3YXZlIEdsb2JhbCBFQ0MgUDI1 -NiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABH77bOYj -43MyCMpg5lOcunSNGLB4kFKA3TjASh3RqMyTpJcGOMoNFWLGjgEqZZ2q3zSRLoHB5DOSMcT9CTqm -P62jQzBBMA8GA1UdEwEB/wQFMAMBAf8wDwYDVR0PAQH/BAUDAwcGADAdBgNVHQ4EFgQUo0EGrJBt -0UrrdaVKEJmzsaGLSvcwCgYIKoZIzj0EAwIDRwAwRAIgB+ZU2g6gWrKuEZ+Hxbb/ad4lvvigtwjz -RM4q3wghDDcCIC0mA6AFvWvR9lz4ZcyGbbOcNEhjhAnFjXca4syc4XR7 ------END CERTIFICATE----- - -Trustwave Global ECC P384 Certification Authority -================================================= ------BEGIN CERTIFICATE----- -MIICnTCCAiSgAwIBAgIMCL2Fl2yZJ6SAaEc7MAoGCCqGSM49BAMDMIGRMQswCQYDVQQGEwJVUzER -MA8GA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAfBgNVBAoTGFRydXN0d2F2ZSBI -b2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3YXZlIEdsb2JhbCBFQ0MgUDM4NCBDZXJ0aWZp -Y2F0aW9uIEF1dGhvcml0eTAeFw0xNzA4MjMxOTM2NDNaFw00MjA4MjMxOTM2NDNaMIGRMQswCQYD -VQQGEwJVUzERMA8GA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAfBgNVBAoTGFRy -dXN0d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3YXZlIEdsb2JhbCBFQ0MgUDM4 -NCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTB2MBAGByqGSM49AgEGBSuBBAAiA2IABGvaDXU1CDFH -Ba5FmVXxERMuSvgQMSOjfoPTfygIOiYaOs+Xgh+AtycJj9GOMMQKmw6sWASr9zZ9lCOkmwqKi6vr -/TklZvFe/oyujUF5nQlgziip04pt89ZF1PKYhDhloKNDMEEwDwYDVR0TAQH/BAUwAwEB/zAPBgNV -HQ8BAf8EBQMDBwYAMB0GA1UdDgQWBBRVqYSJ0sEyvRjLbKYHTsjnnb6CkDAKBggqhkjOPQQDAwNn -ADBkAjA3AZKXRRJ+oPM+rRk6ct30UJMDEr5E0k9BpIycnR+j9sKS50gU/k6bpZFXrsY3crsCMGcl -CrEMXu6pY5Jv5ZAL/mYiykf9ijH3g/56vxC+GCsej/YpHpRZ744hN8tRmKVuSw== ------END CERTIFICATE----- - NAVER Global Root Certification Authority ========================================= -----BEGIN CERTIFICATE----- @@ -2354,36 +1859,6 @@ vLtoURMMA/cVi4RguYv/Uo7njLwcAjA8+RHUjE7AwWHCFUyqqx0LMV87HOIAl0Qx5v5zli/altP+ CAezNIm8BZ/3Hobui3A= -----END CERTIFICATE----- -GLOBALTRUST 2020 -================ ------BEGIN CERTIFICATE----- -MIIFgjCCA2qgAwIBAgILWku9WvtPilv6ZeUwDQYJKoZIhvcNAQELBQAwTTELMAkGA1UEBhMCQVQx -IzAhBgNVBAoTGmUtY29tbWVyY2UgbW9uaXRvcmluZyBHbWJIMRkwFwYDVQQDExBHTE9CQUxUUlVT -VCAyMDIwMB4XDTIwMDIxMDAwMDAwMFoXDTQwMDYxMDAwMDAwMFowTTELMAkGA1UEBhMCQVQxIzAh -BgNVBAoTGmUtY29tbWVyY2UgbW9uaXRvcmluZyBHbWJIMRkwFwYDVQQDExBHTE9CQUxUUlVTVCAy -MDIwMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAri5WrRsc7/aVj6B3GyvTY4+ETUWi -D59bRatZe1E0+eyLinjF3WuvvcTfk0Uev5E4C64OFudBc/jbu9G4UeDLgztzOG53ig9ZYybNpyrO -VPu44sB8R85gfD+yc/LAGbaKkoc1DZAoouQVBGM+uq/ufF7MpotQsjj3QWPKzv9pj2gOlTblzLmM -CcpL3TGQlsjMH/1WljTbjhzqLL6FLmPdqqmV0/0plRPwyJiT2S0WR5ARg6I6IqIoV6Lr/sCMKKCm -fecqQjuCgGOlYx8ZzHyyZqjC0203b+J+BlHZRYQfEs4kUmSFC0iAToexIiIwquuuvuAC4EDosEKA -A1GqtH6qRNdDYfOiaxaJSaSjpCuKAsR49GiKweR6NrFvG5Ybd0mN1MkGco/PU+PcF4UgStyYJ9OR -JitHHmkHr96i5OTUawuzXnzUJIBHKWk7buis/UDr2O1xcSvy6Fgd60GXIsUf1DnQJ4+H4xj04KlG -DfV0OoIu0G4skaMxXDtG6nsEEFZegB31pWXogvziB4xiRfUg3kZwhqG8k9MedKZssCz3AwyIDMvU -clOGvGBG85hqwvG/Q/lwIHfKN0F5VVJjjVsSn8VoxIidrPIwq7ejMZdnrY8XD2zHc+0klGvIg5rQ -mjdJBKuxFshsSUktq6HQjJLyQUp5ISXbY9e2nKd+Qmn7OmMCAwEAAaNjMGEwDwYDVR0TAQH/BAUw -AwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFNwuH9FhN3nkq9XVsxJxaD1qaJwiMB8GA1Ud -IwQYMBaAFNwuH9FhN3nkq9XVsxJxaD1qaJwiMA0GCSqGSIb3DQEBCwUAA4ICAQCR8EICaEDuw2jA -VC/f7GLDw56KoDEoqoOOpFaWEhCGVrqXctJUMHytGdUdaG/7FELYjQ7ztdGl4wJCXtzoRlgHNQIw -4Lx0SsFDKv/bGtCwr2zD/cuz9X9tAy5ZVp0tLTWMstZDFyySCstd6IwPS3BD0IL/qMy/pJTAvoe9 -iuOTe8aPmxadJ2W8esVCgmxcB9CpwYhgROmYhRZf+I/KARDOJcP5YBugxZfD0yyIMaK9MOzQ0MAS -8cE54+X1+NZK3TTN+2/BT+MAi1bikvcoskJ3ciNnxz8RFbLEAwW+uxF7Cr+obuf/WEPPm2eggAe2 -HcqtbepBEX4tdJP7wry+UUTF72glJ4DjyKDUEuzZpTcdN3y0kcra1LGWge9oXHYQSa9+pTeAsRxS -vTOBTI/53WXZFM2KJVj04sWDpQmQ1GwUY7VA3+vA/MRYfg0UFodUJ25W5HCEuGwyEn6CMUO+1918 -oa2u1qsgEu8KwxCMSZY13At1XrFP1U80DhEgB3VDRemjEdqso5nCtnkn4rnvyOL2NSl6dPrFf4IF -YqYK6miyeUcGbvJXqBUzxvd4Sj1Ce2t+/vdG6tHrju+IaFvowdlxfv1k7/9nR4hYJS8+hge9+6jl -gqispdNpQ80xiEmEU5LAsTkbOYMBMMTyqfrQA71yN2BWHzZ8vTmR9W0Nv3vXkg== ------END CERTIFICATE----- - ANF Secure Server Root CA ========================= -----BEGIN CERTIFICATE----- @@ -2708,36 +2183,6 @@ FL//2wmUspO8IFgV6dtxQ/PeEMMA3KgqlbbC1j+Qa3bbbP6MvPJwNQzcmRk13NfIRmPVNnGuV/u3 gm3c -----END CERTIFICATE----- -GTS Root R2 -=========== ------BEGIN CERTIFICATE----- -MIIFVzCCAz+gAwIBAgINAgPlrsWNBCUaqxElqjANBgkqhkiG9w0BAQwFADBHMQswCQYDVQQGEwJV -UzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3Qg -UjIwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UE -ChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjIwggIiMA0G -CSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDO3v2m++zsFDQ8BwZabFn3GTXd98GdVarTzTukk3Lv -CvptnfbwhYBboUhSnznFt+4orO/LdmgUud+tAWyZH8QiHZ/+cnfgLFuv5AS/T3KgGjSY6Dlo7JUl -e3ah5mm5hRm9iYz+re026nO8/4Piy33B0s5Ks40FnotJk9/BW9BuXvAuMC6C/Pq8tBcKSOWIm8Wb -a96wyrQD8Nr0kLhlZPdcTK3ofmZemde4wj7I0BOdre7kRXuJVfeKH2JShBKzwkCX44ofR5GmdFrS -+LFjKBC4swm4VndAoiaYecb+3yXuPuWgf9RhD1FLPD+M2uFwdNjCaKH5wQzpoeJ/u1U8dgbuak7M -kogwTZq9TwtImoS1mKPV+3PBV2HdKFZ1E66HjucMUQkQdYhMvI35ezzUIkgfKtzra7tEscszcTJG -r61K8YzodDqs5xoic4DSMPclQsciOzsSrZYuxsN2B6ogtzVJV+mSSeh2FnIxZyuWfoqjx5RWIr9q -S34BIbIjMt/kmkRtWVtd9QCgHJvGeJeNkP+byKq0rxFROV7Z+2et1VsRnTKaG73VululycslaVNV -J1zgyjbLiGH7HrfQy+4W+9OmTN6SpdTi3/UGVN4unUu0kzCqgc7dGtxRcw1PcOnlthYhGXmy5okL -dWTK1au8CcEYof/UVKGFPP0UJAOyh9OktwIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0T -AQH/BAUwAwEB/zAdBgNVHQ4EFgQUu//KjiOfT5nK2+JopqUVJxce2Q4wDQYJKoZIhvcNAQEMBQAD -ggIBAB/Kzt3HvqGf2SdMC9wXmBFqiN495nFWcrKeGk6c1SuYJF2ba3uwM4IJvd8lRuqYnrYb/oM8 -0mJhwQTtzuDFycgTE1XnqGOtjHsB/ncw4c5omwX4Eu55MaBBRTUoCnGkJE+M3DyCB19m3H0Q/gxh -swWV7uGugQ+o+MePTagjAiZrHYNSVc61LwDKgEDg4XSsYPWHgJ2uNmSRXbBoGOqKYcl3qJfEycel -/FVL8/B/uWU9J2jQzGv6U53hkRrJXRqWbTKH7QMgyALOWr7Z6v2yTcQvG99fevX4i8buMTolUVVn -jWQye+mew4K6Ki3pHrTgSAai/GevHyICc/sgCq+dVEuhzf9gR7A/Xe8bVr2XIZYtCtFenTgCR2y5 -9PYjJbigapordwj6xLEokCZYCDzifqrXPW+6MYgKBesntaFJ7qBFVHvmJ2WZICGoo7z7GJa7Um8M -7YNRTOlZ4iBgxcJlkoKM8xAfDoqXvneCbT+PHV28SSe9zE8P4c52hgQjxcCMElv924SgJPFI/2R8 -0L5cFtHvma3AH/vLrrw4IgYmZNralw4/KBVEqE8AyvCazM90arQ+POuV7LXTWtiBmelDGDfrs7vR -WGJB82bSj6p4lVQgw1oudCvV0b4YacCs1aTPObpRhANl6WLAYv7YTVWW4tAR+kg0Eeye7QUd5MjW -HYbL ------END CERTIFICATE----- - GTS Root R3 =========== -----BEGIN CERTIFICATE----- @@ -3214,23 +2659,6 @@ HVlNjM7IDiPCtyaaEBRx/pOyiriA8A4QntOoUAw3gi/q4Iqd4Sw5/7W0cwDk90imc6y/st53BIe0 o82bNSQ3+pCTE4FCxpgmdTdmQRCsu/WU48IxK63nI1bMNSWSs1A= -----END CERTIFICATE----- -FIRMAPROFESIONAL CA ROOT-A WEB -============================== ------BEGIN CERTIFICATE----- -MIICejCCAgCgAwIBAgIQMZch7a+JQn81QYehZ1ZMbTAKBggqhkjOPQQDAzBuMQswCQYDVQQGEwJF -UzEcMBoGA1UECgwTRmlybWFwcm9mZXNpb25hbCBTQTEYMBYGA1UEYQwPVkFURVMtQTYyNjM0MDY4 -MScwJQYDVQQDDB5GSVJNQVBST0ZFU0lPTkFMIENBIFJPT1QtQSBXRUIwHhcNMjIwNDA2MDkwMTM2 -WhcNNDcwMzMxMDkwMTM2WjBuMQswCQYDVQQGEwJFUzEcMBoGA1UECgwTRmlybWFwcm9mZXNpb25h -bCBTQTEYMBYGA1UEYQwPVkFURVMtQTYyNjM0MDY4MScwJQYDVQQDDB5GSVJNQVBST0ZFU0lPTkFM -IENBIFJPT1QtQSBXRUIwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAARHU+osEaR3xyrq89Zfe9MEkVz6 -iMYiuYMQYneEMy3pA4jU4DP37XcsSmDq5G+tbbT4TIqk5B/K6k84Si6CcyvHZpsKjECcfIr28jlg -st7L7Ljkb+qbXbdTkBgyVcUgt5SjYzBhMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUk+FD -Y1w8ndYn81LsF7Kpryz3dvgwHQYDVR0OBBYEFJPhQ2NcPJ3WJ/NS7Beyqa8s93b4MA4GA1UdDwEB -/wQEAwIBBjAKBggqhkjOPQQDAwNoADBlAjAdfKR7w4l1M+E7qUW/Runpod3JIha3RxEL2Jq68cgL -cFBTApFwhVmpHqTm6iMxoAACMQD94vizrxa5HnPEluPBMBnYfubDl94cT7iJLzPrSA8Z94dGXSaQ -pYXFuXqUPoeovQA= ------END CERTIFICATE----- - TWCA CYBER Root CA ================== -----BEGIN CERTIFICATE----- @@ -3480,8 +2908,8 @@ SM49BAMDA2kAMGYCMQCpKjAd0MKfkFFRQD6VVCHNFmb3U2wIFjnQEnx/Yxvf4zgAOdktUyBFCxxg ZzFDJe0CMQCSia7pXGKDYmH5LVerVrkR3SW+ak5KGoJr3M/TvEqzPNcum9v4KGm8ay3sMaE641c= -----END CERTIFICATE----- - OISTE Server Root RSA G1 -========================= +OISTE Server Root RSA G1 +======================== -----BEGIN CERTIFICATE----- MIIFgzCCA2ugAwIBAgIQVaXZZ5Qoxu0M+ifdWwFNGDANBgkqhkiG9w0BAQwFADBLMQswCQYDVQQG EwJDSDEZMBcGA1UECgwQT0lTVEUgRm91bmRhdGlvbjEhMB8GA1UEAwwYT0lTVEUgU2VydmVyIFJv @@ -3509,3 +2937,21 @@ msuY33OhkKCgxeDoAaijFJzIwZqsFVAzje18KotzlUBDJvyBpCpfOZC3J8tRd/iWkx7P8nd9H0aT olkelUTFLXVksNb54Dxp6gS1HAviRkRNQzuXSXERvSS2wq1yVAb+axj5d9spLFKebXd7Yv0PTY6Y MjAwcRLWJTXjn/hvnLXrahut6hDTlhZyBiElxky8j3C7DOReIoMt0r7+hVu05L0= -----END CERTIFICATE----- + +e-Szigno TLS Root CA 2023 +========================= +-----BEGIN CERTIFICATE----- +MIICzzCCAjGgAwIBAgINAOhvGHvWOWuYSkmYCjAKBggqhkjOPQQDBDB1MQswCQYDVQQGEwJIVTER +MA8GA1UEBwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0ZC4xFzAVBgNVBGEMDlZBVEhV +LTIzNTg0NDk3MSIwIAYDVQQDDBllLVN6aWdubyBUTFMgUm9vdCBDQSAyMDIzMB4XDTIzMDcxNzE0 +MDAwMFoXDTM4MDcxNzE0MDAwMFowdTELMAkGA1UEBhMCSFUxETAPBgNVBAcMCEJ1ZGFwZXN0MRYw +FAYDVQQKDA1NaWNyb3NlYyBMdGQuMRcwFQYDVQRhDA5WQVRIVS0yMzU4NDQ5NzEiMCAGA1UEAwwZ +ZS1Temlnbm8gVExTIFJvb3QgQ0EgMjAyMzCBmzAQBgcqhkjOPQIBBgUrgQQAIwOBhgAEAGgP36J8 +PKp0iGEKjcJMpQEiFNT3YHdCnAo4YKGMZz6zY+n6kbCLS+Y53wLCMAFSAL/fjO1ZrTJlqwlZULUZ +wmgcAOAFX9pQJhzDrAQixTpN7+lXWDajwRlTEArRzT/vSzUaQ49CE0y5LBqcvjC2xN7cS53kpDzL +Ltmt3999Cd8ukv+ho2MwYTAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4E +FgQUWYQCYlpGePVd3I8KECgj3NXW+0UwHwYDVR0jBBgwFoAUWYQCYlpGePVd3I8KECgj3NXW+0Uw +CgYIKoZIzj0EAwQDgYsAMIGHAkIBLdqu9S54tma4n7Zwf2Z0z+yOfP7AAXmazlIC58PRDHpty7Ve +7hekm9sEdu4pKeiv+62sUvTXK9Z3hBC9xdIoaDQCQTV2WnXzkoYI9bIeCvZlC9p2x1L/Cx6AcCIw +wzPbGO2E14vs7dOoY4G1VnxHx1YwlGhza9IuqbnZLBwpvQy6uWWL +-----END CERTIFICATE----- diff --git a/certificates/cacert-merged.pem b/certificates/cacert-merged.pem new file mode 100644 index 00000000..5f0e712c --- /dev/null +++ b/certificates/cacert-merged.pem @@ -0,0 +1,2963 @@ +## +## OpenShock firmware TLS trust store -- MERGED / GENERATED, do not edit by hand. +## +## Produced by certificates/cert_bundle.py. This is what the firmware packs: +## the verbatim curl/Mozilla CA extract (see cacert-curl.pem) + certificates/pinned_certs/. +## Its sha256 intentionally differs from curl's published hash. To change it, edit +## pinned_certs/ and re-run `cert_bundle.py generate` -- never hand-edit this file. +## +-----BEGIN CERTIFICATE----- +MIIEkTCCA3mgAwIBAgIERWtQVDANBgkqhkiG9w0BAQUFADCBsDELMAkGA1UEBhMCVVMxFjAUBgNV +BAoTDUVudHJ1c3QsIEluYy4xOTA3BgNVBAsTMHd3dy5lbnRydXN0Lm5ldC9DUFMgaXMgaW5jb3Jw +b3JhdGVkIGJ5IHJlZmVyZW5jZTEfMB0GA1UECxMWKGMpIDIwMDYgRW50cnVzdCwgSW5jLjEtMCsG +A1UEAxMkRW50cnVzdCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTA2MTEyNzIwMjM0 +MloXDTI2MTEyNzIwNTM0MlowgbAxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMu +MTkwNwYDVQQLEzB3d3cuZW50cnVzdC5uZXQvQ1BTIGlzIGluY29ycG9yYXRlZCBieSByZWZlcmVu +Y2UxHzAdBgNVBAsTFihjKSAyMDA2IEVudHJ1c3QsIEluYy4xLTArBgNVBAMTJEVudHJ1c3QgUm9v +dCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +ALaVtkNC+sZtKm9I35RMOVcF7sN5EUFoNu3s/poBj6E4KPz3EEZmLk0eGrEaTsbRwJWIsMn/MYsz +A9u3g3s+IIRe7bJWKKf44LlAcTfFy0cOlypowCKVYhXbR9n10Cv/gkvJrT7eTNuQgFA/CYqEAOww +Cj0Yzfv9KlmaI5UXLEWeH25DeW0MXJj+SKfFI0dcXv1u5x609mhF0YaDW6KKjbHjKYD+JXGIrb68 +j6xSlkuqUY3kEzEZ6E5Nn9uss2rVvDlUccp6en+Q3X0dgNmBu1kmwhH+5pPi94DkZfs0Nw4pgHBN +rziGLp5/V6+eF67rHMsoIV+2HNjnogQi+dPa2MsCAwEAAaOBsDCBrTAOBgNVHQ8BAf8EBAMCAQYw +DwYDVR0TAQH/BAUwAwEB/zArBgNVHRAEJDAigA8yMDA2MTEyNzIwMjM0MlqBDzIwMjYxMTI3MjA1 +MzQyWjAfBgNVHSMEGDAWgBRokORnpKZTgMeGZqTx90tD+4S9bTAdBgNVHQ4EFgQUaJDkZ6SmU4DH +hmak8fdLQ/uEvW0wHQYJKoZIhvZ9B0EABBAwDhsIVjcuMTo0LjADAgSQMA0GCSqGSIb3DQEBBQUA +A4IBAQCT1DCw1wMgKtD5Y+iRDAUgqV8ZyntyTtSx29CW+1RaGSwMCPeyvIWonX9tO1KzKtvn1ISM +Y/YPyyYBkVBs9F8U4pN0wBOeMDpQ47RgxRzwIkSNcUesyBrJ6ZuaAGAT/3B+XxFNSRuzFVJ7yVTa +v52Vr2ua2J7p8eRDjeIRRDq/r72DQnNSi6q7pynP9WQcCk3RvKqsnyrQ/39/2n3qse0wJcGE2jTS +W3iDVuycNsMm4hH2Z0kdkquM++v/eu6FSqdQgPCnXEqULl8FmTxSQeDNtGPPAUO6nIPcj2A781q0 +tHuu2guQOHXvgR1m0vdXcDazv/wor3ElhVsT/h5/WrQ8 +-----END CERTIFICATE----- + +COMODO ECC Certification Authority +================================== +-----BEGIN CERTIFICATE----- +MIICiTCCAg+gAwIBAgIQH0evqmIAcFBUTAGem2OZKjAKBggqhkjOPQQDAzCBhTELMAkGA1UEBhMC +R0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UE +ChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlvbiBB +dXRob3JpdHkwHhcNMDgwMzA2MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0Ix +GzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMR +Q09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRo +b3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQDR3svdcmCFYX7deSRFtSrYpn1PlILBs5BAH+X +4QokPB0BBO490o0JlwzgdeT6+3eKKvUDYEs2ixYjFq0JcfRK9ChQtP6IHG4/bC8vCVlbpVsLM5ni +wz2J+Wos77LTBumjQjBAMB0GA1UdDgQWBBR1cacZSBm8nZ3qQUfflMRId5nTeTAOBgNVHQ8BAf8E +BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjEA7wNbeqy3eApyt4jf/7VG +FAkK+qDmfQjGGoe9GKhzvSbKYAydzpmfz1wPMOG+FDHqAjAU9JM8SaczepBGR7NjfRObTrdvGDeA +U/7dIOA1mjbRxwG55tzd8/8dLDoWV9mSOdY= +-----END CERTIFICATE----- + +ePKI Root Certification Authority +================================= +-----BEGIN CERTIFICATE----- +MIIFsDCCA5igAwIBAgIQFci9ZUdcr7iXAF7kBtK8nTANBgkqhkiG9w0BAQUFADBeMQswCQYDVQQG +EwJUVzEjMCEGA1UECgwaQ2h1bmdod2EgVGVsZWNvbSBDby4sIEx0ZC4xKjAoBgNVBAsMIWVQS0kg +Um9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNDEyMjAwMjMxMjdaFw0zNDEyMjAwMjMx +MjdaMF4xCzAJBgNVBAYTAlRXMSMwIQYDVQQKDBpDaHVuZ2h3YSBUZWxlY29tIENvLiwgTHRkLjEq +MCgGA1UECwwhZVBLSSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIICIjANBgkqhkiG9w0B +AQEFAAOCAg8AMIICCgKCAgEA4SUP7o3biDN1Z82tH306Tm2d0y8U82N0ywEhajfqhFAHSyZbCUNs +IZ5qyNUD9WBpj8zwIuQf5/dqIjG3LBXy4P4AakP/h2XGtRrBp0xtInAhijHyl3SJCRImHJ7K2RKi +lTza6We/CKBk49ZCt0Xvl/T29de1ShUCWH2YWEtgvM3XDZoTM1PRYfl61dd4s5oz9wCGzh1NlDiv +qOx4UXCKXBCDUSH3ET00hl7lSM2XgYI1TBnsZfZrxQWh7kcT1rMhJ5QQCtkkO7q+RBNGMD+XPNjX +12ruOzjjK9SXDrkb5wdJfzcq+Xd4z1TtW0ado4AOkUPB1ltfFLqfpo0kR0BZv3I4sjZsN/+Z0V0O +WQqraffAsgRFelQArr5T9rXn4fg8ozHSqf4hUmTFpmfwdQcGlBSBVcYn5AGPF8Fqcde+S/uUWH1+ +ETOxQvdibBjWzwloPn9s9h6PYq2lY9sJpx8iQkEeb5mKPtf5P0B6ebClAZLSnT0IFaUQAS2zMnao +lQ2zepr7BxB4EW/hj8e6DyUadCrlHJhBmd8hh+iVBmoKs2pHdmX2Os+PYhcZewoozRrSgx4hxyy/ +vv9haLdnG7t4TY3OZ+XkwY63I2binZB1NJipNiuKmpS5nezMirH4JYlcWrYvjB9teSSnUmjDhDXi +Zo1jDiVN1Rmy5nk3pyKdVDECAwEAAaNqMGgwHQYDVR0OBBYEFB4M97Zn8uGSJglFwFU5Lnc/Qkqi +MAwGA1UdEwQFMAMBAf8wOQYEZyoHAAQxMC8wLQIBADAJBgUrDgMCGgUAMAcGBWcqAwAABBRFsMLH +ClZ87lt4DJX5GFPBphzYEDANBgkqhkiG9w0BAQUFAAOCAgEACbODU1kBPpVJufGBuvl2ICO1J2B0 +1GqZNF5sAFPZn/KmsSQHRGoqxqWOeBLoR9lYGxMqXnmbnwoqZ6YlPwZpVnPDimZI+ymBV3QGypzq +KOg4ZyYr8dW1P2WT+DZdjo2NQCCHGervJ8A9tDkPJXtoUHRVnAxZfVo9QZQlUgjgRywVMRnVvwdV +xrsStZf0X4OFunHB2WyBEXYKCrC/gpf36j36+uwtqSiUO1bd0lEursC9CBWMd1I0ltabrNMdjmEP +NXubrjlpC2JgQCA2j6/7Nu4tCEoduL+bXPjqpRugc6bY+G7gMwRfaKonh+3ZwZCc7b3jajWvY9+r +GNm65ulK6lCKD2GTHuItGeIwlDWSXQ62B68ZgI9HkFFLLk3dheLSClIKF5r8GrBQAuUBo2M3IUxE +xJtRmREOc5wGj1QupyheRDmHVi03vYVElOEMSyycw5KFNGHLD7ibSkNS/jQ6fbjpKdx2qcgw+BRx +gMYeNkh0IkFch4LoGHGLQYlE535YW6i4jRPpp2zDR+2zGp1iro2C6pSe3VkQw63d4k3jMdXH7Ojy +sP6SHhYKGvzZ8/gntsm+HbRsZJB/9OTEW9c3rkIO3aQab3yIVMUWbuF6aC74Or8NpDyJO3inTmOD +BCEIZ43ygknQW/2xzQ+DhNQ+IIX3Sj0rnP0qCglN6oH4EZw= +-----END CERTIFICATE----- + +NetLock Arany (Class Gold) Főtanúsítvány +======================================== +-----BEGIN CERTIFICATE----- +MIIEFTCCAv2gAwIBAgIGSUEs5AAQMA0GCSqGSIb3DQEBCwUAMIGnMQswCQYDVQQGEwJIVTERMA8G +A1UEBwwIQnVkYXBlc3QxFTATBgNVBAoMDE5ldExvY2sgS2Z0LjE3MDUGA1UECwwuVGFuw7pzw610 +dsOhbnlraWFkw7NrIChDZXJ0aWZpY2F0aW9uIFNlcnZpY2VzKTE1MDMGA1UEAwwsTmV0TG9jayBB +cmFueSAoQ2xhc3MgR29sZCkgRsWRdGFuw7pzw610dsOhbnkwHhcNMDgxMjExMTUwODIxWhcNMjgx +MjA2MTUwODIxWjCBpzELMAkGA1UEBhMCSFUxETAPBgNVBAcMCEJ1ZGFwZXN0MRUwEwYDVQQKDAxO +ZXRMb2NrIEtmdC4xNzA1BgNVBAsMLlRhbsO6c8OtdHbDoW55a2lhZMOzayAoQ2VydGlmaWNhdGlv +biBTZXJ2aWNlcykxNTAzBgNVBAMMLE5ldExvY2sgQXJhbnkgKENsYXNzIEdvbGQpIEbFkXRhbsO6 +c8OtdHbDoW55MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxCRec75LbRTDofTjl5Bu +0jBFHjzuZ9lk4BqKf8owyoPjIMHj9DrTlF8afFttvzBPhCf2nx9JvMaZCpDyD/V/Q4Q3Y1GLeqVw +/HpYzY6b7cNGbIRwXdrzAZAj/E4wqX7hJ2Pn7WQ8oLjJM2P+FpD/sLj916jAwJRDC7bVWaaeVtAk +H3B5r9s5VA1lddkVQZQBr17s9o3x/61k/iCa11zr/qYfCGSji3ZVrR47KGAuhyXoqq8fxmRGILdw +fzzeSNuWU7c5d+Qa4scWhHaXWy+7GRWF+GmF9ZmnqfI0p6m2pgP8b4Y9VHx2BJtr+UBdADTHLpl1 +neWIA6pN+APSQnbAGwIDAKiLo0UwQzASBgNVHRMBAf8ECDAGAQH/AgEEMA4GA1UdDwEB/wQEAwIB +BjAdBgNVHQ4EFgQUzPpnk/C2uNClwB7zU/2MU9+D15YwDQYJKoZIhvcNAQELBQADggEBAKt/7hwW +qZw8UQCgwBEIBaeZ5m8BiFRhbvG5GK1Krf6BQCOUL/t1fC8oS2IkgYIL9WHxHG64YTjrgfpioTta +YtOUZcTh5m2C+C8lcLIhJsFyUR+MLMOEkMNaj7rP9KdlpeuY0fsFskZ1FSNqb4VjMIDw1Z4fKRzC +bLBQWV2QWzuoDTDPv31/zvGdg73JRm4gpvlhUbohL3u+pRVjodSVh/GeufOJ8z2FuLjbvrW5Kfna +NwUASZQDhETnv0Mxz3WLJdH0pmT1kvarBes96aULNmLazAZfNou2XjG4Kvte9nHfRCaexOYNkbQu +dZWAUWpLMKawYqGT8ZvYzsRjdT9ZR7E= +-----END CERTIFICATE----- + +Microsec e-Szigno Root CA 2009 +============================== +-----BEGIN CERTIFICATE----- +MIIECjCCAvKgAwIBAgIJAMJ+QwRORz8ZMA0GCSqGSIb3DQEBCwUAMIGCMQswCQYDVQQGEwJIVTER +MA8GA1UEBwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0ZC4xJzAlBgNVBAMMHk1pY3Jv +c2VjIGUtU3ppZ25vIFJvb3QgQ0EgMjAwOTEfMB0GCSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5o +dTAeFw0wOTA2MTYxMTMwMThaFw0yOTEyMzAxMTMwMThaMIGCMQswCQYDVQQGEwJIVTERMA8GA1UE +BwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0ZC4xJzAlBgNVBAMMHk1pY3Jvc2VjIGUt +U3ppZ25vIFJvb3QgQ0EgMjAwOTEfMB0GCSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5odTCCASIw +DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOn4j/NjrdqG2KfgQvvPkd6mJviZpWNwrZuuyjNA +fW2WbqEORO7hE52UQlKavXWFdCyoDh2Tthi3jCyoz/tccbna7P7ofo/kLx2yqHWH2Leh5TvPmUpG +0IMZfcChEhyVbUr02MelTTMuhTlAdX4UfIASmFDHQWe4oIBhVKZsTh/gnQ4H6cm6M+f+wFUoLAKA +pxn1ntxVUwOXewdI/5n7N4okxFnMUBBjjqqpGrCEGob5X7uxUG6k0QrM1XF+H6cbfPVTbiJfyyvm +1HxdrtbCxkzlBQHZ7Vf8wSN5/PrIJIOV87VqUQHQd9bpEqH5GoP7ghu5sJf0dgYzQ0mg/wu1+rUC +AwEAAaOBgDB+MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBTLD8bf +QkPMPcu1SCOhGnqmKrs0aDAfBgNVHSMEGDAWgBTLD8bfQkPMPcu1SCOhGnqmKrs0aDAbBgNVHREE +FDASgRBpbmZvQGUtc3ppZ25vLmh1MA0GCSqGSIb3DQEBCwUAA4IBAQDJ0Q5eLtXMs3w+y/w9/w0o +lZMEyL/azXm4Q5DwpL7v8u8hmLzU1F0G9u5C7DBsoKqpyvGvivo/C3NqPuouQH4frlRheesuCDfX +I/OMn74dseGkddug4lQUsbocKaQY9hK6ohQU4zE1yED/t+AFdlfBHFny+L/k7SViXITwfn4fs775 +tyERzAMBVnCnEJIeGzSBHq2cGsMEPO0CYdYeBvNfOofyK/FFh+U9rNHHV4S9a67c2Pm2G2JwCz02 +yULyMtd6YebS2z3PyKnJm9zbWETXbzivf3jTo60adbocwTZ8jx5tHMN1Rq41Bab2XD0h7lbwyYIi +LXpUq3DDfSJlgnCW +-----END CERTIFICATE----- + +GlobalSign Root CA - R3 +======================= +-----BEGIN CERTIFICATE----- +MIIDXzCCAkegAwIBAgILBAAAAAABIVhTCKIwDQYJKoZIhvcNAQELBQAwTDEgMB4GA1UECxMXR2xv +YmFsU2lnbiBSb290IENBIC0gUjMxEzARBgNVBAoTCkdsb2JhbFNpZ24xEzARBgNVBAMTCkdsb2Jh +bFNpZ24wHhcNMDkwMzE4MTAwMDAwWhcNMjkwMzE4MTAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxT +aWduIFJvb3QgQ0EgLSBSMzETMBEGA1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2ln +bjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMwldpB5BngiFvXAg7aEyiie/QV2EcWt +iHL8RgJDx7KKnQRfJMsuS+FggkbhUqsMgUdwbN1k0ev1LKMPgj0MK66X17YUhhB5uzsTgHeMCOFJ +0mpiLx9e+pZo34knlTifBtc+ycsmWQ1z3rDI6SYOgxXG71uL0gRgykmmKPZpO/bLyCiR5Z2KYVc3 +rHQU3HTgOu5yLy6c+9C7v/U9AOEGM+iCK65TpjoWc4zdQQ4gOsC0p6Hpsk+QLjJg6VfLuQSSaGjl +OCZgdbKfd/+RFO+uIEn8rUAVSNECMWEZXriX7613t2Saer9fwRPvm2L7DWzgVGkWqQPabumDk3F2 +xmmFghcCAwEAAaNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYE +FI/wS3+oLkUkrk1Q+mOai97i3Ru8MA0GCSqGSIb3DQEBCwUAA4IBAQBLQNvAUKr+yAzv95ZURUm7 +lgAJQayzE4aGKAczymvmdLm6AC2upArT9fHxD4q/c2dKg8dEe3jgr25sbwMpjjM5RcOO5LlXbKr8 +EpbsU8Yt5CRsuZRj+9xTaGdWPoO4zzUhw8lo/s7awlOqzJCK6fBdRoyV3XpYKBovHd7NADdBj+1E +bddTKJd+82cEHhXXipa0095MJ6RMG3NzdvQXmcIfeg7jLQitChws/zyrVQ4PkX4268NXSb7hLi18 +YIvDQVETI53O9zJrlAGomecsMx86OyXShkDOOyyGeMlhLxS67ttVb9+E7gUJTb0o2HLO02JQZR7r +kpeDMdmztcpHWD9f +-----END CERTIFICATE----- + +Izenpe.com +========== +-----BEGIN CERTIFICATE----- +MIIF8TCCA9mgAwIBAgIQALC3WhZIX7/hy/WL1xnmfTANBgkqhkiG9w0BAQsFADA4MQswCQYDVQQG +EwJFUzEUMBIGA1UECgwLSVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6ZW5wZS5jb20wHhcNMDcxMjEz +MTMwODI4WhcNMzcxMjEzMDgyNzI1WjA4MQswCQYDVQQGEwJFUzEUMBIGA1UECgwLSVpFTlBFIFMu +QS4xEzARBgNVBAMMCkl6ZW5wZS5jb20wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDJ +03rKDx6sp4boFmVqscIbRTJxldn+EFvMr+eleQGPicPK8lVx93e+d5TzcqQsRNiekpsUOqHnJJAK +ClaOxdgmlOHZSOEtPtoKct2jmRXagaKH9HtuJneJWK3W6wyyQXpzbm3benhB6QiIEn6HLmYRY2xU ++zydcsC8Lv/Ct90NduM61/e0aL6i9eOBbsFGb12N4E3GVFWJGjMxCrFXuaOKmMPsOzTFlUFpfnXC +PCDFYbpRR6AgkJOhkEvzTnyFRVSa0QUmQbC1TR0zvsQDyCV8wXDbO/QJLVQnSKwv4cSsPsjLkkxT +OTcj7NMB+eAJRE1NZMDhDVqHIrytG6P+JrUV86f8hBnp7KGItERphIPzidF0BqnMC9bC3ieFUCbK +F7jJeodWLBoBHmy+E60QrLUk9TiRodZL2vG70t5HtfG8gfZZa88ZU+mNFctKy6lvROUbQc/hhqfK +0GqfvEyNBjNaooXlkDWgYlwWTvDjovoDGrQscbNYLN57C9saD+veIR8GdwYDsMnvmfzAuU8Lhij+ +0rnq49qlw0dpEuDb8PYZi+17cNcC1u2HGCgsBCRMd+RIihrGO5rUD8r6ddIBQFqNeb+Lz0vPqhbB +leStTIo+F5HUsWLlguWABKQDfo2/2n+iD5dPDNMN+9fR5XJ+HMh3/1uaD7euBUbl8agW7EekFwID +AQABo4H2MIHzMIGwBgNVHREEgagwgaWBD2luZm9AaXplbnBlLmNvbaSBkTCBjjFHMEUGA1UECgw+ +SVpFTlBFIFMuQS4gLSBDSUYgQTAxMzM3MjYwLVJNZXJjLlZpdG9yaWEtR2FzdGVpeiBUMTA1NSBG +NjIgUzgxQzBBBgNVBAkMOkF2ZGEgZGVsIE1lZGl0ZXJyYW5lbyBFdG9yYmlkZWEgMTQgLSAwMTAx +MCBWaXRvcmlhLUdhc3RlaXowDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0O +BBYEFB0cZQ6o8iV7tJHP5LGx5r1VdGwFMA0GCSqGSIb3DQEBCwUAA4ICAQB4pgwWSp9MiDrAyw6l +Fn2fuUhfGI8NYjb2zRlrrKvV9pF9rnHzP7MOeIWblaQnIUdCSnxIOvVFfLMMjlF4rJUT3sb9fbga +kEyrkgPH7UIBzg/YsfqikuFgba56awmqxinuaElnMIAkejEWOVt+8Rwu3WwJrfIxwYJOubv5vr8q +hT/AQKM6WfxZSzwoJNu0FXWuDYi6LnPAvViH5ULy617uHjAimcs30cQhbIHsvm0m5hzkQiCeR7Cs +g1lwLDXWrzY0tM07+DKo7+N4ifuNRSzanLh+QBxh5z6ikixL8s36mLYp//Pye6kfLqCTVyvehQP5 +aTfLnnhqBbTFMXiJ7HqnheG5ezzevh55hM6fcA5ZwjUukCox2eRFekGkLhObNA5me0mrZJfQRsN5 +nXJQY6aYWwa9SG3YOYNw6DXwBdGqvOPbyALqfP2C2sJbUjWumDqtujWTI6cfSN01RpiyEGjkpTHC +ClguGYEQyVB1/OpaFs4R1+7vUIgtYf8/QnMFlEPVjjxOAToZpR9GTnfQXeWBIiGH/pR9hNiTrdZo +Q0iy2+tzJOeRf1SktoA+naM8THLCV8Sg1Mw4J87VBp6iSNnpn86CcDaTmjvfliHjWbcM2pE38P1Z +WrOZyGlsQyYBNWNgVYkDOnXYukrZVP/u3oDYLdE41V4tC5h9Pmzb/CaIxw== +-----END CERTIFICATE----- + +Go Daddy Root Certificate Authority - G2 +======================================== +-----BEGIN CERTIFICATE----- +MIIDxTCCAq2gAwIBAgIBADANBgkqhkiG9w0BAQsFADCBgzELMAkGA1UEBhMCVVMxEDAOBgNVBAgT +B0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxGjAYBgNVBAoTEUdvRGFkZHkuY29tLCBJbmMu +MTEwLwYDVQQDEyhHbyBEYWRkeSBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5 +MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgYMxCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6 +b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMRowGAYDVQQKExFHb0RhZGR5LmNvbSwgSW5jLjExMC8G +A1UEAxMoR28gRGFkZHkgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAL9xYgjx+lk09xvJGKP3gElY6SKDE6bFIEMBO4Tx5oVJnyfq +9oQbTqC023CYxzIBsQU+B07u9PpPL1kwIuerGVZr4oAH/PMWdYA5UXvl+TW2dE6pjYIT5LY/qQOD ++qK+ihVqf94Lw7YZFAXK6sOoBJQ7RnwyDfMAZiLIjWltNowRGLfTshxgtDj6AozO091GB94KPutd +fMh8+7ArU6SSYmlRJQVhGkSBjCypQ5Yj36w6gZoOKcUcqeldHraenjAKOc7xiID7S13MMuyFYkMl +NAJWJwGRtDtwKj9useiciAF9n9T521NtYJ2/LOdYq7hfRvzOxBsDPAnrSTFcaUaz4EcCAwEAAaNC +MEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFDqahQcQZyi27/a9 +BUFuIMGU2g/eMA0GCSqGSIb3DQEBCwUAA4IBAQCZ21151fmXWWcDYfF+OwYxdS2hII5PZYe096ac +vNjpL9DbWu7PdIxztDhC2gV7+AJ1uP2lsdeu9tfeE8tTEH6KRtGX+rcuKxGrkLAngPnon1rpN5+r +5N9ss4UXnT3ZJE95kTXWXwTrgIOrmgIttRD02JDHBHNA7XIloKmf7J6raBKZV8aPEjoJpL1E/QYV +N8Gb5DKj7Tjo2GTzLH4U/ALqn83/B2gX2yKQOC16jdFU8WnjXzPKej17CuPKf1855eJ1usV2GDPO +LPAvTK33sefOT6jEm0pUBsV/fdUID+Ic/n4XuKxe9tQWskMJDE32p2u0mYRlynqI4uJEvlz36hz1 +-----END CERTIFICATE----- + +Starfield Root Certificate Authority - G2 +========================================= +-----BEGIN CERTIFICATE----- +MIID3TCCAsWgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBjzELMAkGA1UEBhMCVVMxEDAOBgNVBAgT +B0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoTHFN0YXJmaWVsZCBUZWNobm9s +b2dpZXMsIEluYy4xMjAwBgNVBAMTKVN0YXJmaWVsZCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0 +eSAtIEcyMB4XDTA5MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgY8xCzAJBgNVBAYTAlVTMRAw +DgYDVQQIEwdBcml6b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFyZmllbGQg +VGVjaG5vbG9naWVzLCBJbmMuMTIwMAYDVQQDEylTdGFyZmllbGQgUm9vdCBDZXJ0aWZpY2F0ZSBB +dXRob3JpdHkgLSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL3twQP89o/8ArFv +W59I2Z154qK3A2FWGMNHttfKPTUuiUP3oWmb3ooa/RMgnLRJdzIpVv257IzdIvpy3Cdhl+72WoTs +bhm5iSzchFvVdPtrX8WJpRBSiUZV9Lh1HOZ/5FSuS/hVclcCGfgXcVnrHigHdMWdSL5stPSksPNk +N3mSwOxGXn/hbVNMYq/NHwtjuzqd+/x5AJhhdM8mgkBj87JyahkNmcrUDnXMN/uLicFZ8WJ/X7Nf +ZTD4p7dNdloedl40wOiWVpmKs/B/pM293DIxfJHP4F8R+GuqSVzRmZTRouNjWwl2tVZi4Ut0HZbU +JtQIBFnQmA4O5t78w+wfkPECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC +AQYwHQYDVR0OBBYEFHwMMh+n2TB/xH1oo2Kooc6rB1snMA0GCSqGSIb3DQEBCwUAA4IBAQARWfol +TwNvlJk7mh+ChTnUdgWUXuEok21iXQnCoKjUsHU48TRqneSfioYmUeYs0cYtbpUgSpIB7LiKZ3sx +4mcujJUDJi5DnUox9g61DLu34jd/IroAow57UvtruzvE03lRTs2Q9GcHGcg8RnoNAX3FWOdt5oUw +F5okxBDgBPfg8n/Uqgr/Qh037ZTlZFkSIHc40zI+OIF1lnP6aI+xy84fxez6nH7PfrHxBy22/L/K +pL/QlwVKvOoYKAKQvVR4CSFx09F9HdkWsKlhPdAKACL8x3vLCWRFCztAgfd9fDL1mMpYjn0q7pBZ +c2T5NnReJaH1ZgUufzkVqSr7UIuOhWn0 +-----END CERTIFICATE----- + +Starfield Services Root Certificate Authority - G2 +================================================== +-----BEGIN CERTIFICATE----- +MIID7zCCAtegAwIBAgIBADANBgkqhkiG9w0BAQsFADCBmDELMAkGA1UEBhMCVVMxEDAOBgNVBAgT +B0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoTHFN0YXJmaWVsZCBUZWNobm9s +b2dpZXMsIEluYy4xOzA5BgNVBAMTMlN0YXJmaWVsZCBTZXJ2aWNlcyBSb290IENlcnRpZmljYXRl +IEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgZgxCzAJBgNV +BAYTAlVTMRAwDgYDVQQIEwdBcml6b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxT +dGFyZmllbGQgVGVjaG5vbG9naWVzLCBJbmMuMTswOQYDVQQDEzJTdGFyZmllbGQgU2VydmljZXMg +Um9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC +AQoCggEBANUMOsQq+U7i9b4Zl1+OiFOxHz/Lz58gE20pOsgPfTz3a3Y4Y9k2YKibXlwAgLIvWX/2 +h/klQ4bnaRtSmpDhcePYLQ1Ob/bISdm28xpWriu2dBTrz/sm4xq6HZYuajtYlIlHVv8loJNwU4Pa +hHQUw2eeBGg6345AWh1KTs9DkTvnVtYAcMtS7nt9rjrnvDH5RfbCYM8TWQIrgMw0R9+53pBlbQLP +LJGmpufehRhJfGZOozptqbXuNC66DQO4M99H67FrjSXZm86B0UVGMpZwh94CDklDhbZsc7tk6mFB +rMnUVN+HL8cisibMn1lUaJ/8viovxFUcdUBgF4UCVTmLfwUCAwEAAaNCMEAwDwYDVR0TAQH/BAUw +AwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFJxfAN+qAdcwKziIorhtSpzyEZGDMA0GCSqG +SIb3DQEBCwUAA4IBAQBLNqaEd2ndOxmfZyMIbw5hyf2E3F/YNoHN2BtBLZ9g3ccaaNnRbobhiCPP +E95Dz+I0swSdHynVv/heyNXBve6SbzJ08pGCL72CQnqtKrcgfU28elUSwhXqvfdqlS5sdJ/PHLTy +xQGjhdByPq1zqwubdQxtRbeOlKyWN7Wg0I8VRw7j6IPdj/3vQQF3zCepYoUz8jcI73HPdwbeyBkd +iEDPfUYd/x7H4c7/I9vG+o1VTqkC50cRRj70/b17KSa7qWFiNyi2LSr2EIZkyXCn0q23KXB56jza +YyWf/Wi3MOxw+3WKt21gZ7IeyLnp2KhvAotnDU0mV3HaIPzBSlCNsSi6 +-----END CERTIFICATE----- + +Certum Trusted Network CA +========================= +-----BEGIN CERTIFICATE----- +MIIDuzCCAqOgAwIBAgIDBETAMA0GCSqGSIb3DQEBBQUAMH4xCzAJBgNVBAYTAlBMMSIwIAYDVQQK +ExlVbml6ZXRvIFRlY2hub2xvZ2llcyBTLkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlv +biBBdXRob3JpdHkxIjAgBgNVBAMTGUNlcnR1bSBUcnVzdGVkIE5ldHdvcmsgQ0EwHhcNMDgxMDIy +MTIwNzM3WhcNMjkxMjMxMTIwNzM3WjB+MQswCQYDVQQGEwJQTDEiMCAGA1UEChMZVW5pemV0byBU +ZWNobm9sb2dpZXMgUy5BLjEnMCUGA1UECxMeQ2VydHVtIENlcnRpZmljYXRpb24gQXV0aG9yaXR5 +MSIwIAYDVQQDExlDZXJ0dW0gVHJ1c3RlZCBOZXR3b3JrIENBMIIBIjANBgkqhkiG9w0BAQEFAAOC +AQ8AMIIBCgKCAQEA4/t9o3K6wvDJFIf1awFO4W5AB7ptJ11/91sts1rHUV+rpDKmYYe2bg+G0jAC +l/jXaVehGDldamR5xgFZrDwxSjh80gTSSyjoIF87B6LMTXPb865Px1bVWqeWifrzq2jUI4ZZJ88J +J7ysbnKDHDBy3+Ci6dLhdHUZvSqeexVUBBvXQzmtVSjF4hq79MDkrjhJM8x2hZ85RdKknvISjFH4 +fOQtf/WsX+sWn7Et0brMkUJ3TCXJkDhv2/DM+44el1k+1WBO5gUo7Ul5E0u6SNsv+XLTOcr+H9g0 +cvW0QM8xAcPs3hEtF10fuFDRXhmnad4HMyjKUJX5p1TLVIZQRan5SQIDAQABo0IwQDAPBgNVHRMB +Af8EBTADAQH/MB0GA1UdDgQWBBQIds3LB/8k9sXN7buQvOKEN0Z19zAOBgNVHQ8BAf8EBAMCAQYw +DQYJKoZIhvcNAQEFBQADggEBAKaorSLOAT2mo/9i0Eidi15ysHhE49wcrwn9I0j6vSrEuVUEtRCj +jSfeC4Jj0O7eDDd5QVsisrCaQVymcODU0HfLI9MA4GxWL+FpDQ3Zqr8hgVDZBqWo/5U30Kr+4rP1 +mS1FhIrlQgnXdAIv94nYmem8J9RHjboNRhx3zxSkHLmkMcScKHQDNP8zGSal6Q10tz6XxnboJ5aj +Zt3hrvJBW8qYVoNzcOSGGtIxQbovvi0TWnZvTuhOgQ4/WwMioBK+ZlgRSssDxLQqKi2WF+A5VLxI +03YnnZotBqbJ7DnSq9ufmgsnAjUpsUCV5/nonFWIGUbWtzT1fs45mtk48VH3Tyw= +-----END CERTIFICATE----- + +TWCA Root Certification Authority +================================= +-----BEGIN CERTIFICATE----- +MIIDezCCAmOgAwIBAgIBATANBgkqhkiG9w0BAQUFADBfMQswCQYDVQQGEwJUVzESMBAGA1UECgwJ +VEFJV0FOLUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFUV0NBIFJvb3QgQ2VydGlmaWNh +dGlvbiBBdXRob3JpdHkwHhcNMDgwODI4MDcyNDMzWhcNMzAxMjMxMTU1OTU5WjBfMQswCQYDVQQG +EwJUVzESMBAGA1UECgwJVEFJV0FOLUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFUV0NB +IFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK +AoIBAQCwfnK4pAOU5qfeCTiRShFAh6d8WWQUe7UREN3+v9XAu1bihSX0NXIP+FPQQeFEAcK0HMMx +QhZHhTMidrIKbw/lJVBPhYa+v5guEGcevhEFhgWQxFnQfHgQsIBct+HHK3XLfJ+utdGdIzdjp9xC +oi2SBBtQwXu4PhvJVgSLL1KbralW6cH/ralYhzC2gfeXRfwZVzsrb+RH9JlF/h3x+JejiB03HFyP +4HYlmlD4oFT/RJB2I9IyxsOrBr/8+7/zrX2SYgJbKdM1o5OaQ2RgXbL6Mv87BK9NQGr5x+PvI/1r +y+UPizgN7gr8/g+YnzAx3WxSZfmLgb4i4RxYA7qRG4kHAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIB +BjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqOFsmjd6LWvJPelSDGRjjCDWmujANBgkqhkiG +9w0BAQUFAAOCAQEAPNV3PdrfibqHDAhUaiBQkr6wQT25JmSDCi/oQMCXKCeCMErJk/9q56YAf4lC +mtYR5VPOL8zy2gXE/uJQxDqGfczafhAJO5I1KlOy/usrBdlsXebQ79NqZp4VKIV66IIArB6nCWlW +QtNoURi+VJq/REG6Sb4gumlc7rh3zc5sH62Dlhh9DrUUOYTxKOkto557HnpyWoOzeW/vtPzQCqVY +T0bf+215WfKEIlKuD8z7fDvnaspHYcN6+NOSBB+4IIThNlQWx0DeO4pz3N/GCUzf7Nr/1FNCocny +Yh0igzyXxfkZYiesZSLX0zzG5Y6yU8xJzrww/nsOM5D77dIUkR8Hrw== +-----END CERTIFICATE----- + +Security Communication RootCA2 +============================== +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIBADANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJKUDElMCMGA1UEChMc +U0VDT00gVHJ1c3QgU3lzdGVtcyBDTy4sTFRELjEnMCUGA1UECxMeU2VjdXJpdHkgQ29tbXVuaWNh +dGlvbiBSb290Q0EyMB4XDTA5MDUyOTA1MDAzOVoXDTI5MDUyOTA1MDAzOVowXTELMAkGA1UEBhMC +SlAxJTAjBgNVBAoTHFNFQ09NIFRydXN0IFN5c3RlbXMgQ08uLExURC4xJzAlBgNVBAsTHlNlY3Vy +aXR5IENvbW11bmljYXRpb24gUm9vdENBMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB +ANAVOVKxUrO6xVmCxF1SrjpDZYBLx/KWvNs2l9amZIyoXvDjChz335c9S672XewhtUGrzbl+dp++ ++T42NKA7wfYxEUV0kz1XgMX5iZnK5atq1LXaQZAQwdbWQonCv/Q4EpVMVAX3NuRFg3sUZdbcDE3R +3n4MqzvEFb46VqZab3ZpUql6ucjrappdUtAtCms1FgkQhNBqyjoGADdH5H5XTz+L62e4iKrFvlNV +spHEfbmwhRkGeC7bYRr6hfVKkaHnFtWOojnflLhwHyg/i/xAXmODPIMqGplrz95Zajv8bxbXH/1K +EOtOghY6rCcMU/Gt1SSwawNQwS08Ft1ENCcadfsCAwEAAaNCMEAwHQYDVR0OBBYEFAqFqXdlBZh8 +QIH4D5csOPEK7DzPMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEB +CwUAA4IBAQBMOqNErLlFsceTfsgLCkLfZOoc7llsCLqJX2rKSpWeeo8HxdpFcoJxDjrSzG+ntKEj +u/Ykn8sX/oymzsLS28yN/HH8AynBbF0zX2S2ZTuJbxh2ePXcokgfGT+Ok+vx+hfuzU7jBBJV1uXk +3fs+BXziHV7Gp7yXT2g69ekuCkO2r1dcYmh8t/2jioSgrGK+KwmHNPBqAbubKVY8/gA3zyNs8U6q +tnRGEmyR7jTV7JqR50S+kDFy1UkC9gLl9B/rfNmWVan/7Ir5mUf/NVoCqgTLiluHcSmRvaS0eg29 +mvVXIwAHIRc/SjnRBUkLp7Y3gaVdjKozXoEofKd9J+sAro03 +-----END CERTIFICATE----- + +Actalis Authentication Root CA +============================== +-----BEGIN CERTIFICATE----- +MIIFuzCCA6OgAwIBAgIIVwoRl0LE48wwDQYJKoZIhvcNAQELBQAwazELMAkGA1UEBhMCSVQxDjAM +BgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8wMzM1ODUyMDk2NzEnMCUGA1UE +AwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290IENBMB4XDTExMDkyMjExMjIwMloXDTMwMDky +MjExMjIwMlowazELMAkGA1UEBhMCSVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlz +IFMucC5BLi8wMzM1ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290 +IENBMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAp8bEpSmkLO/lGMWwUKNvUTufClrJ +wkg4CsIcoBh/kbWHuUA/3R1oHwiD1S0eiKD4j1aPbZkCkpAW1V8IbInX4ay8IMKx4INRimlNAJZa +by/ARH6jDuSRzVju3PvHHkVH3Se5CAGfpiEd9UEtL0z9KK3giq0itFZljoZUj5NDKd45RnijMCO6 +zfB9E1fAXdKDa0hMxKufgFpbOr3JpyI/gCczWw63igxdBzcIy2zSekciRDXFzMwujt0q7bd9Zg1f +YVEiVRvjRuPjPdA1YprbrxTIW6HMiRvhMCb8oJsfgadHHwTrozmSBp+Z07/T6k9QnBn+locePGX2 +oxgkg4YQ51Q+qDp2JE+BIcXjDwL4k5RHILv+1A7TaLndxHqEguNTVHnd25zS8gebLra8Pu2Fbe8l +EfKXGkJh90qX6IuxEAf6ZYGyojnP9zz/GPvG8VqLWeICrHuS0E4UT1lF9gxeKF+w6D9Fz8+vm2/7 +hNN3WpVvrJSEnu68wEqPSpP4RCHiMUVhUE4Q2OM1fEwZtN4Fv6MGn8i1zeQf1xcGDXqVdFUNaBr8 +EBtiZJ1t4JWgw5QHVw0U5r0F+7if5t+L4sbnfpb2U8WANFAoWPASUHEXMLrmeGO89LKtmyuy/uE5 +jF66CyCU3nuDuP/jVo23Eek7jPKxwV2dpAtMK9myGPW1n0sCAwEAAaNjMGEwHQYDVR0OBBYEFFLY +iDrIn3hm7YnzezhwlMkCAjbQMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUUtiIOsifeGbt +ifN7OHCUyQICNtAwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBCwUAA4ICAQALe3KHwGCmSUyI +WOYdiPcUZEim2FgKDk8TNd81HdTtBjHIgT5q1d07GjLukD0R0i70jsNjLiNmsGe+b7bAEzlgqqI0 +JZN1Ut6nna0Oh4lScWoWPBkdg/iaKWW+9D+a2fDzWochcYBNy+A4mz+7+uAwTc+G02UQGRjRlwKx +K3JCaKygvU5a2hi/a5iB0P2avl4VSM0RFbnAKVy06Ij3Pjaut2L9HmLecHgQHEhb2rykOLpn7VU+ +Xlff1ANATIGk0k9jpwlCCRT8AKnCgHNPLsBA2RF7SOp6AsDT6ygBJlh0wcBzIm2Tlf05fbsq4/aC +4yyXX04fkZT6/iyj2HYauE2yOE+b+h1IYHkm4vP9qdCa6HCPSXrW5b0KDtst842/6+OkfcvHlXHo +2qN8xcL4dJIEG4aspCJTQLas/kx2z/uUMsA1n3Y/buWQbqCmJqK4LL7RK4X9p2jIugErsWx0Hbhz +lefut8cl8ABMALJ+tguLHPPAUJ4lueAI3jZm/zel0btUZCzJJ7VLkn5l/9Mt4blOvH+kQSGQQXem +OR/qnuOf0GZvBeyqdn6/axag67XH/JJULysRJyU3eExRarDzzFhdFPFqSBX/wge2sY0PjlxQRrM9 +vwGYT7JZVEc+NHt4bVaTLnPqZih4zR0Uv6CPLy64Lo7yFIrM6bV8+2ydDKXhlg== +-----END CERTIFICATE----- + +Buypass Class 2 Root CA +======================= +-----BEGIN CERTIFICATE----- +MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEdMBsGA1UECgwU +QnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3MgQ2xhc3MgMiBSb290IENBMB4X +DTEwMTAyNjA4MzgwM1oXDTQwMTAyNjA4MzgwM1owTjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1 +eXBhc3MgQVMtOTgzMTYzMzI3MSAwHgYDVQQDDBdCdXlwYXNzIENsYXNzIDIgUm9vdCBDQTCCAiIw +DQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANfHXvfBB9R3+0Mh9PT1aeTuMgHbo4Yf5FkNuud1 +g1Lr6hxhFUi7HQfKjK6w3Jad6sNgkoaCKHOcVgb/S2TwDCo3SbXlzwx87vFKu3MwZfPVL4O2fuPn +9Z6rYPnT8Z2SdIrkHJasW4DptfQxh6NR/Md+oW+OU3fUl8FVM5I+GC911K2GScuVr1QGbNgGE41b +/+EmGVnAJLqBcXmQRFBoJJRfuLMR8SlBYaNByyM21cHxMlAQTn/0hpPshNOOvEu/XAFOBz3cFIqU +CqTqc/sLUegTBxj6DvEr0VQVfTzh97QZQmdiXnfgolXsttlpF9U6r0TtSsWe5HonfOV116rLJeff +awrbD02TTqigzXsu8lkBarcNuAeBfos4GzjmCleZPe4h6KP1DBbdi+w0jpwqHAAVF41og9JwnxgI +zRFo1clrUs3ERo/ctfPYV3Me6ZQ5BL/T3jjetFPsaRyifsSP5BtwrfKi+fv3FmRmaZ9JUaLiFRhn +Bkp/1Wy1TbMz4GHrXb7pmA8y1x1LPC5aAVKRCfLf6o3YBkBjqhHk/sM3nhRSP/TizPJhk9H9Z2vX +Uq6/aKtAQ6BXNVN48FP4YUIHZMbXb5tMOA1jrGKvNouicwoN9SG9dKpN6nIDSdvHXx1iY8f93ZHs +M+71bbRuMGjeyNYmsHVee7QHIJihdjK4TWxPAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYD +VR0OBBYEFMmAd+BikoL1RpzzuvdMw964o605MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsF +AAOCAgEAU18h9bqwOlI5LJKwbADJ784g7wbylp7ppHR/ehb8t/W2+xUbP6umwHJdELFx7rxP462s +A20ucS6vxOOto70MEae0/0qyexAQH6dXQbLArvQsWdZHEIjzIVEpMMpghq9Gqx3tOluwlN5E40EI +osHsHdb9T7bWR9AUC8rmyrV7d35BH16Dx7aMOZawP5aBQW9gkOLo+fsicdl9sz1Gv7SEr5AcD48S +aq/v7h56rgJKihcrdv6sVIkkLE8/trKnToyokZf7KcZ7XC25y2a2t6hbElGFtQl+Ynhw/qlqYLYd +DnkM/crqJIByw5c/8nerQyIKx+u2DISCLIBrQYoIwOula9+ZEsuK1V6ADJHgJgg2SMX6OBE1/yWD +LfJ6v9r9jv6ly0UsH8SIU653DtmadsWOLB2jutXsMq7Aqqz30XpN69QH4kj3Io6wpJ9qzo6ysmD0 +oyLQI+uUWnpp3Q+/QFesa1lQ2aOZ4W7+jQF5JyMV3pKdewlNWudLSDBaGOYKbeaP4NK75t98biGC +wWg5TbSYWGZizEqQXsP6JwSxeRV0mcy+rSDeJmAc61ZRpqPq5KM/p/9h3PFaTWwyI0PurKju7koS +CTxdccK+efrCh2gdC/1cacwG0Jp9VJkqyTkaGa9LKkPzY11aWOIv4x3kqdbQCtCev9eBCfHJxyYN +rJgWVqA= +-----END CERTIFICATE----- + +Buypass Class 3 Root CA +======================= +-----BEGIN CERTIFICATE----- +MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEdMBsGA1UECgwU +QnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3MgQ2xhc3MgMyBSb290IENBMB4X +DTEwMTAyNjA4Mjg1OFoXDTQwMTAyNjA4Mjg1OFowTjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1 +eXBhc3MgQVMtOTgzMTYzMzI3MSAwHgYDVQQDDBdCdXlwYXNzIENsYXNzIDMgUm9vdCBDQTCCAiIw +DQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAKXaCpUWUOOV8l6ddjEGMnqb8RB2uACatVI2zSRH +sJ8YZLya9vrVediQYkwiL944PdbgqOkcLNt4EemOaFEVcsfzM4fkoF0LXOBXByow9c3EN3coTRiR +5r/VUv1xLXA+58bEiuPwKAv0dpihi4dVsjoT/Lc+JzeOIuOoTyrvYLs9tznDDgFHmV0ST9tD+leh +7fmdvhFHJlsTmKtdFoqwNxxXnUX/iJY2v7vKB3tvh2PX0DJq1l1sDPGzbjniazEuOQAnFN44wOwZ +ZoYS6J1yFhNkUsepNxz9gjDthBgd9K5c/3ATAOux9TN6S9ZV+AWNS2mw9bMoNlwUxFFzTWsL8TQH +2xc519woe2v1n/MuwU8XKhDzzMro6/1rqy6any2CbgTUUgGTLT2G/H783+9CHaZr77kgxve9oKeV +/afmiSTYzIw0bOIjL9kSGiG5VZFvC5F5GQytQIgLcOJ60g7YaEi7ghM5EFjp2CoHxhLbWNvSO1UQ +RwUVZ2J+GGOmRj8JDlQyXr8NYnon74Do29lLBlo3WiXQCBJ31G8JUJc9yB3D34xFMFbG02SrZvPA +Xpacw8Tvw3xrizp5f7NJzz3iiZ+gMEuFuZyUJHmPfWupRWgPK9Dx2hzLabjKSWJtyNBjYt1gD1iq +j6G8BaVmos8bdrKEZLFMOVLAMLrwjEsCsLa3AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYD +VR0OBBYEFEe4zf/lb+74suwvTg75JbCOPGvDMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsF +AAOCAgEAACAjQTUEkMJAYmDv4jVM1z+s4jSQuKFvdvoWFqRINyzpkMLyPPgKn9iB5btb2iUspKdV +cSQy9sgL8rxq+JOssgfCX5/bzMiKqr5qb+FJEMwx14C7u8jYog5kV+qi9cKpMRXSIGrs/CIBKM+G +uIAeqcwRpTzyFrNHnfzSgCHEy9BHcEGhyoMZCCxt8l13nIoUE9Q2HJLw5QY33KbmkJs4j1xrG0aG +Q0JfPgEHU1RdZX33inOhmlRaHylDFCfChQ+1iHsaO5S3HWCntZznKWlXWpuTekMwGwPXYshApqr8 +ZORK15FTAaggiG6cX0S5y2CBNOxv033aSF/rtJC8LakcC6wc1aJoIIAE1vyxjy+7SjENSoYc6+I2 +KSb12tjE8nVhz36udmNKekBlk4f4HoCMhuWG1o8O/FMsYOgWYRqiPkN7zTlgVGr18okmAWiDSKIz +6MkEkbIRNBE+6tBDGR8Dk5AM/1E9V/RBbuHLoL7ryWPNbczk+DaqaJ3tvV2XcEQNtg413OEMXbug +UZTLfhbrES+jkkXITHHZvMmZUldGL1DPvTVp9D0VzgalLA8+9oG6lLvDu79leNKGef9JOxqDDPDe +eOzI8k1MGt6CKfjBWtrt7uYnXuhF0J0cUahoq0Tj0Itq4/g7u9xN12TyUb7mqqta6THuBrxzvxNi +Cp/HuZc= +-----END CERTIFICATE----- + +T-TeleSec GlobalRoot Class 3 +============================ +-----BEGIN CERTIFICATE----- +MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoM +IlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBU +cnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDMwHhcNMDgx +MDAxMTAyOTU2WhcNMzMxMDAxMjM1OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lz +dGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBD +ZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDMwggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQC9dZPwYiJvJK7genasfb3ZJNW4t/zN8ELg63iIVl6bmlQdTQyK +9tPPcPRStdiTBONGhnFBSivwKixVA9ZIw+A5OO3yXDw/RLyTPWGrTs0NvvAgJ1gORH8EGoel15YU +NpDQSXuhdfsaa3Ox+M6pCSzyU9XDFES4hqX2iys52qMzVNn6chr3IhUciJFrf2blw2qAsCTz34ZF +iP0Zf3WHHx+xGwpzJFu5ZeAsVMhg02YXP+HMVDNzkQI6pn97djmiH5a2OK61yJN0HZ65tOVgnS9W +0eDrXltMEnAMbEQgqxHY9Bn20pxSN+f6tsIxO0rUFJmtxxr1XV/6B7h8DR/Wgx6zAgMBAAGjQjBA +MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS1A/d2O2GCahKqGFPr +AyGUv/7OyjANBgkqhkiG9w0BAQsFAAOCAQEAVj3vlNW92nOyWL6ukK2YJ5f+AbGwUgC4TeQbIXQb +fsDuXmkqJa9c1h3a0nnJ85cp4IaH3gRZD/FZ1GSFS5mvJQQeyUapl96Cshtwn5z2r3Ex3XsFpSzT +ucpH9sry9uetuUg/vBa3wW306gmv7PO15wWeph6KU1HWk4HMdJP2udqmJQV0eVp+QD6CSyYRMG7h +P0HHRwA11fXT91Q+gT3aSWqas+8QPebrb9HIIkfLzM8BMZLZGOMivgkeGj5asuRrDFR6fUNOuIml +e9eiPZaGzPImNC1qkp2aGtAw4l1OBLBfiyB+d8E9lYLRRpo7PHi4b6HQDWSieB4pTpPDpFQUWw== +-----END CERTIFICATE----- + +D-TRUST Root Class 3 CA 2 2009 +============================== +-----BEGIN CERTIFICATE----- +MIIEMzCCAxugAwIBAgIDCYPzMA0GCSqGSIb3DQEBCwUAME0xCzAJBgNVBAYTAkRFMRUwEwYDVQQK +DAxELVRydXN0IEdtYkgxJzAlBgNVBAMMHkQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgMjAwOTAe +Fw0wOTExMDUwODM1NThaFw0yOTExMDUwODM1NThaME0xCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxE +LVRydXN0IEdtYkgxJzAlBgNVBAMMHkQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgMjAwOTCCASIw +DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANOySs96R+91myP6Oi/WUEWJNTrGa9v+2wBoqOAD +ER03UAifTUpolDWzU9GUY6cgVq/eUXjsKj3zSEhQPgrfRlWLJ23DEE0NkVJD2IfgXU42tSHKXzlA +BF9bfsyjxiupQB7ZNoTWSPOSHjRGICTBpFGOShrvUD9pXRl/RcPHAY9RySPocq60vFYJfxLLHLGv +KZAKyVXMD9O0Gu1HNVpK7ZxzBCHQqr0ME7UAyiZsxGsMlFqVlNpQmvH/pStmMaTJOKDfHR+4CS7z +p+hnUquVH+BGPtikw8paxTGA6Eian5Rp/hnd2HN8gcqW3o7tszIFZYQ05ub9VxC1X3a/L7AQDcUC +AwEAAaOCARowggEWMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFP3aFMSfMN4hvR5COfyrYyNJ +4PGEMA4GA1UdDwEB/wQEAwIBBjCB0wYDVR0fBIHLMIHIMIGAoH6gfIZ6bGRhcDovL2RpcmVjdG9y +eS5kLXRydXN0Lm5ldC9DTj1ELVRSVVNUJTIwUm9vdCUyMENsYXNzJTIwMyUyMENBJTIwMiUyMDIw +MDksTz1ELVRydXN0JTIwR21iSCxDPURFP2NlcnRpZmljYXRlcmV2b2NhdGlvbmxpc3QwQ6BBoD+G +PWh0dHA6Ly93d3cuZC10cnVzdC5uZXQvY3JsL2QtdHJ1c3Rfcm9vdF9jbGFzc18zX2NhXzJfMjAw +OS5jcmwwDQYJKoZIhvcNAQELBQADggEBAH+X2zDI36ScfSF6gHDOFBJpiBSVYEQBrLLpME+bUMJm +2H6NMLVwMeniacfzcNsgFYbQDfC+rAF1hM5+n02/t2A7nPPKHeJeaNijnZflQGDSNiH+0LS4F9p0 +o3/U37CYAqxva2ssJSRyoWXuJVrl5jLn8t+rSfrzkGkj2wTZ51xY/GXUl77M/C4KzCUqNQT4YJEV +dT1B/yMfGchs64JTBKbkTCJNjYy6zltz7GRUUG3RnFX7acM2w4y8PIWmawomDeCTmGCufsYkl4ph +X5GOZpIJhzbNi5stPvZR1FDUWSi9g/LMKHtThm3YJohw1+qRzT65ysCQblrGXnRl11z+o+I= +-----END CERTIFICATE----- + +D-TRUST Root Class 3 CA 2 EV 2009 +================================= +-----BEGIN CERTIFICATE----- +MIIEQzCCAyugAwIBAgIDCYP0MA0GCSqGSIb3DQEBCwUAMFAxCzAJBgNVBAYTAkRFMRUwEwYDVQQK +DAxELVRydXN0IEdtYkgxKjAoBgNVBAMMIUQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgRVYgMjAw +OTAeFw0wOTExMDUwODUwNDZaFw0yOTExMDUwODUwNDZaMFAxCzAJBgNVBAYTAkRFMRUwEwYDVQQK +DAxELVRydXN0IEdtYkgxKjAoBgNVBAMMIUQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgRVYgMjAw +OTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJnxhDRwui+3MKCOvXwEz75ivJn9gpfS +egpnljgJ9hBOlSJzmY3aFS3nBfwZcyK3jpgAvDw9rKFs+9Z5JUut8Mxk2og+KbgPCdM03TP1YtHh +zRnp7hhPTFiu4h7WDFsVWtg6uMQYZB7jM7K1iXdODL/ZlGsTl28So/6ZqQTMFexgaDbtCHu39b+T +7WYxg4zGcTSHThfqr4uRjRxWQa4iN1438h3Z0S0NL2lRp75mpoo6Kr3HGrHhFPC+Oh25z1uxav60 +sUYgovseO3Dvk5h9jHOW8sXvhXCtKSb8HgQ+HKDYD8tSg2J87otTlZCpV6LqYQXY+U3EJ/pure35 +11H3a6UCAwEAAaOCASQwggEgMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFNOUikxiEyoZLsyv +cop9NteaHNxnMA4GA1UdDwEB/wQEAwIBBjCB3QYDVR0fBIHVMIHSMIGHoIGEoIGBhn9sZGFwOi8v +ZGlyZWN0b3J5LmQtdHJ1c3QubmV0L0NOPUQtVFJVU1QlMjBSb290JTIwQ2xhc3MlMjAzJTIwQ0El +MjAyJTIwRVYlMjAyMDA5LE89RC1UcnVzdCUyMEdtYkgsQz1ERT9jZXJ0aWZpY2F0ZXJldm9jYXRp +b25saXN0MEagRKBChkBodHRwOi8vd3d3LmQtdHJ1c3QubmV0L2NybC9kLXRydXN0X3Jvb3RfY2xh +c3NfM19jYV8yX2V2XzIwMDkuY3JsMA0GCSqGSIb3DQEBCwUAA4IBAQA07XtaPKSUiO8aEXUHL7P+ +PPoeUSbrh/Yp3uDx1MYkCenBz1UbtDDZzhr+BlGmFaQt77JLvyAoJUnRpjZ3NOhk31KxEcdzes05 +nsKtjHEh8lprr988TlWvsoRlFIm5d8sqMb7Po23Pb0iUMkZv53GMoKaEGTcH8gNFCSuGdXzfX2lX +ANtu2KZyIktQ1HWYVt+3GP9DQ1CuekR78HlR10M9p9OB0/DJT7naxpeG0ILD5EJt/rDiZE4OJudA +NCa1CInXCGNjOCd1HjPqbqjdn5lPdE2BiYBL3ZqXKVwvvoFBuYz/6n1gBp7N1z3TLqMVvKjmJuVv +w9y4AyHqnxbxLFS1 +-----END CERTIFICATE----- + +CA Disig Root R2 +================ +-----BEGIN CERTIFICATE----- +MIIFaTCCA1GgAwIBAgIJAJK4iNuwisFjMA0GCSqGSIb3DQEBCwUAMFIxCzAJBgNVBAYTAlNLMRMw +EQYDVQQHEwpCcmF0aXNsYXZhMRMwEQYDVQQKEwpEaXNpZyBhLnMuMRkwFwYDVQQDExBDQSBEaXNp +ZyBSb290IFIyMB4XDTEyMDcxOTA5MTUzMFoXDTQyMDcxOTA5MTUzMFowUjELMAkGA1UEBhMCU0sx +EzARBgNVBAcTCkJyYXRpc2xhdmExEzARBgNVBAoTCkRpc2lnIGEucy4xGTAXBgNVBAMTEENBIERp +c2lnIFJvb3QgUjIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCio8QACdaFXS1tFPbC +w3OeNcJxVX6B+6tGUODBfEl45qt5WDza/3wcn9iXAng+a0EE6UG9vgMsRfYvZNSrXaNHPWSb6Wia +xswbP7q+sos0Ai6YVRn8jG+qX9pMzk0DIaPY0jSTVpbLTAwAFjxfGs3Ix2ymrdMxp7zo5eFm1tL7 +A7RBZckQrg4FY8aAamkw/dLukO8NJ9+flXP04SXabBbeQTg06ov80egEFGEtQX6sx3dOy1FU+16S +GBsEWmjGycT6txOgmLcRK7fWV8x8nhfRyyX+hk4kLlYMeE2eARKmK6cBZW58Yh2EhN/qwGu1pSqV +g8NTEQxzHQuyRpDRQjrOQG6Vrf/GlK1ul4SOfW+eioANSW1z4nuSHsPzwfPrLgVv2RvPN3YEyLRa +5Beny912H9AZdugsBbPWnDTYltxhh5EF5EQIM8HauQhl1K6yNg3ruji6DOWbnuuNZt2Zz9aJQfYE +koopKW1rOhzndX0CcQ7zwOe9yxndnWCywmZgtrEE7snmhrmaZkCo5xHtgUUDi/ZnWejBBhG93c+A +Ak9lQHhcR1DIm+YfgXvkRKhbhZri3lrVx/k6RGZL5DJUfORsnLMOPReisjQS1n6yqEm70XooQL6i +Fh/f5DcfEXP7kAplQ6INfPgGAVUzfbANuPT1rqVCV3w2EYx7XsQDnYx5nQIDAQABo0IwQDAPBgNV +HRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUtZn4r7CU9eMg1gqtzk5WpC5u +Qu0wDQYJKoZIhvcNAQELBQADggIBACYGXnDnZTPIgm7ZnBc6G3pmsgH2eDtpXi/q/075KMOYKmFM +tCQSin1tERT3nLXK5ryeJ45MGcipvXrA1zYObYVybqjGom32+nNjf7xueQgcnYqfGopTpti72TVV +sRHFqQOzVju5hJMiXn7B9hJSi+osZ7z+Nkz1uM/Rs0mSO9MpDpkblvdhuDvEK7Z4bLQjb/D907Je +dR+Zlais9trhxTF7+9FGs9K8Z7RiVLoJ92Owk6Ka+elSLotgEqv89WBW7xBci8QaQtyDW2QOy7W8 +1k/BfDxujRNt+3vrMNDcTa/F1balTFtxyegxvug4BkihGuLq0t4SOVga/4AOgnXmt8kHbA7v/zjx +mHHEt38OFdAlab0inSvtBfZGR6ztwPDUO+Ls7pZbkBNOHlY667DvlruWIxG68kOGdGSVyCh13x01 +utI3gzhTODY7z2zp+WsO0PsE6E9312UBeIYMej4hYvF/Y3EMyZ9E26gnonW+boE+18DrG5gPcFw0 +sorMwIUY6256s/daoQe/qUKS82Ail+QUoQebTnbAjn39pCXHR+3/H3OszMOl6W8KjptlwlCFtaOg +UxLMVYdh84GuEEZhvUQhuMI9dM9+JDX6HAcOmz0iyu8xL4ysEr3vQCj8KWefshNPZiTEUxnpHikV +7+ZtsH8tZ/3zbBt1RqPlShfppNcL +-----END CERTIFICATE----- + +ACCVRAIZ1 +========= +-----BEGIN CERTIFICATE----- +MIIH0zCCBbugAwIBAgIIXsO3pkN/pOAwDQYJKoZIhvcNAQEFBQAwQjESMBAGA1UEAwwJQUNDVlJB +SVoxMRAwDgYDVQQLDAdQS0lBQ0NWMQ0wCwYDVQQKDARBQ0NWMQswCQYDVQQGEwJFUzAeFw0xMTA1 +MDUwOTM3MzdaFw0zMDEyMzEwOTM3MzdaMEIxEjAQBgNVBAMMCUFDQ1ZSQUlaMTEQMA4GA1UECwwH +UEtJQUNDVjENMAsGA1UECgwEQUNDVjELMAkGA1UEBhMCRVMwggIiMA0GCSqGSIb3DQEBAQUAA4IC +DwAwggIKAoICAQCbqau/YUqXry+XZpp0X9DZlv3P4uRm7x8fRzPCRKPfmt4ftVTdFXxpNRFvu8gM +jmoYHtiP2Ra8EEg2XPBjs5BaXCQ316PWywlxufEBcoSwfdtNgM3802/J+Nq2DoLSRYWoG2ioPej0 +RGy9ocLLA76MPhMAhN9KSMDjIgro6TenGEyxCQ0jVn8ETdkXhBilyNpAlHPrzg5XPAOBOp0KoVdD +aaxXbXmQeOW1tDvYvEyNKKGno6e6Ak4l0Squ7a4DIrhrIA8wKFSVf+DuzgpmndFALW4ir50awQUZ +0m/A8p/4e7MCQvtQqR0tkw8jq8bBD5L/0KIV9VMJcRz/RROE5iZe+OCIHAr8Fraocwa48GOEAqDG +WuzndN9wrqODJerWx5eHk6fGioozl2A3ED6XPm4pFdahD9GILBKfb6qkxkLrQaLjlUPTAYVtjrs7 +8yM2x/474KElB0iryYl0/wiPgL/AlmXz7uxLaL2diMMxs0Dx6M/2OLuc5NF/1OVYm3z61PMOm3WR +5LpSLhl+0fXNWhn8ugb2+1KoS5kE3fj5tItQo05iifCHJPqDQsGH+tUtKSpacXpkatcnYGMN285J +9Y0fkIkyF/hzQ7jSWpOGYdbhdQrqeWZ2iE9x6wQl1gpaepPluUsXQA+xtrn13k/c4LOsOxFwYIRK +Q26ZIMApcQrAZQIDAQABo4ICyzCCAscwfQYIKwYBBQUHAQEEcTBvMEwGCCsGAQUFBzAChkBodHRw +Oi8vd3d3LmFjY3YuZXMvZmlsZWFkbWluL0FyY2hpdm9zL2NlcnRpZmljYWRvcy9yYWl6YWNjdjEu +Y3J0MB8GCCsGAQUFBzABhhNodHRwOi8vb2NzcC5hY2N2LmVzMB0GA1UdDgQWBBTSh7Tj3zcnk1X2 +VuqB5TbMjB4/vTAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFNKHtOPfNyeTVfZW6oHlNsyM +Hj+9MIIBcwYDVR0gBIIBajCCAWYwggFiBgRVHSAAMIIBWDCCASIGCCsGAQUFBwICMIIBFB6CARAA +QQB1AHQAbwByAGkAZABhAGQAIABkAGUAIABDAGUAcgB0AGkAZgBpAGMAYQBjAGkA8wBuACAAUgBh +AO0AegAgAGQAZQAgAGwAYQAgAEEAQwBDAFYAIAAoAEEAZwBlAG4AYwBpAGEAIABkAGUAIABUAGUA +YwBuAG8AbABvAGcA7QBhACAAeQAgAEMAZQByAHQAaQBmAGkAYwBhAGMAaQDzAG4AIABFAGwAZQBj +AHQAcgDzAG4AaQBjAGEALAAgAEMASQBGACAAUQA0ADYAMAAxADEANQA2AEUAKQAuACAAQwBQAFMA +IABlAG4AIABoAHQAdABwADoALwAvAHcAdwB3AC4AYQBjAGMAdgAuAGUAczAwBggrBgEFBQcCARYk +aHR0cDovL3d3dy5hY2N2LmVzL2xlZ2lzbGFjaW9uX2MuaHRtMFUGA1UdHwROMEwwSqBIoEaGRGh0 +dHA6Ly93d3cuYWNjdi5lcy9maWxlYWRtaW4vQXJjaGl2b3MvY2VydGlmaWNhZG9zL3JhaXphY2N2 +MV9kZXIuY3JsMA4GA1UdDwEB/wQEAwIBBjAXBgNVHREEEDAOgQxhY2N2QGFjY3YuZXMwDQYJKoZI +hvcNAQEFBQADggIBAJcxAp/n/UNnSEQU5CmH7UwoZtCPNdpNYbdKl02125DgBS4OxnnQ8pdpD70E +R9m+27Up2pvZrqmZ1dM8MJP1jaGo/AaNRPTKFpV8M9xii6g3+CfYCS0b78gUJyCpZET/LtZ1qmxN +YEAZSUNUY9rizLpm5U9EelvZaoErQNV/+QEnWCzI7UiRfD+mAM/EKXMRNt6GGT6d7hmKG9Ww7Y49 +nCrADdg9ZuM8Db3VlFzi4qc1GwQA9j9ajepDvV+JHanBsMyZ4k0ACtrJJ1vnE5Bc5PUzolVt3OAJ +TS+xJlsndQAJxGJ3KQhfnlmstn6tn1QwIgPBHnFk/vk4CpYY3QIUrCPLBhwepH2NDd4nQeit2hW3 +sCPdK6jT2iWH7ehVRE2I9DZ+hJp4rPcOVkkO1jMl1oRQQmwgEh0q1b688nCBpHBgvgW1m54ERL5h +I6zppSSMEYCUWqKiuUnSwdzRp+0xESyeGabu4VXhwOrPDYTkF7eifKXeVSUG7szAh1xA2syVP1Xg +Nce4hL60Xc16gwFy7ofmXx2utYXGJt/mwZrpHgJHnyqobalbz+xFd3+YJ5oyXSrjhO7FmGYvliAd +3djDJ9ew+f7Zfc3Qn48LFFhRny+Lwzgt3uiP1o2HpPVWQxaZLPSkVrQ0uGE3ycJYgBugl6H8WY3p +EfbRD0tVNEYqi4Y7 +-----END CERTIFICATE----- + +TWCA Global Root CA +=================== +-----BEGIN CERTIFICATE----- +MIIFQTCCAymgAwIBAgICDL4wDQYJKoZIhvcNAQELBQAwUTELMAkGA1UEBhMCVFcxEjAQBgNVBAoT +CVRBSVdBTi1DQTEQMA4GA1UECxMHUm9vdCBDQTEcMBoGA1UEAxMTVFdDQSBHbG9iYWwgUm9vdCBD +QTAeFw0xMjA2MjcwNjI4MzNaFw0zMDEyMzExNTU5NTlaMFExCzAJBgNVBAYTAlRXMRIwEAYDVQQK +EwlUQUlXQU4tQ0ExEDAOBgNVBAsTB1Jvb3QgQ0ExHDAaBgNVBAMTE1RXQ0EgR2xvYmFsIFJvb3Qg +Q0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCwBdvI64zEbooh745NnHEKH1Jw7W2C +nJfF10xORUnLQEK1EjRsGcJ0pDFfhQKX7EMzClPSnIyOt7h52yvVavKOZsTuKwEHktSz0ALfUPZV +r2YOy+BHYC8rMjk1Ujoog/h7FsYYuGLWRyWRzvAZEk2tY/XTP3VfKfChMBwqoJimFb3u/Rk28OKR +Q4/6ytYQJ0lM793B8YVwm8rqqFpD/G2Gb3PpN0Wp8DbHzIh1HrtsBv+baz4X7GGqcXzGHaL3SekV +tTzWoWH1EfcFbx39Eb7QMAfCKbAJTibc46KokWofwpFFiFzlmLhxpRUZyXx1EcxwdE8tmx2RRP1W +KKD+u4ZqyPpcC1jcxkt2yKsi2XMPpfRaAok/T54igu6idFMqPVMnaR1sjjIsZAAmY2E2TqNGtz99 +sy2sbZCilaLOz9qC5wc0GZbpuCGqKX6mOL6OKUohZnkfs8O1CWfe1tQHRvMq2uYiN2DLgbYPoA/p +yJV/v1WRBXrPPRXAb94JlAGD1zQbzECl8LibZ9WYkTunhHiVJqRaCPgrdLQABDzfuBSO6N+pjWxn +kjMdwLfS7JLIvgm/LCkFbwJrnu+8vyq8W8BQj0FwcYeyTbcEqYSjMq+u7msXi7Kx/mzhkIyIqJdI +zshNy/MGz19qCkKxHh53L46g5pIOBvwFItIm4TFRfTLcDwIDAQABoyMwITAOBgNVHQ8BAf8EBAMC +AQYwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAgEAXzSBdu+WHdXltdkCY4QWwa6g +cFGn90xHNcgL1yg9iXHZqjNB6hQbbCEAwGxCGX6faVsgQt+i0trEfJdLjbDorMjupWkEmQqSpqsn +LhpNgb+E1HAerUf+/UqdM+DyucRFCCEK2mlpc3INvjT+lIutwx4116KD7+U4x6WFH6vPNOw/KP4M +8VeGTslV9xzU2KV9Bnpv1d8Q34FOIWWxtuEXeZVFBs5fzNxGiWNoRI2T9GRwoD2dKAXDOXC4Ynsg +/eTb6QihuJ49CcdP+yz4k3ZB3lLg4VfSnQO8d57+nile98FRYB/e2guyLXW3Q0iT5/Z5xoRdgFlg +lPx4mI88k1HtQJAH32RjJMtOcQWh15QaiDLxInQirqWm2BJpTGCjAu4r7NRjkgtevi92a6O2JryP +A9gK8kxkRr05YuWW6zRjESjMlfGt7+/cgFhI6Uu46mWs6fyAtbXIRfmswZ/ZuepiiI7E8UuDEq3m +i4TWnsLrgxifarsbJGAzcMzs9zLzXNl5fe+epP7JI8Mk7hWSsT2RTyaGvWZzJBPqpK5jwa19hAM8 +EHiGG3njxPPyBJUgriOCxLM6AGK/5jYk4Ve6xx6QddVfP5VhK8E7zeWzaGHQRiapIVJpLesux+t3 +zqY6tQMzT3bR51xUAV3LePTJDL/PEo4XLSNolOer/qmyKwbQBM0= +-----END CERTIFICATE----- + +T-TeleSec GlobalRoot Class 2 +============================ +-----BEGIN CERTIFICATE----- +MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoM +IlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBU +cnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDIwHhcNMDgx +MDAxMTA0MDE0WhcNMzMxMDAxMjM1OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lz +dGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBD +ZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDIwggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQCqX9obX+hzkeXaXPSi5kfl82hVYAUdAqSzm1nzHoqvNK38DcLZ +SBnuaY/JIPwhqgcZ7bBcrGXHX+0CfHt8LRvWurmAwhiCFoT6ZrAIxlQjgeTNuUk/9k9uN0goOA/F +vudocP05l03Sx5iRUKrERLMjfTlH6VJi1hKTXrcxlkIF+3anHqP1wvzpesVsqXFP6st4vGCvx970 +2cu+fjOlbpSD8DT6IavqjnKgP6TeMFvvhk1qlVtDRKgQFRzlAVfFmPHmBiiRqiDFt1MmUUOyCxGV +WOHAD3bZwI18gfNycJ5v/hqO2V81xrJvNHy+SE/iWjnX2J14np+GPgNeGYtEotXHAgMBAAGjQjBA +MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS/WSA2AHmgoCJrjNXy +YdK4LMuCSjANBgkqhkiG9w0BAQsFAAOCAQEAMQOiYQsfdOhyNsZt+U2e+iKo4YFWz827n+qrkRk4 +r6p8FU3ztqONpfSO9kSpp+ghla0+AGIWiPACuvxhI+YzmzB6azZie60EI4RYZeLbK4rnJVM3YlNf +vNoBYimipidx5joifsFvHZVwIEoHNN/q/xWA5brXethbdXwFeilHfkCoMRN3zUA7tFFHei4R40cR +3p1m0IvVVGb6g1XqfMIpiRvpb7PO4gWEyS8+eIVibslfwXhjdFjASBgMmTnrpMwatXlajRWc2BQN +9noHV8cigwUtPJslJj0Ys6lDfMjIq2SPDqO/nBudMNva0Bkuqjzx+zOAduTNrRlPBSeOE6Fuwg== +-----END CERTIFICATE----- + +Atos TrustedRoot 2011 +===================== +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIIXDPLYixfszIwDQYJKoZIhvcNAQELBQAwPDEeMBwGA1UEAwwVQXRvcyBU +cnVzdGVkUm9vdCAyMDExMQ0wCwYDVQQKDARBdG9zMQswCQYDVQQGEwJERTAeFw0xMTA3MDcxNDU4 +MzBaFw0zMDEyMzEyMzU5NTlaMDwxHjAcBgNVBAMMFUF0b3MgVHJ1c3RlZFJvb3QgMjAxMTENMAsG +A1UECgwEQXRvczELMAkGA1UEBhMCREUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCV +hTuXbyo7LjvPpvMpNb7PGKw+qtn4TaA+Gke5vJrf8v7MPkfoepbCJI419KkM/IL9bcFyYie96mvr +54rMVD6QUM+A1JX76LWC1BTFtqlVJVfbsVD2sGBkWXppzwO3bw2+yj5vdHLqqjAqc2K+SZFhyBH+ +DgMq92og3AIVDV4VavzjgsG1xZ1kCWyjWZgHJ8cblithdHFsQ/H3NYkQ4J7sVaE3IqKHBAUsR320 +HLliKWYoyrfhk/WklAOZuXCFteZI6o1Q/NnezG8HDt0Lcp2AMBYHlT8oDv3FdU9T1nSatCQujgKR +z3bFmx5VdJx4IbHwLfELn8LVlhgf8FQieowHAgMBAAGjfTB7MB0GA1UdDgQWBBSnpQaxLKYJYO7R +l+lwrrw7GWzbITAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFKelBrEspglg7tGX6XCuvDsZ +bNshMBgGA1UdIAQRMA8wDQYLKwYBBAGwLQMEAQEwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEB +CwUAA4IBAQAmdzTblEiGKkGdLD4GkGDEjKwLVLgfuXvTBznk+j57sj1O7Z8jvZfza1zv7v1Apt+h +k6EKhqzvINB5Ab149xnYJDE0BAGmuhWawyfc2E8PzBhj/5kPDpFrdRbhIfzYJsdHt6bPWHJxfrrh +TZVHO8mvbaG0weyJ9rQPOLXiZNwlz6bb65pcmaHFCN795trV1lpFDMS3wrUU77QR/w4VtfX128a9 +61qn8FYiqTxlVMYVqL2Gns2Dlmh6cYGJ4Qvh6hEbaAjMaZ7snkGeRDImeuKHCnE96+RapNLbxc3G +3mB/ufNPRJLvKrcYPqcZ2Qt9sTdBQrC6YB3y/gkRsPCHe6ed +-----END CERTIFICATE----- + +QuoVadis Root CA 1 G3 +===================== +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIUeFhfLq0sGUvjNwc1NBMotZbUZZMwDQYJKoZIhvcNAQELBQAwSDELMAkG +A1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAcBgNVBAMTFVF1b1ZhZGlzIFJv +b3QgQ0EgMSBHMzAeFw0xMjAxMTIxNzI3NDRaFw00MjAxMTIxNzI3NDRaMEgxCzAJBgNVBAYTAkJN +MRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDEg +RzMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCgvlAQjunybEC0BJyFuTHK3C3kEakE +PBtVwedYMB0ktMPvhd6MLOHBPd+C5k+tR4ds7FtJwUrVu4/sh6x/gpqG7D0DmVIB0jWerNrwU8lm +PNSsAgHaJNM7qAJGr6Qc4/hzWHa39g6QDbXwz8z6+cZM5cOGMAqNF34168Xfuw6cwI2H44g4hWf6 +Pser4BOcBRiYz5P1sZK0/CPTz9XEJ0ngnjybCKOLXSoh4Pw5qlPafX7PGglTvF0FBM+hSo+LdoIN +ofjSxxR3W5A2B4GbPgb6Ul5jxaYA/qXpUhtStZI5cgMJYr2wYBZupt0lwgNm3fME0UDiTouG9G/l +g6AnhF4EwfWQvTA9xO+oabw4m6SkltFi2mnAAZauy8RRNOoMqv8hjlmPSlzkYZqn0ukqeI1RPToV +7qJZjqlc3sX5kCLliEVx3ZGZbHqfPT2YfF72vhZooF6uCyP8Wg+qInYtyaEQHeTTRCOQiJ/GKubX +9ZqzWB4vMIkIG1SitZgj7Ah3HJVdYdHLiZxfokqRmu8hqkkWCKi9YSgxyXSthfbZxbGL0eUQMk1f +iyA6PEkfM4VZDdvLCXVDaXP7a3F98N/ETH3Goy7IlXnLc6KOTk0k+17kBL5yG6YnLUlamXrXXAkg +t3+UuU/xDRxeiEIbEbfnkduebPRq34wGmAOtzCjvpUfzUwIDAQABo0IwQDAPBgNVHRMBAf8EBTAD +AQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUo5fW816iEOGrRZ88F2Q87gFwnMwwDQYJKoZI +hvcNAQELBQADggIBABj6W3X8PnrHX3fHyt/PX8MSxEBd1DKquGrX1RUVRpgjpeaQWxiZTOOtQqOC +MTaIzen7xASWSIsBx40Bz1szBpZGZnQdT+3Btrm0DWHMY37XLneMlhwqI2hrhVd2cDMT/uFPpiN3 +GPoajOi9ZcnPP/TJF9zrx7zABC4tRi9pZsMbj/7sPtPKlL92CiUNqXsCHKnQO18LwIE6PWThv6ct +Tr1NxNgpxiIY0MWscgKCP6o6ojoilzHdCGPDdRS5YCgtW2jgFqlmgiNR9etT2DGbe+m3nUvriBbP ++V04ikkwj+3x6xn0dxoxGE1nVGwvb2X52z3sIexe9PSLymBlVNFxZPT5pqOBMzYzcfCkeF9OrYMh +3jRJjehZrJ3ydlo28hP0r+AJx2EqbPfgna67hkooby7utHnNkDPDs3b69fBsnQGQ+p6Q9pxyz0fa +wx/kNSBT8lTR32GDpgLiJTjehTItXnOQUl1CxM49S+H5GYQd1aJQzEH7QRTDvdbJWqNjZgKAvQU6 +O0ec7AAmTPWIUb+oI38YB7AL7YsmoWTTYUrrXJ/es69nA7Mf3W1daWhpq1467HxpvMc7hU6eFbm0 +FU/DlXpY18ls6Wy58yljXrQs8C097Vpl4KlbQMJImYFtnh8GKjwStIsPm6Ik8KaN1nrgS7ZklmOV +hMJKzRwuJIczYOXD +-----END CERTIFICATE----- + +QuoVadis Root CA 2 G3 +===================== +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIURFc0JFuBiZs18s64KztbpybwdSgwDQYJKoZIhvcNAQELBQAwSDELMAkG +A1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAcBgNVBAMTFVF1b1ZhZGlzIFJv +b3QgQ0EgMiBHMzAeFw0xMjAxMTIxODU5MzJaFw00MjAxMTIxODU5MzJaMEgxCzAJBgNVBAYTAkJN +MRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDIg +RzMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQChriWyARjcV4g/Ruv5r+LrI3HimtFh +ZiFfqq8nUeVuGxbULX1QsFN3vXg6YOJkApt8hpvWGo6t/x8Vf9WVHhLL5hSEBMHfNrMWn4rjyduY +NM7YMxcoRvynyfDStNVNCXJJ+fKH46nafaF9a7I6JaltUkSs+L5u+9ymc5GQYaYDFCDy54ejiK2t +oIz/pgslUiXnFgHVy7g1gQyjO/Dh4fxaXc6AcW34Sas+O7q414AB+6XrW7PFXmAqMaCvN+ggOp+o +MiwMzAkd056OXbxMmO7FGmh77FOm6RQ1o9/NgJ8MSPsc9PG/Srj61YxxSscfrf5BmrODXfKEVu+l +V0POKa2Mq1W/xPtbAd0jIaFYAI7D0GoT7RPjEiuA3GfmlbLNHiJuKvhB1PLKFAeNilUSxmn1uIZo +L1NesNKqIcGY5jDjZ1XHm26sGahVpkUG0CM62+tlXSoREfA7T8pt9DTEceT/AFr2XK4jYIVz8eQQ +sSWu1ZK7E8EM4DnatDlXtas1qnIhO4M15zHfeiFuuDIIfR0ykRVKYnLP43ehvNURG3YBZwjgQQvD +6xVu+KQZ2aKrr+InUlYrAoosFCT5v0ICvybIxo/gbjh9Uy3l7ZizlWNof/k19N+IxWA1ksB8aRxh +lRbQ694Lrz4EEEVlWFA4r0jyWbYW8jwNkALGcC4BrTwV1wIDAQABo0IwQDAPBgNVHRMBAf8EBTAD +AQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQU7edvdlq/YOxJW8ald7tyFnGbxD0wDQYJKoZI +hvcNAQELBQADggIBAJHfgD9DCX5xwvfrs4iP4VGyvD11+ShdyLyZm3tdquXK4Qr36LLTn91nMX66 +AarHakE7kNQIXLJgapDwyM4DYvmL7ftuKtwGTTwpD4kWilhMSA/ohGHqPHKmd+RCroijQ1h5fq7K +pVMNqT1wvSAZYaRsOPxDMuHBR//47PERIjKWnML2W2mWeyAMQ0GaW/ZZGYjeVYg3UQt4XAoeo0L9 +x52ID8DyeAIkVJOviYeIyUqAHerQbj5hLja7NQ4nlv1mNDthcnPxFlxHBlRJAHpYErAK74X9sbgz +dWqTHBLmYF5vHX/JHyPLhGGfHoJE+V+tYlUkmlKY7VHnoX6XOuYvHxHaU4AshZ6rNRDbIl9qxV6X +U/IyAgkwo1jwDQHVcsaxfGl7w/U2Rcxhbl5MlMVerugOXou/983g7aEOGzPuVBj+D77vfoRrQ+Nw +mNtddbINWQeFFSM51vHfqSYP1kjHs6Yi9TM3WpVHn3u6GBVv/9YUZINJ0gpnIdsPNWNgKCLjsZWD +zYWm3S8P52dSbrsvhXz1SnPnxT7AvSESBT/8twNJAlvIJebiVDj1eYeMHVOyToV7BjjHLPj4sHKN +JeV3UvQDHEimUF+IIDBu8oJDqz2XhOdT+yHBTw8imoa4WSr2Rz0ZiC3oheGe7IUIarFsNMkd7Egr +O3jtZsSOeWmD3n+M +-----END CERTIFICATE----- + +QuoVadis Root CA 3 G3 +===================== +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIULvWbAiin23r/1aOp7r0DoM8Sah0wDQYJKoZIhvcNAQELBQAwSDELMAkG +A1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAcBgNVBAMTFVF1b1ZhZGlzIFJv +b3QgQ0EgMyBHMzAeFw0xMjAxMTIyMDI2MzJaFw00MjAxMTIyMDI2MzJaMEgxCzAJBgNVBAYTAkJN +MRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDMg +RzMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCzyw4QZ47qFJenMioKVjZ/aEzHs286 +IxSR/xl/pcqs7rN2nXrpixurazHb+gtTTK/FpRp5PIpM/6zfJd5O2YIyC0TeytuMrKNuFoM7pmRL +Mon7FhY4futD4tN0SsJiCnMK3UmzV9KwCoWdcTzeo8vAMvMBOSBDGzXRU7Ox7sWTaYI+FrUoRqHe +6okJ7UO4BUaKhvVZR74bbwEhELn9qdIoyhA5CcoTNs+cra1AdHkrAj80//ogaX3T7mH1urPnMNA3 +I4ZyYUUpSFlob3emLoG+B01vr87ERRORFHAGjx+f+IdpsQ7vw4kZ6+ocYfx6bIrc1gMLnia6Et3U +VDmrJqMz6nWB2i3ND0/kA9HvFZcba5DFApCTZgIhsUfei5pKgLlVj7WiL8DWM2fafsSntARE60f7 +5li59wzweyuxwHApw0BiLTtIadwjPEjrewl5qW3aqDCYz4ByA4imW0aucnl8CAMhZa634RylsSqi +Md5mBPfAdOhx3v89WcyWJhKLhZVXGqtrdQtEPREoPHtht+KPZ0/l7DxMYIBpVzgeAVuNVejH38DM +dyM0SXV89pgR6y3e7UEuFAUCf+D+IOs15xGsIs5XPd7JMG0QA4XN8f+MFrXBsj6IbGB/kE+V9/Yt +rQE5BwT6dYB9v0lQ7e/JxHwc64B+27bQ3RP+ydOc17KXqQIDAQABo0IwQDAPBgNVHRMBAf8EBTAD +AQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUxhfQvKjqAkPyGwaZXSuQILnXnOQwDQYJKoZI +hvcNAQELBQADggIBADRh2Va1EodVTd2jNTFGu6QHcrxfYWLopfsLN7E8trP6KZ1/AvWkyaiTt3px +KGmPc+FSkNrVvjrlt3ZqVoAh313m6Tqe5T72omnHKgqwGEfcIHB9UqM+WXzBusnIFUBhynLWcKzS +t/Ac5IYp8M7vaGPQtSCKFWGafoaYtMnCdvvMujAWzKNhxnQT5WvvoxXqA/4Ti2Tk08HS6IT7SdEQ +TXlm66r99I0xHnAUrdzeZxNMgRVhvLfZkXdxGYFgu/BYpbWcC/ePIlUnwEsBbTuZDdQdm2NnL9Du +DcpmvJRPpq3t/O5jrFc/ZSXPsoaP0Aj/uHYUbt7lJ+yreLVTubY/6CD50qi+YUbKh4yE8/nxoGib +Ih6BJpsQBJFxwAYf3KDTuVan45gtf4Od34wrnDKOMpTwATwiKp9Dwi7DmDkHOHv8XgBCH/MyJnmD +hPbl8MFREsALHgQjDFSlTC9JxUrRtm5gDWv8a4uFJGS3iQ6rJUdbPM9+Sb3H6QrG2vd+DhcI00iX +0HGS8A85PjRqHH3Y8iKuu2n0M7SmSFXRDw4m6Oy2Cy2nhTXN/VnIn9HNPlopNLk9hM6xZdRZkZFW +dSHBd575euFgndOtBBj0fOtek49TSiIp+EgrPk2GrFt/ywaZWWDYWGWVjUTR939+J399roD1B0y2 +PpxxVJkES/1Y+Zj0 +-----END CERTIFICATE----- + +DigiCert Assured ID Root G2 +=========================== +-----BEGIN CERTIFICATE----- +MIIDljCCAn6gAwIBAgIQC5McOtY5Z+pnI7/Dr5r0SzANBgkqhkiG9w0BAQsFADBlMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSQw +IgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzIwHhcNMTMwODAxMTIwMDAwWhcNMzgw +MTE1MTIwMDAwWjBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQL +ExB3d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzIw +ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDZ5ygvUj82ckmIkzTz+GoeMVSAn61UQbVH +35ao1K+ALbkKz3X9iaV9JPrjIgwrvJUXCzO/GU1BBpAAvQxNEP4HteccbiJVMWWXvdMX0h5i89vq +bFCMP4QMls+3ywPgym2hFEwbid3tALBSfK+RbLE4E9HpEgjAALAcKxHad3A2m67OeYfcgnDmCXRw +VWmvo2ifv922ebPynXApVfSr/5Vh88lAbx3RvpO704gqu52/clpWcTs/1PPRCv4o76Pu2ZmvA9OP +YLfykqGxvYmJHzDNw6YuYjOuFgJ3RFrngQo8p0Quebg/BLxcoIfhG69Rjs3sLPr4/m3wOnyqi+Rn +lTGNAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBTO +w0q5mVXyuNtgv6l+vVa1lzan1jANBgkqhkiG9w0BAQsFAAOCAQEAyqVVjOPIQW5pJ6d1Ee88hjZv +0p3GeDgdaZaikmkuOGybfQTUiaWxMTeKySHMq2zNixya1r9I0jJmwYrA8y8678Dj1JGG0VDjA9tz +d29KOVPt3ibHtX2vK0LRdWLjSisCx1BL4GnilmwORGYQRI+tBev4eaymG+g3NJ1TyWGqolKvSnAW +hsI6yLETcDbYz+70CjTVW0z9B5yiutkBclzzTcHdDrEcDcRjvq30FPuJ7KJBDkzMyFdA0G4Dqs0M +jomZmWzwPDCvON9vvKO+KSAnq3T/EyJ43pdSVR6DtVQgA+6uwE9W3jfMw3+qBCe703e4YtsXfJwo +IhNzbM8m9Yop5w== +-----END CERTIFICATE----- + +DigiCert Assured ID Root G3 +=========================== +-----BEGIN CERTIFICATE----- +MIICRjCCAc2gAwIBAgIQC6Fa+h3foLVJRK/NJKBs7DAKBggqhkjOPQQDAzBlMQswCQYDVQQGEwJV +UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSQwIgYD +VQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzMwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1 +MTIwMDAwWjBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzMwdjAQ +BgcqhkjOPQIBBgUrgQQAIgNiAAQZ57ysRGXtzbg/WPuNsVepRC0FFfLvC/8QdJ+1YlJfZn4f5dwb +RXkLzMZTCp2NXQLZqVneAlr2lSoOjThKiknGvMYDOAdfVdp+CW7if17QRSAPWXYQ1qAk8C3eNvJs +KTmjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBTL0L2p4ZgF +UaFNN6KDec6NHSrkhDAKBggqhkjOPQQDAwNnADBkAjAlpIFFAmsSS3V0T8gj43DydXLefInwz5Fy +YZ5eEJJZVrmDxxDnOOlYJjZ91eQ0hjkCMHw2U/Aw5WJjOpnitqM7mzT6HtoQknFekROn3aRukswy +1vUhZscv6pZjamVFkpUBtA== +-----END CERTIFICATE----- + +DigiCert Global Root G2 +======================= +-----BEGIN CERTIFICATE----- +MIIDjjCCAnagAwIBAgIQAzrx5qcRqaC7KGSxHQn65TANBgkqhkiG9w0BAQsFADBhMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSAw +HgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBHMjAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUx +MjAwMDBaMGExCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3 +dy5kaWdpY2VydC5jb20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEcyMIIBIjANBgkq +hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzfNNNx7a8myaJCtSnX/RrohCgiN9RlUyfuI2/Ou8jqJ +kTx65qsGGmvPrC3oXgkkRLpimn7Wo6h+4FR1IAWsULecYxpsMNzaHxmx1x7e/dfgy5SDN67sH0NO +3Xss0r0upS/kqbitOtSZpLYl6ZtrAGCSYP9PIUkY92eQq2EGnI/yuum06ZIya7XzV+hdG82MHauV +BJVJ8zUtluNJbd134/tJS7SsVQepj5WztCO7TG1F8PapspUwtP1MVYwnSlcUfIKdzXOS0xZKBgyM +UNGPHgm+F6HmIcr9g+UQvIOlCsRnKPZzFBQ9RnbDhxSJITRNrw9FDKZJobq7nMWxM4MphQIDAQAB +o0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUTiJUIBiV5uNu +5g/6+rkS7QYXjzkwDQYJKoZIhvcNAQELBQADggEBAGBnKJRvDkhj6zHd6mcY1Yl9PMWLSn/pvtsr +F9+wX3N3KjITOYFnQoQj8kVnNeyIv/iPsGEMNKSuIEyExtv4NeF22d+mQrvHRAiGfzZ0JFrabA0U +WTW98kndth/Jsw1HKj2ZL7tcu7XUIOGZX1NGFdtom/DzMNU+MeKNhJ7jitralj41E6Vf8PlwUHBH +QRFXGU7Aj64GxJUTFy8bJZ918rGOmaFvE7FBcf6IKshPECBV1/MUReXgRPTqh5Uykw7+U0b6LJ3/ +iyK5S9kJRaTepLiaWN0bfVKfjllDiIGknibVb63dDcY3fe0Dkhvld1927jyNxF1WW6LZZm6zNTfl +MrY= +-----END CERTIFICATE----- + +DigiCert Global Root G3 +======================= +-----BEGIN CERTIFICATE----- +MIICPzCCAcWgAwIBAgIQBVVWvPJepDU1w6QP1atFcjAKBggqhkjOPQQDAzBhMQswCQYDVQQGEwJV +UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSAwHgYD +VQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBHMzAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAw +MDBaMGExCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5k +aWdpY2VydC5jb20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEczMHYwEAYHKoZIzj0C +AQYFK4EEACIDYgAE3afZu4q4C/sLfyHS8L6+c/MzXRq8NOrexpu80JX28MzQC7phW1FGfp4tn+6O +YwwX7Adw9c+ELkCDnOg/QW07rdOkFFk2eJ0DQ+4QE2xy3q6Ip6FrtUPOZ9wj/wMco+I+o0IwQDAP +BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUs9tIpPmhxdiuNkHMEWNp +Yim8S8YwCgYIKoZIzj0EAwMDaAAwZQIxAK288mw/EkrRLTnDCgmXc/SINoyIJ7vmiI1Qhadj+Z4y +3maTD/HMsQmP3Wyr+mt/oAIwOWZbwmSNuJ5Q3KjVSaLtx9zRSX8XAbjIho9OjIgrqJqpisXRAL34 +VOKa5Vt8sycX +-----END CERTIFICATE----- + +DigiCert Trusted Root G4 +======================== +-----BEGIN CERTIFICATE----- +MIIFkDCCA3igAwIBAgIQBZsbV56OITLiOQe9p3d1XDANBgkqhkiG9w0BAQwFADBiMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSEw +HwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1 +MTIwMDAwWjBiMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwggIiMA0G +CSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC/5pBzaN675F1KPDAiMGkz7MKnJS7JIT3yithZwuEp +pz1Yq3aaza57G4QNxDAf8xukOBbrVsaXbR2rsnnyyhHS5F/WBTxSD1Ifxp4VpX6+n6lXFllVcq9o +k3DCsrp1mWpzMpTREEQQLt+C8weE5nQ7bXHiLQwb7iDVySAdYyktzuxeTsiT+CFhmzTrBcZe7Fsa +vOvJz82sNEBfsXpm7nfISKhmV1efVFiODCu3T6cw2Vbuyntd463JT17lNecxy9qTXtyOj4DatpGY +QJB5w3jHtrHEtWoYOAMQjdjUN6QuBX2I9YI+EJFwq1WCQTLX2wRzKm6RAXwhTNS8rhsDdV14Ztk6 +MUSaM0C/CNdaSaTC5qmgZ92kJ7yhTzm1EVgX9yRcRo9k98FpiHaYdj1ZXUJ2h4mXaXpI8OCiEhtm +mnTK3kse5w5jrubU75KSOp493ADkRSWJtppEGSt+wJS00mFt6zPZxd9LBADMfRyVw4/3IbKyEbe7 +f/LVjHAsQWCqsWMYRJUadmJ+9oCw++hkpjPRiQfhvbfmQ6QYuKZ3AeEPlAwhHbJUKSWJbOUOUlFH +dL4mrLZBdd56rF+NP8m800ERElvlEFDrMcXKchYiCd98THU/Y+whX8QgUWtvsauGi0/C1kVfnSD8 +oR7FwI+isX4KJpn15GkvmB0t9dmpsh3lGwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1Ud +DwEB/wQEAwIBhjAdBgNVHQ4EFgQU7NfjgtJxXWRM3y5nP+e6mK4cD08wDQYJKoZIhvcNAQEMBQAD +ggIBALth2X2pbL4XxJEbw6GiAI3jZGgPVs93rnD5/ZpKmbnJeFwMDF/k5hQpVgs2SV1EY+CtnJYY +ZhsjDT156W1r1lT40jzBQ0CuHVD1UvyQO7uYmWlrx8GnqGikJ9yd+SeuMIW59mdNOj6PWTkiU0Tr +yF0Dyu1Qen1iIQqAyHNm0aAFYF/opbSnr6j3bTWcfFqK1qI4mfN4i/RN0iAL3gTujJtHgXINwBQy +7zBZLq7gcfJW5GqXb5JQbZaNaHqasjYUegbyJLkJEVDXCLG4iXqEI2FCKeWjzaIgQdfRnGTZ6iah +ixTXTBmyUEFxPT9NcCOGDErcgdLMMpSEDQgJlxxPwO5rIHQw0uA5NBCFIRUBCOhVMt5xSdkoF1BN +5r5N0XWs0Mr7QbhDparTwwVETyw2m+L64kW4I1NsBm9nVX9GtUw/bihaeSbSpKhil9Ie4u1Ki7wb +/UdKDd9nZn6yW0HQO+T0O/QEY+nvwlQAUaCKKsnOeMzV6ocEGLPOr0mIr/OSmbaz5mEP0oUA51Aa +5BuVnRmhuZyxm7EAHu/QD09CbMkKvO5D+jpxpchNJqU1/YldvIViHTLSoCtU7ZpXwdv6EM8Zt4tK +G48BtieVU+i2iW1bvGjUI+iLUaJW+fCmgKDWHrO8Dw9TdSmq6hN35N6MgSGtBxBHEa2HPQfRdbzP +82Z+ +-----END CERTIFICATE----- + +COMODO RSA Certification Authority +================================== +-----BEGIN CERTIFICATE----- +MIIF2DCCA8CgAwIBAgIQTKr5yttjb+Af907YWwOGnTANBgkqhkiG9w0BAQwFADCBhTELMAkGA1UE +BhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgG +A1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNhdGlv +biBBdXRob3JpdHkwHhcNMTAwMTE5MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMC +R0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UE +ChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNhdGlvbiBB +dXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCR6FSS0gpWsawNJN3Fz0Rn +dJkrN6N9I3AAcbxT38T6KhKPS38QVr2fcHK3YX/JSw8Xpz3jsARh7v8Rl8f0hj4K+j5c+ZPmNHrZ +FGvnnLOFoIJ6dq9xkNfs/Q36nGz637CC9BR++b7Epi9Pf5l/tfxnQ3K9DADWietrLNPtj5gcFKt+ +5eNu/Nio5JIk2kNrYrhV/erBvGy2i/MOjZrkm2xpmfh4SDBF1a3hDTxFYPwyllEnvGfDyi62a+pG +x8cgoLEfZd5ICLqkTqnyg0Y3hOvozIFIQ2dOciqbXL1MGyiKXCJ7tKuY2e7gUYPDCUZObT6Z+pUX +2nwzV0E8jVHtC7ZcryxjGt9XyD+86V3Em69FmeKjWiS0uqlWPc9vqv9JWL7wqP/0uK3pN/u6uPQL +OvnoQ0IeidiEyxPx2bvhiWC4jChWrBQdnArncevPDt09qZahSL0896+1DSJMwBGB7FY79tOi4lu3 +sgQiUpWAk2nojkxl8ZEDLXB0AuqLZxUpaVICu9ffUGpVRr+goyhhf3DQw6KqLCGqR84onAZFdr+C +GCe01a60y1Dma/RMhnEw6abfFobg2P9A3fvQQoh/ozM6LlweQRGBY84YcWsr7KaKtzFcOmpH4MN5 +WdYgGq/yapiqcrxXStJLnbsQ/LBMQeXtHT1eKJ2czL+zUdqnR+WEUwIDAQABo0IwQDAdBgNVHQ4E +FgQUu69+Aj36pvE8hI6t7jiY7NkyMtQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8w +DQYJKoZIhvcNAQEMBQADggIBAArx1UaEt65Ru2yyTUEUAJNMnMvlwFTPoCWOAvn9sKIN9SCYPBMt +rFaisNZ+EZLpLrqeLppysb0ZRGxhNaKatBYSaVqM4dc+pBroLwP0rmEdEBsqpIt6xf4FpuHA1sj+ +nq6PK7o9mfjYcwlYRm6mnPTXJ9OV2jeDchzTc+CiR5kDOF3VSXkAKRzH7JsgHAckaVd4sjn8OoSg +tZx8jb8uk2IntznaFxiuvTwJaP+EmzzV1gsD41eeFPfR60/IvYcjt7ZJQ3mFXLrrkguhxuhoqEwW +sRqZCuhTLJK7oQkYdQxlqHvLI7cawiiFwxv/0Cti76R7CZGYZ4wUAc1oBmpjIXUDgIiKboHGhfKp +pC3n9KUkEEeDys30jXlYsQab5xoq2Z0B15R97QNKyvDb6KkBPvVWmckejkk9u+UJueBPSZI9FoJA +zMxZxuY67RIuaTxslbH9qh17f4a+Hg4yRvv7E491f0yLS0Zj/gA0QHDBw7mh3aZw4gSzQbzpgJHq +ZJx64SIDqZxubw5lT2yHh17zbqD5daWbQOhTsiedSrnAdyGN/4fy3ryM7xfft0kL0fJuMAsaDk52 +7RH89elWsn2/x20Kk4yl0MC2Hb46TpSi125sC8KKfPog88Tk5c0NqMuRkrF8hey1FGlmDoLnzc7I +LaZRfyHBNVOFBkpdn627G190 +-----END CERTIFICATE----- + +USERTrust RSA Certification Authority +===================================== +-----BEGIN CERTIFICATE----- +MIIF3jCCA8agAwIBAgIQAf1tMPyjylGoG7xkDjUDLTANBgkqhkiG9w0BAQwFADCBiDELMAkGA1UE +BhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQK +ExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNh +dGlvbiBBdXRob3JpdHkwHhcNMTAwMjAxMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UE +BhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQK +ExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNh +dGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCAEmUXNg7D2wiz +0KxXDXbtzSfTTK1Qg2HiqiBNCS1kCdzOiZ/MPans9s/B3PHTsdZ7NygRK0faOca8Ohm0X6a9fZ2j +Y0K2dvKpOyuR+OJv0OwWIJAJPuLodMkYtJHUYmTbf6MG8YgYapAiPLz+E/CHFHv25B+O1ORRxhFn +RghRy4YUVD+8M/5+bJz/Fp0YvVGONaanZshyZ9shZrHUm3gDwFA66Mzw3LyeTP6vBZY1H1dat//O ++T23LLb2VN3I5xI6Ta5MirdcmrS3ID3KfyI0rn47aGYBROcBTkZTmzNg95S+UzeQc0PzMsNT79uq +/nROacdrjGCT3sTHDN/hMq7MkztReJVni+49Vv4M0GkPGw/zJSZrM233bkf6c0Plfg6lZrEpfDKE +Y1WJxA3Bk1QwGROs0303p+tdOmw1XNtB1xLaqUkL39iAigmTYo61Zs8liM2EuLE/pDkP2QKe6xJM +lXzzawWpXhaDzLhn4ugTncxbgtNMs+1b/97lc6wjOy0AvzVVdAlJ2ElYGn+SNuZRkg7zJn0cTRe8 +yexDJtC/QV9AqURE9JnnV4eeUB9XVKg+/XRjL7FQZQnmWEIuQxpMtPAlR1n6BB6T1CZGSlCBst6+ +eLf8ZxXhyVeEHg9j1uliutZfVS7qXMYoCAQlObgOK6nyTJccBz8NUvXt7y+CDwIDAQABo0IwQDAd +BgNVHQ4EFgQUU3m/WqorSs9UgOHYm8Cd8rIDZsswDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQF +MAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAFzUfA3P9wF9QZllDHPFUp/L+M+ZBn8b2kMVn54CVVeW +FPFSPCeHlCjtHzoBN6J2/FNQwISbxmtOuowhT6KOVWKR82kV2LyI48SqC/3vqOlLVSoGIG1VeCkZ +7l8wXEskEVX/JJpuXior7gtNn3/3ATiUFJVDBwn7YKnuHKsSjKCaXqeYalltiz8I+8jRRa8YFWSQ +Eg9zKC7F4iRO/Fjs8PRF/iKz6y+O0tlFYQXBl2+odnKPi4w2r78NBc5xjeambx9spnFixdjQg3IM +8WcRiQycE0xyNN+81XHfqnHd4blsjDwSXWXavVcStkNr/+XeTWYRUc+ZruwXtuhxkYzeSf7dNXGi +FSeUHM9h4ya7b6NnJSFd5t0dCy5oGzuCr+yDZ4XUmFF0sbmZgIn/f3gZXHlKYC6SQK5MNyosycdi +yA5d9zZbyuAlJQG03RoHnHcAP9Dc1ew91Pq7P8yF1m9/qS3fuQL39ZeatTXaw2ewh0qpKJ4jjv9c +J2vhsE/zB+4ALtRZh8tSQZXq9EfX7mRBVXyNWQKV3WKdwrnuWih0hKWbt5DHDAff9Yk2dDLWKMGw +sAvgnEzDHNb842m1R0aBL6KCq9NjRHDEjf8tM7qtj3u1cIiuPhnPQCjY/MiQu12ZIvVS5ljFH4gx +Q+6IHdfGjjxDah2nGN59PRbxYvnKkKj9 +-----END CERTIFICATE----- + +USERTrust ECC Certification Authority +===================================== +-----BEGIN CERTIFICATE----- +MIICjzCCAhWgAwIBAgIQXIuZxVqUxdJxVt7NiYDMJjAKBggqhkjOPQQDAzCBiDELMAkGA1UEBhMC +VVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVU +aGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBFQ0MgQ2VydGlmaWNhdGlv +biBBdXRob3JpdHkwHhcNMTAwMjAxMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMC +VVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVU +aGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBFQ0MgQ2VydGlmaWNhdGlv +biBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQarFRaqfloI+d61SRvU8Za2EurxtW2 +0eZzca7dnNYMYf3boIkDuAUU7FfO7l0/4iGzzvfUinngo4N+LZfQYcTxmdwlkWOrfzCjtHDix6Ez +nPO/LlxTsV+zfTJ/ijTjeXmjQjBAMB0GA1UdDgQWBBQ64QmG1M8ZwpZ2dEl23OA1xmNjmjAOBgNV +HQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjA2Z6EWCNzklwBB +HU6+4WMBzzuqQhFkoJ2UOQIReVx7Hfpkue4WQrO/isIJxOzksU0CMQDpKmFHjFJKS04YcPbWRNZu +9YO6bVi9JNlWSOrvxKJGgYhqOkbRqZtNyWHa0V1Xahg= +-----END CERTIFICATE----- + +GlobalSign ECC Root CA - R5 +=========================== +-----BEGIN CERTIFICATE----- +MIICHjCCAaSgAwIBAgIRYFlJ4CYuu1X5CneKcflK2GwwCgYIKoZIzj0EAwMwUDEkMCIGA1UECxMb +R2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI1MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQD +EwpHbG9iYWxTaWduMB4XDTEyMTExMzAwMDAwMFoXDTM4MDExOTAzMTQwN1owUDEkMCIGA1UECxMb +R2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI1MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQD +EwpHbG9iYWxTaWduMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAER0UOlvt9Xb/pOdEh+J8LttV7HpI6 +SFkc8GIxLcB6KP4ap1yztsyX50XUWPrRd21DosCHZTQKH3rd6zwzocWdTaRvQZU4f8kehOvRnkmS +h5SHDDqFSmafnVmTTZdhBoZKo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAd +BgNVHQ4EFgQUPeYpSJvqB8ohREom3m7e0oPQn1kwCgYIKoZIzj0EAwMDaAAwZQIxAOVpEslu28Yx +uglB4Zf4+/2a4n0Sye18ZNPLBSWLVtmg515dTguDnFt2KaAJJiFqYgIwcdK1j1zqO+F4CYWodZI7 +yFz9SO8NdCKoCOJuxUnOxwy8p2Fp8fc74SrL+SvzZpA3 +-----END CERTIFICATE----- + +IdenTrust Commercial Root CA 1 +============================== +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIQCgFCgAAAAUUjyES1AAAAAjANBgkqhkiG9w0BAQsFADBKMQswCQYDVQQG +EwJVUzESMBAGA1UEChMJSWRlblRydXN0MScwJQYDVQQDEx5JZGVuVHJ1c3QgQ29tbWVyY2lhbCBS +b290IENBIDEwHhcNMTQwMTE2MTgxMjIzWhcNMzQwMTE2MTgxMjIzWjBKMQswCQYDVQQGEwJVUzES +MBAGA1UEChMJSWRlblRydXN0MScwJQYDVQQDEx5JZGVuVHJ1c3QgQ29tbWVyY2lhbCBSb290IENB +IDEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCnUBneP5k91DNG8W9RYYKyqU+PZ4ld +hNlT3Qwo2dfw/66VQ3KZ+bVdfIrBQuExUHTRgQ18zZshq0PirK1ehm7zCYofWjK9ouuU+ehcCuz/ +mNKvcbO0U59Oh++SvL3sTzIwiEsXXlfEU8L2ApeN2WIrvyQfYo3fw7gpS0l4PJNgiCL8mdo2yMKi +1CxUAGc1bnO/AljwpN3lsKImesrgNqUZFvX9t++uP0D1bVoE/c40yiTcdCMbXTMTEl3EASX2MN0C +XZ/g1Ue9tOsbobtJSdifWwLziuQkkORiT0/Br4sOdBeo0XKIanoBScy0RnnGF7HamB4HWfp1IYVl +3ZBWzvurpWCdxJ35UrCLvYf5jysjCiN2O/cz4ckA82n5S6LgTrx+kzmEB/dEcH7+B1rlsazRGMzy +NeVJSQjKVsk9+w8YfYs7wRPCTY/JTw436R+hDmrfYi7LNQZReSzIJTj0+kuniVyc0uMNOYZKdHzV +WYfCP04MXFL0PfdSgvHqo6z9STQaKPNBiDoT7uje/5kdX7rL6B7yuVBgwDHTc+XvvqDtMwt0viAg +xGds8AgDelWAf0ZOlqf0Hj7h9tgJ4TNkK2PXMl6f+cB7D3hvl7yTmvmcEpB4eoCHFddydJxVdHix +uuFucAS6T6C6aMN7/zHwcz09lCqxC0EOoP5NiGVreTO01wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMC +AQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU7UQZwNPwBovupHu+QucmVMiONnYwDQYJKoZI +hvcNAQELBQADggIBAA2ukDL2pkt8RHYZYR4nKM1eVO8lvOMIkPkp165oCOGUAFjvLi5+U1KMtlwH +6oi6mYtQlNeCgN9hCQCTrQ0U5s7B8jeUeLBfnLOic7iPBZM4zY0+sLj7wM+x8uwtLRvM7Kqas6pg +ghstO8OEPVeKlh6cdbjTMM1gCIOQ045U8U1mwF10A0Cj7oV+wh93nAbowacYXVKV7cndJZ5t+qnt +ozo00Fl72u1Q8zW/7esUTTHHYPTa8Yec4kjixsU3+wYQ+nVZZjFHKdp2mhzpgq7vmrlR94gjmmmV +YjzlVYA211QC//G5Xc7UI2/YRYRKW2XviQzdFKcgyxilJbQN+QHwotL0AMh0jqEqSI5l2xPE4iUX +feu+h1sXIFRRk0pTAwvsXcoz7WL9RccvW9xYoIA55vrX/hMUpu09lEpCdNTDd1lzzY9GvlU47/ro +kTLql1gEIt44w8y8bckzOmoKaT+gyOpyj4xjhiO9bTyWnpXgSUyqorkqG5w2gXjtw+hG4iZZRHUe +2XWJUc0QhJ1hYMtd+ZciTY6Y5uN/9lu7rs3KSoFrXgvzUeF0K+l+J6fZmUlO+KWA2yUPHGNiiskz +Z2s8EIPGrd6ozRaOjfAHN3Gf8qv8QfXBi+wAN10J5U6A7/qxXDgGpRtK4dw4LTzcqx+QGtVKnO7R +cGzM7vRX+Bi6hG6H +-----END CERTIFICATE----- + +IdenTrust Public Sector Root CA 1 +================================= +-----BEGIN CERTIFICATE----- +MIIFZjCCA06gAwIBAgIQCgFCgAAAAUUjz0Z8AAAAAjANBgkqhkiG9w0BAQsFADBNMQswCQYDVQQG +EwJVUzESMBAGA1UEChMJSWRlblRydXN0MSowKAYDVQQDEyFJZGVuVHJ1c3QgUHVibGljIFNlY3Rv +ciBSb290IENBIDEwHhcNMTQwMTE2MTc1MzMyWhcNMzQwMTE2MTc1MzMyWjBNMQswCQYDVQQGEwJV +UzESMBAGA1UEChMJSWRlblRydXN0MSowKAYDVQQDEyFJZGVuVHJ1c3QgUHVibGljIFNlY3RvciBS +b290IENBIDEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC2IpT8pEiv6EdrCvsnduTy +P4o7ekosMSqMjbCpwzFrqHd2hCa2rIFCDQjrVVi7evi8ZX3yoG2LqEfpYnYeEe4IFNGyRBb06tD6 +Hi9e28tzQa68ALBKK0CyrOE7S8ItneShm+waOh7wCLPQ5CQ1B5+ctMlSbdsHyo+1W/CD80/HLaXI +rcuVIKQxKFdYWuSNG5qrng0M8gozOSI5Cpcu81N3uURF/YTLNiCBWS2ab21ISGHKTN9T0a9SvESf +qy9rg3LvdYDaBjMbXcjaY8ZNzaxmMc3R3j6HEDbhuaR672BQssvKplbgN6+rNBM5Jeg5ZuSYeqoS +mJxZZoY+rfGwyj4GD3vwEUs3oERte8uojHH01bWRNszwFcYr3lEXsZdMUD2xlVl8BX0tIdUAvwFn +ol57plzy9yLxkA2T26pEUWbMfXYD62qoKjgZl3YNa4ph+bz27nb9cCvdKTz4Ch5bQhyLVi9VGxyh +LrXHFub4qjySjmm2AcG1hp2JDws4lFTo6tyePSW8Uybt1as5qsVATFSrsrTZ2fjXctscvG29ZV/v +iDUqZi/u9rNl8DONfJhBaUYPQxxp+pu10GFqzcpL2UyQRqsVWaFHVCkugyhfHMKiq3IXAAaOReyL +4jM9f9oZRORicsPfIsbyVtTdX5Vy7W1f90gDW/3FKqD2cyOEEBsB5wIDAQABo0IwQDAOBgNVHQ8B +Af8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU43HgntinQtnbcZFrlJPrw6PRFKMw +DQYJKoZIhvcNAQELBQADggIBAEf63QqwEZE4rU1d9+UOl1QZgkiHVIyqZJnYWv6IAcVYpZmxI1Qj +t2odIFflAWJBF9MJ23XLblSQdf4an4EKwt3X9wnQW3IV5B4Jaj0z8yGa5hV+rVHVDRDtfULAj+7A +mgjVQdZcDiFpboBhDhXAuM/FSRJSzL46zNQuOAXeNf0fb7iAaJg9TaDKQGXSc3z1i9kKlT/YPyNt +GtEqJBnZhbMX73huqVjRI9PHE+1yJX9dsXNw0H8GlwmEKYBhHfpe/3OsoOOJuBxxFcbeMX8S3OFt +m6/n6J91eEyrRjuazr8FGF1NFTwWmhlQBJqymm9li1JfPFgEKCXAZmExfrngdbkaqIHWchezxQMx +NRF4eKLg6TCMf4DfWN88uieW4oA0beOY02QnrEh+KHdcxiVhJfiFDGX6xDIvpZgF5PgLZxYWxoK4 +Mhn5+bl53B/N66+rDt0b20XkeucC4pVd/GnwU2lhlXV5C15V5jgclKlZM57IcXR5f1GJtshquDDI +ajjDbp7hNxbqBWJMWxJH7ae0s1hWx0nzfxJoCTFx8G34Tkf71oXuxVhAGaQdp/lLQzfcaFpPz+vC +ZHTetBXZ9FRUGi8c15dxVJCO2SCdUyt/q4/i6jC8UDfv8Ue1fXwsBOxonbRJRBD0ckscZOf85muQ +3Wl9af0AVqW3rLatt8o+Ae+c +-----END CERTIFICATE----- + +CFCA EV ROOT +============ +-----BEGIN CERTIFICATE----- +MIIFjTCCA3WgAwIBAgIEGErM1jANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJDTjEwMC4GA1UE +CgwnQ2hpbmEgRmluYW5jaWFsIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRUwEwYDVQQDDAxDRkNB +IEVWIFJPT1QwHhcNMTIwODA4MDMwNzAxWhcNMjkxMjMxMDMwNzAxWjBWMQswCQYDVQQGEwJDTjEw +MC4GA1UECgwnQ2hpbmEgRmluYW5jaWFsIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRUwEwYDVQQD +DAxDRkNBIEVWIFJPT1QwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDXXWvNED8fBVnV +BU03sQ7smCuOFR36k0sXgiFxEFLXUWRwFsJVaU2OFW2fvwwbwuCjZ9YMrM8irq93VCpLTIpTUnrD +7i7es3ElweldPe6hL6P3KjzJIx1qqx2hp/Hz7KDVRM8Vz3IvHWOX6Jn5/ZOkVIBMUtRSqy5J35DN +uF++P96hyk0g1CXohClTt7GIH//62pCfCqktQT+x8Rgp7hZZLDRJGqgG16iI0gNyejLi6mhNbiyW +ZXvKWfry4t3uMCz7zEasxGPrb382KzRzEpR/38wmnvFyXVBlWY9ps4deMm/DGIq1lY+wejfeWkU7 +xzbh72fROdOXW3NiGUgthxwG+3SYIElz8AXSG7Ggo7cbcNOIabla1jj0Ytwli3i/+Oh+uFzJlU9f +py25IGvPa931DfSCt/SyZi4QKPaXWnuWFo8BGS1sbn85WAZkgwGDg8NNkt0yxoekN+kWzqotaK8K +gWU6cMGbrU1tVMoqLUuFG7OA5nBFDWteNfB/O7ic5ARwiRIlk9oKmSJgamNgTnYGmE69g60dWIol +hdLHZR4tjsbftsbhf4oEIRUpdPA+nJCdDC7xij5aqgwJHsfVPKPtl8MeNPo4+QgO48BdK4PRVmrJ +tqhUUy54Mmc9gn900PvhtgVguXDbjgv5E1hvcWAQUhC5wUEJ73IfZzF4/5YFjQIDAQABo2MwYTAf +BgNVHSMEGDAWgBTj/i39KNALtbq2osS/BqoFjJP7LzAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB +/wQEAwIBBjAdBgNVHQ4EFgQU4/4t/SjQC7W6tqLEvwaqBYyT+y8wDQYJKoZIhvcNAQELBQADggIB +ACXGumvrh8vegjmWPfBEp2uEcwPenStPuiB/vHiyz5ewG5zz13ku9Ui20vsXiObTej/tUxPQ4i9q +ecsAIyjmHjdXNYmEwnZPNDatZ8POQQaIxffu2Bq41gt/UP+TqhdLjOztUmCypAbqTuv0axn96/Ua +4CUqmtzHQTb3yHQFhDmVOdYLO6Qn+gjYXB74BGBSESgoA//vU2YApUo0FmZ8/Qmkrp5nGm9BC2sG +E5uPhnEFtC+NiWYzKXZUmhH4J/qyP5Hgzg0b8zAarb8iXRvTvyUFTeGSGn+ZnzxEk8rUQElsgIfX +BDrDMlI1Dlb4pd19xIsNER9Tyx6yF7Zod1rg1MvIB671Oi6ON7fQAUtDKXeMOZePglr4UeWJoBjn +aH9dCi77o0cOPaYjesYBx4/IXr9tgFa+iiS6M+qf4TIRnvHST4D2G0CvOJ4RUHlzEhLN5mydLIhy +PDCBBpEi6lmt2hkuIsKNuYyH4Ga8cyNfIWRjgEj1oDwYPZTISEEdQLpe/v5WOaHIz16eGWRGENoX +kbcFgKyLmZJ956LYBws2J+dIeWCKw9cTXPhyQN9Ky8+ZAAoACxGV2lZFA4gKn2fQ1XmxqI1AbQ3C +ekD6819kR5LLU7m7Wc5P/dAVUwHY3+vZ5nbv0CO7O6l5s9UCKc2Jo5YPSjXnTkLAdc0Hz+Ys63su +-----END CERTIFICATE----- + +OISTE WISeKey Global Root GB CA +=============================== +-----BEGIN CERTIFICATE----- +MIIDtTCCAp2gAwIBAgIQdrEgUnTwhYdGs/gjGvbCwDANBgkqhkiG9w0BAQsFADBtMQswCQYDVQQG +EwJDSDEQMA4GA1UEChMHV0lTZUtleTEiMCAGA1UECxMZT0lTVEUgRm91bmRhdGlvbiBFbmRvcnNl +ZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9iYWwgUm9vdCBHQiBDQTAeFw0xNDEyMDExNTAw +MzJaFw0zOTEyMDExNTEwMzFaMG0xCzAJBgNVBAYTAkNIMRAwDgYDVQQKEwdXSVNlS2V5MSIwIAYD +VQQLExlPSVNURSBGb3VuZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBXSVNlS2V5IEds +b2JhbCBSb290IEdCIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2Be3HEokKtaX +scriHvt9OO+Y9bI5mE4nuBFde9IllIiCFSZqGzG7qFshISvYD06fWvGxWuR51jIjK+FTzJlFXHtP +rby/h0oLS5daqPZI7H17Dc0hBt+eFf1Biki3IPShehtX1F1Q/7pn2COZH8g/497/b1t3sWtuuMlk +9+HKQUYOKXHQuSP8yYFfTvdv37+ErXNku7dCjmn21HYdfp2nuFeKUWdy19SouJVUQHMD9ur06/4o +Qnc/nSMbsrY9gBQHTC5P99UKFg29ZkM3fiNDecNAhvVMKdqOmq0NpQSHiB6F4+lT1ZvIiwNjeOvg +GUpuuy9rM2RYk61pv48b74JIxwIDAQABo1EwTzALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB +/zAdBgNVHQ4EFgQUNQ/INmNe4qPs+TtmFc5RUuORmj0wEAYJKwYBBAGCNxUBBAMCAQAwDQYJKoZI +hvcNAQELBQADggEBAEBM+4eymYGQfp3FsLAmzYh7KzKNbrghcViXfa43FK8+5/ea4n32cZiZBKpD +dHij40lhPnOMTZTg+XHEthYOU3gf1qKHLwI5gSk8rxWYITD+KJAAjNHhy/peyP34EEY7onhCkRd0 +VQreUGdNZtGn//3ZwLWoo4rOZvUPQ82nK1d7Y0Zqqi5S2PTt4W2tKZB4SLrhI6qjiey1q5bAtEui +HZeeevJuQHHfaPFlTc58Bd9TZaml8LGXBHAVRgOY1NK/VLSgWH1Sb9pWJmLU2NuJMW8c8CLC02Ic +Nc1MaRVUGpCY3useX8p3x8uOPUNpnJpY0CQ73xtAln41rYHHTnG6iBM= +-----END CERTIFICATE----- + +SZAFIR ROOT CA2 +=============== +-----BEGIN CERTIFICATE----- +MIIDcjCCAlqgAwIBAgIUPopdB+xV0jLVt+O2XwHrLdzk1uQwDQYJKoZIhvcNAQELBQAwUTELMAkG +A1UEBhMCUEwxKDAmBgNVBAoMH0tyYWpvd2EgSXpiYSBSb3psaWN6ZW5pb3dhIFMuQS4xGDAWBgNV +BAMMD1NaQUZJUiBST09UIENBMjAeFw0xNTEwMTkwNzQzMzBaFw0zNTEwMTkwNzQzMzBaMFExCzAJ +BgNVBAYTAlBMMSgwJgYDVQQKDB9LcmFqb3dhIEl6YmEgUm96bGljemVuaW93YSBTLkEuMRgwFgYD +VQQDDA9TWkFGSVIgUk9PVCBDQTIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC3vD5Q +qEvNQLXOYeeWyrSh2gwisPq1e3YAd4wLz32ohswmUeQgPYUM1ljj5/QqGJ3a0a4m7utT3PSQ1hNK +DJA8w/Ta0o4NkjrcsbH/ON7Dui1fgLkCvUqdGw+0w8LBZwPd3BucPbOw3gAeqDRHu5rr/gsUvTaE +2g0gv/pby6kWIK05YO4vdbbnl5z5Pv1+TW9NL++IDWr63fE9biCloBK0TXC5ztdyO4mTp4CEHCdJ +ckm1/zuVnsHMyAHs6A6KCpbns6aH5db5BSsNl0BwPLqsdVqc1U2dAgrSS5tmS0YHF2Wtn2yIANwi +ieDhZNRnvDF5YTy7ykHNXGoAyDw4jlivAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0P +AQH/BAQDAgEGMB0GA1UdDgQWBBQuFqlKGLXLzPVvUPMjX/hd56zwyDANBgkqhkiG9w0BAQsFAAOC +AQEAtXP4A9xZWx126aMqe5Aosk3AM0+qmrHUuOQn/6mWmc5G4G18TKI4pAZw8PRBEew/R40/cof5 +O/2kbytTAOD/OblqBw7rHRz2onKQy4I9EYKL0rufKq8h5mOGnXkZ7/e7DDWQw4rtTw/1zBLZpD67 +oPwglV9PJi8RI4NOdQcPv5vRtB3pEAT+ymCPoky4rc/hkA/NrgrHXXu3UNLUYfrVFdvXn4dRVOul +4+vJhaAlIDf7js4MNIThPIGyd05DpYhfhmehPea0XGG2Ptv+tyjFogeutcrKjSoS75ftwjCkySp6 ++/NNIxuZMzSgLvWpCz/UXeHPhJ/iGcJfitYgHuNztw== +-----END CERTIFICATE----- + +Certum Trusted Network CA 2 +=========================== +-----BEGIN CERTIFICATE----- +MIIF0jCCA7qgAwIBAgIQIdbQSk8lD8kyN/yqXhKN6TANBgkqhkiG9w0BAQ0FADCBgDELMAkGA1UE +BhMCUEwxIjAgBgNVBAoTGVVuaXpldG8gVGVjaG5vbG9naWVzIFMuQS4xJzAlBgNVBAsTHkNlcnR1 +bSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEkMCIGA1UEAxMbQ2VydHVtIFRydXN0ZWQgTmV0d29y +ayBDQSAyMCIYDzIwMTExMDA2MDgzOTU2WhgPMjA0NjEwMDYwODM5NTZaMIGAMQswCQYDVQQGEwJQ +TDEiMCAGA1UEChMZVW5pemV0byBUZWNobm9sb2dpZXMgUy5BLjEnMCUGA1UECxMeQ2VydHVtIENl +cnRpZmljYXRpb24gQXV0aG9yaXR5MSQwIgYDVQQDExtDZXJ0dW0gVHJ1c3RlZCBOZXR3b3JrIENB +IDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC9+Xj45tWADGSdhhuWZGc/IjoedQF9 +7/tcZ4zJzFxrqZHmuULlIEub2pt7uZld2ZuAS9eEQCsn0+i6MLs+CRqnSZXvK0AkwpfHp+6bJe+o +CgCXhVqqndwpyeI1B+twTUrWwbNWuKFBOJvR+zF/j+Bf4bE/D44WSWDXBo0Y+aomEKsq09DRZ40b +Rr5HMNUuctHFY9rnY3lEfktjJImGLjQ/KUxSiyqnwOKRKIm5wFv5HdnnJ63/mgKXwcZQkpsCLL2p +uTRZCr+ESv/f/rOf69me4Jgj7KZrdxYq28ytOxykh9xGc14ZYmhFV+SQgkK7QtbwYeDBoz1mo130 +GO6IyY0XRSmZMnUCMe4pJshrAua1YkV/NxVaI2iJ1D7eTiew8EAMvE0Xy02isx7QBlrd9pPPV3WZ +9fqGGmd4s7+W/jTcvedSVuWz5XV710GRBdxdaeOVDUO5/IOWOZV7bIBaTxNyxtd9KXpEulKkKtVB +Rgkg/iKgtlswjbyJDNXXcPiHUv3a76xRLgezTv7QCdpw75j6VuZt27VXS9zlLCUVyJ4ueE742pye +hizKV/Ma5ciSixqClnrDvFASadgOWkaLOusm+iPJtrCBvkIApPjW/jAux9JG9uWOdf3yzLnQh1vM +BhBgu4M1t15n3kfsmUjxpKEV/q2MYo45VU85FrmxY53/twIDAQABo0IwQDAPBgNVHRMBAf8EBTAD +AQH/MB0GA1UdDgQWBBS2oVQ5AsOgP46KvPrU+Bym0ToO/TAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZI +hvcNAQENBQADggIBAHGlDs7k6b8/ONWJWsQCYftMxRQXLYtPU2sQF/xlhMcQSZDe28cmk4gmb3DW +Al45oPePq5a1pRNcgRRtDoGCERuKTsZPpd1iHkTfCVn0W3cLN+mLIMb4Ck4uWBzrM9DPhmDJ2vuA +L55MYIR4PSFk1vtBHxgP58l1cb29XN40hz5BsA72udY/CROWFC/emh1auVbONTqwX3BNXuMp8SMo +clm2q8KMZiYcdywmdjWLKKdpoPk79SPdhRB0yZADVpHnr7pH1BKXESLjokmUbOe3lEu6LaTaM4tM +pkT/WjzGHWTYtTHkpjx6qFcL2+1hGsvxznN3Y6SHb0xRONbkX8eftoEq5IVIeVheO/jbAoJnwTnb +w3RLPTYe+SmTiGhbqEQZIfCn6IENLOiTNrQ3ssqwGyZ6miUfmpqAnksqP/ujmv5zMnHCnsZy4Ypo +J/HkD7TETKVhk/iXEAcqMCWpuchxuO9ozC1+9eB+D4Kob7a6bINDd82Kkhehnlt4Fj1F4jNy3eFm +ypnTycUm/Q1oBEauttmbjL4ZvrHG8hnjXALKLNhvSgfZyTXaQHXyxKcZb55CEJh15pWLYLztxRLX +is7VmFxWlgPF7ncGNf/P5O4/E2Hu29othfDNrp2yGAlFw5Khchf8R7agCyzxxN5DaAhqXzvwdmP7 +zAYspsbiDrW5viSP +-----END CERTIFICATE----- + +Hellenic Academic and Research Institutions RootCA 2015 +======================================================= +-----BEGIN CERTIFICATE----- +MIIGCzCCA/OgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBpjELMAkGA1UEBhMCR1IxDzANBgNVBAcT +BkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJlc2VhcmNoIEluc3RpdHV0 +aW9ucyBDZXJ0LiBBdXRob3JpdHkxQDA+BgNVBAMTN0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNl +YXJjaCBJbnN0aXR1dGlvbnMgUm9vdENBIDIwMTUwHhcNMTUwNzA3MTAxMTIxWhcNNDAwNjMwMTAx +MTIxWjCBpjELMAkGA1UEBhMCR1IxDzANBgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMg +QWNhZGVtaWMgYW5kIFJlc2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkxQDA+BgNV +BAMTN0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgUm9vdENBIDIw +MTUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDC+Kk/G4n8PDwEXT2QNrCROnk8Zlrv +bTkBSRq0t89/TSNTt5AA4xMqKKYx8ZEA4yjsriFBzh/a/X0SWwGDD7mwX5nh8hKDgE0GPt+sr+eh +iGsxr/CL0BgzuNtFajT0AoAkKAoCFZVedioNmToUW/bLy1O8E00BiDeUJRtCvCLYjqOWXjrZMts+ +6PAQZe104S+nfK8nNLspfZu2zwnI5dMK/IhlZXQK3HMcXM1AsRzUtoSMTFDPaI6oWa7CJ06CojXd +FPQf/7J31Ycvqm59JCfnxssm5uX+Zwdj2EUN3TpZZTlYepKZcj2chF6IIbjV9Cz82XBST3i4vTwr +i5WY9bPRaM8gFH5MXF/ni+X1NYEZN9cRCLdmvtNKzoNXADrDgfgXy5I2XdGj2HUb4Ysn6npIQf1F +GQatJ5lOwXBH3bWfgVMS5bGMSF0xQxfjjMZ6Y5ZLKTBOhE5iGV48zpeQpX8B653g+IuJ3SWYPZK2 +fu/Z8VFRfS0myGlZYeCsargqNhEEelC9MoS+L9xy1dcdFkfkR2YgP/SWxa+OAXqlD3pk9Q0Yh9mu +iNX6hME6wGkoLfINaFGq46V3xqSQDqE3izEjR8EJCOtu93ib14L8hCCZSRm2Ekax+0VVFqmjZayc +Bw/qa9wfLgZy7IaIEuQt218FL+TwA9MmM+eAws1CoRc0CwIDAQABo0IwQDAPBgNVHRMBAf8EBTAD +AQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUcRVnyMjJvXVdctA4GGqd83EkVAswDQYJKoZI +hvcNAQELBQADggIBAHW7bVRLqhBYRjTyYtcWNl0IXtVsyIe9tC5G8jH4fOpCtZMWVdyhDBKg2mF+ +D1hYc2Ryx+hFjtyp8iY/xnmMsVMIM4GwVhO+5lFc2JsKT0ucVlMC6U/2DWDqTUJV6HwbISHTGzrM +d/K4kPFox/la/vot9L/J9UUbzjgQKjeKeaO04wlshYaT/4mWJ3iBj2fjRnRUjtkNaeJK9E10A/+y +d+2VZ5fkscWrv2oj6NSU4kQoYsRL4vDY4ilrGnB+JGGTe08DMiUNRSQrlrRGar9KC/eaj8GsGsVn +82800vpzY4zvFrCopEYq+OsS7HK07/grfoxSwIuEVPkvPuNVqNxmsdnhX9izjFk0WaSrT2y7Hxjb +davYy5LNlDhhDgcGH0tGEPEVvo2FXDtKK4F5D7Rpn0lQl033DlZdwJVqwjbDG2jJ9SrcR5q+ss7F +Jej6A7na+RZukYT1HCjI/CbM1xyQVqdfbzoEvM14iQuODy+jqk+iGxI9FghAD/FGTNeqewjBCvVt +J94Cj8rDtSvK6evIIVM4pcw72Hc3MKJP2W/R8kCtQXoXxdZKNYm3QdV8hn9VTYNKpXMgwDqvkPGa +JI7ZjnHKe7iG2rKPmT4dEw0SEe7Uq/DpFXYC5ODfqiAeW2GFZECpkJcNrVPSWh2HagCXZWK0vm9q +p/UsQu0yrbYhnr68 +-----END CERTIFICATE----- + +Hellenic Academic and Research Institutions ECC RootCA 2015 +=========================================================== +-----BEGIN CERTIFICATE----- +MIICwzCCAkqgAwIBAgIBADAKBggqhkjOPQQDAjCBqjELMAkGA1UEBhMCR1IxDzANBgNVBAcTBkF0 +aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJlc2VhcmNoIEluc3RpdHV0aW9u +cyBDZXJ0LiBBdXRob3JpdHkxRDBCBgNVBAMTO0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJj +aCBJbnN0aXR1dGlvbnMgRUNDIFJvb3RDQSAyMDE1MB4XDTE1MDcwNzEwMzcxMloXDTQwMDYzMDEw +MzcxMlowgaoxCzAJBgNVBAYTAkdSMQ8wDQYDVQQHEwZBdGhlbnMxRDBCBgNVBAoTO0hlbGxlbmlj +IEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgQ2VydC4gQXV0aG9yaXR5MUQwQgYD +VQQDEztIZWxsZW5pYyBBY2FkZW1pYyBhbmQgUmVzZWFyY2ggSW5zdGl0dXRpb25zIEVDQyBSb290 +Q0EgMjAxNTB2MBAGByqGSM49AgEGBSuBBAAiA2IABJKgQehLgoRc4vgxEZmGZE4JJS+dQS8KrjVP +dJWyUWRrjWvmP3CV8AVER6ZyOFB2lQJajq4onvktTpnvLEhvTCUp6NFxW98dwXU3tNf6e3pCnGoK +Vlp8aQuqgAkkbH7BRqNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0O +BBYEFLQiC4KZJAEOnLvkDv2/+5cgk5kqMAoGCCqGSM49BAMCA2cAMGQCMGfOFmI4oqxiRaeplSTA +GiecMjvAwNW6qef4BENThe5SId6d9SWDPp5YSy/XZxMOIQIwBeF1Ad5o7SofTUwJCA3sS61kFyjn +dc5FZXIhF8siQQ6ME5g4mlRtm8rifOoCWCKR +-----END CERTIFICATE----- + +ISRG Root X1 +============ +-----BEGIN CERTIFICATE----- +MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAwTzELMAkGA1UE +BhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2VhcmNoIEdyb3VwMRUwEwYDVQQD +EwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQG +EwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMT +DElTUkcgUm9vdCBYMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54r +Vygch77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+0TM8ukj1 +3Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6UA5/TR5d8mUgjU+g4rk8K +b4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sWT8KOEUt+zwvo/7V3LvSye0rgTBIlDHCN +Aymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyHB5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ +4Q7e2RCOFvu396j3x+UCB5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf +1b0SHzUvKBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWnOlFu +hjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTnjh8BCNAw1FtxNrQH +usEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbwqHyGO0aoSCqI3Haadr8faqU9GY/r +OPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CIrU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4G +A1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY +9umbbjANBgkqhkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL +ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ3BebYhtF8GaV +0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KKNFtY2PwByVS5uCbMiogziUwt +hDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJw +TdwJx4nLCgdNbOhdjsnvzqvHu7UrTkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nx +e5AW0wdeRlN8NwdCjNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZA +JzVcoyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq4RgqsahD +YVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPAmRGunUHBcnWEvgJBQl9n +JEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57demyPxgcYxn/eR44/KJ4EBs+lVDR3veyJ +m+kXQ99b21/+jh5Xos1AnX5iItreGCc= +-----END CERTIFICATE----- + +AC RAIZ FNMT-RCM +================ +-----BEGIN CERTIFICATE----- +MIIFgzCCA2ugAwIBAgIPXZONMGc2yAYdGsdUhGkHMA0GCSqGSIb3DQEBCwUAMDsxCzAJBgNVBAYT +AkVTMREwDwYDVQQKDAhGTk1ULVJDTTEZMBcGA1UECwwQQUMgUkFJWiBGTk1ULVJDTTAeFw0wODEw +MjkxNTU5NTZaFw0zMDAxMDEwMDAwMDBaMDsxCzAJBgNVBAYTAkVTMREwDwYDVQQKDAhGTk1ULVJD +TTEZMBcGA1UECwwQQUMgUkFJWiBGTk1ULVJDTTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC +ggIBALpxgHpMhm5/yBNtwMZ9HACXjywMI7sQmkCpGreHiPibVmr75nuOi5KOpyVdWRHbNi63URcf +qQgfBBckWKo3Shjf5TnUV/3XwSyRAZHiItQDwFj8d0fsjz50Q7qsNI1NOHZnjrDIbzAzWHFctPVr +btQBULgTfmxKo0nRIBnuvMApGGWn3v7v3QqQIecaZ5JCEJhfTzC8PhxFtBDXaEAUwED653cXeuYL +j2VbPNmaUtu1vZ5Gzz3rkQUCwJaydkxNEJY7kvqcfw+Z374jNUUeAlz+taibmSXaXvMiwzn15Cou +08YfxGyqxRxqAQVKL9LFwag0Jl1mpdICIfkYtwb1TplvqKtMUejPUBjFd8g5CSxJkjKZqLsXF3mw +WsXmo8RZZUc1g16p6DULmbvkzSDGm0oGObVo/CK67lWMK07q87Hj/LaZmtVC+nFNCM+HHmpxffnT +tOmlcYF7wk5HlqX2doWjKI/pgG6BU6VtX7hI+cL5NqYuSf+4lsKMB7ObiFj86xsc3i1w4peSMKGJ +47xVqCfWS+2QrYv6YyVZLag13cqXM7zlzced0ezvXg5KkAYmY6252TUtB7p2ZSysV4999AeU14EC +ll2jB0nVetBX+RvnU0Z1qrB5QstocQjpYL05ac70r8NWQMetUqIJ5G+GR4of6ygnXYMgrwTJbFaa +i0b1AgMBAAGjgYMwgYAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE +FPd9xf3E6Jobd2Sn9R2gzL+HYJptMD4GA1UdIAQ3MDUwMwYEVR0gADArMCkGCCsGAQUFBwIBFh1o +dHRwOi8vd3d3LmNlcnQuZm5tdC5lcy9kcGNzLzANBgkqhkiG9w0BAQsFAAOCAgEAB5BK3/MjTvDD +nFFlm5wioooMhfNzKWtN/gHiqQxjAb8EZ6WdmF/9ARP67Jpi6Yb+tmLSbkyU+8B1RXxlDPiyN8+s +D8+Nb/kZ94/sHvJwnvDKuO+3/3Y3dlv2bojzr2IyIpMNOmqOFGYMLVN0V2Ue1bLdI4E7pWYjJ2cJ +j+F3qkPNZVEI7VFY/uY5+ctHhKQV8Xa7pO6kO8Rf77IzlhEYt8llvhjho6Tc+hj507wTmzl6NLrT +Qfv6MooqtyuGC2mDOL7Nii4LcK2NJpLuHvUBKwrZ1pebbuCoGRw6IYsMHkCtA+fdZn71uSANA+iW ++YJF1DngoABd15jmfZ5nc8OaKveri6E6FO80vFIOiZiaBECEHX5FaZNXzuvO+FB8TxxuBEOb+dY7 +Ixjp6o7RTUaN8Tvkasq6+yO3m/qZASlaWFot4/nUbQ4mrcFuNLwy+AwF+mWj2zs3gyLp1txyM/1d +8iC9djwj2ij3+RvrWWTV3F9yfiD8zYm1kGdNYno/Tq0dwzn+evQoFt9B9kiABdcPUXmsEKvU7ANm +5mqwujGSQkBqvjrTcuFqN1W8rB2Vt2lh8kORdOag0wokRqEIr9baRRmW1FMdW4R58MD3R++Lj8UG +rp1MYp3/RgT408m2ECVAdf4WqslKYIYvuu8wd+RU4riEmViAqhOLUTpPSPaLtrM= +-----END CERTIFICATE----- + +Amazon Root CA 1 +================ +-----BEGIN CERTIFICATE----- +MIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsFADA5MQswCQYD +VQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24gUm9vdCBDQSAxMB4XDTE1 +MDUyNjAwMDAwMFoXDTM4MDExNzAwMDAwMFowOTELMAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpv +bjEZMBcGA1UEAxMQQW1hem9uIFJvb3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBALJ4gHHKeNXjca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgH +FzZM9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qwIFAGbHrQ +gLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6VOujw5H5SNz/0egwLX0t +dHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L93FcXmn/6pUCyziKrlA4b9v7LWIbxcce +VOF34GfID5yHI9Y/QCB/IIDEgEw+OyQmjgSubJrIqg0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB +/zAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0OBBYEFIQYzIU07LwMlJQuCFmcx7IQTgoIMA0GCSqGSIb3 +DQEBCwUAA4IBAQCY8jdaQZChGsV2USggNiMOruYou6r4lK5IpDB/G/wkjUu0yKGX9rbxenDIU5PM +CCjjmCXPI6T53iHTfIUJrU6adTrCC2qJeHZERxhlbI1Bjjt/msv0tadQ1wUsN+gDS63pYaACbvXy +8MWy7Vu33PqUXHeeE6V/Uq2V8viTO96LXFvKWlJbYK8U90vvo/ufQJVtMVT8QtPHRh8jrdkPSHCa +2XV4cdFyQzR1bldZwgJcJmApzyMZFo6IQ6XU5MsI+yMRQ+hDKXJioaldXgjUkK642M4UwtBV8ob2 +xJNDd2ZhwLnoQdeXeGADbkpyrqXRfboQnoZsG4q5WTP468SQvvG5 +-----END CERTIFICATE----- + +Amazon Root CA 2 +================ +-----BEGIN CERTIFICATE----- +MIIFQTCCAymgAwIBAgITBmyf0pY1hp8KD+WGePhbJruKNzANBgkqhkiG9w0BAQwFADA5MQswCQYD +VQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24gUm9vdCBDQSAyMB4XDTE1 +MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpv +bjEZMBcGA1UEAxMQQW1hem9uIFJvb3QgQ0EgMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC +ggIBAK2Wny2cSkxKgXlRmeyKy2tgURO8TW0G/LAIjd0ZEGrHJgw12MBvIITplLGbhQPDW9tK6Mj4 +kHbZW0/jTOgGNk3Mmqw9DJArktQGGWCsN0R5hYGCrVo34A3MnaZMUnbqQ523BNFQ9lXg1dKmSYXp +N+nKfq5clU1Imj+uIFptiJXZNLhSGkOQsL9sBbm2eLfq0OQ6PBJTYv9K8nu+NQWpEjTj82R0Yiw9 +AElaKP4yRLuH3WUnAnE72kr3H9rN9yFVkE8P7K6C4Z9r2UXTu/Bfh+08LDmG2j/e7HJV63mjrdvd +fLC6HM783k81ds8P+HgfajZRRidhW+mez/CiVX18JYpvL7TFz4QuK/0NURBs+18bvBt+xa47mAEx +kv8LV/SasrlX6avvDXbR8O70zoan4G7ptGmh32n2M8ZpLpcTnqWHsFcQgTfJU7O7f/aS0ZzQGPSS +btqDT6ZjmUyl+17vIWR6IF9sZIUVyzfpYgwLKhbcAS4y2j5L9Z469hdAlO+ekQiG+r5jqFoz7Mt0 +Q5X5bGlSNscpb/xVA1wf+5+9R+vnSUeVC06JIglJ4PVhHvG/LopyboBZ/1c6+XUyo05f7O0oYtlN +c/LMgRdg7c3r3NunysV+Ar3yVAhU/bQtCSwXVEqY0VThUWcI0u1ufm8/0i2BWSlmy5A5lREedCf+ +3euvAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSw +DPBMMPQFWAJI/TPlUq9LhONmUjANBgkqhkiG9w0BAQwFAAOCAgEAqqiAjw54o+Ci1M3m9Zh6O+oA +A7CXDpO8Wqj2LIxyh6mx/H9z/WNxeKWHWc8w4Q0QshNabYL1auaAn6AFC2jkR2vHat+2/XcycuUY ++gn0oJMsXdKMdYV2ZZAMA3m3MSNjrXiDCYZohMr/+c8mmpJ5581LxedhpxfL86kSk5Nrp+gvU5LE +YFiwzAJRGFuFjWJZY7attN6a+yb3ACfAXVU3dJnJUH/jWS5E4ywl7uxMMne0nxrpS10gxdr9HIcW +xkPo1LsmmkVwXqkLN1PiRnsn/eBG8om3zEK2yygmbtmlyTrIQRNg91CMFa6ybRoVGld45pIq2WWQ +gj9sAq+uEjonljYE1x2igGOpm/HlurR8FLBOybEfdF849lHqm/osohHUqS0nGkWxr7JOcQ3AWEbW +aQbLU8uz/mtBzUF+fUwPfHJ5elnNXkoOrJupmHN5fLT0zLm4BwyydFy4x2+IoZCn9Kr5v2c69BoV +Yh63n749sSmvZ6ES8lgQGVMDMBu4Gon2nL2XA46jCfMdiyHxtN/kHNGfZQIG6lzWE7OE76KlXIx3 +KadowGuuQNKotOrN8I1LOJwZmhsoVLiJkO/KdYE+HvJkJMcYr07/R54H9jVlpNMKVv/1F2Rs76gi +JUmTtt8AF9pYfl3uxRuw0dFfIRDH+fO6AgonB8Xx1sfT4PsJYGw= +-----END CERTIFICATE----- + +Amazon Root CA 3 +================ +-----BEGIN CERTIFICATE----- +MIIBtjCCAVugAwIBAgITBmyf1XSXNmY/Owua2eiedgPySjAKBggqhkjOPQQDAjA5MQswCQYDVQQG +EwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24gUm9vdCBDQSAzMB4XDTE1MDUy +NjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZ +MBcGA1UEAxMQQW1hem9uIFJvb3QgQ0EgMzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABCmXp8ZB +f8ANm+gBG1bG8lKlui2yEujSLtf6ycXYqm0fc4E7O5hrOXwzpcVOho6AF2hiRVd9RFgdszflZwjr +Zt6jQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSrttvXBp43 +rDCGB5Fwx5zEGbF4wDAKBggqhkjOPQQDAgNJADBGAiEA4IWSoxe3jfkrBqWTrBqYaGFy+uGh0Psc +eGCmQ5nFuMQCIQCcAu/xlJyzlvnrxir4tiz+OpAUFteMYyRIHN8wfdVoOw== +-----END CERTIFICATE----- + +Amazon Root CA 4 +================ +-----BEGIN CERTIFICATE----- +MIIB8jCCAXigAwIBAgITBmyf18G7EEwpQ+Vxe3ssyBrBDjAKBggqhkjOPQQDAzA5MQswCQYDVQQG +EwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24gUm9vdCBDQSA0MB4XDTE1MDUy +NjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZ +MBcGA1UEAxMQQW1hem9uIFJvb3QgQ0EgNDB2MBAGByqGSM49AgEGBSuBBAAiA2IABNKrijdPo1MN +/sGKe0uoe0ZLY7Bi9i0b2whxIdIA6GO9mif78DluXeo9pcmBqqNbIJhFXRbb/egQbeOc4OO9X4Ri +83BkM6DLJC9wuoihKqB1+IGuYgbEgds5bimwHvouXKNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNV +HQ8BAf8EBAMCAYYwHQYDVR0OBBYEFNPsxzplbszh2naaVvuc84ZtV+WBMAoGCCqGSM49BAMDA2gA +MGUCMDqLIfG9fhGt0O9Yli/W651+kI0rz2ZVwyzjKKlwCkcO8DdZEv8tmZQoTipPNU0zWgIxAOp1 +AE47xDqUEpHJWEadIRNyp4iciuRMStuW1KyLa2tJElMzrdfkviT8tQp21KW8EA== +-----END CERTIFICATE----- + +TUBITAK Kamu SM SSL Kok Sertifikasi - Surum 1 +============================================= +-----BEGIN CERTIFICATE----- +MIIEYzCCA0ugAwIBAgIBATANBgkqhkiG9w0BAQsFADCB0jELMAkGA1UEBhMCVFIxGDAWBgNVBAcT +D0dlYnplIC0gS29jYWVsaTFCMEAGA1UEChM5VHVya2l5ZSBCaWxpbXNlbCB2ZSBUZWtub2xvamlr +IEFyYXN0aXJtYSBLdXJ1bXUgLSBUVUJJVEFLMS0wKwYDVQQLEyRLYW11IFNlcnRpZmlrYXN5b24g +TWVya2V6aSAtIEthbXUgU00xNjA0BgNVBAMTLVRVQklUQUsgS2FtdSBTTSBTU0wgS29rIFNlcnRp +ZmlrYXNpIC0gU3VydW0gMTAeFw0xMzExMjUwODI1NTVaFw00MzEwMjUwODI1NTVaMIHSMQswCQYD +VQQGEwJUUjEYMBYGA1UEBxMPR2ViemUgLSBLb2NhZWxpMUIwQAYDVQQKEzlUdXJraXllIEJpbGlt +c2VsIHZlIFRla25vbG9qaWsgQXJhc3Rpcm1hIEt1cnVtdSAtIFRVQklUQUsxLTArBgNVBAsTJEth +bXUgU2VydGlmaWthc3lvbiBNZXJrZXppIC0gS2FtdSBTTTE2MDQGA1UEAxMtVFVCSVRBSyBLYW11 +IFNNIFNTTCBLb2sgU2VydGlmaWthc2kgLSBTdXJ1bSAxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEAr3UwM6q7a9OZLBI3hNmNe5eA027n/5tQlT6QlVZC1xl8JoSNkvoBHToP4mQ4t4y8 +6Ij5iySrLqP1N+RAjhgleYN1Hzv/bKjFxlb4tO2KRKOrbEz8HdDc72i9z+SqzvBV96I01INrN3wc +wv61A+xXzry0tcXtAA9TNypN9E8Mg/uGz8v+jE69h/mniyFXnHrfA2eJLJ2XYacQuFWQfw4tJzh0 +3+f92k4S400VIgLI4OD8D62K18lUUMw7D8oWgITQUVbDjlZ/iSIzL+aFCr2lqBs23tPcLG07xxO9 +WSMs5uWk99gL7eqQQESolbuT1dCANLZGeA4fAJNG4e7p+exPFwIDAQABo0IwQDAdBgNVHQ4EFgQU +ZT/HiobGPN08VFw1+DrtUgxHV8gwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJ +KoZIhvcNAQELBQADggEBACo/4fEyjq7hmFxLXs9rHmoJ0iKpEsdeV31zVmSAhHqT5Am5EM2fKifh +AHe+SMg1qIGf5LgsyX8OsNJLN13qudULXjS99HMpw+0mFZx+CFOKWI3QSyjfwbPfIPP54+M638yc +lNhOT8NrF7f3cuitZjO1JVOr4PhMqZ398g26rrnZqsZr+ZO7rqu4lzwDGrpDxpa5RXI4s6ehlj2R +e37AIVNMh+3yC1SVUZPVIqUNivGTDj5UDrDYyU7c8jEyVupk+eq1nRZmQnLzf9OxMUP8pI4X8W0j +q5Rm+K37DwhuJi1/FwcJsoz7UMCflo3Ptv0AnVoUmr8CRPXBwp8iXqIPoeM= +-----END CERTIFICATE----- + +GDCA TrustAUTH R5 ROOT +====================== +-----BEGIN CERTIFICATE----- +MIIFiDCCA3CgAwIBAgIIfQmX/vBH6nowDQYJKoZIhvcNAQELBQAwYjELMAkGA1UEBhMCQ04xMjAw +BgNVBAoMKUdVQU5HIERPTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZIENPLixMVEQuMR8wHQYDVQQD +DBZHRENBIFRydXN0QVVUSCBSNSBST09UMB4XDTE0MTEyNjA1MTMxNVoXDTQwMTIzMTE1NTk1OVow +YjELMAkGA1UEBhMCQ04xMjAwBgNVBAoMKUdVQU5HIERPTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZ +IENPLixMVEQuMR8wHQYDVQQDDBZHRENBIFRydXN0QVVUSCBSNSBST09UMIICIjANBgkqhkiG9w0B +AQEFAAOCAg8AMIICCgKCAgEA2aMW8Mh0dHeb7zMNOwZ+Vfy1YI92hhJCfVZmPoiC7XJjDp6L3TQs +AlFRwxn9WVSEyfFrs0yw6ehGXTjGoqcuEVe6ghWinI9tsJlKCvLriXBjTnnEt1u9ol2x8kECK62p +OqPseQrsXzrj/e+APK00mxqriCZ7VqKChh/rNYmDf1+uKU49tm7srsHwJ5uu4/Ts765/94Y9cnrr +pftZTqfrlYwiOXnhLQiPzLyRuEH3FMEjqcOtmkVEs7LXLM3GKeJQEK5cy4KOFxg2fZfmiJqwTTQJ +9Cy5WmYqsBebnh52nUpmMUHfP/vFBu8btn4aRjb3ZGM74zkYI+dndRTVdVeSN72+ahsmUPI2JgaQ +xXABZG12ZuGR224HwGGALrIuL4xwp9E7PLOR5G62xDtw8mySlwnNR30YwPO7ng/Wi64HtloPzgsM +R6flPri9fcebNaBhlzpBdRfMK5Z3KpIhHtmVdiBnaM8Nvd/WHwlqmuLMc3GkL30SgLdTMEZeS1SZ +D2fJpcjyIMGC7J0R38IC+xo70e0gmu9lZJIQDSri3nDxGGeCjGHeuLzRL5z7D9Ar7Rt2ueQ5Vfj4 +oR24qoAATILnsn8JuLwwoC8N9VKejveSswoAHQBUlwbgsQfZxw9cZX08bVlX5O2ljelAU58VS6Bx +9hoh49pwBiFYFIeFd3mqgnkCAwEAAaNCMEAwHQYDVR0OBBYEFOLJQJ9NzuiaoXzPDj9lxSmIahlR +MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQDRSVfg +p8xoWLoBDysZzY2wYUWsEe1jUGn4H3++Fo/9nesLqjJHdtJnJO29fDMylyrHBYZmDRd9FBUb1Ov9 +H5r2XpdptxolpAqzkT9fNqyL7FeoPueBihhXOYV0GkLH6VsTX4/5COmSdI31R9KrO9b7eGZONn35 +6ZLpBN79SWP8bfsUcZNnL0dKt7n/HipzcEYwv1ryL3ml4Y0M2fmyYzeMN2WFcGpcWwlyua1jPLHd ++PwyvzeG5LuOmCd+uh8W4XAR8gPfJWIyJyYYMoSf/wA6E7qaTfRPuBRwIrHKK5DOKcFw9C+df/KQ +HtZa37dG/OaG+svgIHZ6uqbL9XzeYqWxi+7egmaKTjowHz+Ay60nugxe19CxVsp3cbK1daFQqUBD +F8Io2c9Si1vIY9RCPqAzekYu9wogRlR+ak8x8YF+QnQ4ZXMn7sZ8uI7XpTrXmKGcjBBV09tL7ECQ +8s1uV9JiDnxXk7Gnbc2dg7sq5+W2O3FYrf3RRbxake5TFW/TRQl1brqQXR4EzzffHqhmsYzmIGrv +/EhOdJhCrylvLmrH+33RZjEizIYAfmaDDEL0vTSSwxrqT8p+ck0LcIymSLumoRT2+1hEmRSuqguT +aaApJUqlyyvdimYHFngVV3Eb7PVHhPOeMTd61X8kreS8/f3MboPoDKi3QWwH3b08hpcv0g== +-----END CERTIFICATE----- + +SSL.com Root Certification Authority RSA +======================================== +-----BEGIN CERTIFICATE----- +MIIF3TCCA8WgAwIBAgIIeyyb0xaAMpkwDQYJKoZIhvcNAQELBQAwfDELMAkGA1UEBhMCVVMxDjAM +BgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9TU0wgQ29ycG9yYXRpb24x +MTAvBgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSBSU0EwHhcNMTYw +MjEyMTczOTM5WhcNNDEwMjEyMTczOTM5WjB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMx +EDAOBgNVBAcMB0hvdXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NM +LmNvbSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IFJTQTCCAiIwDQYJKoZIhvcNAQEBBQAD +ggIPADCCAgoCggIBAPkP3aMrfcvQKv7sZ4Wm5y4bunfh4/WvpOz6Sl2RxFdHaxh3a3by/ZPkPQ/C +Fp4LZsNWlJ4Xg4XOVu/yFv0AYvUiCVToZRdOQbngT0aXqhvIuG5iXmmxX9sqAn78bMrzQdjt0Oj8 +P2FI7bADFB0QDksZ4LtO7IZl/zbzXmcCC52GVWH9ejjt/uIZALdvoVBidXQ8oPrIJZK0bnoix/ge +oeOy3ZExqysdBP+lSgQ36YWkMyv94tZVNHwZpEpox7Ko07fKoZOI68GXvIz5HdkihCR0xwQ9aqkp +k8zruFvh/l8lqjRYyMEjVJ0bmBHDOJx+PYZspQ9AhnwC9FwCTyjLrnGfDzrIM/4RJTXq/LrFYD3Z +fBjVsqnTdXgDciLKOsMf7yzlLqn6niy2UUb9rwPW6mBo6oUWNmuF6R7As93EJNyAKoFBbZQ+yODJ +gUEAnl6/f8UImKIYLEJAs/lvOCdLToD0PYFH4Ih86hzOtXVcUS4cK38acijnALXRdMbX5J+tB5O2 +UzU1/Dfkw/ZdFr4hc96SCvigY2q8lpJqPvi8ZVWb3vUNiSYE/CUapiVpy8JtynziWV+XrOvvLsi8 +1xtZPCvM8hnIk2snYxnP/Okm+Mpxm3+T/jRnhE6Z6/yzeAkzcLpmpnbtG3PrGqUNxCITIJRWCk4s +bE6x/c+cCbqiM+2HAgMBAAGjYzBhMB0GA1UdDgQWBBTdBAkHovV6fVJTEpKV7jiAJQ2mWTAPBgNV +HRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFN0ECQei9Xp9UlMSkpXuOIAlDaZZMA4GA1UdDwEB/wQE +AwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAIBgRlCn7Jp0cHh5wYfGVcpNxJK1ok1iOMq8bs3AD/CUr +dIWQPXhq9LmLpZc7tRiRux6n+UBbkflVma8eEdBcHadm47GUBwwyOabqG7B52B2ccETjit3E+ZUf +ijhDPwGFpUenPUayvOUiaPd7nNgsPgohyC0zrL/FgZkxdMF1ccW+sfAjRfSda/wZY52jvATGGAsl +u1OJD7OAUN5F7kR/q5R4ZJjT9ijdh9hwZXT7DrkT66cPYakylszeu+1jTBi7qUD3oFRuIIhxdRjq +erQ0cuAjJ3dctpDqhiVAq+8zD8ufgr6iIPv2tS0a5sKFsXQP+8hlAqRSAUfdSSLBv9jra6x+3uxj +MxW3IwiPxg+NQVrdjsW5j+VFP3jbutIbQLH+cU0/4IGiul607BXgk90IH37hVZkLId6Tngr75qNJ +vTYw/ud3sqB1l7UtgYgXZSD32pAAn8lSzDLKNXz1PQ/YK9f1JmzJBjSWFupwWRoyeXkLtoh/D1JI +Pb9s2KJELtFOt3JY04kTlf5Eq/jXixtunLwsoFvVagCvXzfh1foQC5ichucmj87w7G6KVwuA406y +wKBjYZC6VWg3dGq2ktufoYYitmUnDuy2n0Jg5GfCtdpBC8TTi2EbvPofkSvXRAdeuims2cXp71NI +WuuA8ShYIc2wBlX7Jz9TkHCpBB5XJ7k= +-----END CERTIFICATE----- + +SSL.com Root Certification Authority ECC +======================================== +-----BEGIN CERTIFICATE----- +MIICjTCCAhSgAwIBAgIIdebfy8FoW6gwCgYIKoZIzj0EAwIwfDELMAkGA1UEBhMCVVMxDjAMBgNV +BAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9TU0wgQ29ycG9yYXRpb24xMTAv +BgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSBFQ0MwHhcNMTYwMjEy +MTgxNDAzWhcNNDEwMjEyMTgxNDAzWjB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAO +BgNVBAcMB0hvdXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NMLmNv +bSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IEVDQzB2MBAGByqGSM49AgEGBSuBBAAiA2IA +BEVuqVDEpiM2nl8ojRfLliJkP9x6jh3MCLOicSS6jkm5BBtHllirLZXI7Z4INcgn64mMU1jrYor+ +8FsPazFSY0E7ic3s7LaNGdM0B9y7xgZ/wkWV7Mt/qCPgCemB+vNH06NjMGEwHQYDVR0OBBYEFILR +hXMw5zUE044CkvvlpNHEIejNMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUgtGFczDnNQTT +jgKS++Wk0cQh6M0wDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2cAMGQCMG/n61kRpGDPYbCW +e+0F+S8Tkdzt5fxQaxFGRrMcIQBiu77D5+jNB5n5DQtdcj7EqgIwH7y6C+IwJPt8bYBVCpk+gA0z +5Wajs6O7pdWLjwkspl1+4vAHCGht0nxpbl/f5Wpl +-----END CERTIFICATE----- + +SSL.com EV Root Certification Authority RSA R2 +============================================== +-----BEGIN CERTIFICATE----- +MIIF6zCCA9OgAwIBAgIIVrYpzTS8ePYwDQYJKoZIhvcNAQELBQAwgYIxCzAJBgNVBAYTAlVTMQ4w +DAYDVQQIDAVUZXhhczEQMA4GA1UEBwwHSG91c3RvbjEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9u +MTcwNQYDVQQDDC5TU0wuY29tIEVWIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgUlNBIFIy +MB4XDTE3MDUzMTE4MTQzN1oXDTQyMDUzMDE4MTQzN1owgYIxCzAJBgNVBAYTAlVTMQ4wDAYDVQQI +DAVUZXhhczEQMA4GA1UEBwwHSG91c3RvbjEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMTcwNQYD +VQQDDC5TU0wuY29tIEVWIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgUlNBIFIyMIICIjAN +BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAjzZlQOHWTcDXtOlG2mvqM0fNTPl9fb69LT3w23jh +hqXZuglXaO1XPqDQCEGD5yhBJB/jchXQARr7XnAjssufOePPxU7Gkm0mxnu7s9onnQqG6YE3Bf7w +cXHswxzpY6IXFJ3vG2fThVUCAtZJycxa4bH3bzKfydQ7iEGonL3Lq9ttewkfokxykNorCPzPPFTO +Zw+oz12WGQvE43LrrdF9HSfvkusQv1vrO6/PgN3B0pYEW3p+pKk8OHakYo6gOV7qd89dAFmPZiw+ +B6KjBSYRaZfqhbcPlgtLyEDhULouisv3D5oi53+aNxPN8k0TayHRwMwi8qFG9kRpnMphNQcAb9Zh +CBHqurj26bNg5U257J8UZslXWNvNh2n4ioYSA0e/ZhN2rHd9NCSFg83XqpyQGp8hLH94t2S42Oim +9HizVcuE0jLEeK6jj2HdzghTreyI/BXkmg3mnxp3zkyPuBQVPWKchjgGAGYS5Fl2WlPAApiiECto +RHuOec4zSnaqW4EWG7WK2NAAe15itAnWhmMOpgWVSbooi4iTsjQc2KRVbrcc0N6ZVTsj9CLg+Slm +JuwgUHfbSguPvuUCYHBBXtSuUDkiFCbLsjtzdFVHB3mBOagwE0TlBIqulhMlQg+5U8Sb/M3kHN48 ++qvWBkofZ6aYMBzdLNvcGJVXZsb/XItW9XcCAwEAAaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNV +HSMEGDAWgBT5YLvU49U09rj1BoAlp3PbRmmonjAdBgNVHQ4EFgQU+WC71OPVNPa49QaAJadz20Zp +qJ4wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQBWs47LCp1Jjr+kxJG7ZhcFUZh1 +++VQLHqe8RT6q9OKPv+RKY9ji9i0qVQBDb6Thi/5Sm3HXvVX+cpVHBK+Rw82xd9qt9t1wkclf7nx +Y/hoLVUE0fKNsKTPvDxeH3jnpaAgcLAExbf3cqfeIg29MyVGjGSSJuM+LmOW2puMPfgYCdcDzH2G +guDKBAdRUNf/ktUM79qGn5nX67evaOI5JpS6aLe/g9Pqemc9YmeuJeVy6OLk7K4S9ksrPJ/psEDz +OFSz/bdoyNrGj1E8svuR3Bznm53htw1yj+KkxKl4+esUrMZDBcJlOSgYAsOCsp0FvmXtll9ldDz7 +CTUue5wT/RsPXcdtgTpWD8w74a8CLyKsRspGPKAcTNZEtF4uXBVmCeEmKf7GUmG6sXP/wwyc5Wxq +lD8UykAWlYTzWamsX0xhk23RO8yilQwipmdnRC652dKKQbNmC1r7fSOl8hqw/96bg5Qu0T/fkreR +rwU7ZcegbLHNYhLDkBvjJc40vG93drEQw/cFGsDWr3RiSBd3kmmQYRzelYB0VI8YHMPzA9C/pEN1 +hlMYegouCRw2n5H9gooiS9EOUCXdywMMF8mDAAhONU2Ki+3wApRmLER/y5UnlhetCTCstnEXbosX +9hwJ1C07mKVx01QT2WDz9UtmT/rx7iASjbSsV7FFY6GsdqnC+w== +-----END CERTIFICATE----- + +SSL.com EV Root Certification Authority ECC +=========================================== +-----BEGIN CERTIFICATE----- +MIIClDCCAhqgAwIBAgIILCmcWxbtBZUwCgYIKoZIzj0EAwIwfzELMAkGA1UEBhMCVVMxDjAMBgNV +BAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9TU0wgQ29ycG9yYXRpb24xNDAy +BgNVBAMMK1NTTC5jb20gRVYgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSBFQ0MwHhcNMTYw +MjEyMTgxNTIzWhcNNDEwMjEyMTgxNTIzWjB/MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMx +EDAOBgNVBAcMB0hvdXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjE0MDIGA1UEAwwrU1NM +LmNvbSBFViBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IEVDQzB2MBAGByqGSM49AgEGBSuB +BAAiA2IABKoSR5CYG/vvw0AHgyBO8TCCogbR8pKGYfL2IWjKAMTH6kMAVIbc/R/fALhBYlzccBYy +3h+Z1MzFB8gIH2EWB1E9fVwHU+M1OIzfzZ/ZLg1KthkuWnBaBu2+8KGwytAJKaNjMGEwHQYDVR0O +BBYEFFvKXuXe0oGqzagtZFG22XKbl+ZPMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUW8pe +5d7SgarNqC1kUbbZcpuX5k8wDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2gAMGUCMQCK5kCJ +N+vp1RPZytRrJPOwPYdGWBrssd9v+1a6cGvHOMzosYxPD/fxZ3YOg9AeUY8CMD32IygmTMZgh5Mm +m7I1HrrW9zzRHM76JTymGoEVW/MSD2zuZYrJh6j5B+BimoxcSg== +-----END CERTIFICATE----- + +GlobalSign Root CA - R6 +======================= +-----BEGIN CERTIFICATE----- +MIIFgzCCA2ugAwIBAgIORea7A4Mzw4VlSOb/RVEwDQYJKoZIhvcNAQEMBQAwTDEgMB4GA1UECxMX +R2xvYmFsU2lnbiBSb290IENBIC0gUjYxEzARBgNVBAoTCkdsb2JhbFNpZ24xEzARBgNVBAMTCkds +b2JhbFNpZ24wHhcNMTQxMjEwMDAwMDAwWhcNMzQxMjEwMDAwMDAwWjBMMSAwHgYDVQQLExdHbG9i +YWxTaWduIFJvb3QgQ0EgLSBSNjETMBEGA1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFs +U2lnbjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAJUH6HPKZvnsFMp7PPcNCPG0RQss +grRIxutbPK6DuEGSMxSkb3/pKszGsIhrxbaJ0cay/xTOURQh7ErdG1rG1ofuTToVBu1kZguSgMpE +3nOUTvOniX9PeGMIyBJQbUJmL025eShNUhqKGoC3GYEOfsSKvGRMIRxDaNc9PIrFsmbVkJq3MQbF +vuJtMgamHvm566qjuL++gmNQ0PAYid/kD3n16qIfKtJwLnvnvJO7bVPiSHyMEAc4/2ayd2F+4OqM +PKq0pPbzlUoSB239jLKJz9CgYXfIWHSw1CM69106yqLbnQneXUQtkPGBzVeS+n68UARjNN9rkxi+ +azayOeSsJDa38O+2HBNXk7besvjihbdzorg1qkXy4J02oW9UivFyVm4uiMVRQkQVlO6jxTiWm05O +WgtH8wY2SXcwvHE35absIQh1/OZhFj931dmRl4QKbNQCTXTAFO39OfuD8l4UoQSwC+n+7o/hbguy +CLNhZglqsQY6ZZZZwPA1/cnaKI0aEYdwgQqomnUdnjqGBQCe24DWJfncBZ4nWUx2OVvq+aWh2IMP +0f/fMBH5hc8zSPXKbWQULHpYT9NLCEnFlWQaYw55PfWzjMpYrZxCRXluDocZXFSxZba/jJvcE+kN +b7gu3GduyYsRtYQUigAZcIN5kZeR1BonvzceMgfYFGM8KEyvAgMBAAGjYzBhMA4GA1UdDwEB/wQE +AwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSubAWjkxPioufi1xzWx/B/yGdToDAfBgNV +HSMEGDAWgBSubAWjkxPioufi1xzWx/B/yGdToDANBgkqhkiG9w0BAQwFAAOCAgEAgyXt6NH9lVLN +nsAEoJFp5lzQhN7craJP6Ed41mWYqVuoPId8AorRbrcWc+ZfwFSY1XS+wc3iEZGtIxg93eFyRJa0 +lV7Ae46ZeBZDE1ZXs6KzO7V33EByrKPrmzU+sQghoefEQzd5Mr6155wsTLxDKZmOMNOsIeDjHfrY +BzN2VAAiKrlNIC5waNrlU/yDXNOd8v9EDERm8tLjvUYAGm0CuiVdjaExUd1URhxN25mW7xocBFym +Fe944Hn+Xds+qkxV/ZoVqW/hpvvfcDDpw+5CRu3CkwWJ+n1jez/QcYF8AOiYrg54NMMl+68KnyBr +3TsTjxKM4kEaSHpzoHdpx7Zcf4LIHv5YGygrqGytXm3ABdJ7t+uA/iU3/gKbaKxCXcPu9czc8FB1 +0jZpnOZ7BN9uBmm23goJSFmH63sUYHpkqmlD75HHTOwY3WzvUy2MmeFe8nI+z1TIvWfspA9MRf/T +uTAjB0yPEL+GltmZWrSZVxykzLsViVO6LAUP5MSeGbEYNNVMnbrt9x+vJJUEeKgDu+6B5dpffItK +oZB0JaezPkvILFa9x8jvOOJckvB595yEunQtYQEgfn7R8k8HWV+LLUNS60YMlOH1Zkd5d9VUWx+t +JDfLRVpOoERIyNiwmcUVhAn21klJwGW45hpxbqCo8YLoRT5s1gLXCmeDBVrJpBA= +-----END CERTIFICATE----- + +OISTE WISeKey Global Root GC CA +=============================== +-----BEGIN CERTIFICATE----- +MIICaTCCAe+gAwIBAgIQISpWDK7aDKtARb8roi066jAKBggqhkjOPQQDAzBtMQswCQYDVQQGEwJD +SDEQMA4GA1UEChMHV0lTZUtleTEiMCAGA1UECxMZT0lTVEUgRm91bmRhdGlvbiBFbmRvcnNlZDEo +MCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9iYWwgUm9vdCBHQyBDQTAeFw0xNzA1MDkwOTQ4MzRa +Fw00MjA1MDkwOTU4MzNaMG0xCzAJBgNVBAYTAkNIMRAwDgYDVQQKEwdXSVNlS2V5MSIwIAYDVQQL +ExlPSVNURSBGb3VuZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBXSVNlS2V5IEdsb2Jh +bCBSb290IEdDIENBMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAETOlQwMYPchi82PG6s4nieUqjFqdr +VCTbUf/q9Akkwwsin8tqJ4KBDdLArzHkdIJuyiXZjHWd8dvQmqJLIX4Wp2OQ0jnUsYd4XxiWD1Ab +NTcPasbc2RNNpI6QN+a9WzGRo1QwUjAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAd +BgNVHQ4EFgQUSIcUrOPDnpBgOtfKie7TrYy0UGYwEAYJKwYBBAGCNxUBBAMCAQAwCgYIKoZIzj0E +AwMDaAAwZQIwJsdpW9zV57LnyAyMjMPdeYwbY9XJUpROTYJKcx6ygISpJcBMWm1JKWB4E+J+SOtk +AjEA2zQgMgj/mkkCtojeFK9dbJlxjRo/i9fgojaGHAeCOnZT/cKi7e97sIBPWA9LUzm9 +-----END CERTIFICATE----- + +UCA Global G2 Root +================== +-----BEGIN CERTIFICATE----- +MIIFRjCCAy6gAwIBAgIQXd+x2lqj7V2+WmUgZQOQ7zANBgkqhkiG9w0BAQsFADA9MQswCQYDVQQG +EwJDTjERMA8GA1UECgwIVW5pVHJ1c3QxGzAZBgNVBAMMElVDQSBHbG9iYWwgRzIgUm9vdDAeFw0x +NjAzMTEwMDAwMDBaFw00MDEyMzEwMDAwMDBaMD0xCzAJBgNVBAYTAkNOMREwDwYDVQQKDAhVbmlU +cnVzdDEbMBkGA1UEAwwSVUNBIEdsb2JhbCBHMiBSb290MIICIjANBgkqhkiG9w0BAQEFAAOCAg8A +MIICCgKCAgEAxeYrb3zvJgUno4Ek2m/LAfmZmqkywiKHYUGRO8vDaBsGxUypK8FnFyIdK+35KYmT +oni9kmugow2ifsqTs6bRjDXVdfkX9s9FxeV67HeToI8jrg4aA3++1NDtLnurRiNb/yzmVHqUwCoV +8MmNsHo7JOHXaOIxPAYzRrZUEaalLyJUKlgNAQLx+hVRZ2zA+te2G3/RVogvGjqNO7uCEeBHANBS +h6v7hn4PJGtAnTRnvI3HLYZveT6OqTwXS3+wmeOwcWDcC/Vkw85DvG1xudLeJ1uK6NjGruFZfc8o +LTW4lVYa8bJYS7cSN8h8s+1LgOGN+jIjtm+3SJUIsUROhYw6AlQgL9+/V087OpAh18EmNVQg7Mc/ +R+zvWr9LesGtOxdQXGLYD0tK3Cv6brxzks3sx1DoQZbXqX5t2Okdj4q1uViSukqSKwxW/YDrCPBe +KW4bHAyvj5OJrdu9o54hyokZ7N+1wxrrFv54NkzWbtA+FxyQF2smuvt6L78RHBgOLXMDj6DlNaBa +4kx1HXHhOThTeEDMg5PXCp6dW4+K5OXgSORIskfNTip1KnvyIvbJvgmRlld6iIis7nCs+dwp4wwc +OxJORNanTrAmyPPZGpeRaOrvjUYG0lZFWJo8DA+DuAUlwznPO6Q0ibd5Ei9Hxeepl2n8pndntd97 +8XplFeRhVmUCAwEAAaNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0O +BBYEFIHEjMz15DD/pQwIX4wVZyF0Ad/fMA0GCSqGSIb3DQEBCwUAA4ICAQATZSL1jiutROTL/7lo +5sOASD0Ee/ojL3rtNtqyzm325p7lX1iPyzcyochltq44PTUbPrw7tgTQvPlJ9Zv3hcU2tsu8+Mg5 +1eRfB70VVJd0ysrtT7q6ZHafgbiERUlMjW+i67HM0cOU2kTC5uLqGOiiHycFutfl1qnN3e92mI0A +Ds0b+gO3joBYDic/UvuUospeZcnWhNq5NXHzJsBPd+aBJ9J3O5oUb3n09tDh05S60FdRvScFDcH9 +yBIw7m+NESsIndTUv4BFFJqIRNow6rSn4+7vW4LVPtateJLbXDzz2K36uGt/xDYotgIVilQsnLAX +c47QN6MUPJiVAAwpBVueSUmxX8fjy88nZY41F7dXyDDZQVu5FLbowg+UMaeUmMxq67XhJ/UQqAHo +jhJi6IjMtX9Gl8CbEGY4GjZGXyJoPd/JxhMnq1MGrKI8hgZlb7F+sSlEmqO6SWkoaY/X5V+tBIZk +bxqgDMUIYs6Ao9Dz7GjevjPHF1t/gMRMTLGmhIrDO7gJzRSBuhjjVFc2/tsvfEehOjPI+Vg7RE+x +ygKJBJYoaMVLuCaJu9YzL1DV/pqJuhgyklTGW+Cd+V7lDSKb9triyCGyYiGqhkCyLmTTX8jjfhFn +RR8F/uOi77Oos/N9j/gMHyIfLXC0uAE0djAA5SN4p1bXUB+K+wb1whnw0A== +-----END CERTIFICATE----- + +UCA Extended Validation Root +============================ +-----BEGIN CERTIFICATE----- +MIIFWjCCA0KgAwIBAgIQT9Irj/VkyDOeTzRYZiNwYDANBgkqhkiG9w0BAQsFADBHMQswCQYDVQQG +EwJDTjERMA8GA1UECgwIVW5pVHJ1c3QxJTAjBgNVBAMMHFVDQSBFeHRlbmRlZCBWYWxpZGF0aW9u +IFJvb3QwHhcNMTUwMzEzMDAwMDAwWhcNMzgxMjMxMDAwMDAwWjBHMQswCQYDVQQGEwJDTjERMA8G +A1UECgwIVW5pVHJ1c3QxJTAjBgNVBAMMHFVDQSBFeHRlbmRlZCBWYWxpZGF0aW9uIFJvb3QwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCpCQcoEwKwmeBkqh5DFnpzsZGgdT6o+uM4AHrs +iWogD4vFsJszA1qGxliG1cGFu0/GnEBNyr7uaZa4rYEwmnySBesFK5pI0Lh2PpbIILvSsPGP2KxF +Rv+qZ2C0d35qHzwaUnoEPQc8hQ2E0B92CvdqFN9y4zR8V05WAT558aopO2z6+I9tTcg1367r3CTu +eUWnhbYFiN6IXSV8l2RnCdm/WhUFhvMJHuxYMjMR83dksHYf5BA1FxvyDrFspCqjc/wJHx4yGVMR +59mzLC52LqGj3n5qiAno8geK+LLNEOfic0CTuwjRP+H8C5SzJe98ptfRr5//lpr1kXuYC3fUfugH +0mK1lTnj8/FtDw5lhIpjVMWAtuCeS31HJqcBCF3RiJ7XwzJE+oJKCmhUfzhTA8ykADNkUVkLo4KR +el7sFsLzKuZi2irbWWIQJUoqgQtHB0MGcIfS+pMRKXpITeuUx3BNr2fVUbGAIAEBtHoIppB/TuDv +B0GHr2qlXov7z1CymlSvw4m6WC31MJixNnI5fkkE/SmnTHnkBVfblLkWU41Gsx2VYVdWf6/wFlth +WG82UBEL2KwrlRYaDh8IzTY0ZRBiZtWAXxQgXy0MoHgKaNYs1+lvK9JKBZP8nm9rZ/+I8U6laUpS +NwXqxhaN0sSZ0YIrO7o1dfdRUVjzyAfd5LQDfwIDAQABo0IwQDAdBgNVHQ4EFgQU2XQ65DA9DfcS +3H5aBZ8eNJr34RQwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQEL +BQADggIBADaNl8xCFWQpN5smLNb7rhVpLGsaGvdftvkHTFnq88nIua7Mui563MD1sC3AO6+fcAUR +ap8lTwEpcOPlDOHqWnzcSbvBHiqB9RZLcpHIojG5qtr8nR/zXUACE/xOHAbKsxSQVBcZEhrxH9cM +aVr2cXj0lH2RC47skFSOvG+hTKv8dGT9cZr4QQehzZHkPJrgmzI5c6sq1WnIeJEmMX3ixzDx/BR4 +dxIOE/TdFpS/S2d7cFOFyrC78zhNLJA5wA3CXWvp4uXViI3WLL+rG761KIcSF3Ru/H38j9CHJrAb ++7lsq+KePRXBOy5nAliRn+/4Qh8st2j1da3Ptfb/EX3C8CSlrdP6oDyp+l3cpaDvRKS+1ujl5BOW +F3sGPjLtx7dCvHaj2GU4Kzg1USEODm8uNBNA4StnDG1KQTAYI1oyVZnJF+A83vbsea0rWBmirSwi +GpWOvpaQXUJXxPkUAzUrHC1RVwinOt4/5Mi0A3PCwSaAuwtCH60NryZy2sy+s6ODWA2CxR9GUeOc +GMyNm43sSet1UNWMKFnKdDTajAshqx7qG+XH/RU+wBeq+yNuJkbL+vmxcmtpzyKEC2IPrNkZAJSi +djzULZrtBJ4tBmIQN1IchXIbJ+XMxjHsN+xjWZsLHXbMfjKaiJUINlK73nZfdklJrX+9ZSCyycEr +dhh2n1ax +-----END CERTIFICATE----- + +Certigna Root CA +================ +-----BEGIN CERTIFICATE----- +MIIGWzCCBEOgAwIBAgIRAMrpG4nxVQMNo+ZBbcTjpuEwDQYJKoZIhvcNAQELBQAwWjELMAkGA1UE +BhMCRlIxEjAQBgNVBAoMCURoaW15b3RpczEcMBoGA1UECwwTMDAwMiA0ODE0NjMwODEwMDAzNjEZ +MBcGA1UEAwwQQ2VydGlnbmEgUm9vdCBDQTAeFw0xMzEwMDEwODMyMjdaFw0zMzEwMDEwODMyMjda +MFoxCzAJBgNVBAYTAkZSMRIwEAYDVQQKDAlEaGlteW90aXMxHDAaBgNVBAsMEzAwMDIgNDgxNDYz +MDgxMDAwMzYxGTAXBgNVBAMMEENlcnRpZ25hIFJvb3QgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4IC +DwAwggIKAoICAQDNGDllGlmx6mQWDoyUJJV8g9PFOSbcDO8WV43X2KyjQn+Cyu3NW9sOty3tRQgX +stmzy9YXUnIo245Onoq2C/mehJpNdt4iKVzSs9IGPjA5qXSjklYcoW9MCiBtnyN6tMbaLOQdLNyz +KNAT8kxOAkmhVECe5uUFoC2EyP+YbNDrihqECB63aCPuI9Vwzm1RaRDuoXrC0SIxwoKF0vJVdlB8 +JXrJhFwLrN1CTivngqIkicuQstDuI7pmTLtipPlTWmR7fJj6o0ieD5Wupxj0auwuA0Wv8HT4Ks16 +XdG+RCYyKfHx9WzMfgIhC59vpD++nVPiz32pLHxYGpfhPTc3GGYo0kDFUYqMwy3OU4gkWGQwFsWq +4NYKpkDfePb1BHxpE4S80dGnBs8B92jAqFe7OmGtBIyT46388NtEbVncSVmurJqZNjBBe3YzIoej +wpKGbvlw7q6Hh5UbxHq9MfPU0uWZ/75I7HX1eBYdpnDBfzwboZL7z8g81sWTCo/1VTp2lc5ZmIoJ +lXcymoO6LAQ6l73UL77XbJuiyn1tJslV1c/DeVIICZkHJC1kJWumIWmbat10TWuXekG9qxf5kBdI +jzb5LdXF2+6qhUVB+s06RbFo5jZMm5BX7CO5hwjCxAnxl4YqKE3idMDaxIzb3+KhF1nOJFl0Mdp/ +/TBt2dzhauH8XwIDAQABo4IBGjCCARYwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYw +HQYDVR0OBBYEFBiHVuBud+4kNTxOc5of1uHieX4rMB8GA1UdIwQYMBaAFBiHVuBud+4kNTxOc5of +1uHieX4rMEQGA1UdIAQ9MDswOQYEVR0gADAxMC8GCCsGAQUFBwIBFiNodHRwczovL3d3d3cuY2Vy +dGlnbmEuZnIvYXV0b3JpdGVzLzBtBgNVHR8EZjBkMC+gLaArhilodHRwOi8vY3JsLmNlcnRpZ25h +LmZyL2NlcnRpZ25hcm9vdGNhLmNybDAxoC+gLYYraHR0cDovL2NybC5kaGlteW90aXMuY29tL2Nl +cnRpZ25hcm9vdGNhLmNybDANBgkqhkiG9w0BAQsFAAOCAgEAlLieT/DjlQgi581oQfccVdV8AOIt +OoldaDgvUSILSo3L6btdPrtcPbEo/uRTVRPPoZAbAh1fZkYJMyjhDSSXcNMQH+pkV5a7XdrnxIxP +TGRGHVyH41neQtGbqH6mid2PHMkwgu07nM3A6RngatgCdTer9zQoKJHyBApPNeNgJgH60BGM+RFq +7q89w1DTj18zeTyGqHNFkIwgtnJzFyO+B2XleJINugHA64wcZr+shncBlA2c5uk5jR+mUYyZDDl3 +4bSb+hxnV29qao6pK0xXeXpXIs/NX2NGjVxZOob4Mkdio2cNGJHc+6Zr9UhhcyNZjgKnvETq9Emd +8VRY+WCv2hikLyhF3HqgiIZd8zvn/yk1gPxkQ5Tm4xxvvq0OKmOZK8l+hfZx6AYDlf7ej0gcWtSS +6Cvu5zHbugRqh5jnxV/vfaci9wHYTfmJ0A6aBVmknpjZbyvKcL5kwlWj9Omvw5Ip3IgWJJk8jSaY +tlu3zM63Nwf9JtmYhST/WSMDmu2dnajkXjjO11INb9I/bbEFa0nOipFGc/T2L/Coc3cOZayhjWZS +aX5LaAzHHjcng6WMxwLkFM1JAbBzs/3GkDpv0mztO+7skb6iQ12LAEpmJURw3kAP+HwV96LOPNde +E4yBFxgX0b3xdxA61GU5wSesVywlVP+i2k+KYTlerj1KjL0= +-----END CERTIFICATE----- + +emSign Root CA - G1 +=================== +-----BEGIN CERTIFICATE----- +MIIDlDCCAnygAwIBAgIKMfXkYgxsWO3W2DANBgkqhkiG9w0BAQsFADBnMQswCQYDVQQGEwJJTjET +MBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNobm9sb2dpZXMgTGltaXRl +ZDEcMBoGA1UEAxMTZW1TaWduIFJvb3QgQ0EgLSBHMTAeFw0xODAyMTgxODMwMDBaFw00MzAyMTgx +ODMwMDBaMGcxCzAJBgNVBAYTAklOMRMwEQYDVQQLEwplbVNpZ24gUEtJMSUwIwYDVQQKExxlTXVk +aHJhIFRlY2hub2xvZ2llcyBMaW1pdGVkMRwwGgYDVQQDExNlbVNpZ24gUm9vdCBDQSAtIEcxMIIB +IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAk0u76WaK7p1b1TST0Bsew+eeuGQzf2N4aLTN +LnF115sgxk0pvLZoYIr3IZpWNVrzdr3YzZr/k1ZLpVkGoZM0Kd0WNHVO8oG0x5ZOrRkVUkr+PHB1 +cM2vK6sVmjM8qrOLqs1D/fXqcP/tzxE7lM5OMhbTI0Aqd7OvPAEsbO2ZLIvZTmmYsvePQbAyeGHW +DV/D+qJAkh1cF+ZwPjXnorfCYuKrpDhMtTk1b+oDafo6VGiFbdbyL0NVHpENDtjVaqSW0RM8LHhQ +6DqS0hdW5TUaQBw+jSztOd9C4INBdN+jzcKGYEho42kLVACL5HZpIQ15TjQIXhTCzLG3rdd8cIrH +hQIDAQABo0IwQDAdBgNVHQ4EFgQU++8Nhp6w492pufEhF38+/PB3KxowDgYDVR0PAQH/BAQDAgEG +MA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAFn/8oz1h31xPaOfG1vR2vjTnGs2 +vZupYeveFix0PZ7mddrXuqe8QhfnPZHr5X3dPpzxz5KsbEjMwiI/aTvFthUvozXGaCocV685743Q +NcMYDHsAVhzNixl03r4PEuDQqqE/AjSxcM6dGNYIAwlG7mDgfrbESQRRfXBgvKqy/3lyeqYdPV8q ++Mri/Tm3R7nrft8EI6/6nAYH6ftjk4BAtcZsCjEozgyfz7MjNYBBjWzEN3uBL4ChQEKF6dk4jeih +U80Bv2noWgbyRQuQ+q7hv53yrlc8pa6yVvSLZUDp/TGBLPQ5Cdjua6e0ph0VpZj3AYHYhX3zUVxx +iN66zB+Afko= +-----END CERTIFICATE----- + +emSign ECC Root CA - G3 +======================= +-----BEGIN CERTIFICATE----- +MIICTjCCAdOgAwIBAgIKPPYHqWhwDtqLhDAKBggqhkjOPQQDAzBrMQswCQYDVQQGEwJJTjETMBEG +A1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNobm9sb2dpZXMgTGltaXRlZDEg +MB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0gRzMwHhcNMTgwMjE4MTgzMDAwWhcNNDMwMjE4 +MTgzMDAwWjBrMQswCQYDVQQGEwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11 +ZGhyYSBUZWNobm9sb2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0g +RzMwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQjpQy4LRL1KPOxst3iAhKAnjlfSU2fySU0WXTsuwYc +58Byr+iuL+FBVIcUqEqy6HyC5ltqtdyzdc6LBtCGI79G1Y4PPwT01xySfvalY8L1X44uT6EYGQIr +MgqCZH0Wk9GjQjBAMB0GA1UdDgQWBBR8XQKEE9TMipuBzhccLikenEhjQjAOBgNVHQ8BAf8EBAMC +AQYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNpADBmAjEAvvNhzwIQHWSVB7gYboiFBS+D +CBeQyh+KTOgNG3qxrdWBCUfvO6wIBHxcmbHtRwfSAjEAnbpV/KlK6O3t5nYBQnvI+GDZjVGLVTv7 +jHvrZQnD+JbNR6iC8hZVdyR+EhCVBCyj +-----END CERTIFICATE----- + +emSign Root CA - C1 +=================== +-----BEGIN CERTIFICATE----- +MIIDczCCAlugAwIBAgILAK7PALrEzzL4Q7IwDQYJKoZIhvcNAQELBQAwVjELMAkGA1UEBhMCVVMx +EzARBgNVBAsTCmVtU2lnbiBQS0kxFDASBgNVBAoTC2VNdWRocmEgSW5jMRwwGgYDVQQDExNlbVNp +Z24gUm9vdCBDQSAtIEMxMB4XDTE4MDIxODE4MzAwMFoXDTQzMDIxODE4MzAwMFowVjELMAkGA1UE +BhMCVVMxEzARBgNVBAsTCmVtU2lnbiBQS0kxFDASBgNVBAoTC2VNdWRocmEgSW5jMRwwGgYDVQQD +ExNlbVNpZ24gUm9vdCBDQSAtIEMxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz+up +ufGZBczYKCFK83M0UYRWEPWgTywS4/oTmifQz/l5GnRfHXk5/Fv4cI7gklL35CX5VIPZHdPIWoU/ +Xse2B+4+wM6ar6xWQio5JXDWv7V7Nq2s9nPczdcdioOl+yuQFTdrHCZH3DspVpNqs8FqOp099cGX +OFgFixwR4+S0uF2FHYP+eF8LRWgYSKVGczQ7/g/IdrvHGPMF0Ybzhe3nudkyrVWIzqa2kbBPrH4V +I5b2P/AgNBbeCsbEBEV5f6f9vtKppa+cxSMq9zwhbL2vj07FOrLzNBL834AaSaTUqZX3noleooms +lMuoaJuvimUnzYnu3Yy1aylwQ6BpC+S5DwIDAQABo0IwQDAdBgNVHQ4EFgQU/qHgcB4qAzlSWkK+ +XJGFehiqTbUwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQAD +ggEBAMJKVvoVIXsoounlHfv4LcQ5lkFMOycsxGwYFYDGrK9HWS8mC+M2sO87/kOXSTKZEhVb3xEp +/6tT+LvBeA+snFOvV71ojD1pM/CjoCNjO2RnIkSt1XHLVip4kqNPEjE2NuLe/gDEo2APJ62gsIq1 +NnpSob0n9CAnYuhNlCQT5AoE6TyrLshDCUrGYQTlSTR+08TI9Q/Aqum6VF7zYytPT1DU/rl7mYw9 +wC68AivTxEDkigcxHpvOJpkT+xHqmiIMERnHXhuBUDDIlhJu58tBf5E7oke3VIAb3ADMmpDqw8NQ +BmIMMMAVSKeoWXzhriKi4gp6D/piq1JM4fHfyr6DDUI= +-----END CERTIFICATE----- + +emSign ECC Root CA - C3 +======================= +-----BEGIN CERTIFICATE----- +MIICKzCCAbGgAwIBAgIKe3G2gla4EnycqDAKBggqhkjOPQQDAzBaMQswCQYDVQQGEwJVUzETMBEG +A1UECxMKZW1TaWduIFBLSTEUMBIGA1UEChMLZU11ZGhyYSBJbmMxIDAeBgNVBAMTF2VtU2lnbiBF +Q0MgUm9vdCBDQSAtIEMzMB4XDTE4MDIxODE4MzAwMFoXDTQzMDIxODE4MzAwMFowWjELMAkGA1UE +BhMCVVMxEzARBgNVBAsTCmVtU2lnbiBQS0kxFDASBgNVBAoTC2VNdWRocmEgSW5jMSAwHgYDVQQD +ExdlbVNpZ24gRUNDIFJvb3QgQ0EgLSBDMzB2MBAGByqGSM49AgEGBSuBBAAiA2IABP2lYa57JhAd +6bciMK4G9IGzsUJxlTm801Ljr6/58pc1kjZGDoeVjbk5Wum739D+yAdBPLtVb4OjavtisIGJAnB9 +SMVK4+kiVCJNk7tCDK93nCOmfddhEc5lx/h//vXyqaNCMEAwHQYDVR0OBBYEFPtaSNCAIEDyqOkA +B2kZd6fmw/TPMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49BAMDA2gA +MGUCMQC02C8Cif22TGK6Q04ThHK1rt0c3ta13FaPWEBaLd4gTCKDypOofu4SQMfWh0/434UCMBwU +ZOR8loMRnLDRWmFLpg9J0wD8ofzkpf9/rdcw0Md3f76BB1UwUCAU9Vc4CqgxUQ== +-----END CERTIFICATE----- + +Hongkong Post Root CA 3 +======================= +-----BEGIN CERTIFICATE----- +MIIFzzCCA7egAwIBAgIUCBZfikyl7ADJk0DfxMauI7gcWqQwDQYJKoZIhvcNAQELBQAwbzELMAkG +A1UEBhMCSEsxEjAQBgNVBAgTCUhvbmcgS29uZzESMBAGA1UEBxMJSG9uZyBLb25nMRYwFAYDVQQK +Ew1Ib25na29uZyBQb3N0MSAwHgYDVQQDExdIb25na29uZyBQb3N0IFJvb3QgQ0EgMzAeFw0xNzA2 +MDMwMjI5NDZaFw00MjA2MDMwMjI5NDZaMG8xCzAJBgNVBAYTAkhLMRIwEAYDVQQIEwlIb25nIEtv +bmcxEjAQBgNVBAcTCUhvbmcgS29uZzEWMBQGA1UEChMNSG9uZ2tvbmcgUG9zdDEgMB4GA1UEAxMX +SG9uZ2tvbmcgUG9zdCBSb290IENBIDMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCz +iNfqzg8gTr7m1gNt7ln8wlffKWihgw4+aMdoWJwcYEuJQwy51BWy7sFOdem1p+/l6TWZ5Mwc50tf +jTMwIDNT2aa71T4Tjukfh0mtUC1Qyhi+AViiE3CWu4mIVoBc+L0sPOFMV4i707mV78vH9toxdCim +5lSJ9UExyuUmGs2C4HDaOym71QP1mbpV9WTRYA6ziUm4ii8F0oRFKHyPaFASePwLtVPLwpgchKOe +sL4jpNrcyCse2m5FHomY2vkALgbpDDtw1VAliJnLzXNg99X/NWfFobxeq81KuEXryGgeDQ0URhLj +0mRiikKYvLTGCAj4/ahMZJx2Ab0vqWwzD9g/KLg8aQFChn5pwckGyuV6RmXpwtZQQS4/t+TtbNe/ +JgERohYpSms0BpDsE9K2+2p20jzt8NYt3eEV7KObLyzJPivkaTv/ciWxNoZbx39ri1UbSsUgYT2u +y1DhCDq+sI9jQVMwCFk8mB13umOResoQUGC/8Ne8lYePl8X+l2oBlKN8W4UdKjk60FSh0Tlxnf0h ++bV78OLgAo9uliQlLKAeLKjEiafv7ZkGL7YKTE/bosw3Gq9HhS2KX8Q0NEwA/RiTZxPRN+ZItIsG +xVd7GYYKecsAyVKvQv83j+GjHno9UKtjBucVtT+2RTeUN7F+8kjDf8V1/peNRY8apxpyKBpADwID +AQABo2MwYTAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAfBgNVHSMEGDAWgBQXnc0e +i9Y5K3DTXNSguB+wAPzFYTAdBgNVHQ4EFgQUF53NHovWOStw01zUoLgfsAD8xWEwDQYJKoZIhvcN +AQELBQADggIBAFbVe27mIgHSQpsY1Q7XZiNc4/6gx5LS6ZStS6LG7BJ8dNVI0lkUmcDrudHr9Egw +W62nV3OZqdPlt9EuWSRY3GguLmLYauRwCy0gUCCkMpXRAJi70/33MvJJrsZ64Ee+bs7Lo3I6LWld +y8joRTnU+kLBEUx3XZL7av9YROXrgZ6voJmtvqkBZss4HTzfQx/0TW60uhdG/H39h4F5ag0zD/ov ++BS5gLNdTaqX4fnkGMX41TiMJjz98iji7lpJiCzfeT2OnpA8vUFKOt1b9pq0zj8lMH8yfaIDlNDc +eqFS3m6TjRgm/VWsvY+b0s+v54Ysyx8Jb6NvqYTUc79NoXQbTiNg8swOqn+knEwlqLJmOzj/2ZQw +9nKEvmhVEA/GcywWaZMH/rFF7buiVWqw2rVKAiUnhde3t4ZEFolsgCs+l6mc1X5VTMbeRRAc6uk7 +nwNT7u56AQIWeNTowr5GdogTPyK7SBIdUgC0An4hGh6cJfTzPV4e0hz5sy229zdcxsshTrD3mUcY +hcErulWuBurQB7Lcq9CClnXO0lD+mefPL5/ndtFhKvshuzHQqp9HpLIiyhY6UFfEW0NnxWViA0kB +60PZ2Pierc+xYw5F9KBaLJstxabArahH9CdMOA0uG0k7UvToiIMrVCjU8jVStDKDYmlkDJGcn5fq +dBb9HxEGmpv0 +-----END CERTIFICATE----- + +Microsoft ECC Root Certificate Authority 2017 +============================================= +-----BEGIN CERTIFICATE----- +MIICWTCCAd+gAwIBAgIQZvI9r4fei7FK6gxXMQHC7DAKBggqhkjOPQQDAzBlMQswCQYDVQQGEwJV +UzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYDVQQDEy1NaWNyb3NvZnQgRUND +IFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwHhcNMTkxMjE4MjMwNjQ1WhcNNDIwNzE4 +MjMxNjA0WjBlMQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYw +NAYDVQQDEy1NaWNyb3NvZnQgRUNDIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwdjAQ +BgcqhkjOPQIBBgUrgQQAIgNiAATUvD0CQnVBEyPNgASGAlEvaqiBYgtlzPbKnR5vSmZRogPZnZH6 +thaxjG7efM3beaYvzrvOcS/lpaso7GMEZpn4+vKTEAXhgShC48Zo9OYbhGBKia/teQ87zvH2RPUB +eMCjVDBSMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTIy5lycFIM ++Oa+sgRXKSrPQhDtNTAQBgkrBgEEAYI3FQEEAwIBADAKBggqhkjOPQQDAwNoADBlAjBY8k3qDPlf +Xu5gKcs68tvWMoQZP3zVL8KxzJOuULsJMsbG7X7JNpQS5GiFBqIb0C8CMQCZ6Ra0DvpWSNSkMBaR +eNtUjGUBiudQZsIxtzm6uBoiB078a1QWIP8rtedMDE2mT3M= +-----END CERTIFICATE----- + +Microsoft RSA Root Certificate Authority 2017 +============================================= +-----BEGIN CERTIFICATE----- +MIIFqDCCA5CgAwIBAgIQHtOXCV/YtLNHcB6qvn9FszANBgkqhkiG9w0BAQwFADBlMQswCQYDVQQG +EwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYDVQQDEy1NaWNyb3NvZnQg +UlNBIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwHhcNMTkxMjE4MjI1MTIyWhcNNDIw +NzE4MjMwMDIzWjBlMQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9u +MTYwNAYDVQQDEy1NaWNyb3NvZnQgUlNBIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcw +ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKW76UM4wplZEWCpW9R2LBifOZNt9GkMml +7Xhqb0eRaPgnZ1AzHaGm++DlQ6OEAlcBXZxIQIJTELy/xztokLaCLeX0ZdDMbRnMlfl7rEqUrQ7e +S0MdhweSE5CAg2Q1OQT85elss7YfUJQ4ZVBcF0a5toW1HLUX6NZFndiyJrDKxHBKrmCk3bPZ7Pw7 +1VdyvD/IybLeS2v4I2wDwAW9lcfNcztmgGTjGqwu+UcF8ga2m3P1eDNbx6H7JyqhtJqRjJHTOoI+ +dkC0zVJhUXAoP8XFWvLJjEm7FFtNyP9nTUwSlq31/niol4fX/V4ggNyhSyL71Imtus5Hl0dVe49F +yGcohJUcaDDv70ngNXtk55iwlNpNhTs+VcQor1fznhPbRiefHqJeRIOkpcrVE7NLP8TjwuaGYaRS +MLl6IE9vDzhTyzMMEyuP1pq9KsgtsRx9S1HKR9FIJ3Jdh+vVReZIZZ2vUpC6W6IYZVcSn2i51BVr +lMRpIpj0M+Dt+VGOQVDJNE92kKz8OMHY4Xu54+OU4UZpyw4KUGsTuqwPN1q3ErWQgR5WrlcihtnJ +0tHXUeOrO8ZV/R4O03QK0dqq6mm4lyiPSMQH+FJDOvTKVTUssKZqwJz58oHhEmrARdlns87/I6KJ +ClTUFLkqqNfs+avNJVgyeY+QW5g5xAgGwax/Dj0ApQIDAQABo1QwUjAOBgNVHQ8BAf8EBAMCAYYw +DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUCctZf4aycI8awznjwNnpv7tNsiMwEAYJKwYBBAGC +NxUBBAMCAQAwDQYJKoZIhvcNAQEMBQADggIBAKyvPl3CEZaJjqPnktaXFbgToqZCLgLNFgVZJ8og +6Lq46BrsTaiXVq5lQ7GPAJtSzVXNUzltYkyLDVt8LkS/gxCP81OCgMNPOsduET/m4xaRhPtthH80 +dK2Jp86519efhGSSvpWhrQlTM93uCupKUY5vVau6tZRGrox/2KJQJWVggEbbMwSubLWYdFQl3JPk ++ONVFT24bcMKpBLBaYVu32TxU5nhSnUgnZUP5NbcA/FZGOhHibJXWpS2qdgXKxdJ5XbLwVaZOjex +/2kskZGT4d9Mozd2TaGf+G0eHdP67Pv0RR0Tbc/3WeUiJ3IrhvNXuzDtJE3cfVa7o7P4NHmJweDy +AmH3pvwPuxwXC65B2Xy9J6P9LjrRk5Sxcx0ki69bIImtt2dmefU6xqaWM/5TkshGsRGRxpl/j8nW +ZjEgQRCHLQzWwa80mMpkg/sTV9HB8Dx6jKXB/ZUhoHHBk2dxEuqPiAppGWSZI1b7rCoucL5mxAyE +7+WL85MB+GqQk2dLsmijtWKP6T+MejteD+eMuMZ87zf9dOLITzNy4ZQ5bb0Sr74MTnB8G2+NszKT +c0QWbej09+CVgI+WXTik9KveCjCHk9hNAHFiRSdLOkKEW39lt2c0Ui2cFmuqqNh7o0JMcccMyj6D +5KbvtwEwXlGjefVwaaZBRA+GsCyRxj3qrg+E +-----END CERTIFICATE----- + +e-Szigno Root CA 2017 +===================== +-----BEGIN CERTIFICATE----- +MIICQDCCAeWgAwIBAgIMAVRI7yH9l1kN9QQKMAoGCCqGSM49BAMCMHExCzAJBgNVBAYTAkhVMREw +DwYDVQQHDAhCdWRhcGVzdDEWMBQGA1UECgwNTWljcm9zZWMgTHRkLjEXMBUGA1UEYQwOVkFUSFUt +MjM1ODQ0OTcxHjAcBgNVBAMMFWUtU3ppZ25vIFJvb3QgQ0EgMjAxNzAeFw0xNzA4MjIxMjA3MDZa +Fw00MjA4MjIxMjA3MDZaMHExCzAJBgNVBAYTAkhVMREwDwYDVQQHDAhCdWRhcGVzdDEWMBQGA1UE +CgwNTWljcm9zZWMgTHRkLjEXMBUGA1UEYQwOVkFUSFUtMjM1ODQ0OTcxHjAcBgNVBAMMFWUtU3pp +Z25vIFJvb3QgQ0EgMjAxNzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJbcPYrYsHtvxie+RJCx +s1YVe45DJH0ahFnuY2iyxl6H0BVIHqiQrb1TotreOpCmYF9oMrWGQd+HWyx7xf58etqjYzBhMA8G +A1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBSHERUI0arBeAyxr87GyZDv +vzAEwDAfBgNVHSMEGDAWgBSHERUI0arBeAyxr87GyZDvvzAEwDAKBggqhkjOPQQDAgNJADBGAiEA +tVfd14pVCzbhhkT61NlojbjcI4qKDdQvfepz7L9NbKgCIQDLpbQS+ue16M9+k/zzNY9vTlp8tLxO +svxyqltZ+efcMQ== +-----END CERTIFICATE----- + +certSIGN Root CA G2 +=================== +-----BEGIN CERTIFICATE----- +MIIFRzCCAy+gAwIBAgIJEQA0tk7GNi02MA0GCSqGSIb3DQEBCwUAMEExCzAJBgNVBAYTAlJPMRQw +EgYDVQQKEwtDRVJUU0lHTiBTQTEcMBoGA1UECxMTY2VydFNJR04gUk9PVCBDQSBHMjAeFw0xNzAy +MDYwOTI3MzVaFw00MjAyMDYwOTI3MzVaMEExCzAJBgNVBAYTAlJPMRQwEgYDVQQKEwtDRVJUU0lH +TiBTQTEcMBoGA1UECxMTY2VydFNJR04gUk9PVCBDQSBHMjCCAiIwDQYJKoZIhvcNAQEBBQADggIP +ADCCAgoCggIBAMDFdRmRfUR0dIf+DjuW3NgBFszuY5HnC2/OOwppGnzC46+CjobXXo9X69MhWf05 +N0IwvlDqtg+piNguLWkh59E3GE59kdUWX2tbAMI5Qw02hVK5U2UPHULlj88F0+7cDBrZuIt4Imfk +abBoxTzkbFpG583H+u/E7Eu9aqSs/cwoUe+StCmrqzWaTOTECMYmzPhpn+Sc8CnTXPnGFiWeI8Mg +wT0PPzhAsP6CRDiqWhqKa2NYOLQV07YRaXseVO6MGiKscpc/I1mbySKEwQdPzH/iV8oScLumZfNp +dWO9lfsbl83kqK/20U6o2YpxJM02PbyWxPFsqa7lzw1uKA2wDrXKUXt4FMMgL3/7FFXhEZn91Qqh +ngLjYl/rNUssuHLoPj1PrCy7Lobio3aP5ZMqz6WryFyNSwb/EkaseMsUBzXgqd+L6a8VTxaJW732 +jcZZroiFDsGJ6x9nxUWO/203Nit4ZoORUSs9/1F3dmKh7Gc+PoGD4FapUB8fepmrY7+EF3fxDTvf +95xhszWYijqy7DwaNz9+j5LP2RIUZNoQAhVB/0/E6xyjyfqZ90bp4RjZsbgyLcsUDFDYg2WD7rlc +z8sFWkz6GZdr1l0T08JcVLwyc6B49fFtHsufpaafItzRUZ6CeWRgKRM+o/1Pcmqr4tTluCRVLERL +iohEnMqE0yo7AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1Ud +DgQWBBSCIS1mxteg4BXrzkwJd8RgnlRuAzANBgkqhkiG9w0BAQsFAAOCAgEAYN4auOfyYILVAzOB +ywaK8SJJ6ejqkX/GM15oGQOGO0MBzwdw5AgeZYWR5hEit/UCI46uuR59H35s5r0l1ZUa8gWmr4UC +b6741jH/JclKyMeKqdmfS0mbEVeZkkMR3rYzpMzXjWR91M08KCy0mpbqTfXERMQlqiCA2ClV9+BB +/AYm/7k29UMUA2Z44RGx2iBfRgB4ACGlHgAoYXhvqAEBj500mv/0OJD7uNGzcgbJceaBxXntC6Z5 +8hMLnPddDnskk7RI24Zf3lCGeOdA5jGokHZwYa+cNywRtYK3qq4kNFtyDGkNzVmf9nGvnAvRCjj5 +BiKDUyUM/FHE5r7iOZULJK2v0ZXkltd0ZGtxTgI8qoXzIKNDOXZbbFD+mpwUHmUUihW9o4JFWklW +atKcsWMy5WHgUyIOpwpJ6st+H6jiYoD2EEVSmAYY3qXNL3+q1Ok+CHLsIwMCPKaq2LxndD0UF/tU +Sxfj03k9bWtJySgOLnRQvwzZRjoQhsmnP+mg7H/rpXdYaXHmgwo38oZJar55CJD2AhZkPuXaTH4M +NMn5X7azKFGnpyuqSfqNZSlO42sTp5SjLVFteAxEy9/eCG/Oo2Sr05WE1LlSVHJ7liXMvGnjSG4N +0MedJ5qq+BOS3R7fY581qRY27Iy4g/Q9iY/NtBde17MXQRBdJ3NghVdJIgc= +-----END CERTIFICATE----- + +NAVER Global Root Certification Authority +========================================= +-----BEGIN CERTIFICATE----- +MIIFojCCA4qgAwIBAgIUAZQwHqIL3fXFMyqxQ0Rx+NZQTQ0wDQYJKoZIhvcNAQEMBQAwaTELMAkG +A1UEBhMCS1IxJjAkBgNVBAoMHU5BVkVSIEJVU0lORVNTIFBMQVRGT1JNIENvcnAuMTIwMAYDVQQD +DClOQVZFUiBHbG9iYWwgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0xNzA4MTgwODU4 +NDJaFw0zNzA4MTgyMzU5NTlaMGkxCzAJBgNVBAYTAktSMSYwJAYDVQQKDB1OQVZFUiBCVVNJTkVT +UyBQTEFURk9STSBDb3JwLjEyMDAGA1UEAwwpTkFWRVIgR2xvYmFsIFJvb3QgQ2VydGlmaWNhdGlv +biBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC21PGTXLVAiQqrDZBb +UGOukJR0F0Vy1ntlWilLp1agS7gvQnXp2XskWjFlqxcX0TM62RHcQDaH38dq6SZeWYp34+hInDEW ++j6RscrJo+KfziFTowI2MMtSAuXaMl3Dxeb57hHHi8lEHoSTGEq0n+USZGnQJoViAbbJAh2+g1G7 +XNr4rRVqmfeSVPc0W+m/6imBEtRTkZazkVrd/pBzKPswRrXKCAfHcXLJZtM0l/aM9BhK4dA9WkW2 +aacp+yPOiNgSnABIqKYPszuSjXEOdMWLyEz59JuOuDxp7W87UC9Y7cSw0BwbagzivESq2M0UXZR4 +Yb8ObtoqvC8MC3GmsxY/nOb5zJ9TNeIDoKAYv7vxvvTWjIcNQvcGufFt7QSUqP620wbGQGHfnZ3z +VHbOUzoBppJB7ASjjw2i1QnK1sua8e9DXcCrpUHPXFNwcMmIpi3Ua2FzUCaGYQ5fG8Ir4ozVu53B +A0K6lNpfqbDKzE0K70dpAy8i+/Eozr9dUGWokG2zdLAIx6yo0es+nPxdGoMuK8u180SdOqcXYZai +cdNwlhVNt0xz7hlcxVs+Qf6sdWA7G2POAN3aCJBitOUt7kinaxeZVL6HSuOpXgRM6xBtVNbv8ejy +YhbLgGvtPe31HzClrkvJE+2KAQHJuFFYwGY6sWZLxNUxAmLpdIQM201GLQIDAQABo0IwQDAdBgNV +HQ4EFgQU0p+I36HNLL3s9TsBAZMzJ7LrYEswDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMB +Af8wDQYJKoZIhvcNAQEMBQADggIBADLKgLOdPVQG3dLSLvCkASELZ0jKbY7gyKoNqo0hV4/GPnrK +21HUUrPUloSlWGB/5QuOH/XcChWB5Tu2tyIvCZwTFrFsDDUIbatjcu3cvuzHV+YwIHHW1xDBE1UB +jCpD5EHxzzp6U5LOogMFDTjfArsQLtk70pt6wKGm+LUx5vR1yblTmXVHIloUFcd4G7ad6Qz4G3bx +hYTeodoS76TiEJd6eN4MUZeoIUCLhr0N8F5OSza7OyAfikJW4Qsav3vQIkMsRIz75Sq0bBwcupTg +E34h5prCy8VCZLQelHsIJchxzIdFV4XTnyliIoNRlwAYl3dqmJLJfGBs32x9SuRwTMKeuB330DTH +D8z7p/8Dvq1wkNoL3chtl1+afwkyQf3NosxabUzyqkn+Zvjp2DXrDige7kgvOtB5CTh8piKCk5XQ +A76+AqAF3SAi428diDRgxuYKuQl1C/AH6GmWNcf7I4GOODm4RStDeKLRLBT/DShycpWbXgnbiUSY +qqFJu3FS8r/2/yehNq+4tneI3TqkbZs0kNwUXTC/t+sX5Ie3cdCh13cV1ELX8vMxmV2b3RZtP+oG +I/hGoiLtk/bdmuYqh7GYVPEi92tF4+KOdh2ajcQGjTa3FPOdVGm3jjzVpG2Tgbet9r1ke8LJaDmg +kpzNNIaRkPpkUZ3+/uul9XXeifdy +-----END CERTIFICATE----- + +AC RAIZ FNMT-RCM SERVIDORES SEGUROS +=================================== +-----BEGIN CERTIFICATE----- +MIICbjCCAfOgAwIBAgIQYvYybOXE42hcG2LdnC6dlTAKBggqhkjOPQQDAzB4MQswCQYDVQQGEwJF +UzERMA8GA1UECgwIRk5NVC1SQ00xDjAMBgNVBAsMBUNlcmVzMRgwFgYDVQRhDA9WQVRFUy1RMjgy +NjAwNEoxLDAqBgNVBAMMI0FDIFJBSVogRk5NVC1SQ00gU0VSVklET1JFUyBTRUdVUk9TMB4XDTE4 +MTIyMDA5MzczM1oXDTQzMTIyMDA5MzczM1oweDELMAkGA1UEBhMCRVMxETAPBgNVBAoMCEZOTVQt +UkNNMQ4wDAYDVQQLDAVDZXJlczEYMBYGA1UEYQwPVkFURVMtUTI4MjYwMDRKMSwwKgYDVQQDDCNB +QyBSQUlaIEZOTVQtUkNNIFNFUlZJRE9SRVMgU0VHVVJPUzB2MBAGByqGSM49AgEGBSuBBAAiA2IA +BPa6V1PIyqvfNkpSIeSX0oNnnvBlUdBeh8dHsVnyV0ebAAKTRBdp20LHsbI6GA60XYyzZl2hNPk2 +LEnb80b8s0RpRBNm/dfF/a82Tc4DTQdxz69qBdKiQ1oKUm8BA06Oi6NCMEAwDwYDVR0TAQH/BAUw +AwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFAG5L++/EYZg8k/QQW6rcx/n0m5JMAoGCCqG +SM49BAMDA2kAMGYCMQCuSuMrQMN0EfKVrRYj3k4MGuZdpSRea0R7/DjiT8ucRRcRTBQnJlU5dUoD +zBOQn5ICMQD6SmxgiHPz7riYYqnOK8LZiqZwMR2vsJRM60/G49HzYqc8/5MuB1xJAWdpEgJyv+c= +-----END CERTIFICATE----- + +GlobalSign Root R46 +=================== +-----BEGIN CERTIFICATE----- +MIIFWjCCA0KgAwIBAgISEdK7udcjGJ5AXwqdLdDfJWfRMA0GCSqGSIb3DQEBDAUAMEYxCzAJBgNV +BAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRwwGgYDVQQDExNHbG9iYWxTaWduIFJv +b3QgUjQ2MB4XDTE5MDMyMDAwMDAwMFoXDTQ2MDMyMDAwMDAwMFowRjELMAkGA1UEBhMCQkUxGTAX +BgNVBAoTEEdsb2JhbFNpZ24gbnYtc2ExHDAaBgNVBAMTE0dsb2JhbFNpZ24gUm9vdCBSNDYwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCsrHQy6LNl5brtQyYdpokNRbopiLKkHWPd08Es +CVeJOaFV6Wc0dwxu5FUdUiXSE2te4R2pt32JMl8Nnp8semNgQB+msLZ4j5lUlghYruQGvGIFAha/ +r6gjA7aUD7xubMLL1aa7DOn2wQL7Id5m3RerdELv8HQvJfTqa1VbkNud316HCkD7rRlr+/fKYIje +2sGP1q7Vf9Q8g+7XFkyDRTNrJ9CG0Bwta/OrffGFqfUo0q3v84RLHIf8E6M6cqJaESvWJ3En7YEt +bWaBkoe0G1h6zD8K+kZPTXhc+CtI4wSEy132tGqzZfxCnlEmIyDLPRT5ge1lFgBPGmSXZgjPjHvj +K8Cd+RTyG/FWaha/LIWFzXg4mutCagI0GIMXTpRW+LaCtfOW3T3zvn8gdz57GSNrLNRyc0NXfeD4 +12lPFzYE+cCQYDdF3uYM2HSNrpyibXRdQr4G9dlkbgIQrImwTDsHTUB+JMWKmIJ5jqSngiCNI/on +ccnfxkF0oE32kRbcRoxfKWMxWXEM2G/CtjJ9++ZdU6Z+Ffy7dXxd7Pj2Fxzsx2sZy/N78CsHpdls +eVR2bJ0cpm4O6XkMqCNqo98bMDGfsVR7/mrLZqrcZdCinkqaByFrgY/bxFn63iLABJzjqls2k+g9 +vXqhnQt2sQvHnf3PmKgGwvgqo6GDoLclcqUC4wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYD +VR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA1yrc4GHqMywptWU4jaWSf8FmSwwDQYJKoZIhvcNAQEM +BQADggIBAHx47PYCLLtbfpIrXTncvtgdokIzTfnvpCo7RGkerNlFo048p9gkUbJUHJNOxO97k4Vg +JuoJSOD1u8fpaNK7ajFxzHmuEajwmf3lH7wvqMxX63bEIaZHU1VNaL8FpO7XJqti2kM3S+LGteWy +gxk6x9PbTZ4IevPuzz5i+6zoYMzRx6Fcg0XERczzF2sUyQQCPtIkpnnpHs6i58FZFZ8d4kuaPp92 +CC1r2LpXFNqD6v6MVenQTqnMdzGxRBF6XLE+0xRFFRhiJBPSy03OXIPBNvIQtQ6IbbjhVp+J3pZm +OUdkLG5NrmJ7v2B0GbhWrJKsFjLtrWhV/pi60zTe9Mlhww6G9kuEYO4Ne7UyWHmRVSyBQ7N0H3qq +JZ4d16GLuc1CLgSkZoNNiTW2bKg2SnkheCLQQrzRQDGQob4Ez8pn7fXwgNNgyYMqIgXQBztSvwye +qiv5u+YfjyW6hY0XHgL+XVAEV8/+LbzvXMAaq7afJMbfc2hIkCwU9D9SGuTSyxTDYWnP4vkYxboz +nxSjBF25cfe1lNj2M8FawTSLfJvdkzrnE6JwYZ+vj+vYxXX4M2bUdGc6N3ec592kD3ZDZopD8p/7 +DEJ4Y9HiD2971KE9dJeFt0g5QdYg/NA6s/rob8SKunE3vouXsXgxT7PntgMTzlSdriVZzH81Xwj3 +QEUxeCp6 +-----END CERTIFICATE----- + +GlobalSign Root E46 +=================== +-----BEGIN CERTIFICATE----- +MIICCzCCAZGgAwIBAgISEdK7ujNu1LzmJGjFDYQdmOhDMAoGCCqGSM49BAMDMEYxCzAJBgNVBAYT +AkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRwwGgYDVQQDExNHbG9iYWxTaWduIFJvb3Qg +RTQ2MB4XDTE5MDMyMDAwMDAwMFoXDTQ2MDMyMDAwMDAwMFowRjELMAkGA1UEBhMCQkUxGTAXBgNV +BAoTEEdsb2JhbFNpZ24gbnYtc2ExHDAaBgNVBAMTE0dsb2JhbFNpZ24gUm9vdCBFNDYwdjAQBgcq +hkjOPQIBBgUrgQQAIgNiAAScDrHPt+ieUnd1NPqlRqetMhkytAepJ8qUuwzSChDH2omwlwxwEwkB +jtjqR+q+soArzfwoDdusvKSGN+1wCAB16pMLey5SnCNoIwZD7JIvU4Tb+0cUB+hflGddyXqBPCCj +QjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBQxCpCPtsad0kRL +gLWi5h+xEk8blTAKBggqhkjOPQQDAwNoADBlAjEA31SQ7Zvvi5QCkxeCmb6zniz2C5GMn0oUsfZk +vLtoURMMA/cVi4RguYv/Uo7njLwcAjA8+RHUjE7AwWHCFUyqqx0LMV87HOIAl0Qx5v5zli/altP+ +CAezNIm8BZ/3Hobui3A= +-----END CERTIFICATE----- + +ANF Secure Server Root CA +========================= +-----BEGIN CERTIFICATE----- +MIIF7zCCA9egAwIBAgIIDdPjvGz5a7EwDQYJKoZIhvcNAQELBQAwgYQxEjAQBgNVBAUTCUc2MzI4 +NzUxMDELMAkGA1UEBhMCRVMxJzAlBgNVBAoTHkFORiBBdXRvcmlkYWQgZGUgQ2VydGlmaWNhY2lv +bjEUMBIGA1UECxMLQU5GIENBIFJhaXoxIjAgBgNVBAMTGUFORiBTZWN1cmUgU2VydmVyIFJvb3Qg +Q0EwHhcNMTkwOTA0MTAwMDM4WhcNMzkwODMwMTAwMDM4WjCBhDESMBAGA1UEBRMJRzYzMjg3NTEw +MQswCQYDVQQGEwJFUzEnMCUGA1UEChMeQU5GIEF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uMRQw +EgYDVQQLEwtBTkYgQ0EgUmFpejEiMCAGA1UEAxMZQU5GIFNlY3VyZSBTZXJ2ZXIgUm9vdCBDQTCC +AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANvrayvmZFSVgpCjcqQZAZ2cC4Ffc0m6p6zz +BE57lgvsEeBbphzOG9INgxwruJ4dfkUyYA8H6XdYfp9qyGFOtibBTI3/TO80sh9l2Ll49a2pcbnv +T1gdpd50IJeh7WhM3pIXS7yr/2WanvtH2Vdy8wmhrnZEE26cLUQ5vPnHO6RYPUG9tMJJo8gN0pcv +B2VSAKduyK9o7PQUlrZXH1bDOZ8rbeTzPvY1ZNoMHKGESy9LS+IsJJ1tk0DrtSOOMspvRdOoiXse +zx76W0OLzc2oD2rKDF65nkeP8Nm2CgtYZRczuSPkdxl9y0oukntPLxB3sY0vaJxizOBQ+OyRp1RM +VwnVdmPF6GUe7m1qzwmd+nxPrWAI/VaZDxUse6mAq4xhj0oHdkLePfTdsiQzW7i1o0TJrH93PB0j +7IKppuLIBkwC/qxcmZkLLxCKpvR/1Yd0DVlJRfbwcVw5Kda/SiOL9V8BY9KHcyi1Swr1+KuCLH5z +JTIdC2MKF4EA/7Z2Xue0sUDKIbvVgFHlSFJnLNJhiQcND85Cd8BEc5xEUKDbEAotlRyBr+Qc5RQe +8TZBAQIvfXOn3kLMTOmJDVb3n5HUA8ZsyY/b2BzgQJhdZpmYgG4t/wHFzstGH6wCxkPmrqKEPMVO +Hj1tyRRM4y5Bu8o5vzY8KhmqQYdOpc5LMnndkEl/AgMBAAGjYzBhMB8GA1UdIwQYMBaAFJxf0Gxj +o1+TypOYCK2Mh6UsXME3MB0GA1UdDgQWBBScX9BsY6Nfk8qTmAitjIelLFzBNzAOBgNVHQ8BAf8E +BAMCAYYwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAgEATh65isagmD9uw2nAalxJ +UqzLK114OMHVVISfk/CHGT0sZonrDUL8zPB1hT+L9IBdeeUXZ701guLyPI59WzbLWoAAKfLOKyzx +j6ptBZNscsdW699QIyjlRRA96Gejrw5VD5AJYu9LWaL2U/HANeQvwSS9eS9OICI7/RogsKQOLHDt +dD+4E5UGUcjohybKpFtqFiGS3XNgnhAY3jyB6ugYw3yJ8otQPr0R4hUDqDZ9MwFsSBXXiJCZBMXM +5gf0vPSQ7RPi6ovDj6MzD8EpTBNO2hVWcXNyglD2mjN8orGoGjR0ZVzO0eurU+AagNjqOknkJjCb +5RyKqKkVMoaZkgoQI1YS4PbOTOK7vtuNknMBZi9iPrJyJ0U27U1W45eZ/zo1PqVUSlJZS2Db7v54 +EX9K3BR5YLZrZAPbFYPhor72I5dQ8AkzNqdxliXzuUJ92zg/LFis6ELhDtjTO0wugumDLmsx2d1H +hk9tl5EuT+IocTUW0fJz/iUrB0ckYyfI+PbZa/wSMVYIwFNCr5zQM378BvAxRAMU8Vjq8moNqRGy +g77FGr8H6lnco4g175x2MjxNBiLOFeXdntiP2t7SxDnlF4HPOEfrf4htWRvfn0IUrn7PqLBmZdo3 +r5+qPeoott7VMVgWglvquxl1AnMaykgaIZOQCo6ThKd9OyMYkomgjaw= +-----END CERTIFICATE----- + +Certum EC-384 CA +================ +-----BEGIN CERTIFICATE----- +MIICZTCCAeugAwIBAgIQeI8nXIESUiClBNAt3bpz9DAKBggqhkjOPQQDAzB0MQswCQYDVQQGEwJQ +TDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2Vy +dGlmaWNhdGlvbiBBdXRob3JpdHkxGTAXBgNVBAMTEENlcnR1bSBFQy0zODQgQ0EwHhcNMTgwMzI2 +MDcyNDU0WhcNNDMwMzI2MDcyNDU0WjB0MQswCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERh +dGEgU3lzdGVtcyBTLkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkx +GTAXBgNVBAMTEENlcnR1bSBFQy0zODQgQ0EwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAATEKI6rGFtq +vm5kN2PkzeyrOvfMobgOgknXhimfoZTy42B4mIF4Bk3y7JoOV2CDn7TmFy8as10CW4kjPMIRBSqn +iBMY81CE1700LCeJVf/OTOffph8oxPBUw7l8t1Ot68KjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYD +VR0OBBYEFI0GZnQkdjrzife81r1HfS+8EF9LMA4GA1UdDwEB/wQEAwIBBjAKBggqhkjOPQQDAwNo +ADBlAjADVS2m5hjEfO/JUG7BJw+ch69u1RsIGL2SKcHvlJF40jocVYli5RsJHrpka/F2tNQCMQC0 +QoSZ/6vnnvuRlydd3LBbMHHOXjgaatkl5+r3YZJW+OraNsKHZZYuciUvf9/DE8k= +-----END CERTIFICATE----- + +Certum Trusted Root CA +====================== +-----BEGIN CERTIFICATE----- +MIIFwDCCA6igAwIBAgIQHr9ZULjJgDdMBvfrVU+17TANBgkqhkiG9w0BAQ0FADB6MQswCQYDVQQG +EwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEuMScwJQYDVQQLEx5DZXJ0dW0g +Q2VydGlmaWNhdGlvbiBBdXRob3JpdHkxHzAdBgNVBAMTFkNlcnR1bSBUcnVzdGVkIFJvb3QgQ0Ew +HhcNMTgwMzE2MTIxMDEzWhcNNDMwMzE2MTIxMDEzWjB6MQswCQYDVQQGEwJQTDEhMB8GA1UEChMY +QXNzZWNvIERhdGEgU3lzdGVtcyBTLkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBB +dXRob3JpdHkxHzAdBgNVBAMTFkNlcnR1bSBUcnVzdGVkIFJvb3QgQ0EwggIiMA0GCSqGSIb3DQEB +AQUAA4ICDwAwggIKAoICAQDRLY67tzbqbTeRn06TpwXkKQMlzhyC93yZn0EGze2jusDbCSzBfN8p +fktlL5On1AFrAygYo9idBcEq2EXxkd7fO9CAAozPOA/qp1x4EaTByIVcJdPTsuclzxFUl6s1wB52 +HO8AU5853BSlLCIls3Jy/I2z5T4IHhQqNwuIPMqw9MjCoa68wb4pZ1Xi/K1ZXP69VyywkI3C7Te2 +fJmItdUDmj0VDT06qKhF8JVOJVkdzZhpu9PMMsmN74H+rX2Ju7pgE8pllWeg8xn2A1bUatMn4qGt +g/BKEiJ3HAVz4hlxQsDsdUaakFjgao4rpUYwBI4Zshfjvqm6f1bxJAPXsiEodg42MEx51UGamqi4 +NboMOvJEGyCI98Ul1z3G4z5D3Yf+xOr1Uz5MZf87Sst4WmsXXw3Hw09Omiqi7VdNIuJGmj8PkTQk +fVXjjJU30xrwCSss0smNtA0Aq2cpKNgB9RkEth2+dv5yXMSFytKAQd8FqKPVhJBPC/PgP5sZ0jeJ +P/J7UhyM9uH3PAeXjA6iWYEMspA90+NZRu0PqafegGtaqge2Gcu8V/OXIXoMsSt0Puvap2ctTMSY +njYJdmZm/Bo/6khUHL4wvYBQv3y1zgD2DGHZ5yQD4OMBgQ692IU0iL2yNqh7XAjlRICMb/gv1SHK +HRzQ+8S1h9E6Tsd2tTVItQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSM+xx1 +vALTn04uSNn5YFSqxLNP+jAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQENBQADggIBAEii1QAL +LtA/vBzVtVRJHlpr9OTy4EA34MwUe7nJ+jW1dReTagVphZzNTxl4WxmB82M+w85bj/UvXgF2Ez8s +ALnNllI5SW0ETsXpD4YN4fqzX4IS8TrOZgYkNCvozMrnadyHncI013nR03e4qllY/p0m+jiGPp2K +h2RX5Rc64vmNueMzeMGQ2Ljdt4NR5MTMI9UGfOZR0800McD2RrsLrfw9EAUqO0qRJe6M1ISHgCq8 +CYyqOhNf6DR5UMEQGfnTKB7U0VEwKbOukGfWHwpjscWpxkIxYxeU72nLL/qMFH3EQxiJ2fAyQOaA +4kZf5ePBAFmo+eggvIksDkc0C+pXwlM2/KfUrzHN/gLldfq5Jwn58/U7yn2fqSLLiMmq0Uc9Nneo +WWRrJ8/vJ8HjJLWG965+Mk2weWjROeiQWMODvA8s1pfrzgzhIMfatz7DP78v3DSk+yshzWePS/Tj +6tQ/50+6uaWTRRxmHyH6ZF5v4HaUMst19W7l9o/HuKTMqJZ9ZPskWkoDbGs4xugDQ5r3V7mzKWmT +OPQD8rv7gmsHINFSH5pkAnuYZttcTVoP0ISVoDwUQwbKytu4QTbaakRnh6+v40URFWkIsr4WOZck +bxJF0WddCajJFdr60qZfE2Efv4WstK2tBZQIgx51F9NxO5NQI1mg7TyRVJ12AMXDuDjb +-----END CERTIFICATE----- + +TunTrust Root CA +================ +-----BEGIN CERTIFICATE----- +MIIFszCCA5ugAwIBAgIUEwLV4kBMkkaGFmddtLu7sms+/BMwDQYJKoZIhvcNAQELBQAwYTELMAkG +A1UEBhMCVE4xNzA1BgNVBAoMLkFnZW5jZSBOYXRpb25hbGUgZGUgQ2VydGlmaWNhdGlvbiBFbGVj +dHJvbmlxdWUxGTAXBgNVBAMMEFR1blRydXN0IFJvb3QgQ0EwHhcNMTkwNDI2MDg1NzU2WhcNNDQw +NDI2MDg1NzU2WjBhMQswCQYDVQQGEwJUTjE3MDUGA1UECgwuQWdlbmNlIE5hdGlvbmFsZSBkZSBD +ZXJ0aWZpY2F0aW9uIEVsZWN0cm9uaXF1ZTEZMBcGA1UEAwwQVHVuVHJ1c3QgUm9vdCBDQTCCAiIw +DQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMPN0/y9BFPdDCA61YguBUtB9YOCfvdZn56eY+hz +2vYGqU8ftPkLHzmMmiDQfgbU7DTZhrx1W4eI8NLZ1KMKsmwb60ksPqxd2JQDoOw05TDENX37Jk0b +bjBU2PWARZw5rZzJJQRNmpA+TkBuimvNKWfGzC3gdOgFVwpIUPp6Q9p+7FuaDmJ2/uqdHYVy7BG7 +NegfJ7/Boce7SBbdVtfMTqDhuazb1YMZGoXRlJfXyqNlC/M4+QKu3fZnz8k/9YosRxqZbwUN/dAd +gjH8KcwAWJeRTIAAHDOFli/LQcKLEITDCSSJH7UP2dl3RxiSlGBcx5kDPP73lad9UKGAwqmDrViW +VSHbhlnUr8a83YFuB9tgYv7sEG7aaAH0gxupPqJbI9dkxt/con3YS7qC0lH4Zr8GRuR5KiY2eY8f +Tpkdso8MDhz/yV3A/ZAQprE38806JG60hZC/gLkMjNWb1sjxVj8agIl6qeIbMlEsPvLfe/ZdeikZ +juXIvTZxi11Mwh0/rViizz1wTaZQmCXcI/m4WEEIcb9PuISgjwBUFfyRbVinljvrS5YnzWuioYas +DXxU5mZMZl+QviGaAkYt5IPCgLnPSz7ofzwB7I9ezX/SKEIBlYrilz0QIX32nRzFNKHsLA4KUiwS +VXAkPcvCFDVDXSdOvsC9qnyW5/yeYa1E0wCXAgMBAAGjYzBhMB0GA1UdDgQWBBQGmpsfU33x9aTI +04Y+oXNZtPdEITAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFAaamx9TffH1pMjThj6hc1m0 +90QhMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAqgVutt0Vyb+zxiD2BkewhpMl +0425yAA/l/VSJ4hxyXT968pk21vvHl26v9Hr7lxpuhbI87mP0zYuQEkHDVneixCwSQXi/5E/S7fd +Ao74gShczNxtr18UnH1YeA32gAm56Q6XKRm4t+v4FstVEuTGfbvE7Pi1HE4+Z7/FXxttbUcoqgRY +YdZ2vyJ/0Adqp2RT8JeNnYA/u8EH22Wv5psymsNUk8QcCMNE+3tjEUPRahphanltkE8pjkcFwRJp +adbGNjHh/PqAulxPxOu3Mqz4dWEX1xAZufHSCe96Qp1bWgvUxpVOKs7/B9dPfhgGiPEZtdmYu65x +xBzndFlY7wyJz4sfdZMaBBSSSFCp61cpABbjNhzI+L/wM9VBD8TMPN3pM0MBkRArHtG5Xc0yGYuP +jCB31yLEQtyEFpslbei0VXF/sHyz03FJuc9SpAQ/3D2gu68zngowYI7bnV2UqL1g52KAdoGDDIzM +MEZJ4gzSqK/rYXHv5yJiqfdcZGyfFoxnNidF9Ql7v/YQCvGwjVRDjAS6oz/v4jXH+XTgbzRB0L9z +ZVcg+ZtnemZoJE6AZb0QmQZZ8mWvuMZHu/2QeItBcy6vVR/cO5JyboTT0GFMDcx2V+IthSIVNg3r +AZ3r2OvEhJn7wAzMMujjd9qDRIueVSjAi1jTkD5OGwDxFa2DK5o= +-----END CERTIFICATE----- + +HARICA TLS RSA Root CA 2021 +=========================== +-----BEGIN CERTIFICATE----- +MIIFpDCCA4ygAwIBAgIQOcqTHO9D88aOk8f0ZIk4fjANBgkqhkiG9w0BAQsFADBsMQswCQYDVQQG +EwJHUjE3MDUGA1UECgwuSGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJlc2VhcmNoIEluc3RpdHV0aW9u +cyBDQTEkMCIGA1UEAwwbSEFSSUNBIFRMUyBSU0EgUm9vdCBDQSAyMDIxMB4XDTIxMDIxOTEwNTUz +OFoXDTQ1MDIxMzEwNTUzN1owbDELMAkGA1UEBhMCR1IxNzA1BgNVBAoMLkhlbGxlbmljIEFjYWRl +bWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgQ0ExJDAiBgNVBAMMG0hBUklDQSBUTFMgUlNB +IFJvb3QgQ0EgMjAyMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAIvC569lmwVnlskN +JLnQDmT8zuIkGCyEf3dRywQRNrhe7Wlxp57kJQmXZ8FHws+RFjZiPTgE4VGC/6zStGndLuwRo0Xu +a2s7TL+MjaQenRG56Tj5eg4MmOIjHdFOY9TnuEFE+2uva9of08WRiFukiZLRgeaMOVig1mlDqa2Y +Ulhu2wr7a89o+uOkXjpFc5gH6l8Cct4MpbOfrqkdtx2z/IpZ525yZa31MJQjB/OCFks1mJxTuy/K +5FrZx40d/JiZ+yykgmvwKh+OC19xXFyuQnspiYHLA6OZyoieC0AJQTPb5lh6/a6ZcMBaD9YThnEv +dmn8kN3bLW7R8pv1GmuebxWMevBLKKAiOIAkbDakO/IwkfN4E8/BPzWr8R0RI7VDIp4BkrcYAuUR +0YLbFQDMYTfBKnya4dC6s1BG7oKsnTH4+yPiAwBIcKMJJnkVU2DzOFytOOqBAGMUuTNe3QvboEUH +GjMJ+E20pwKmafTCWQWIZYVWrkvL4N48fS0ayOn7H6NhStYqE613TBoYm5EPWNgGVMWX+Ko/IIqm +haZ39qb8HOLubpQzKoNQhArlT4b4UEV4AIHrW2jjJo3Me1xR9BQsQL4aYB16cmEdH2MtiKrOokWQ +CPxrvrNQKlr9qEgYRtaQQJKQCoReaDH46+0N0x3GfZkYVVYnZS6NRcUk7M7jAgMBAAGjQjBAMA8G +A1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFApII6ZgpJIKM+qTW8VX6iVNvRLuMA4GA1UdDwEB/wQE +AwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAPpBIqm5iFSVmewzVjIuJndftTgfvnNAUX15QvWiWkKQU +EapobQk1OUAJ2vQJLDSle1mESSmXdMgHHkdt8s4cUCbjnj1AUz/3f5Z2EMVGpdAgS1D0NTsY9FVq +QRtHBmg8uwkIYtlfVUKqrFOFrJVWNlar5AWMxajaH6NpvVMPxP/cyuN+8kyIhkdGGvMA9YCRotxD +QpSbIPDRzbLrLFPCU3hKTwSUQZqPJzLB5UkZv/HywouoCjkxKLR9YjYsTewfM7Z+d21+UPCfDtcR +j88YxeMn/ibvBZ3PzzfF0HvaO7AWhAw6k9a+F9sPPg4ZeAnHqQJyIkv3N3a6dcSFA1pj1bF1BcK5 +vZStjBWZp5N99sXzqnTPBIWUmAD04vnKJGW/4GKvyMX6ssmeVkjaef2WdhW+o45WxLM0/L5H9MG0 +qPzVMIho7suuyWPEdr6sOBjhXlzPrjoiUevRi7PzKzMHVIf6tLITe7pTBGIBnfHAT+7hOtSLIBD6 +Alfm78ELt5BGnBkpjNxvoEppaZS3JGWg/6w/zgH7IS79aPib8qXPMThcFarmlwDB31qlpzmq6YR/ +PFGoOtmUW4y/Twhx5duoXNTSpv4Ao8YWxw/ogM4cKGR0GQjTQuPOAF1/sdwTsOEFy9EgqoZ0njnn +kf3/W9b3raYvAwtt41dU63ZTGI0RmLo= +-----END CERTIFICATE----- + +HARICA TLS ECC Root CA 2021 +=========================== +-----BEGIN CERTIFICATE----- +MIICVDCCAdugAwIBAgIQZ3SdjXfYO2rbIvT/WeK/zjAKBggqhkjOPQQDAzBsMQswCQYDVQQGEwJH +UjE3MDUGA1UECgwuSGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJlc2VhcmNoIEluc3RpdHV0aW9ucyBD +QTEkMCIGA1UEAwwbSEFSSUNBIFRMUyBFQ0MgUm9vdCBDQSAyMDIxMB4XDTIxMDIxOTExMDExMFoX +DTQ1MDIxMzExMDEwOVowbDELMAkGA1UEBhMCR1IxNzA1BgNVBAoMLkhlbGxlbmljIEFjYWRlbWlj +IGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgQ0ExJDAiBgNVBAMMG0hBUklDQSBUTFMgRUNDIFJv +b3QgQ0EgMjAyMTB2MBAGByqGSM49AgEGBSuBBAAiA2IABDgI/rGgltJ6rK9JOtDA4MM7KKrxcm1l +AEeIhPyaJmuqS7psBAqIXhfyVYf8MLA04jRYVxqEU+kw2anylnTDUR9YSTHMmE5gEYd103KUkE+b +ECUqqHgtvpBBWJAVcqeht6NCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUyRtTgRL+BNUW +0aq8mm+3oJUZbsowDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMDA2cAMGQCMBHervjcToiwqfAi +rcJRQO9gcS3ujwLEXQNwSaSS6sUUiHCm0w2wqsosQJz76YJumgIwK0eaB8bRwoF8yguWGEEbo/Qw +CZ61IygNnxS2PFOiTAZpffpskcYqSUXm7LcT4Tps +-----END CERTIFICATE----- + +Autoridad de Certificacion Firmaprofesional CIF A62634068 +========================================================= +-----BEGIN CERTIFICATE----- +MIIGFDCCA/ygAwIBAgIIG3Dp0v+ubHEwDQYJKoZIhvcNAQELBQAwUTELMAkGA1UEBhMCRVMxQjBA +BgNVBAMMOUF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uIEZpcm1hcHJvZmVzaW9uYWwgQ0lGIEE2 +MjYzNDA2ODAeFw0xNDA5MjMxNTIyMDdaFw0zNjA1MDUxNTIyMDdaMFExCzAJBgNVBAYTAkVTMUIw +QAYDVQQDDDlBdXRvcmlkYWQgZGUgQ2VydGlmaWNhY2lvbiBGaXJtYXByb2Zlc2lvbmFsIENJRiBB +NjI2MzQwNjgwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKlmuO6vj78aI14H9M2uDD +Utd9thDIAl6zQyrET2qyyhxdKJp4ERppWVevtSBC5IsP5t9bpgOSL/UR5GLXMnE42QQMcas9UX4P +B99jBVzpv5RvwSmCwLTaUbDBPLutN0pcyvFLNg4kq7/DhHf9qFD0sefGL9ItWY16Ck6WaVICqjaY +7Pz6FIMMNx/Jkjd/14Et5cS54D40/mf0PmbR0/RAz15iNA9wBj4gGFrO93IbJWyTdBSTo3OxDqqH +ECNZXyAFGUftaI6SEspd/NYrspI8IM/hX68gvqB2f3bl7BqGYTM+53u0P6APjqK5am+5hyZvQWyI +plD9amML9ZMWGxmPsu2bm8mQ9QEM3xk9Dz44I8kvjwzRAv4bVdZO0I08r0+k8/6vKtMFnXkIoctX +MbScyJCyZ/QYFpM6/EfY0XiWMR+6KwxfXZmtY4laJCB22N/9q06mIqqdXuYnin1oKaPnirjaEbsX +LZmdEyRG98Xi2J+Of8ePdG1asuhy9azuJBCtLxTa/y2aRnFHvkLfuwHb9H/TKI8xWVvTyQKmtFLK +bpf7Q8UIJm+K9Lv9nyiqDdVF8xM6HdjAeI9BZzwelGSuewvF6NkBiDkal4ZkQdU7hwxu+g/GvUgU +vzlN1J5Bto+WHWOWk9mVBngxaJ43BjuAiUVhOSPHG0SjFeUc+JIwuwIDAQABo4HvMIHsMB0GA1Ud +DgQWBBRlzeurNR4APn7VdMActHNHDhpkLzASBgNVHRMBAf8ECDAGAQH/AgEBMIGmBgNVHSAEgZ4w +gZswgZgGBFUdIAAwgY8wLwYIKwYBBQUHAgEWI2h0dHA6Ly93d3cuZmlybWFwcm9mZXNpb25hbC5j +b20vY3BzMFwGCCsGAQUFBwICMFAeTgBQAGEAcwBlAG8AIABkAGUAIABsAGEAIABCAG8AbgBhAG4A +bwB2AGEAIAA0ADcAIABCAGEAcgBjAGUAbABvAG4AYQAgADAAOAAwADEANzAOBgNVHQ8BAf8EBAMC +AQYwDQYJKoZIhvcNAQELBQADggIBAHSHKAIrdx9miWTtj3QuRhy7qPj4Cx2Dtjqn6EWKB7fgPiDL +4QjbEwj4KKE1soCzC1HA01aajTNFSa9J8OA9B3pFE1r/yJfY0xgsfZb43aJlQ3CTkBW6kN/oGbDb +LIpgD7dvlAceHabJhfa9NPhAeGIQcDq+fUs5gakQ1JZBu/hfHAsdCPKxsIl68veg4MSPi3i1O1il +I45PVf42O+AMt8oqMEEgtIDNrvx2ZnOorm7hfNoD6JQg5iKj0B+QXSBTFCZX2lSX3xZEEAEeiGaP +cjiT3SC3NL7X8e5jjkd5KAb881lFJWAiMxujX6i6KtoaPc1A6ozuBRWV1aUsIC+nmCjuRfzxuIgA +LI9C2lHVnOUTaHFFQ4ueCyE8S1wF3BqfmI7avSKecs2tCsvMo2ebKHTEm9caPARYpoKdrcd7b/+A +lun4jWq9GJAd/0kakFI3ky88Al2CdgtR5xbHV/g4+afNmyJU72OwFW1TZQNKXkqgsqeOSQBZONXH +9IBk9W6VULgRfhVwOEqwf9DEMnDAGf/JOC0ULGb0QkTmVXYbgBVX/8Cnp6o5qtjTcNAuuuuUavpf +NIbnYrX9ivAwhZTJryQCL2/W3Wf+47BVTwSYT6RBVuKT0Gro1vP7ZeDOdcQxWQzugsgMYDNKGbqE +ZycPvEJdvSRUDewdcAZfpLz6IHxV +-----END CERTIFICATE----- + +vTrus ECC Root CA +================= +-----BEGIN CERTIFICATE----- +MIICDzCCAZWgAwIBAgIUbmq8WapTvpg5Z6LSa6Q75m0c1towCgYIKoZIzj0EAwMwRzELMAkGA1UE +BhMCQ04xHDAaBgNVBAoTE2lUcnVzQ2hpbmEgQ28uLEx0ZC4xGjAYBgNVBAMTEXZUcnVzIEVDQyBS +b290IENBMB4XDTE4MDczMTA3MjY0NFoXDTQzMDczMTA3MjY0NFowRzELMAkGA1UEBhMCQ04xHDAa +BgNVBAoTE2lUcnVzQ2hpbmEgQ28uLEx0ZC4xGjAYBgNVBAMTEXZUcnVzIEVDQyBSb290IENBMHYw +EAYHKoZIzj0CAQYFK4EEACIDYgAEZVBKrox5lkqqHAjDo6LN/llWQXf9JpRCux3NCNtzslt188+c +ToL0v/hhJoVs1oVbcnDS/dtitN9Ti72xRFhiQgnH+n9bEOf+QP3A2MMrMudwpremIFUde4BdS49n +TPEQo0IwQDAdBgNVHQ4EFgQUmDnNvtiyjPeyq+GtJK97fKHbH88wDwYDVR0TAQH/BAUwAwEB/zAO +BgNVHQ8BAf8EBAMCAQYwCgYIKoZIzj0EAwMDaAAwZQIwV53dVvHH4+m4SVBrm2nDb+zDfSXkV5UT +QJtS0zvzQBm8JsctBp61ezaf9SXUY2sAAjEA6dPGnlaaKsyh2j/IZivTWJwghfqrkYpwcBE4YGQL +YgmRWAD5Tfs0aNoJrSEGGJTO +-----END CERTIFICATE----- + +vTrus Root CA +============= +-----BEGIN CERTIFICATE----- +MIIFVjCCAz6gAwIBAgIUQ+NxE9izWRRdt86M/TX9b7wFjUUwDQYJKoZIhvcNAQELBQAwQzELMAkG +A1UEBhMCQ04xHDAaBgNVBAoTE2lUcnVzQ2hpbmEgQ28uLEx0ZC4xFjAUBgNVBAMTDXZUcnVzIFJv +b3QgQ0EwHhcNMTgwNzMxMDcyNDA1WhcNNDMwNzMxMDcyNDA1WjBDMQswCQYDVQQGEwJDTjEcMBoG +A1UEChMTaVRydXNDaGluYSBDby4sTHRkLjEWMBQGA1UEAxMNdlRydXMgUm9vdCBDQTCCAiIwDQYJ +KoZIhvcNAQEBBQADggIPADCCAgoCggIBAL1VfGHTuB0EYgWgrmy3cLRB6ksDXhA/kFocizuwZots +SKYcIrrVQJLuM7IjWcmOvFjai57QGfIvWcaMY1q6n6MLsLOaXLoRuBLpDLvPbmyAhykUAyyNJJrI +ZIO1aqwTLDPxn9wsYTwaP3BVm60AUn/PBLn+NvqcwBauYv6WTEN+VRS+GrPSbcKvdmaVayqwlHeF +XgQPYh1jdfdr58tbmnDsPmcF8P4HCIDPKNsFxhQnL4Z98Cfe/+Z+M0jnCx5Y0ScrUw5XSmXX+6KA +YPxMvDVTAWqXcoKv8R1w6Jz1717CbMdHflqUhSZNO7rrTOiwCcJlwp2dCZtOtZcFrPUGoPc2BX70 +kLJrxLT5ZOrpGgrIDajtJ8nU57O5q4IikCc9Kuh8kO+8T/3iCiSn3mUkpF3qwHYw03dQ+A0Em5Q2 +AXPKBlim0zvc+gRGE1WKyURHuFE5Gi7oNOJ5y1lKCn+8pu8fA2dqWSslYpPZUxlmPCdiKYZNpGvu +/9ROutW04o5IWgAZCfEF2c6Rsffr6TlP9m8EQ5pV9T4FFL2/s1m02I4zhKOQUqqzApVg+QxMaPnu +1RcN+HFXtSXkKe5lXa/R7jwXC1pDxaWG6iSe4gUH3DRCEpHWOXSuTEGC2/KmSNGzm/MzqvOmwMVO +9fSddmPmAsYiS8GVP1BkLFTltvA8Kc9XAgMBAAGjQjBAMB0GA1UdDgQWBBRUYnBj8XWEQ1iO0RYg +scasGrz2iTAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOC +AgEAKbqSSaet8PFww+SX8J+pJdVrnjT+5hpk9jprUrIQeBqfTNqK2uwcN1LgQkv7bHbKJAs5EhWd +nxEt/Hlk3ODg9d3gV8mlsnZwUKT+twpw1aA08XXXTUm6EdGz2OyC/+sOxL9kLX1jbhd47F18iMjr +jld22VkE+rxSH0Ws8HqA7Oxvdq6R2xCOBNyS36D25q5J08FsEhvMKar5CKXiNxTKsbhm7xqC5PD4 +8acWabfbqWE8n/Uxy+QARsIvdLGx14HuqCaVvIivTDUHKgLKeBRtRytAVunLKmChZwOgzoy8sHJn +xDHO2zTlJQNgJXtxmOTAGytfdELSS8VZCAeHvsXDf+eW2eHcKJfWjwXj9ZtOyh1QRwVTsMo554Wg +icEFOwE30z9J4nfrI8iIZjs9OXYhRvHsXyO466JmdXTBQPfYaJqT4i2pLr0cox7IdMakLXogqzu4 +sEb9b91fUlV1YvCXoHzXOP0l382gmxDPi7g4Xl7FtKYCNqEeXxzP4padKar9mK5S4fNBUvupLnKW +nyfjqnN9+BojZns7q2WwMgFLFT49ok8MKzWixtlnEjUwzXYuFrOZnk1PTi07NEPhmg4NpGaXutIc +SkwsKouLgU9xGqndXHt7CMUADTdA43x7VF8vhV929vensBxXVsFy6K2ir40zSbofitzmdHxghm+H +l3s= +-----END CERTIFICATE----- + +ISRG Root X2 +============ +-----BEGIN CERTIFICATE----- +MIICGzCCAaGgAwIBAgIQQdKd0XLq7qeAwSxs6S+HUjAKBggqhkjOPQQDAzBPMQswCQYDVQQGEwJV +UzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElT +UkcgUm9vdCBYMjAeFw0yMDA5MDQwMDAwMDBaFw00MDA5MTcxNjAwMDBaME8xCzAJBgNVBAYTAlVT +MSkwJwYDVQQKEyBJbnRlcm5ldCBTZWN1cml0eSBSZXNlYXJjaCBHcm91cDEVMBMGA1UEAxMMSVNS +RyBSb290IFgyMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEzZvVn4CDCuwJSvMWSj5cz3es3mcFDR0H +ttwW+1qLFNvicWDEukWVEYmO6gbf9yoWHKS5xcUy4APgHoIYOIvXRdgKam7mAHf7AlF9ItgKbppb +d9/w+kHsOdx1ymgHDB/qo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV +HQ4EFgQUfEKWrt5LSDv6kviejM9ti6lyN5UwCgYIKoZIzj0EAwMDaAAwZQIwe3lORlCEwkSHRhtF +cP9Ymd70/aTSVaYgLXTWNLxBo1BfASdWtL4ndQavEi51mI38AjEAi/V3bNTIZargCyzuFJ0nN6T5 +U6VR5CmD1/iQMVtCnwr1/q4AaOeMSQ+2b1tbFfLn +-----END CERTIFICATE----- + +HiPKI Root CA - G1 +================== +-----BEGIN CERTIFICATE----- +MIIFajCCA1KgAwIBAgIQLd2szmKXlKFD6LDNdmpeYDANBgkqhkiG9w0BAQsFADBPMQswCQYDVQQG +EwJUVzEjMCEGA1UECgwaQ2h1bmdod2EgVGVsZWNvbSBDby4sIEx0ZC4xGzAZBgNVBAMMEkhpUEtJ +IFJvb3QgQ0EgLSBHMTAeFw0xOTAyMjIwOTQ2MDRaFw0zNzEyMzExNTU5NTlaME8xCzAJBgNVBAYT +AlRXMSMwIQYDVQQKDBpDaHVuZ2h3YSBUZWxlY29tIENvLiwgTHRkLjEbMBkGA1UEAwwSSGlQS0kg +Um9vdCBDQSAtIEcxMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA9B5/UnMyDHPkvRN0 +o9QwqNCuS9i233VHZvR85zkEHmpwINJaR3JnVfSl6J3VHiGh8Ge6zCFovkRTv4354twvVcg3Px+k +wJyz5HdcoEb+d/oaoDjq7Zpy3iu9lFc6uux55199QmQ5eiY29yTw1S+6lZgRZq2XNdZ1AYDgr/SE +YYwNHl98h5ZeQa/rh+r4XfEuiAU+TCK72h8q3VJGZDnzQs7ZngyzsHeXZJzA9KMuH5UHsBffMNsA +GJZMoYFL3QRtU6M9/Aes1MU3guvklQgZKILSQjqj2FPseYlgSGDIcpJQ3AOPgz+yQlda22rpEZfd +hSi8MEyr48KxRURHH+CKFgeW0iEPU8DtqX7UTuybCeyvQqww1r/REEXgphaypcXTT3OUM3ECoWqj +1jOXTyFjHluP2cFeRXF3D4FdXyGarYPM+l7WjSNfGz1BryB1ZlpK9p/7qxj3ccC2HTHsOyDry+K4 +9a6SsvfhhEvyovKTmiKe0xRvNlS9H15ZFblzqMF8b3ti6RZsR1pl8w4Rm0bZ/W3c1pzAtH2lsN0/ +Vm+h+fbkEkj9Bn8SV7apI09bA8PgcSojt/ewsTu8mL3WmKgMa/aOEmem8rJY5AIJEzypuxC00jBF +8ez3ABHfZfjcK0NVvxaXxA/VLGGEqnKG/uY6fsI/fe78LxQ+5oXdUG+3Se0CAwEAAaNCMEAwDwYD +VR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU8ncX+l6o/vY9cdVouslGDDjYr7AwDgYDVR0PAQH/BAQD +AgGGMA0GCSqGSIb3DQEBCwUAA4ICAQBQUfB13HAE4/+qddRxosuej6ip0691x1TPOhwEmSKsxBHi +7zNKpiMdDg1H2DfHb680f0+BazVP6XKlMeJ45/dOlBhbQH3PayFUhuaVevvGyuqcSE5XCV0vrPSl +tJczWNWseanMX/mF+lLFjfiRFOs6DRfQUsJ748JzjkZ4Bjgs6FzaZsT0pPBWGTMpWmWSBUdGSquE +wx4noR8RkpkndZMPvDY7l1ePJlsMu5wP1G4wB9TcXzZoZjmDlicmisjEOf6aIW/Vcobpf2Lll07Q +JNBAsNB1CI69aO4I1258EHBGG3zgiLKecoaZAeO/n0kZtCW+VmWuF2PlHt/o/0elv+EmBYTksMCv +5wiZqAxeJoBF1PhoL5aPruJKHJwWDBNvOIf2u8g0X5IDUXlwpt/L9ZlNec1OvFefQ05rLisY+Gpz +jLrFNe85akEez3GoorKGB1s6yeHvP2UEgEcyRHCVTjFnanRbEEV16rCf0OY1/k6fi8wrkkVbbiVg +hUbN0aqwdmaTd5a+g744tiROJgvM7XpWGuDpWsZkrUx6AEhEL7lAuxM+vhV4nYWBSipX3tUZQ9rb +yltHhoMLP7YNdnhzeSJesYAfz77RP1YQmCuVh6EfnWQUYDksswBVLuT1sw5XxJFBAJw/6KXf6vb/ +yPCtbVKoF6ubYfwSUTXkJf2vqmqGOQ== +-----END CERTIFICATE----- + +GlobalSign ECC Root CA - R4 +=========================== +-----BEGIN CERTIFICATE----- +MIIB3DCCAYOgAwIBAgINAgPlfvU/k/2lCSGypjAKBggqhkjOPQQDAjBQMSQwIgYDVQQLExtHbG9i +YWxTaWduIEVDQyBSb290IENBIC0gUjQxEzARBgNVBAoTCkdsb2JhbFNpZ24xEzARBgNVBAMTCkds +b2JhbFNpZ24wHhcNMTIxMTEzMDAwMDAwWhcNMzgwMTE5MDMxNDA3WjBQMSQwIgYDVQQLExtHbG9i +YWxTaWduIEVDQyBSb290IENBIC0gUjQxEzARBgNVBAoTCkdsb2JhbFNpZ24xEzARBgNVBAMTCkds +b2JhbFNpZ24wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAS4xnnTj2wlDp8uORkcA6SumuU5BwkW +ymOxuYb4ilfBV85C+nOh92VC/x7BALJucw7/xyHlGKSq2XE/qNS5zowdo0IwQDAOBgNVHQ8BAf8E +BAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUVLB7rUW44kB/+wpu+74zyTyjhNUwCgYI +KoZIzj0EAwIDRwAwRAIgIk90crlgr/HmnKAWBVBfw147bmF0774BxL4YSFlhgjICICadVGNA3jdg +UM/I2O2dgq43mLyjj0xMqTQrbO/7lZsm +-----END CERTIFICATE----- + +GTS Root R1 +=========== +-----BEGIN CERTIFICATE----- +MIIFVzCCAz+gAwIBAgINAgPlk28xsBNJiGuiFzANBgkqhkiG9w0BAQwFADBHMQswCQYDVQQGEwJV +UzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3Qg +UjEwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UE +ChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwggIiMA0G +CSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC2EQKLHuOhd5s73L+UPreVp0A8of2C+X0yBoJx9vaM +f/vo27xqLpeXo4xL+Sv2sfnOhB2x+cWX3u+58qPpvBKJXqeqUqv4IyfLpLGcY9vXmX7wCl7raKb0 +xlpHDU0QM+NOsROjyBhsS+z8CZDfnWQpJSMHobTSPS5g4M/SCYe7zUjwTcLCeoiKu7rPWRnWr4+w +B7CeMfGCwcDfLqZtbBkOtdh+JhpFAz2weaSUKK0PfyblqAj+lug8aJRT7oM6iCsVlgmy4HqMLnXW +nOunVmSPlk9orj2XwoSPwLxAwAtcvfaHszVsrBhQf4TgTM2S0yDpM7xSma8ytSmzJSq0SPly4cpk +9+aCEI3oncKKiPo4Zor8Y/kB+Xj9e1x3+naH+uzfsQ55lVe0vSbv1gHR6xYKu44LtcXFilWr06zq +kUspzBmkMiVOKvFlRNACzqrOSbTqn3yDsEB750Orp2yjj32JgfpMpf/VjsPOS+C12LOORc92wO1A +K/1TD7Cn1TsNsYqiA94xrcx36m97PtbfkSIS5r762DL8EGMUUXLeXdYWk70paDPvOmbsB4om3xPX +V2V4J95eSRQAogB/mqghtqmxlbCluQ0WEdrHbEg8QOB+DVrNVjzRlwW5y0vtOUucxD/SVRNuJLDW +cfr0wbrM7Rv1/oFB2ACYPTrIrnqYNxgFlQIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0T +AQH/BAUwAwEB/zAdBgNVHQ4EFgQU5K8rJnEaK0gnhS9SZizv8IkTcT4wDQYJKoZIhvcNAQEMBQAD +ggIBAJ+qQibbC5u+/x6Wki4+omVKapi6Ist9wTrYggoGxval3sBOh2Z5ofmmWJyq+bXmYOfg6LEe +QkEzCzc9zolwFcq1JKjPa7XSQCGYzyI0zzvFIoTgxQ6KfF2I5DUkzps+GlQebtuyh6f88/qBVRRi +ClmpIgUxPoLW7ttXNLwzldMXG+gnoot7TiYaelpkttGsN/H9oPM47HLwEXWdyzRSjeZ2axfG34ar +J45JK3VmgRAhpuo+9K4l/3wV3s6MJT/KYnAK9y8JZgfIPxz88NtFMN9iiMG1D53Dn0reWVlHxYci +NuaCp+0KueIHoI17eko8cdLiA6EfMgfdG+RCzgwARWGAtQsgWSl4vflVy2PFPEz0tv/bal8xa5me +LMFrUKTX5hgUvYU/Z6tGn6D/Qqc6f1zLXbBwHSs09dR2CQzreExZBfMzQsNhFRAbd03OIozUhfJF +fbdT6u9AWpQKXCBfTkBdYiJ23//OYb2MI3jSNwLgjt7RETeJ9r/tSQdirpLsQBqvFAnZ0E6yove+ +7u7Y/9waLd64NnHi/Hm3lCXRSHNboTXns5lndcEZOitHTtNCjv0xyBZm2tIMPNuzjsmhDYAPexZ3 +FL//2wmUspO8IFgV6dtxQ/PeEMMA3KgqlbbC1j+Qa3bbbP6MvPJwNQzcmRk13NfIRmPVNnGuV/u3 +gm3c +-----END CERTIFICATE----- + +GTS Root R3 +=========== +-----BEGIN CERTIFICATE----- +MIICCTCCAY6gAwIBAgINAgPluILrIPglJ209ZjAKBggqhkjOPQQDAzBHMQswCQYDVQQGEwJVUzEi +MCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjMw +HhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZ +R29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjMwdjAQBgcqhkjO +PQIBBgUrgQQAIgNiAAQfTzOHMymKoYTey8chWEGJ6ladK0uFxh1MJ7x/JlFyb+Kf1qPKzEUURout +736GjOyxfi//qXGdGIRFBEFVbivqJn+7kAHjSxm65FSWRQmx1WyRRK2EE46ajA2ADDL24CejQjBA +MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTB8Sa6oC2uhYHP0/Eq +Er24Cmf9vDAKBggqhkjOPQQDAwNpADBmAjEA9uEglRR7VKOQFhG/hMjqb2sXnh5GmCCbn9MN2azT +L818+FsuVbu/3ZL3pAzcMeGiAjEA/JdmZuVDFhOD3cffL74UOO0BzrEXGhF16b0DjyZ+hOXJYKaV +11RZt+cRLInUue4X +-----END CERTIFICATE----- + +GTS Root R4 +=========== +-----BEGIN CERTIFICATE----- +MIICCTCCAY6gAwIBAgINAgPlwGjvYxqccpBQUjAKBggqhkjOPQQDAzBHMQswCQYDVQQGEwJVUzEi +MCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjQw +HhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZ +R29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjQwdjAQBgcqhkjO +PQIBBgUrgQQAIgNiAATzdHOnaItgrkO4NcWBMHtLSZ37wWHO5t5GvWvVYRg1rkDdc/eJkTBa6zzu +hXyiQHY7qca4R9gq55KRanPpsXI5nymfopjTX15YhmUPoYRlBtHci8nHc8iMai/lxKvRHYqjQjBA +MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSATNbrdP9JNqPV2Py1 +PsVq8JQdjDAKBggqhkjOPQQDAwNpADBmAjEA6ED/g94D9J+uHXqnLrmvT/aDHQ4thQEd0dlq7A/C +r8deVl5c1RxYIigL9zC2L7F8AjEA8GE8p/SgguMh1YQdc4acLa/KNJvxn7kjNuK8YAOdgLOaVsjh +4rsUecrNIdSUtUlD +-----END CERTIFICATE----- + +Telia Root CA v2 +================ +-----BEGIN CERTIFICATE----- +MIIFdDCCA1ygAwIBAgIPAWdfJ9b+euPkrL4JWwWeMA0GCSqGSIb3DQEBCwUAMEQxCzAJBgNVBAYT +AkZJMRowGAYDVQQKDBFUZWxpYSBGaW5sYW5kIE95ajEZMBcGA1UEAwwQVGVsaWEgUm9vdCBDQSB2 +MjAeFw0xODExMjkxMTU1NTRaFw00MzExMjkxMTU1NTRaMEQxCzAJBgNVBAYTAkZJMRowGAYDVQQK +DBFUZWxpYSBGaW5sYW5kIE95ajEZMBcGA1UEAwwQVGVsaWEgUm9vdCBDQSB2MjCCAiIwDQYJKoZI +hvcNAQEBBQADggIPADCCAgoCggIBALLQPwe84nvQa5n44ndp586dpAO8gm2h/oFlH0wnrI4AuhZ7 +6zBqAMCzdGh+sq/H1WKzej9Qyow2RCRj0jbpDIX2Q3bVTKFgcmfiKDOlyzG4OiIjNLh9vVYiQJ3q +9HsDrWj8soFPmNB06o3lfc1jw6P23pLCWBnglrvFxKk9pXSW/q/5iaq9lRdU2HhE8Qx3FZLgmEKn +pNaqIJLNwaCzlrI6hEKNfdWV5Nbb6WLEWLN5xYzTNTODn3WhUidhOPFZPY5Q4L15POdslv5e2QJl +tI5c0BE0312/UqeBAMN/mUWZFdUXyApT7GPzmX3MaRKGwhfwAZ6/hLzRUssbkmbOpFPlob/E2wnW +5olWK8jjfN7j/4nlNW4o6GwLI1GpJQXrSPjdscr6bAhR77cYbETKJuFzxokGgeWKrLDiKca5JLNr +RBH0pUPCTEPlcDaMtjNXepUugqD0XBCzYYP2AgWGLnwtbNwDRm41k9V6lS/eINhbfpSQBGq6WT0E +BXWdN6IOLj3rwaRSg/7Qa9RmjtzG6RJOHSpXqhC8fF6CfaamyfItufUXJ63RDolUK5X6wK0dmBR4 +M0KGCqlztft0DbcbMBnEWg4cJ7faGND/isgFuvGqHKI3t+ZIpEYslOqodmJHixBTB0hXbOKSTbau +BcvcwUpej6w9GU7C7WB1K9vBykLVAgMBAAGjYzBhMB8GA1UdIwQYMBaAFHKs5DN5qkWH9v2sHZ7W +xy+G2CQ5MB0GA1UdDgQWBBRyrOQzeapFh/b9rB2e1scvhtgkOTAOBgNVHQ8BAf8EBAMCAQYwDwYD +VR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAgEAoDtZpwmUPjaE0n4vOaWWl/oRrfxn83EJ +8rKJhGdEr7nv7ZbsnGTbMjBvZ5qsfl+yqwE2foH65IRe0qw24GtixX1LDoJt0nZi0f6X+J8wfBj5 +tFJ3gh1229MdqfDBmgC9bXXYfef6xzijnHDoRnkDry5023X4blMMA8iZGok1GTzTyVR8qPAs5m4H +eW9q4ebqkYJpCh3DflminmtGFZhb069GHWLIzoBSSRE/yQQSwxN8PzuKlts8oB4KtItUsiRnDe+C +y748fdHif64W1lZYudogsYMVoe+KTTJvQS8TUoKU1xrBeKJR3Stwbbca+few4GeXVtt8YVMJAygC +QMez2P2ccGrGKMOF6eLtGpOg3kuYooQ+BXcBlj37tCAPnHICehIv1aO6UXivKitEZU61/Qrowc15 +h2Er3oBXRb9n8ZuRXqWk7FlIEA04x7D6w0RtBPV4UBySllva9bguulvP5fBqnUsvWHMtTy3EHD70 +sz+rFQ47GUGKpMFXEmZxTPpT41frYpUJnlTd0cI8Vzy9OK2YZLe4A5pTVmBds9hCG1xLEooc6+t9 +xnppxyd/pPiL8uSUZodL6ZQHCRJ5irLrdATczvREWeAWysUsWNc8e89ihmpQfTU2Zqf7N+cox9jQ +raVplI/owd8k+BsHMYeB2F326CjYSlKArBPuUBQemMc= +-----END CERTIFICATE----- + +D-TRUST BR Root CA 1 2020 +========================= +-----BEGIN CERTIFICATE----- +MIIC2zCCAmCgAwIBAgIQfMmPK4TX3+oPyWWa00tNljAKBggqhkjOPQQDAzBIMQswCQYDVQQGEwJE +RTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlELVRSVVNUIEJSIFJvb3QgQ0EgMSAy +MDIwMB4XDTIwMDIxMTA5NDUwMFoXDTM1MDIxMTA5NDQ1OVowSDELMAkGA1UEBhMCREUxFTATBgNV +BAoTDEQtVHJ1c3QgR21iSDEiMCAGA1UEAxMZRC1UUlVTVCBCUiBSb290IENBIDEgMjAyMDB2MBAG +ByqGSM49AgEGBSuBBAAiA2IABMbLxyjR+4T1mu9CFCDhQ2tuda38KwOE1HaTJddZO0Flax7mNCq7 +dPYSzuht56vkPE4/RAiLzRZxy7+SmfSk1zxQVFKQhYN4lGdnoxwJGT11NIXe7WB9xwy0QVK5buXu +QqOCAQ0wggEJMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFHOREKv/VbNafAkl1bK6CKBrqx9t +MA4GA1UdDwEB/wQEAwIBBjCBxgYDVR0fBIG+MIG7MD6gPKA6hjhodHRwOi8vY3JsLmQtdHJ1c3Qu +bmV0L2NybC9kLXRydXN0X2JyX3Jvb3RfY2FfMV8yMDIwLmNybDB5oHegdYZzbGRhcDovL2RpcmVj +dG9yeS5kLXRydXN0Lm5ldC9DTj1ELVRSVVNUJTIwQlIlMjBSb290JTIwQ0ElMjAxJTIwMjAyMCxP +PUQtVHJ1c3QlMjBHbWJILEM9REU/Y2VydGlmaWNhdGVyZXZvY2F0aW9ubGlzdDAKBggqhkjOPQQD +AwNpADBmAjEAlJAtE/rhY/hhY+ithXhUkZy4kzg+GkHaQBZTQgjKL47xPoFWwKrY7RjEsK70Pvom +AjEA8yjixtsrmfu3Ubgko6SUeho/5jbiA1czijDLgsfWFBHVdWNbFJWcHwHP2NVypw87 +-----END CERTIFICATE----- + +D-TRUST EV Root CA 1 2020 +========================= +-----BEGIN CERTIFICATE----- +MIIC2zCCAmCgAwIBAgIQXwJB13qHfEwDo6yWjfv/0DAKBggqhkjOPQQDAzBIMQswCQYDVQQGEwJE +RTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlELVRSVVNUIEVWIFJvb3QgQ0EgMSAy +MDIwMB4XDTIwMDIxMTEwMDAwMFoXDTM1MDIxMTA5NTk1OVowSDELMAkGA1UEBhMCREUxFTATBgNV +BAoTDEQtVHJ1c3QgR21iSDEiMCAGA1UEAxMZRC1UUlVTVCBFViBSb290IENBIDEgMjAyMDB2MBAG +ByqGSM49AgEGBSuBBAAiA2IABPEL3YZDIBnfl4XoIkqbz52Yv7QFJsnL46bSj8WeeHsxiamJrSc8 +ZRCC/N/DnU7wMyPE0jL1HLDfMxddxfCxivnvubcUyilKwg+pf3VlSSowZ/Rk99Yad9rDwpdhQntJ +raOCAQ0wggEJMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFH8QARY3OqQo5FD4pPfsazK2/umL +MA4GA1UdDwEB/wQEAwIBBjCBxgYDVR0fBIG+MIG7MD6gPKA6hjhodHRwOi8vY3JsLmQtdHJ1c3Qu +bmV0L2NybC9kLXRydXN0X2V2X3Jvb3RfY2FfMV8yMDIwLmNybDB5oHegdYZzbGRhcDovL2RpcmVj +dG9yeS5kLXRydXN0Lm5ldC9DTj1ELVRSVVNUJTIwRVYlMjBSb290JTIwQ0ElMjAxJTIwMjAyMCxP +PUQtVHJ1c3QlMjBHbWJILEM9REU/Y2VydGlmaWNhdGVyZXZvY2F0aW9ubGlzdDAKBggqhkjOPQQD +AwNpADBmAjEAyjzGKnXCXnViOTYAYFqLwZOZzNnbQTs7h5kXO9XMT8oi96CAy/m0sRtW9XLS/BnR +AjEAkfcwkz8QRitxpNA7RJvAKQIFskF3UfN5Wp6OFKBOQtJbgfM0agPnIjhQW+0ZT0MW +-----END CERTIFICATE----- + +DigiCert TLS ECC P384 Root G5 +============================= +-----BEGIN CERTIFICATE----- +MIICGTCCAZ+gAwIBAgIQCeCTZaz32ci5PhwLBCou8zAKBggqhkjOPQQDAzBOMQswCQYDVQQGEwJV +UzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJjAkBgNVBAMTHURpZ2lDZXJ0IFRMUyBFQ0MgUDM4 +NCBSb290IEc1MB4XDTIxMDExNTAwMDAwMFoXDTQ2MDExNDIzNTk1OVowTjELMAkGA1UEBhMCVVMx +FzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMSYwJAYDVQQDEx1EaWdpQ2VydCBUTFMgRUNDIFAzODQg +Um9vdCBHNTB2MBAGByqGSM49AgEGBSuBBAAiA2IABMFEoc8Rl1Ca3iOCNQfN0MsYndLxf3c1Tzvd +lHJS7cI7+Oz6e2tYIOyZrsn8aLN1udsJ7MgT9U7GCh1mMEy7H0cKPGEQQil8pQgO4CLp0zVozptj +n4S1mU1YoI71VOeVyaNCMEAwHQYDVR0OBBYEFMFRRVBZqz7nLFr6ICISB4CIfBFqMA4GA1UdDwEB +/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49BAMDA2gAMGUCMQCJao1H5+z8blUD2Wds +Jk6Dxv3J+ysTvLd6jLRl0mlpYxNjOyZQLgGheQaRnUi/wr4CMEfDFXuxoJGZSZOoPHzoRgaLLPIx +AJSdYsiJvRmEFOml+wG4DXZDjC5Ty3zfDBeWUA== +-----END CERTIFICATE----- + +DigiCert TLS RSA4096 Root G5 +============================ +-----BEGIN CERTIFICATE----- +MIIFZjCCA06gAwIBAgIQCPm0eKj6ftpqMzeJ3nzPijANBgkqhkiG9w0BAQwFADBNMQswCQYDVQQG +EwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJTAjBgNVBAMTHERpZ2lDZXJ0IFRMUyBSU0E0 +MDk2IFJvb3QgRzUwHhcNMjEwMTE1MDAwMDAwWhcNNDYwMTE0MjM1OTU5WjBNMQswCQYDVQQGEwJV +UzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJTAjBgNVBAMTHERpZ2lDZXJ0IFRMUyBSU0E0MDk2 +IFJvb3QgRzUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCz0PTJeRGd/fxmgefM1eS8 +7IE+ajWOLrfn3q/5B03PMJ3qCQuZvWxX2hhKuHisOjmopkisLnLlvevxGs3npAOpPxG02C+JFvuU +AT27L/gTBaF4HI4o4EXgg/RZG5Wzrn4DReW+wkL+7vI8toUTmDKdFqgpwgscONyfMXdcvyej/Ces +tyu9dJsXLfKB2l2w4SMXPohKEiPQ6s+d3gMXsUJKoBZMpG2T6T867jp8nVid9E6P/DsjyG244gXa +zOvswzH016cpVIDPRFtMbzCe88zdH5RDnU1/cHAN1DrRN/BsnZvAFJNY781BOHW8EwOVfH/jXOnV +DdXifBBiqmvwPXbzP6PosMH976pXTayGpxi0KcEsDr9kvimM2AItzVwv8n/vFfQMFawKsPHTDU9q +TXeXAaDxZre3zu/O7Oyldcqs4+Fj97ihBMi8ez9dLRYiVu1ISf6nL3kwJZu6ay0/nTvEF+cdLvvy +z6b84xQslpghjLSR6Rlgg/IwKwZzUNWYOwbpx4oMYIwo+FKbbuH2TbsGJJvXKyY//SovcfXWJL5/ +MZ4PbeiPT02jP/816t9JXkGPhvnxd3lLG7SjXi/7RgLQZhNeXoVPzthwiHvOAbWWl9fNff2C+MIk +wcoBOU+NosEUQB+cZtUMCUbW8tDRSHZWOkPLtgoRObqME2wGtZ7P6wIDAQABo0IwQDAdBgNVHQ4E +FgQUUTMc7TZArxfTJc1paPKvTiM+s0EwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8w +DQYJKoZIhvcNAQEMBQADggIBAGCmr1tfV9qJ20tQqcQjNSH/0GEwhJG3PxDPJY7Jv0Y02cEhJhxw +GXIeo8mH/qlDZJY6yFMECrZBu8RHANmfGBg7sg7zNOok992vIGCukihfNudd5N7HPNtQOa27PShN +lnx2xlv0wdsUpasZYgcYQF+Xkdycx6u1UQ3maVNVzDl92sURVXLFO4uJ+DQtpBflF+aZfTCIITfN +MBc9uPK8qHWgQ9w+iUuQrm0D4ByjoJYJu32jtyoQREtGBzRj7TG5BO6jm5qu5jF49OokYTurWGT/ +u4cnYiWB39yhL/btp/96j1EuMPikAdKFOV8BmZZvWltwGUb+hmA+rYAQCd05JS9Yf7vSdPD3Rh9G +OUrYU9DzLjtxpdRv/PNn5AeP3SYZ4Y1b+qOTEZvpyDrDVWiakuFSdjjo4bq9+0/V77PnSIMx8IIh +47a+p6tv75/fTM8BuGJqIz3nCU2AG3swpMPdB380vqQmsvZB6Akd4yCYqjdP//fx4ilwMUc/dNAU +FvohigLVigmUdy7yWSiLfFCSCmZ4OIN1xLVaqBHG5cGdZlXPU8Sv13WFqUITVuwhd4GTWgzqltlJ +yqEI8pc7bZsEGCREjnwB8twl2F6GmrE52/WRMmrRpnCKovfepEWFJqgejF0pW8hL2JpqA15w8oVP +bEtoL8pU9ozaMv7Da4M/OMZ+ +-----END CERTIFICATE----- + +Certainly Root R1 +================= +-----BEGIN CERTIFICATE----- +MIIFRzCCAy+gAwIBAgIRAI4P+UuQcWhlM1T01EQ5t+AwDQYJKoZIhvcNAQELBQAwPTELMAkGA1UE +BhMCVVMxEjAQBgNVBAoTCUNlcnRhaW5seTEaMBgGA1UEAxMRQ2VydGFpbmx5IFJvb3QgUjEwHhcN +MjEwNDAxMDAwMDAwWhcNNDYwNDAxMDAwMDAwWjA9MQswCQYDVQQGEwJVUzESMBAGA1UEChMJQ2Vy +dGFpbmx5MRowGAYDVQQDExFDZXJ0YWlubHkgUm9vdCBSMTCCAiIwDQYJKoZIhvcNAQEBBQADggIP +ADCCAgoCggIBANA21B/q3avk0bbm+yLA3RMNansiExyXPGhjZjKcA7WNpIGD2ngwEc/csiu+kr+O +5MQTvqRoTNoCaBZ0vrLdBORrKt03H2As2/X3oXyVtwxwhi7xOu9S98zTm/mLvg7fMbedaFySpvXl +8wo0tf97ouSHocavFwDvA5HtqRxOcT3Si2yJ9HiG5mpJoM610rCrm/b01C7jcvk2xusVtyWMOvwl +DbMicyF0yEqWYZL1LwsYpfSt4u5BvQF5+paMjRcCMLT5r3gajLQ2EBAHBXDQ9DGQilHFhiZ5shGI +XsXwClTNSaa/ApzSRKft43jvRl5tcdF5cBxGX1HpyTfcX35pe0HfNEXgO4T0oYoKNp43zGJS4YkN +KPl6I7ENPT2a/Z2B7yyQwHtETrtJ4A5KVpK8y7XdeReJkd5hiXSSqOMyhb5OhaRLWcsrxXiOcVTQ +AjeZjOVJ6uBUcqQRBi8LjMFbvrWhsFNunLhgkR9Za/kt9JQKl7XsxXYDVBtlUrpMklZRNaBA2Cnb +rlJ2Oy0wQJuK0EJWtLeIAaSHO1OWzaMWj/Nmqhexx2DgwUMFDO6bW2BvBlyHWyf5QBGenDPBt+U1 +VwV/J84XIIwc/PH72jEpSe31C4SnT8H2TsIonPru4K8H+zMReiFPCyEQtkA6qyI6BJyLm4SGcprS +p6XEtHWRqSsjAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud +DgQWBBTgqj8ljZ9EXME66C6ud0yEPmcM9DANBgkqhkiG9w0BAQsFAAOCAgEAuVevuBLaV4OPaAsz +HQNTVfSVcOQrPbA56/qJYv331hgELyE03fFo8NWWWt7CgKPBjcZq91l3rhVkz1t5BXdm6ozTaw3d +8VkswTOlMIAVRQdFGjEitpIAq5lNOo93r6kiyi9jyhXWx8bwPWz8HA2YEGGeEaIi1wrykXprOQ4v +MMM2SZ/g6Q8CRFA3lFV96p/2O7qUpUzpvD5RtOjKkjZUbVwlKNrdrRT90+7iIgXr0PK3aBLXWopB +GsaSpVo7Y0VPv+E6dyIvXL9G+VoDhRNCX8reU9ditaY1BMJH/5n9hN9czulegChB8n3nHpDYT3Y+ +gjwN/KUD+nsa2UUeYNrEjvn8K8l7lcUq/6qJ34IxD3L/DCfXCh5WAFAeDJDBlrXYFIW7pw0WwfgH +JBu6haEaBQmAupVjyTrsJZ9/nbqkRxWbRHDxakvWOF5D8xh+UG7pWijmZeZ3Gzr9Hb4DJqPb1OG7 +fpYnKx3upPvaJVQTA945xsMfTZDsjxtK0hzthZU4UHlG1sGQUDGpXJpuHfUzVounmdLyyCwzk5Iw +x06MZTMQZBf9JBeW0Y3COmor6xOLRPIh80oat3df1+2IpHLlOR+Vnb5nwXARPbv0+Em34yaXOp/S +X3z7wJl8OSngex2/DaeP0ik0biQVy96QXr8axGbqwua6OV+KmalBWQewLK8= +-----END CERTIFICATE----- + +Certainly Root E1 +================= +-----BEGIN CERTIFICATE----- +MIIB9zCCAX2gAwIBAgIQBiUzsUcDMydc+Y2aub/M+DAKBggqhkjOPQQDAzA9MQswCQYDVQQGEwJV +UzESMBAGA1UEChMJQ2VydGFpbmx5MRowGAYDVQQDExFDZXJ0YWlubHkgUm9vdCBFMTAeFw0yMTA0 +MDEwMDAwMDBaFw00NjA0MDEwMDAwMDBaMD0xCzAJBgNVBAYTAlVTMRIwEAYDVQQKEwlDZXJ0YWlu +bHkxGjAYBgNVBAMTEUNlcnRhaW5seSBSb290IEUxMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE3m/4 +fxzf7flHh4axpMCK+IKXgOqPyEpeKn2IaKcBYhSRJHpcnqMXfYqGITQYUBsQ3tA3SybHGWCA6TS9 +YBk2QNYphwk8kXr2vBMj3VlOBF7PyAIcGFPBMdjaIOlEjeR2o0IwQDAOBgNVHQ8BAf8EBAMCAQYw +DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU8ygYy2R17ikq6+2uI1g4hevIIgcwCgYIKoZIzj0E +AwMDaAAwZQIxALGOWiDDshliTd6wT99u0nCK8Z9+aozmut6Dacpps6kFtZaSF4fC0urQe87YQVt8 +rgIwRt7qy12a7DLCZRawTDBcMPPaTnOGBtjOiQRINzf43TNRnXCve1XYAS59BWQOhriR +-----END CERTIFICATE----- + +Security Communication ECC RootCA1 +================================== +-----BEGIN CERTIFICATE----- +MIICODCCAb6gAwIBAgIJANZdm7N4gS7rMAoGCCqGSM49BAMDMGExCzAJBgNVBAYTAkpQMSUwIwYD +VQQKExxTRUNPTSBUcnVzdCBTeXN0ZW1zIENPLixMVEQuMSswKQYDVQQDEyJTZWN1cml0eSBDb21t +dW5pY2F0aW9uIEVDQyBSb290Q0ExMB4XDTE2MDYxNjA1MTUyOFoXDTM4MDExODA1MTUyOFowYTEL +MAkGA1UEBhMCSlAxJTAjBgNVBAoTHFNFQ09NIFRydXN0IFN5c3RlbXMgQ08uLExURC4xKzApBgNV +BAMTIlNlY3VyaXR5IENvbW11bmljYXRpb24gRUNDIFJvb3RDQTEwdjAQBgcqhkjOPQIBBgUrgQQA +IgNiAASkpW9gAwPDvTH00xecK4R1rOX9PVdu12O/5gSJko6BnOPpR27KkBLIE+CnnfdldB9sELLo +5OnvbYUymUSxXv3MdhDYW72ixvnWQuRXdtyQwjWpS4g8EkdtXP9JTxpKULGjQjBAMB0GA1UdDgQW +BBSGHOf+LaVKiwj+KBH6vqNm+GBZLzAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAK +BggqhkjOPQQDAwNoADBlAjAVXUI9/Lbu9zuxNuie9sRGKEkz0FhDKmMpzE2xtHqiuQ04pV1IKv3L +snNdo4gIxwwCMQDAqy0Obe0YottT6SXbVQjgUMzfRGEWgqtJsLKB7HOHeLRMsmIbEvoWTSVLY70e +N9k= +-----END CERTIFICATE----- + +BJCA Global Root CA1 +==================== +-----BEGIN CERTIFICATE----- +MIIFdDCCA1ygAwIBAgIQVW9l47TZkGobCdFsPsBsIDANBgkqhkiG9w0BAQsFADBUMQswCQYDVQQG +EwJDTjEmMCQGA1UECgwdQkVJSklORyBDRVJUSUZJQ0FURSBBVVRIT1JJVFkxHTAbBgNVBAMMFEJK +Q0EgR2xvYmFsIFJvb3QgQ0ExMB4XDTE5MTIxOTAzMTYxN1oXDTQ0MTIxMjAzMTYxN1owVDELMAkG +A1UEBhMCQ04xJjAkBgNVBAoMHUJFSUpJTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZMR0wGwYDVQQD +DBRCSkNBIEdsb2JhbCBSb290IENBMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAPFm +CL3ZxRVhy4QEQaVpN3cdwbB7+sN3SJATcmTRuHyQNZ0YeYjjlwE8R4HyDqKYDZ4/N+AZspDyRhyS +sTphzvq3Rp4Dhtczbu33RYx2N95ulpH3134rhxfVizXuhJFyV9xgw8O558dnJCNPYwpj9mZ9S1Wn +P3hkSWkSl+BMDdMJoDIwOvqfwPKcxRIqLhy1BDPapDgRat7GGPZHOiJBhyL8xIkoVNiMpTAK+BcW +yqw3/XmnkRd4OJmtWO2y3syJfQOcs4ll5+M7sSKGjwZteAf9kRJ/sGsciQ35uMt0WwfCyPQ10WRj +eulumijWML3mG90Vr4TqnMfK9Q7q8l0ph49pczm+LiRvRSGsxdRpJQaDrXpIhRMsDQa4bHlW/KNn +MoH1V6XKV0Jp6VwkYe/iMBhORJhVb3rCk9gZtt58R4oRTklH2yiUAguUSiz5EtBP6DF+bHq/pj+b +OT0CFqMYs2esWz8sgytnOYFcuX6U1WTdno9uruh8W7TXakdI136z1C2OVnZOz2nxbkRs1CTqjSSh +GL+9V/6pmTW12xB3uD1IutbB5/EjPtffhZ0nPNRAvQoMvfXnjSXWgXSHRtQpdaJCbPdzied9v3pK +H9MiyRVVz99vfFXQpIsHETdfg6YmV6YBW37+WGgHqel62bno/1Afq8K0wM7o6v0PvY1NuLxxAgMB +AAGjQjBAMB0GA1UdDgQWBBTF7+3M2I0hxkjk49cULqcWk+WYATAPBgNVHRMBAf8EBTADAQH/MA4G +A1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAUoKsITQfI/Ki2Pm4rzc2IInRNwPWaZ+4 +YRC6ojGYWUfo0Q0lHhVBDOAqVdVXUsv45Mdpox1NcQJeXyFFYEhcCY5JEMEE3KliawLwQ8hOnThJ +dMkycFRtwUf8jrQ2ntScvd0g1lPJGKm1Vrl2i5VnZu69mP6u775u+2D2/VnGKhs/I0qUJDAnyIm8 +60Qkmss9vk/Ves6OF8tiwdneHg56/0OGNFK8YT88X7vZdrRTvJez/opMEi4r89fO4aL/3Xtw+zuh +TaRjAv04l5U/BXCga99igUOLtFkNSoxUnMW7gZ/NfaXvCyUeOiDbHPwfmGcCCtRzRBPbUYQaVQNW +4AB+dAb/OMRyHdOoP2gxXdMJxy6MW2Pg6Nwe0uxhHvLe5e/2mXZgLR6UcnHGCyoyx5JO1UbXHfmp +GQrI+pXObSOYqgs4rZpWDW+N8TEAiMEXnM0ZNjX+VVOg4DwzX5Ze4jLp3zO7Bkqp2IRzznfSxqxx +4VyjHQy7Ct9f4qNx2No3WqB4K/TUfet27fJhcKVlmtOJNBir+3I+17Q9eVzYH6Eze9mCUAyTF6ps +3MKCuwJXNq+YJyo5UOGwifUll35HaBC07HPKs5fRJNz2YqAo07WjuGS3iGJCz51TzZm+ZGiPTx4S +SPfSKcOYKMryMguTjClPPGAyzQWWYezyr/6zcCwupvI= +-----END CERTIFICATE----- + +BJCA Global Root CA2 +==================== +-----BEGIN CERTIFICATE----- +MIICJTCCAaugAwIBAgIQLBcIfWQqwP6FGFkGz7RK6zAKBggqhkjOPQQDAzBUMQswCQYDVQQGEwJD +TjEmMCQGA1UECgwdQkVJSklORyBDRVJUSUZJQ0FURSBBVVRIT1JJVFkxHTAbBgNVBAMMFEJKQ0Eg +R2xvYmFsIFJvb3QgQ0EyMB4XDTE5MTIxOTAzMTgyMVoXDTQ0MTIxMjAzMTgyMVowVDELMAkGA1UE +BhMCQ04xJjAkBgNVBAoMHUJFSUpJTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZMR0wGwYDVQQDDBRC +SkNBIEdsb2JhbCBSb290IENBMjB2MBAGByqGSM49AgEGBSuBBAAiA2IABJ3LgJGNU2e1uVCxA/jl +SR9BIgmwUVJY1is0j8USRhTFiy8shP8sbqjV8QnjAyEUxEM9fMEsxEtqSs3ph+B99iK++kpRuDCK +/eHeGBIK9ke35xe/J4rUQUyWPGCWwf0VHKNCMEAwHQYDVR0OBBYEFNJKsVF/BvDRgh9Obl+rg/xI +1LCRMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2gAMGUCMBq8 +W9f+qdJUDkpd0m2xQNz0Q9XSSpkZElaA94M04TVOSG0ED1cxMDAtsaqdAzjbBgIxAMvMh1PLet8g +UXOQwKhbYdDFUDn9hf7B43j4ptZLvZuHjw/l1lOWqzzIQNph91Oj9w== +-----END CERTIFICATE----- + +Sectigo Public Server Authentication Root E46 +============================================= +-----BEGIN CERTIFICATE----- +MIICOjCCAcGgAwIBAgIQQvLM2htpN0RfFf51KBC49DAKBggqhkjOPQQDAzBfMQswCQYDVQQGEwJH +QjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1TZWN0aWdvIFB1YmxpYyBTZXJ2 +ZXIgQXV0aGVudGljYXRpb24gUm9vdCBFNDYwHhcNMjEwMzIyMDAwMDAwWhcNNDYwMzIxMjM1OTU5 +WjBfMQswCQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1TZWN0 +aWdvIFB1YmxpYyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBFNDYwdjAQBgcqhkjOPQIBBgUr +gQQAIgNiAAR2+pmpbiDt+dd34wc7qNs9Xzjoq1WmVk/WSOrsfy2qw7LFeeyZYX8QeccCWvkEN/U0 +NSt3zn8gj1KjAIns1aeibVvjS5KToID1AZTc8GgHHs3u/iVStSBDHBv+6xnOQ6OjQjBAMB0GA1Ud +DgQWBBTRItpMWfFLXyY4qp3W7usNw/upYTAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB +/zAKBggqhkjOPQQDAwNnADBkAjAn7qRaqCG76UeXlImldCBteU/IvZNeWBj7LRoAasm4PdCkT0RH +lAFWovgzJQxC36oCMB3q4S6ILuH5px0CMk7yn2xVdOOurvulGu7t0vzCAxHrRVxgED1cf5kDW21U +SAGKcw== +-----END CERTIFICATE----- + +Sectigo Public Server Authentication Root R46 +============================================= +-----BEGIN CERTIFICATE----- +MIIFijCCA3KgAwIBAgIQdY39i658BwD6qSWn4cetFDANBgkqhkiG9w0BAQwFADBfMQswCQYDVQQG +EwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1TZWN0aWdvIFB1YmxpYyBT +ZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBSNDYwHhcNMjEwMzIyMDAwMDAwWhcNNDYwMzIxMjM1 +OTU5WjBfMQswCQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1T +ZWN0aWdvIFB1YmxpYyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBSNDYwggIiMA0GCSqGSIb3 +DQEBAQUAA4ICDwAwggIKAoICAQCTvtU2UnXYASOgHEdCSe5jtrch/cSV1UgrJnwUUxDaef0rty2k +1Cz66jLdScK5vQ9IPXtamFSvnl0xdE8H/FAh3aTPaE8bEmNtJZlMKpnzSDBh+oF8HqcIStw+Kxwf +GExxqjWMrfhu6DtK2eWUAtaJhBOqbchPM8xQljeSM9xfiOefVNlI8JhD1mb9nxc4Q8UBUQvX4yMP +FF1bFOdLvt30yNoDN9HWOaEhUTCDsG3XME6WW5HwcCSrv0WBZEMNvSE6Lzzpng3LILVCJ8zab5vu +ZDCQOc2TZYEhMbUjUDM3IuM47fgxMMxF/mL50V0yeUKH32rMVhlATc6qu/m1dkmU8Sf4kaWD5Qaz +Yw6A3OASVYCmO2a0OYctyPDQ0RTp5A1NDvZdV3LFOxxHVp3i1fuBYYzMTYCQNFu31xR13NgESJ/A +wSiItOkcyqex8Va3e0lMWeUgFaiEAin6OJRpmkkGj80feRQXEgyDet4fsZfu+Zd4KKTIRJLpfSYF +plhym3kT2BFfrsU4YjRosoYwjviQYZ4ybPUHNs2iTG7sijbt8uaZFURww3y8nDnAtOFr94MlI1fZ +EoDlSfB1D++N6xybVCi0ITz8fAr/73trdf+LHaAZBav6+CuBQug4urv7qv094PPK306Xlynt8xhW +6aWWrL3DkJiy4Pmi1KZHQ3xtzwIDAQABo0IwQDAdBgNVHQ4EFgQUVnNYZJX5khqwEioEYnmhQBWI +IUkwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAC9c +mTz8Bl6MlC5w6tIyMY208FHVvArzZJ8HXtXBc2hkeqK5Duj5XYUtqDdFqij0lgVQYKlJfp/imTYp +E0RHap1VIDzYm/EDMrraQKFz6oOht0SmDpkBm+S8f74TlH7Kph52gDY9hAaLMyZlbcp+nv4fjFg4 +exqDsQ+8FxG75gbMY/qB8oFM2gsQa6H61SilzwZAFv97fRheORKkU55+MkIQpiGRqRxOF3yEvJ+M +0ejf5lG5Nkc/kLnHvALcWxxPDkjBJYOcCj+esQMzEhonrPcibCTRAUH4WAP+JWgiH5paPHxsnnVI +84HxZmduTILA7rpXDhjvLpr3Etiga+kFpaHpaPi8TD8SHkXoUsCjvxInebnMMTzD9joiFgOgyY9m +pFuiTdaBJQbpdqQACj7LzTWb4OE4y2BThihCQRxEV+ioratF4yUQvNs+ZUH7G6aXD+u5dHn5Hrwd +Vw1Hr8Mvn4dGp+smWg9WY7ViYG4A++MnESLn/pmPNPW56MORcr3Ywx65LvKRRFHQV80MNNVIIb/b +E/FmJUNS0nAiNs2fxBx1IK1jcmMGDw4nztJqDby1ORrp0XZ60Vzk50lJLVU3aPAaOpg+VBeHVOmm +J1CJeyAvP/+/oYtKR5j/K3tJPsMpRmAYQqszKbrAKbkTidOIijlBO8n9pu0f9GBj39ItVQGL +-----END CERTIFICATE----- + +SSL.com TLS RSA Root CA 2022 +============================ +-----BEGIN CERTIFICATE----- +MIIFiTCCA3GgAwIBAgIQb77arXO9CEDii02+1PdbkTANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQG +EwJVUzEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMSUwIwYDVQQDDBxTU0wuY29tIFRMUyBSU0Eg +Um9vdCBDQSAyMDIyMB4XDTIyMDgyNTE2MzQyMloXDTQ2MDgxOTE2MzQyMVowTjELMAkGA1UEBhMC +VVMxGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjElMCMGA1UEAwwcU1NMLmNvbSBUTFMgUlNBIFJv +b3QgQ0EgMjAyMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANCkCXJPQIgSYT41I57u +9nTPL3tYPc48DRAokC+X94xI2KDYJbFMsBFMF3NQ0CJKY7uB0ylu1bUJPiYYf7ISf5OYt6/wNr/y +7hienDtSxUcZXXTzZGbVXcdotL8bHAajvI9AI7YexoS9UcQbOcGV0insS657Lb85/bRi3pZ7Qcac +oOAGcvvwB5cJOYF0r/c0WRFXCsJbwST0MXMwgsadugL3PnxEX4MN8/HdIGkWCVDi1FW24IBydm5M +R7d1VVm0U3TZlMZBrViKMWYPHqIbKUBOL9975hYsLfy/7PO0+r4Y9ptJ1O4Fbtk085zx7AGL0SDG +D6C1vBdOSHtRwvzpXGk3R2azaPgVKPC506QVzFpPulJwoxJF3ca6TvvC0PeoUidtbnm1jPx7jMEW +TO6Af77wdr5BUxIzrlo4QqvXDz5BjXYHMtWrifZOZ9mxQnUjbvPNQrL8VfVThxc7wDNY8VLS+YCk +8OjwO4s4zKTGkH8PnP2L0aPP2oOnaclQNtVcBdIKQXTbYxE3waWglksejBYSd66UNHsef8JmAOSq +g+qKkK3ONkRN0VHpvB/zagX9wHQfJRlAUW7qglFA35u5CCoGAtUjHBPW6dvbxrB6y3snm/vg1UYk +7RBLY0ulBY+6uB0rpvqR4pJSvezrZ5dtmi2fgTIFZzL7SAg/2SW4BCUvAgMBAAGjYzBhMA8GA1Ud +EwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAU+y437uOEeicuzRk1sTN8/9REQrkwHQYDVR0OBBYEFPsu +N+7jhHonLs0ZNbEzfP/UREK5MA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAjYlt +hEUY8U+zoO9opMAdrDC8Z2awms22qyIZZtM7QbUQnRC6cm4pJCAcAZli05bg4vsMQtfhWsSWTVTN +j8pDU/0quOr4ZcoBwq1gaAafORpR2eCNJvkLTqVTJXojpBzOCBvfR4iyrT7gJ4eLSYwfqUdYe5by +iB0YrrPRpgqU+tvT5TgKa3kSM/tKWTcWQA673vWJDPFs0/dRa1419dvAJuoSc06pkZCmF8NsLzjU +o3KUQyxi4U5cMj29TH0ZR6LDSeeWP4+a0zvkEdiLA9z2tmBVGKaBUfPhqBVq6+AL8BQx1rmMRTqo +ENjwuSfr98t67wVylrXEj5ZzxOhWc5y8aVFjvO9nHEMaX3cZHxj4HCUp+UmZKbaSPaKDN7Egkaib +MOlqbLQjk2UEqxHzDh1TJElTHaE/nUiSEeJ9DU/1172iWD54nR4fK/4huxoTtrEoZP2wAgDHbICi +vRZQIA9ygV/MlP+7mea6kMvq+cYMwq7FGc4zoWtcu358NFcXrfA/rs3qr5nsLFR+jM4uElZI7xc7 +P0peYNLcdDa8pUNjyw9bowJWCZ4kLOGGgYz+qxcs+sjiMho6/4UIyYOf8kpIEFR3N+2ivEC+5BB0 +9+Rbu7nzifmPQdjH5FCQNYA+HLhNkNPU98OwoX6EyneSMSy4kLGCenROmxMmtNVQZlR4rmA= +-----END CERTIFICATE----- + +SSL.com TLS ECC Root CA 2022 +============================ +-----BEGIN CERTIFICATE----- +MIICOjCCAcCgAwIBAgIQFAP1q/s3ixdAW+JDsqXRxDAKBggqhkjOPQQDAzBOMQswCQYDVQQGEwJV +UzEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMSUwIwYDVQQDDBxTU0wuY29tIFRMUyBFQ0MgUm9v +dCBDQSAyMDIyMB4XDTIyMDgyNTE2MzM0OFoXDTQ2MDgxOTE2MzM0N1owTjELMAkGA1UEBhMCVVMx +GDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjElMCMGA1UEAwwcU1NMLmNvbSBUTFMgRUNDIFJvb3Qg +Q0EgMjAyMjB2MBAGByqGSM49AgEGBSuBBAAiA2IABEUpNXP6wrgjzhR9qLFNoFs27iosU8NgCTWy +JGYmacCzldZdkkAZDsalE3D07xJRKF3nzL35PIXBz5SQySvOkkJYWWf9lCcQZIxPBLFNSeR7T5v1 +5wj4A4j3p8OSSxlUgaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBSJjy+j6CugFFR7 +81a4Jl9nOAuc0DAdBgNVHQ4EFgQUiY8vo+groBRUe/NWuCZfZzgLnNAwDgYDVR0PAQH/BAQDAgGG +MAoGCCqGSM49BAMDA2gAMGUCMFXjIlbp15IkWE8elDIPDAI2wv2sdDJO4fscgIijzPvX6yv/N33w +7deedWo1dlJF4AIxAMeNb0Igj762TVntd00pxCAgRWSGOlDGxK0tk/UYfXLtqc/ErFc2KAhl3zx5 +Zn6g6g== +-----END CERTIFICATE----- + +Atos TrustedRoot Root CA ECC TLS 2021 +===================================== +-----BEGIN CERTIFICATE----- +MIICFTCCAZugAwIBAgIQPZg7pmY9kGP3fiZXOATvADAKBggqhkjOPQQDAzBMMS4wLAYDVQQDDCVB +dG9zIFRydXN0ZWRSb290IFJvb3QgQ0EgRUNDIFRMUyAyMDIxMQ0wCwYDVQQKDARBdG9zMQswCQYD +VQQGEwJERTAeFw0yMTA0MjIwOTI2MjNaFw00MTA0MTcwOTI2MjJaMEwxLjAsBgNVBAMMJUF0b3Mg +VHJ1c3RlZFJvb3QgUm9vdCBDQSBFQ0MgVExTIDIwMjExDTALBgNVBAoMBEF0b3MxCzAJBgNVBAYT +AkRFMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEloZYKDcKZ9Cg3iQZGeHkBQcfl+3oZIK59sRxUM6K +DP/XtXa7oWyTbIOiaG6l2b4siJVBzV3dscqDY4PMwL502eCdpO5KTlbgmClBk1IQ1SQ4AjJn8ZQS +b+/Xxd4u/RmAo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR2KCXWfeBmmnoJsmo7jjPX +NtNPojAOBgNVHQ8BAf8EBAMCAYYwCgYIKoZIzj0EAwMDaAAwZQIwW5kp85wxtolrbNa9d+F851F+ +uDrNozZffPc8dz7kUK2o59JZDCaOMDtuCCrCp1rIAjEAmeMM56PDr9NJLkaCI2ZdyQAUEv049OGY +a3cpetskz2VAv9LcjBHo9H1/IISpQuQo +-----END CERTIFICATE----- + +Atos TrustedRoot Root CA RSA TLS 2021 +===================================== +-----BEGIN CERTIFICATE----- +MIIFZDCCA0ygAwIBAgIQU9XP5hmTC/srBRLYwiqipDANBgkqhkiG9w0BAQwFADBMMS4wLAYDVQQD +DCVBdG9zIFRydXN0ZWRSb290IFJvb3QgQ0EgUlNBIFRMUyAyMDIxMQ0wCwYDVQQKDARBdG9zMQsw +CQYDVQQGEwJERTAeFw0yMTA0MjIwOTIxMTBaFw00MTA0MTcwOTIxMDlaMEwxLjAsBgNVBAMMJUF0 +b3MgVHJ1c3RlZFJvb3QgUm9vdCBDQSBSU0EgVExTIDIwMjExDTALBgNVBAoMBEF0b3MxCzAJBgNV +BAYTAkRFMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAtoAOxHm9BYx9sKOdTSJNy/BB +l01Z4NH+VoyX8te9j2y3I49f1cTYQcvyAh5x5en2XssIKl4w8i1mx4QbZFc4nXUtVsYvYe+W/CBG +vevUez8/fEc4BKkbqlLfEzfTFRVOvV98r61jx3ncCHvVoOX3W3WsgFWZkmGbzSoXfduP9LVq6hdK +ZChmFSlsAvFr1bqjM9xaZ6cF4r9lthawEO3NUDPJcFDsGY6wx/J0W2tExn2WuZgIWWbeKQGb9Cpt +0xU6kGpn8bRrZtkh68rZYnxGEFzedUlnnkL5/nWpo63/dgpnQOPF943HhZpZnmKaau1Fh5hnstVK +PNe0OwANwI8f4UDErmwh3El+fsqyjW22v5MvoVw+j8rtgI5Y4dtXz4U2OLJxpAmMkokIiEjxQGMY +sluMWuPD0xeqqxmjLBvk1cbiZnrXghmmOxYsL3GHX0WelXOTwkKBIROW1527k2gV+p2kHYzygeBY +Br3JtuP2iV2J+axEoctr+hbxx1A9JNr3w+SH1VbxT5Aw+kUJWdo0zuATHAR8ANSbhqRAvNncTFd+ +rrcztl524WWLZt+NyteYr842mIycg5kDcPOvdO3GDjbnvezBc6eUWsuSZIKmAMFwoW4sKeFYV+xa +fJlrJaSQOoD0IJ2azsct+bJLKZWD6TWNp0lIpw9MGZHQ9b8Q4HECAwEAAaNCMEAwDwYDVR0TAQH/ +BAUwAwEB/zAdBgNVHQ4EFgQUdEmZ0f+0emhFdcN+tNzMzjkz2ggwDgYDVR0PAQH/BAQDAgGGMA0G +CSqGSIb3DQEBDAUAA4ICAQAjQ1MkYlxt/T7Cz1UAbMVWiLkO3TriJQ2VSpfKgInuKs1l+NsW4AmS +4BjHeJi78+xCUvuppILXTdiK/ORO/auQxDh1MoSf/7OwKwIzNsAQkG8dnK/haZPso0UvFJ/1TCpl +Q3IM98P4lYsU84UgYt1UU90s3BiVaU+DR3BAM1h3Egyi61IxHkzJqM7F78PRreBrAwA0JrRUITWX +AdxfG/F851X6LWh3e9NpzNMOa7pNdkTWwhWaJuywxfW70Xp0wmzNxbVe9kzmWy2B27O3Opee7c9G +slA9hGCZcbUztVdF5kJHdWoOsAgMrr3e97sPWD2PAzHoPYJQyi9eDF20l74gNAf0xBLh7tew2Vkt +afcxBPTy+av5EzH4AXcOPUIjJsyacmdRIXrMPIWo6iFqO9taPKU0nprALN+AnCng33eU0aKAQv9q +TFsR0PXNor6uzFFcw9VUewyu1rkGd4Di7wcaaMxZUa1+XGdrudviB0JbuAEFWDlN5LuYo7Ey7Nmj +1m+UI/87tyll5gfp77YZ6ufCOB0yiJA8EytuzO+rdwY0d4RPcuSBhPm5dDTedk+SKlOxJTnbPP/l +PqYO5Wue/9vsL3SD3460s6neFE3/MaNFcyT6lSnMEpcEoji2jbDwN/zIIX8/syQbPYtuzE2wFg2W +HYMfRsCbvUOZ58SWLs5fyQ== +-----END CERTIFICATE----- + +TrustAsia Global Root CA G3 +=========================== +-----BEGIN CERTIFICATE----- +MIIFpTCCA42gAwIBAgIUZPYOZXdhaqs7tOqFhLuxibhxkw8wDQYJKoZIhvcNAQEMBQAwWjELMAkG +A1UEBhMCQ04xJTAjBgNVBAoMHFRydXN0QXNpYSBUZWNobm9sb2dpZXMsIEluYy4xJDAiBgNVBAMM +G1RydXN0QXNpYSBHbG9iYWwgUm9vdCBDQSBHMzAeFw0yMTA1MjAwMjEwMTlaFw00NjA1MTkwMjEw +MTlaMFoxCzAJBgNVBAYTAkNOMSUwIwYDVQQKDBxUcnVzdEFzaWEgVGVjaG5vbG9naWVzLCBJbmMu +MSQwIgYDVQQDDBtUcnVzdEFzaWEgR2xvYmFsIFJvb3QgQ0EgRzMwggIiMA0GCSqGSIb3DQEBAQUA +A4ICDwAwggIKAoICAQDAMYJhkuSUGwoqZdC+BqmHO1ES6nBBruL7dOoKjbmzTNyPtxNST1QY4Sxz +lZHFZjtqz6xjbYdT8PfxObegQ2OwxANdV6nnRM7EoYNl9lA+sX4WuDqKAtCWHwDNBSHvBm3dIZwZ +Q0WhxeiAysKtQGIXBsaqvPPW5vxQfmZCHzyLpnl5hkA1nyDvP+uLRx+PjsXUjrYsyUQE49RDdT/V +P68czH5GX6zfZBCK70bwkPAPLfSIC7Epqq+FqklYqL9joDiR5rPmd2jE+SoZhLsO4fWvieylL1Ag +dB4SQXMeJNnKziyhWTXAyB1GJ2Faj/lN03J5Zh6fFZAhLf3ti1ZwA0pJPn9pMRJpxx5cynoTi+jm +9WAPzJMshH/x/Gr8m0ed262IPfN2dTPXS6TIi/n1Q1hPy8gDVI+lhXgEGvNz8teHHUGf59gXzhqc +D0r83ERoVGjiQTz+LISGNzzNPy+i2+f3VANfWdP3kXjHi3dqFuVJhZBFcnAvkV34PmVACxmZySYg +WmjBNb9Pp1Hx2BErW+Canig7CjoKH8GB5S7wprlppYiU5msTf9FkPz2ccEblooV7WIQn3MSAPmea +mseaMQ4w7OYXQJXZRe0Blqq/DPNL0WP3E1jAuPP6Z92bfW1K/zJMtSU7/xxnD4UiWQWRkUF3gdCF +TIcQcf+eQxuulXUtgQIDAQABo2MwYTAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFEDk5PIj +7zjKsK5Xf/IhMBY027ySMB0GA1UdDgQWBBRA5OTyI+84yrCuV3/yITAWNNu8kjAOBgNVHQ8BAf8E +BAMCAQYwDQYJKoZIhvcNAQEMBQADggIBACY7UeFNOPMyGLS0XuFlXsSUT9SnYaP4wM8zAQLpw6o1 +D/GUE3d3NZ4tVlFEbuHGLige/9rsR82XRBf34EzC4Xx8MnpmyFq2XFNFV1pF1AWZLy4jVe5jaN/T +G3inEpQGAHUNcoTpLrxaatXeL1nHo+zSh2bbt1S1JKv0Q3jbSwTEb93mPmY+KfJLaHEih6D4sTNj +duMNhXJEIlU/HHzp/LgV6FL6qj6jITk1dImmasI5+njPtqzn59ZW/yOSLlALqbUHM/Q4X6RJpstl +cHboCoWASzY9M/eVVHUl2qzEc4Jl6VL1XP04lQJqaTDFHApXB64ipCz5xUG3uOyfT0gA+QEEVcys ++TIxxHWVBqB/0Y0n3bOppHKH/lmLmnp0Ft0WpWIp6zqW3IunaFnT63eROfjXy9mPX1onAX1daBli +2MjN9LdyR75bl87yraKZk62Uy5P2EgmVtqvXO9A/EcswFi55gORngS1d7XB4tmBZrOFdRWOPyN9y +aFvqHbgB8X7754qz41SgOAngPN5C8sLtLpvzHzW2NtjjgKGLzZlkD8Kqq7HK9W+eQ42EVJmzbsAS +ZthwEPEGNTNDqJwuuhQxzhB/HIbjj9LV+Hfsm6vxL2PZQl/gZ4FkkfGXL/xuJvYz+NO1+MRiqzFR +JQJ6+N1rZdVtTTDIZbpoFGWsJwt0ivKH +-----END CERTIFICATE----- + +TrustAsia Global Root CA G4 +=========================== +-----BEGIN CERTIFICATE----- +MIICVTCCAdygAwIBAgIUTyNkuI6XY57GU4HBdk7LKnQV1tcwCgYIKoZIzj0EAwMwWjELMAkGA1UE +BhMCQ04xJTAjBgNVBAoMHFRydXN0QXNpYSBUZWNobm9sb2dpZXMsIEluYy4xJDAiBgNVBAMMG1Ry +dXN0QXNpYSBHbG9iYWwgUm9vdCBDQSBHNDAeFw0yMTA1MjAwMjEwMjJaFw00NjA1MTkwMjEwMjJa +MFoxCzAJBgNVBAYTAkNOMSUwIwYDVQQKDBxUcnVzdEFzaWEgVGVjaG5vbG9naWVzLCBJbmMuMSQw +IgYDVQQDDBtUcnVzdEFzaWEgR2xvYmFsIFJvb3QgQ0EgRzQwdjAQBgcqhkjOPQIBBgUrgQQAIgNi +AATxs8045CVD5d4ZCbuBeaIVXxVjAd7Cq92zphtnS4CDr5nLrBfbK5bKfFJV4hrhPVbwLxYI+hW8 +m7tH5j/uqOFMjPXTNvk4XatwmkcN4oFBButJ+bAp3TPsUKV/eSm4IJijYzBhMA8GA1UdEwEB/wQF +MAMBAf8wHwYDVR0jBBgwFoAUpbtKl86zK3+kMd6Xg1mDpm9xy94wHQYDVR0OBBYEFKW7SpfOsyt/ +pDHel4NZg6ZvccveMA4GA1UdDwEB/wQEAwIBBjAKBggqhkjOPQQDAwNnADBkAjBe8usGzEkxn0AA +bbd+NvBNEU/zy4k6LHiRUKNbwMp1JvK/kF0LgoxgKJ/GcJpo5PECMFxYDlZ2z1jD1xCMuo6u47xk +dUfFVZDj/bpV6wfEU6s3qe4hsiFbYI89MvHVI5TWWA== +-----END CERTIFICATE----- + +Telekom Security TLS ECC Root 2020 +================================== +-----BEGIN CERTIFICATE----- +MIICQjCCAcmgAwIBAgIQNjqWjMlcsljN0AFdxeVXADAKBggqhkjOPQQDAzBjMQswCQYDVQQGEwJE +RTEnMCUGA1UECgweRGV1dHNjaGUgVGVsZWtvbSBTZWN1cml0eSBHbWJIMSswKQYDVQQDDCJUZWxl +a29tIFNlY3VyaXR5IFRMUyBFQ0MgUm9vdCAyMDIwMB4XDTIwMDgyNTA3NDgyMFoXDTQ1MDgyNTIz +NTk1OVowYzELMAkGA1UEBhMCREUxJzAlBgNVBAoMHkRldXRzY2hlIFRlbGVrb20gU2VjdXJpdHkg +R21iSDErMCkGA1UEAwwiVGVsZWtvbSBTZWN1cml0eSBUTFMgRUNDIFJvb3QgMjAyMDB2MBAGByqG +SM49AgEGBSuBBAAiA2IABM6//leov9Wq9xCazbzREaK9Z0LMkOsVGJDZos0MKiXrPk/OtdKPD/M1 +2kOLAoC+b1EkHQ9rK8qfwm9QMuU3ILYg/4gND21Ju9sGpIeQkpT0CdDPf8iAC8GXs7s1J8nCG6NC +MEAwHQYDVR0OBBYEFONyzG6VmUex5rNhTNHLq+O6zd6fMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0P +AQH/BAQDAgEGMAoGCCqGSM49BAMDA2cAMGQCMHVSi7ekEE+uShCLsoRbQuHmKjYC2qBuGT8lv9pZ +Mo7k+5Dck2TOrbRBR2Diz6fLHgIwN0GMZt9Ba9aDAEH9L1r3ULRn0SyocddDypwnJJGDSA3PzfdU +ga/sf+Rn27iQ7t0l +-----END CERTIFICATE----- + +Telekom Security TLS RSA Root 2023 +================================== +-----BEGIN CERTIFICATE----- +MIIFszCCA5ugAwIBAgIQIZxULej27HF3+k7ow3BXlzANBgkqhkiG9w0BAQwFADBjMQswCQYDVQQG +EwJERTEnMCUGA1UECgweRGV1dHNjaGUgVGVsZWtvbSBTZWN1cml0eSBHbWJIMSswKQYDVQQDDCJU +ZWxla29tIFNlY3VyaXR5IFRMUyBSU0EgUm9vdCAyMDIzMB4XDTIzMDMyODEyMTY0NVoXDTQ4MDMy +NzIzNTk1OVowYzELMAkGA1UEBhMCREUxJzAlBgNVBAoMHkRldXRzY2hlIFRlbGVrb20gU2VjdXJp +dHkgR21iSDErMCkGA1UEAwwiVGVsZWtvbSBTZWN1cml0eSBUTFMgUlNBIFJvb3QgMjAyMzCCAiIw +DQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAO01oYGA88tKaVvC+1GDrib94W7zgRJ9cUD/h3VC +KSHtgVIs3xLBGYSJwb3FKNXVS2xE1kzbB5ZKVXrKNoIENqil/Cf2SfHVcp6R+SPWcHu79ZvB7JPP +GeplfohwoHP89v+1VmLhc2o0mD6CuKyVU/QBoCcHcqMAU6DksquDOFczJZSfvkgdmOGjup5czQRx +UX11eKvzWarE4GC+j4NSuHUaQTXtvPM6Y+mpFEXX5lLRbtLevOP1Czvm4MS9Q2QTps70mDdsipWo +l8hHD/BeEIvnHRz+sTugBTNoBUGCwQMrAcjnj02r6LX2zWtEtefdi+zqJbQAIldNsLGyMcEWzv/9 +FIS3R/qy8XDe24tsNlikfLMR0cN3f1+2JeANxdKz+bi4d9s3cXFH42AYTyS2dTd4uaNir73Jco4v +zLuu2+QVUhkHM/tqty1LkCiCc/4YizWN26cEar7qwU02OxY2kTLvtkCJkUPg8qKrBC7m8kwOFjQg +rIfBLX7JZkcXFBGk8/ehJImr2BrIoVyxo/eMbcgByU/J7MT8rFEz0ciD0cmfHdRHNCk+y7AO+oML +KFjlKdw/fKifybYKu6boRhYPluV75Gp6SG12mAWl3G0eQh5C2hrgUve1g8Aae3g1LDj1H/1Joy7S +WWO/gLCMk3PLNaaZlSJhZQNg+y+TS/qanIA7AgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIBBjAdBgNV +HQ4EFgQUtqeXgj10hZv3PJ+TmpV5dVKMbUcwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBS2 +p5eCPXSFm/c8n5OalXl1UoxtRzANBgkqhkiG9w0BAQwFAAOCAgEAqMxhpr51nhVQpGv7qHBFfLp+ +sVr8WyP6Cnf4mHGCDG3gXkaqk/QeoMPhk9tLrbKmXauw1GLLXrtm9S3ul0A8Yute1hTWjOKWi0Fp +kzXmuZlrYrShF2Y0pmtjxrlO8iLpWA1WQdH6DErwM807u20hOq6OcrXDSvvpfeWxm4bu4uB9tPcy +/SKE8YXJN3nptT+/XOR0so8RYgDdGGah2XsjX/GO1WfoVNpbOms2b/mBsTNHM3dA+VKq3dSDz4V4 +mZqTuXNnQkYRIer+CqkbGmVps4+uFrb2S1ayLfmlyOw7YqPta9BO1UAJpB+Y1zqlklkg5LB9zVtz +aL1txKITDmcZuI1CfmwMmm6gJC3VRRvcxAIU/oVbZZfKTpBQCHpCNfnqwmbU+AGuHrS+w6jv/naa +oqYfRvaE7fzbzsQCzndILIyy7MMAo+wsVRjBfhnu4S/yrYObnqsZ38aKL4x35bcF7DvB7L6Gs4a8 +wPfc5+pbrrLMtTWGS9DiP7bY+A4A7l3j941Y/8+LN+ljX273CXE2whJdV/LItM3z7gLfEdxquVeE +HVlNjM7IDiPCtyaaEBRx/pOyiriA8A4QntOoUAw3gi/q4Iqd4Sw5/7W0cwDk90imc6y/st53BIe0 +o82bNSQ3+pCTE4FCxpgmdTdmQRCsu/WU48IxK63nI1bMNSWSs1A= +-----END CERTIFICATE----- + +TWCA CYBER Root CA +================== +-----BEGIN CERTIFICATE----- +MIIFjTCCA3WgAwIBAgIQQAE0jMIAAAAAAAAAATzyxjANBgkqhkiG9w0BAQwFADBQMQswCQYDVQQG +EwJUVzESMBAGA1UEChMJVEFJV0FOLUNBMRAwDgYDVQQLEwdSb290IENBMRswGQYDVQQDExJUV0NB +IENZQkVSIFJvb3QgQ0EwHhcNMjIxMTIyMDY1NDI5WhcNNDcxMTIyMTU1OTU5WjBQMQswCQYDVQQG +EwJUVzESMBAGA1UEChMJVEFJV0FOLUNBMRAwDgYDVQQLEwdSb290IENBMRswGQYDVQQDExJUV0NB +IENZQkVSIFJvb3QgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDG+Moe2Qkgfh1s +Ts6P40czRJzHyWmqOlt47nDSkvgEs1JSHWdyKKHfi12VCv7qze33Kc7wb3+szT3vsxxFavcokPFh +V8UMxKNQXd7UtcsZyoC5dc4pztKFIuwCY8xEMCDa6pFbVuYdHNWdZsc/34bKS1PE2Y2yHer43CdT +o0fhYcx9tbD47nORxc5zb87uEB8aBs/pJ2DFTxnk684iJkXXYJndzk834H/nY62wuFm40AZoNWDT +Nq5xQwTxaWV4fPMf88oon1oglWa0zbfuj3ikRRjpJi+NmykosaS3Om251Bw4ckVYsV7r8Cibt4LK +/c/WMw+f+5eesRycnupfXtuq3VTpMCEobY5583WSjCb+3MX2w7DfRFlDo7YDKPYIMKoNM+HvnKkH +IuNZW0CP2oi3aQiotyMuRAlZN1vH4xfyIutuOVLF3lSnmMlLIJXcRolftBL5hSmO68gnFSDAS9TM +fAxsNAwmmyYxpjyn9tnQS6Jk/zuZQXLB4HCX8SS7K8R0IrGsayIyJNN4KsDAoS/xUgXJP+92ZuJF +2A09rZXIx4kmyA+upwMu+8Ff+iDhcK2wZSA3M2Cw1a/XDBzCkHDXShi8fgGwsOsVHkQGzaRP6AzR +wyAQ4VRlnrZR0Bp2a0JaWHY06rc3Ga4udfmW5cFZ95RXKSWNOkyrTZpB0F8mAwIDAQABo2MwYTAO +BgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBSdhWEUfMFib5do5E83 +QOGt4A1WNzAdBgNVHQ4EFgQUnYVhFHzBYm+XaORPN0DhreANVjcwDQYJKoZIhvcNAQEMBQADggIB +AGSPesRiDrWIzLjHhg6hShbNcAu3p4ULs3a2D6f/CIsLJc+o1IN1KriWiLb73y0ttGlTITVX1olN +c79pj3CjYcya2x6a4CD4bLubIp1dhDGaLIrdaqHXKGnK/nZVekZn68xDiBaiA9a5F/gZbG0jAn/x +X9AKKSM70aoK7akXJlQKTcKlTfjF/biBzysseKNnTKkHmvPfXvt89YnNdJdhEGoHK4Fa0o635yDR +IG4kqIQnoVesqlVYL9zZyvpoBJ7tRCT5dEA7IzOrg1oYJkK2bVS1FmAwbLGg+LhBoF1JSdJlBTrq +/p1hvIbZv97Tujqxf36SNI7JAG7cmL3c7IAFrQI932XtCwP39xaEBDG6k5TY8hL4iuO/Qq+n1M0R +FxbIQh0UqEL20kCGoE8jypZFVmAGzbdVAaYBlGX+bgUJurSkquLvWL69J1bY73NxW0Qz8ppy6rBe +Pm6pUlvscG21h483XjyMnM7k8M4MZ0HMzvaAq07MTFb1wWFZk7Q+ptq4NxKfKjLji7gh7MMrZQzv +It6IKTtM1/r+t+FHvpw+PoP7UV31aPcuIYXcv/Fa4nzXxeSDwWrruoBa3lwtcHb4yOWHh8qgnaHl +IhInD0Q9HWzq1MKLL295q39QpsQZp6F6t5b5wR9iWqJDB0BeJsas7a5wFsWqynKKTbDPAYsDP27X +-----END CERTIFICATE----- + +SecureSign Root CA12 +==================== +-----BEGIN CERTIFICATE----- +MIIDcjCCAlqgAwIBAgIUZvnHwa/swlG07VOX5uaCwysckBYwDQYJKoZIhvcNAQELBQAwUTELMAkG +A1UEBhMCSlAxIzAhBgNVBAoTGkN5YmVydHJ1c3QgSmFwYW4gQ28uLCBMdGQuMR0wGwYDVQQDExRT +ZWN1cmVTaWduIFJvb3QgQ0ExMjAeFw0yMDA0MDgwNTM2NDZaFw00MDA0MDgwNTM2NDZaMFExCzAJ +BgNVBAYTAkpQMSMwIQYDVQQKExpDeWJlcnRydXN0IEphcGFuIENvLiwgTHRkLjEdMBsGA1UEAxMU +U2VjdXJlU2lnbiBSb290IENBMTIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC6OcE3 +emhFKxS06+QT61d1I02PJC0W6K6OyX2kVzsqdiUzg2zqMoqUm048luT9Ub+ZyZN+v/mtp7JIKwcc +J/VMvHASd6SFVLX9kHrko+RRWAPNEHl57muTH2SOa2SroxPjcf59q5zdJ1M3s6oYwlkm7Fsf0uZl +fO+TvdhYXAvA42VvPMfKWeP+bl+sg779XSVOKik71gurFzJ4pOE+lEa+Ym6b3kaosRbnhW70CEBF +EaCeVESE99g2zvVQR9wsMJvuwPWW0v4JhscGWa5Pro4RmHvzC1KqYiaqId+OJTN5lxZJjfU+1Uef +NzFJM3IFTQy2VYzxV4+Kh9GtxRESOaCtAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0P +AQH/BAQDAgEGMB0GA1UdDgQWBBRXNPN0zwRL1SXm8UC2LEzZLemgrTANBgkqhkiG9w0BAQsFAAOC +AQEAPrvbFxbS8hQBICw4g0utvsqFepq2m2um4fylOqyttCg6r9cBg0krY6LdmmQOmFxv3Y67ilQi +LUoT865AQ9tPkbeGGuwAtEGBpE/6aouIs3YIcipJQMPTw4WJmBClnW8Zt7vPemVV2zfrPIpyMpce +mik+rY3moxtt9XUa5rBouVui7mlHJzWhhpmA8zNL4WukJsPvdFlseqJkth5Ew1DgDzk9qTPxpfPS +vWKErI4cqc1avTc7bgoitPQV55FYxTpE05Uo2cBl6XLK0A+9H7MV2anjpEcJnuDLN/v9vZfVvhga +aaI5gdka9at/yOPiZwud9AzqVN/Ssq+xIvEg37xEHA== +-----END CERTIFICATE----- + +SecureSign Root CA14 +==================== +-----BEGIN CERTIFICATE----- +MIIFcjCCA1qgAwIBAgIUZNtaDCBO6Ncpd8hQJ6JaJ90t8sswDQYJKoZIhvcNAQEMBQAwUTELMAkG +A1UEBhMCSlAxIzAhBgNVBAoTGkN5YmVydHJ1c3QgSmFwYW4gQ28uLCBMdGQuMR0wGwYDVQQDExRT +ZWN1cmVTaWduIFJvb3QgQ0ExNDAeFw0yMDA0MDgwNzA2MTlaFw00NTA0MDgwNzA2MTlaMFExCzAJ +BgNVBAYTAkpQMSMwIQYDVQQKExpDeWJlcnRydXN0IEphcGFuIENvLiwgTHRkLjEdMBsGA1UEAxMU +U2VjdXJlU2lnbiBSb290IENBMTQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDF0nqh +1oq/FjHQmNE6lPxauG4iwWL3pwon71D2LrGeaBLwbCRjOfHw3xDG3rdSINVSW0KZnvOgvlIfX8xn +bacuUKLBl422+JX1sLrcneC+y9/3OPJH9aaakpUqYllQC6KxNedlsmGy6pJxaeQp8E+BgQQ8sqVb +1MWoWWd7VRxJq3qdwudzTe/NCcLEVxLbAQ4jeQkHO6Lo/IrPj8BGJJw4J+CDnRugv3gVEOuGTgpa +/d/aLIJ+7sr2KeH6caH3iGicnPCNvg9JkdjqOvn90Ghx2+m1K06Ckm9mH+Dw3EzsytHqunQG+bOE +kJTRX45zGRBdAuVwpcAQ0BB8b8VYSbSwbprafZX1zNoCr7gsfXmPvkPx+SgojQlD+Ajda8iLLCSx +jVIHvXiby8posqTdDEx5YMaZ0ZPxMBoH064iwurO8YQJzOAUbn8/ftKChazcqRZOhaBgy/ac18iz +ju3Gm5h1DVXoX+WViwKkrkMpKBGk5hIwAUt1ax5mnXkvpXYvHUC0bcl9eQjs0Wq2XSqypWa9a4X0 +dFbD9ed1Uigspf9mR6XU/v6eVL9lfgHWMI+lNpyiUBzuOIABSMbHdPTGrMNASRZhdCyvjG817XsY +AFs2PJxQDcqSMxDxJklt33UkN4Ii1+iW/RVLApY+B3KVfqs9TC7XyvDf4Fg/LS8EmjijAQIDAQAB +o0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUBpOjCl4oaTeq +YR3r6/wtbyPk86AwDQYJKoZIhvcNAQEMBQADggIBAJaAcgkGfpzMkwQWu6A6jZJOtxEaCnFxEM0E +rX+lRVAQZk5KQaID2RFPeje5S+LGjzJmdSX7684/AykmjbgWHfYfM25I5uj4V7Ibed87hwriZLoA +ymzvftAj63iP/2SbNDefNWWipAA9EiOWWF3KY4fGoweITedpdopTzfFP7ELyk+OZpDc8h7hi2/Ds +Hzc/N19DzFGdtfCXwreFamgLRB7lUe6TzktuhsHSDCRZNhqfLJGP4xjblJUK7ZGqDpncllPjYYPG +FrojutzdfhrGe0K22VoF3Jpf1d+42kd92jjbrDnVHmtsKheMYc2xbXIBw8MgAGJoFjHVdqqGuw6q +nsb58Nn4DSEC5MUoFlkRudlpcyqSeLiSV5sI8jrlL5WwWLdrIBRtFO8KvH7YVdiI2i/6GaX7i+B/ +OfVyK4XELKzvGUWSTLNhB9xNH27SgRNcmvMSZ4PPmz+Ln52kuaiWA3rF7iDeM9ovnhp6dB7h7sxa +OgTdsxoEqBRjrLdHEoOabPXm6RUVkRqEGQ6UROcSjiVbgGcZ3GOTEAtlLor6CZpO2oYofaphNdgO +pygau1LgePhsumywbrmHXumZNTfxPWQrqaA0k89jL9WB365jJ6UeTo3cKXhZ+PmhIIynJkBugnLN +eLLIjzwec+fBH7/PzqUqm9tEZDKgu39cJRNItX+S +-----END CERTIFICATE----- + +SecureSign Root CA15 +==================== +-----BEGIN CERTIFICATE----- +MIICIzCCAamgAwIBAgIUFhXHw9hJp75pDIqI7fBw+d23PocwCgYIKoZIzj0EAwMwUTELMAkGA1UE +BhMCSlAxIzAhBgNVBAoTGkN5YmVydHJ1c3QgSmFwYW4gQ28uLCBMdGQuMR0wGwYDVQQDExRTZWN1 +cmVTaWduIFJvb3QgQ0ExNTAeFw0yMDA0MDgwODMyNTZaFw00NTA0MDgwODMyNTZaMFExCzAJBgNV +BAYTAkpQMSMwIQYDVQQKExpDeWJlcnRydXN0IEphcGFuIENvLiwgTHRkLjEdMBsGA1UEAxMUU2Vj +dXJlU2lnbiBSb290IENBMTUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQLUHSNZDKZmbPSYAi4Io5G +dCx4wCtELW1fHcmuS1Iggz24FG1Th2CeX2yF2wYUleDHKP+dX+Sq8bOLbe1PL0vJSpSRZHX+AezB +2Ot6lHhWGENfa4HL9rzatAy2KZMIaY+jQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQD +AgEGMB0GA1UdDgQWBBTrQciu/NWeUUj1vYv0hyCTQSvT9DAKBggqhkjOPQQDAwNoADBlAjEA2S6J +fl5OpBEHvVnCB96rMjhTKkZEBhd6zlHp4P9mLQlO4E/0BdGF9jVg3PVys0Z9AjBEmEYagoUeYWmJ +SwdLZrWeqrqgHkHZAXQ6bkU6iYAZezKYVWOr62Nuk22rGwlgMU4= +-----END CERTIFICATE----- + +D-TRUST BR Root CA 2 2023 +========================= +-----BEGIN CERTIFICATE----- +MIIFqTCCA5GgAwIBAgIQczswBEhb2U14LnNLyaHcZjANBgkqhkiG9w0BAQ0FADBIMQswCQYDVQQG +EwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlELVRSVVNUIEJSIFJvb3QgQ0Eg +MiAyMDIzMB4XDTIzMDUwOTA4NTYzMVoXDTM4MDUwOTA4NTYzMFowSDELMAkGA1UEBhMCREUxFTAT +BgNVBAoTDEQtVHJ1c3QgR21iSDEiMCAGA1UEAxMZRC1UUlVTVCBCUiBSb290IENBIDIgMjAyMzCC +AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK7/CVmRgApKaOYkP7in5Mg6CjoWzckjYaCT +cfKri3OPoGdlYNJUa2NRb0kz4HIHE304zQaSBylSa053bATTlfrdTIzZXcFhfUvnKLNEgXtRr90z +sWh81k5M/itoucpmacTsXld/9w3HnDY25QdgrMBM6ghs7wZ8T1soegj8k12b9py0i4a6Ibn08OhZ +WiihNIQaJZG2tY/vsvmA+vk9PBFy2OMvhnbFeSzBqZCTRphny4NqoFAjpzv2gTng7fC5v2Xx2Mt6 +++9zA84A9H3X4F07ZrjcjrqDy4d2A/wl2ecjbwb9Z/Pg/4S8R7+1FhhGaRTMBffb00msa8yr5LUL +QyReS2tNZ9/WtT5PeB+UcSTq3nD88ZP+npNa5JRal1QMNXtfbO4AHyTsA7oC9Xb0n9Sa7YUsOCIv +x9gvdhFP/Wxc6PWOJ4d/GUohR5AdeY0cW/jPSoXk7bNbjb7EZChdQcRurDhaTyN0dKkSw/bSuREV +MweR2Ds3OmMwBtHFIjYoYiMQ4EbMl6zWK11kJNXuHA7e+whadSr2Y23OC0K+0bpwHJwh5Q8xaRfX +/Aq03u2AnMuStIv13lmiWAmlY0cL4UEyNEHZmrHZqLAbWt4NDfTisl01gLmB1IRpkQLLddCNxbU9 +CZEJjxShFHR5PtbJFR2kWVki3PaKRT08EtY+XTIvAgMBAAGjgY4wgYswDwYDVR0TAQH/BAUwAwEB +/zAdBgNVHQ4EFgQUZ5Dw1t61GNVGKX5cq/ieCLxklRAwDgYDVR0PAQH/BAQDAgEGMEkGA1UdHwRC +MEAwPqA8oDqGOGh0dHA6Ly9jcmwuZC10cnVzdC5uZXQvY3JsL2QtdHJ1c3RfYnJfcm9vdF9jYV8y +XzIwMjMuY3JsMA0GCSqGSIb3DQEBDQUAA4ICAQA097N3U9swFrktpSHxQCF16+tIFoE9c+CeJyrr +d6kTpGoKWloUMz1oH4Guaf2Mn2VsNELZLdB/eBaxOqwjMa1ef67nriv6uvw8l5VAk1/DLQOj7aRv +U9f6QA4w9QAgLABMjDu0ox+2v5Eyq6+SmNMW5tTRVFxDWy6u71cqqLRvpO8NVhTaIasgdp4D/Ca4 +nj8+AybmTNudX0KEPUUDAxxZiMrcLmEkWqTqJwtzEr5SswrPMhfiHocaFpVIbVrg0M8JkiZmkdij +YQ6qgYF/6FKC0ULn4B0Y+qSFNueG4A3rvNTJ1jxD8V1Jbn6Bm2m1iWKPiFLY1/4nwSPFyysCu7Ff +/vtDhQNGvl3GyiEm/9cCnnRK3PgTFbGBVzbLZVzRHTF36SXDw7IyN9XxmAnkbWOACKsGkoHU6XCP +pz+y7YaMgmo1yEJagtFSGkUPFaUA8JR7ZSdXOUPPfH/mvTWze/EZTN46ls/pdu4D58JDUjxqgejB +WoC9EV2Ta/vH5mQ/u2kc6d0li690yVRAysuTEwrt+2aSEcr1wPrYg1UDfNPFIkZ1cGt5SAYqgpq/ +5usWDiJFAbzdNpQ0qTUmiteXue4Icr80knCDgKs4qllo3UCkGJCy89UDyibK79XH4I9TjvAA46jt +n/mtd+ArY0+ew+43u3gJhJ65bvspmZDogNOfJA== +-----END CERTIFICATE----- + +TrustAsia TLS ECC Root CA +========================= +-----BEGIN CERTIFICATE----- +MIICMTCCAbegAwIBAgIUNnThTXxlE8msg1UloD5Sfi9QaMcwCgYIKoZIzj0EAwMwWDELMAkGA1UE +BhMCQ04xJTAjBgNVBAoTHFRydXN0QXNpYSBUZWNobm9sb2dpZXMsIEluYy4xIjAgBgNVBAMTGVRy +dXN0QXNpYSBUTFMgRUNDIFJvb3QgQ0EwHhcNMjQwNTE1MDU0MTU2WhcNNDQwNTE1MDU0MTU1WjBY +MQswCQYDVQQGEwJDTjElMCMGA1UEChMcVHJ1c3RBc2lhIFRlY2hub2xvZ2llcywgSW5jLjEiMCAG +A1UEAxMZVHJ1c3RBc2lhIFRMUyBFQ0MgUm9vdCBDQTB2MBAGByqGSM49AgEGBSuBBAAiA2IABLh/ +pVs/AT598IhtrimY4ZtcU5nb9wj/1WrgjstEpvDBjL1P1M7UiFPoXlfXTr4sP/MSpwDpguMqWzJ8 +S5sUKZ74LYO1644xST0mYekdcouJtgq7nDM1D9rs3qlKH8kzsaNCMEAwDwYDVR0TAQH/BAUwAwEB +/zAdBgNVHQ4EFgQULIVTu7FDzTLqnqOH/qKYqKaT6RAwDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49 +BAMDA2gAMGUCMFRH18MtYYZI9HlaVQ01L18N9mdsd0AaRuf4aFtOJx24mH1/k78ITcTaRTChD15K +eAIxAKORh/IRM4PDwYqROkwrULG9IpRdNYlzg8WbGf60oenUoWa2AaU2+dhoYSi3dOGiMQ== +-----END CERTIFICATE----- + +TrustAsia TLS RSA Root CA +========================= +-----BEGIN CERTIFICATE----- +MIIFgDCCA2igAwIBAgIUHBjYz+VTPyI1RlNUJDxsR9FcSpwwDQYJKoZIhvcNAQEMBQAwWDELMAkG +A1UEBhMCQ04xJTAjBgNVBAoTHFRydXN0QXNpYSBUZWNobm9sb2dpZXMsIEluYy4xIjAgBgNVBAMT +GVRydXN0QXNpYSBUTFMgUlNBIFJvb3QgQ0EwHhcNMjQwNTE1MDU0MTU3WhcNNDQwNTE1MDU0MTU2 +WjBYMQswCQYDVQQGEwJDTjElMCMGA1UEChMcVHJ1c3RBc2lhIFRlY2hub2xvZ2llcywgSW5jLjEi +MCAGA1UEAxMZVHJ1c3RBc2lhIFRMUyBSU0EgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIP +ADCCAgoCggIBAMMWuBtqpERz5dZO9LnPWwvB0ZqB9WOwj0PBuwhaGnrhB3YmH49pVr7+NmDQDIPN +lOrnxS1cLwUWAp4KqC/lYCZUlviYQB2srp10Zy9U+5RjmOMmSoPGlbYJQ1DNDX3eRA5gEk9bNb2/ +mThtfWza4mhzH/kxpRkQcwUqwzIZheo0qt1CHjCNP561HmHVb70AcnKtEj+qpklz8oYVlQwQX1Fk +zv93uMltrOXVmPGZLmzjyUT5tUMnCE32ft5EebuyjBza00tsLtbDeLdM1aTk2tyKjg7/D8OmYCYo +zza/+lcK7Fs/6TAWe8TbxNRkoDD75f0dcZLdKY9BWN4ArTr9PXwaqLEX8E40eFgl1oUh63kd0Nyr +z2I8sMeXi9bQn9P+PN7F4/w6g3CEIR0JwqH8uyghZVNgepBtljhb//HXeltt08lwSUq6HTrQUNoy +IBnkiz/r1RYmNzz7dZ6wB3C4FGB33PYPXFIKvF1tjVEK2sUYyJtt3LCDs3+jTnhMmCWr8n4uIF6C +FabW2I+s5c0yhsj55NqJ4js+k8UTav/H9xj8Z7XvGCxUq0DTbE3txci3OE9kxJRMT6DNrqXGJyV1 +J23G2pyOsAWZ1SgRxSHUuPzHlqtKZFlhaxP8S8ySpg+kUb8OWJDZgoM5pl+z+m6Ss80zDoWo8SnT +q1mt1tve1CuBAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFLgHkXlcBvRG/XtZ +ylomkadFK/hTMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQwFAAOCAgEAIZtqBSBdGBanEqT3 +Rz/NyjuujsCCztxIJXgXbODgcMTWltnZ9r96nBO7U5WS/8+S4PPFJzVXqDuiGev4iqME3mmL5Dw8 +veWv0BIb5Ylrc5tvJQJLkIKvQMKtuppgJFqBTQUYo+IzeXoLH5Pt7DlK9RME7I10nYEKqG/odv6L +TytpEoYKNDbdgptvT+Bz3Ul/KD7JO6NXBNiT2Twp2xIQaOHEibgGIOcberyxk2GaGUARtWqFVwHx +tlotJnMnlvm5P1vQiJ3koP26TpUJg3933FEFlJ0gcXax7PqJtZwuhfG5WyRasQmr2soaB82G39tp +27RIGAAtvKLEiUUjpQ7hRGU+isFqMB3iYPg6qocJQrmBktwliJiJ8Xw18WLK7nn4GS/+X/jbh87q +qA8MpugLoDzga5SYnH+tBuYc6kIQX+ImFTw3OffXvO645e8D7r0i+yiGNFjEWn9hongPXvPKnbwb +PKfILfanIhHKA9jnZwqKDss1jjQ52MjqjZ9k4DewbNfFj8GQYSbbJIweSsCI3zWQzj8C9GRh3sfI +B5XeMhg6j6JCQCTl1jNdfK7vsU1P1FeQNWrcrgSXSYk0ly4wBOeY99sLAZDBHwo/+ML+TvrbmnNz +FrwFuHnYWa8G5z9nODmxfKuU4CkUpijy323imttUQ/hHWKNddBWcwauwxzQ= +-----END CERTIFICATE----- + +D-TRUST EV Root CA 2 2023 +========================= +-----BEGIN CERTIFICATE----- +MIIFqTCCA5GgAwIBAgIQaSYJfoBLTKCnjHhiU19abzANBgkqhkiG9w0BAQ0FADBIMQswCQYDVQQG +EwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlELVRSVVNUIEVWIFJvb3QgQ0Eg +MiAyMDIzMB4XDTIzMDUwOTA5MTAzM1oXDTM4MDUwOTA5MTAzMlowSDELMAkGA1UEBhMCREUxFTAT +BgNVBAoTDEQtVHJ1c3QgR21iSDEiMCAGA1UEAxMZRC1UUlVTVCBFViBSb290IENBIDIgMjAyMzCC +AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANiOo4mAC7JXUtypU0w3uX9jFxPvp1sjW2l1 +sJkKF8GLxNuo4MwxusLyzV3pt/gdr2rElYfXR8mV2IIEUD2BCP/kPbOx1sWy/YgJ25yE7CUXFId/ +MHibaljJtnMoPDT3mfd/06b4HEV8rSyMlD/YZxBTfiLNTiVR8CUkNRFeEMbsh2aJgWi6zCudR3Mf +vc2RpHJqnKIbGKBv7FD0fUDCqDDPvXPIEysQEx6Lmqg6lHPTGGkKSv/BAQP/eX+1SH977ugpbzZM +lWGG2Pmic4ruri+W7mjNPU0oQvlFKzIbRlUWaqZLKfm7lVa/Rh3sHZMdwGWyH6FDrlaeoLGPaxK3 +YG14C8qKXO0elg6DpkiVjTujIcSuWMYAsoS0I6SWhjW42J7YrDRJmGOVxcttSEfi8i4YHtAxq910 +7PncjLgcjmgjutDzUNzPZY9zOjLHfP7KgiJPvo5iR2blzYfi6NUPGJ/lBHJLRjwQ8kTCZFZxTnXo +nMkmdMV9WdEKWw9t/p51HBjGGjp82A0EzM23RWV6sY+4roRIPrN6TagD4uJ+ARZZaBhDM7DS3LAa +QzXupdqpRlyuhoFBAUp0JuyfBr/CBTdkdXgpaP3F9ev+R/nkhbDhezGdpn9yo7nELC7MmVcOIQxF +AZRl62UJxmMiCzNJkkg8/M3OsD6Onov4/knFNXJHAgMBAAGjgY4wgYswDwYDVR0TAQH/BAUwAwEB +/zAdBgNVHQ4EFgQUqvyREBuHkV8Wub9PS5FeAByxMoAwDgYDVR0PAQH/BAQDAgEGMEkGA1UdHwRC +MEAwPqA8oDqGOGh0dHA6Ly9jcmwuZC10cnVzdC5uZXQvY3JsL2QtdHJ1c3RfZXZfcm9vdF9jYV8y +XzIwMjMuY3JsMA0GCSqGSIb3DQEBDQUAA4ICAQCTy6UfmRHsmg1fLBWTxj++EI14QvBukEdHjqOS +Mo1wj/Zbjb6JzkcBahsgIIlbyIIQbODnmaprxiqgYzWRaoUlrRc4pZt+UPJ26oUFKidBK7GB0aL2 +QHWpDsvxVUjY7NHss+jOFKE17MJeNRqrphYBBo7q3C+jisosketSjl8MmxfPy3MHGcRqwnNU73xD +UmPBEcrCRbH0O1P1aa4846XerOhUt7KR/aypH/KH5BfGSah82ApB9PI+53c0BFLd6IHyTS9URZ0V +4U/M5d40VxDJI3IXcI1QcB9WbMy5/zpaT2N6w25lBx2Eof+pDGOJbbJAiDnXH3dotfyc1dZnaVuo +dNv8ifYbMvekJKZ2t0dT741Jj6m2g1qllpBFYfXeA08mD6iL8AOWsKwV0HFaanuU5nCT2vFp4LJi +TZ6P/4mdm13NRemUAiKN4DV/6PEEeXFsVIP4M7kFMhtYVRFP0OUnR3Hs7dpn1mKmS00PaaLJvOwi +S5THaJQXfuKOKD62xur1NGyfN4gHONuGcfrNlUhDbqNPgofXNJhuS5N5YHVpD/Aa1VP6IQzCP+k/ +HxiMkl14p3ZnGbuy6n/pcAlWVqOwDAstNl7F6cTVg8uGF5csbBNvh1qvSaYd2804BC5f4ko1Di1L ++KIkBI3Y4WNeApI02phhXBxvWHZks/wCuPWdCg== +-----END CERTIFICATE----- + +SwissSign RSA TLS Root CA 2022 - 1 +================================== +-----BEGIN CERTIFICATE----- +MIIFkzCCA3ugAwIBAgIUQ/oMX04bgBhE79G0TzUfRPSA7cswDQYJKoZIhvcNAQELBQAwUTELMAkG +A1UEBhMCQ0gxFTATBgNVBAoTDFN3aXNzU2lnbiBBRzErMCkGA1UEAxMiU3dpc3NTaWduIFJTQSBU +TFMgUm9vdCBDQSAyMDIyIC0gMTAeFw0yMjA2MDgxMTA4MjJaFw00NzA2MDgxMTA4MjJaMFExCzAJ +BgNVBAYTAkNIMRUwEwYDVQQKEwxTd2lzc1NpZ24gQUcxKzApBgNVBAMTIlN3aXNzU2lnbiBSU0Eg +VExTIFJvb3QgQ0EgMjAyMiAtIDEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDLKmji +C8NXvDVjvHClO/OMPE5Xlm7DTjak9gLKHqquuN6orx122ro10JFwB9+zBvKK8i5VUXu7LCTLf5Im +gKO0lPaCoaTo+nUdWfMHamFk4saMla+ju45vVs9xzF6BYQ1t8qsCLqSX5XH8irCRIFucdFJtrhUn +WXjyCcplDn/L9Ovn3KlMd/YrFgSVrpxxpT8q2kFC5zyEEPThPYxr4iuRR1VPuFa+Rd4iUU1OKNlf +GUEGjw5NBuBwQCMBauTLE5tzrE0USJIt/m2n+IdreXXhvhCxqohAWVTXz8TQm0SzOGlkjIHRI36q +OTw7D59Ke4LKa2/KIj4x0LDQKhySio/YGZxH5D4MucLNvkEM+KRHBdvBFzA4OmnczcNpI/2aDwLO +EGrOyvi5KaM2iYauC8BPY7kGWUleDsFpswrzd34unYyzJ5jSmY0lpx+Gs6ZUcDj8fV3oT4MM0ZPl +EuRU2j7yrTrePjxF8CgPBrnh25d7mUWe3f6VWQQvdT/TromZhqwUtKiE+shdOxtYk8EXlFXIC+OC +eYSf8wCENO7cMdWP8vpPlkwGqnj73mSiI80fPsWMvDdUDrtaclXvyFu1cvh43zcgTFeRc5JzrBh3 +Q4IgaezprClG5QtO+DdziZaKHG29777YtvTKwP1H8K4LWCDFyB02rpeNUIMmJCn3nTsPBQIDAQAB +o2MwYTAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAfBgNVHSMEGDAWgBRvjmKLk0Ow +4UD2p8P98Q+4DxU4pTAdBgNVHQ4EFgQUb45ii5NDsOFA9qfD/fEPuA8VOKUwDQYJKoZIhvcNAQEL +BQADggIBAKwsKUF9+lz1GpUYvyypiqkkVHX1uECry6gkUSsYP2OprphWKwVDIqO310aewCoSPY6W +lkDfDDOLazeROpW7OSltwAJsipQLBwJNGD77+3v1dj2b9l4wBlgzHqp41eZUBDqyggmNzhYzWUUo +8aWjlw5DI/0LIICQ/+Mmz7hkkeUFjxOgdg3XNwwQiJb0Pr6VvfHDffCjw3lHC1ySFWPtUnWK50Zp +y1FVCypM9fJkT6lc/2cyjlUtMoIcgC9qkfjLvH4YoiaoLqNTKIftV+Vlek4ASltOU8liNr3Cjlvr +zG4ngRhZi0Rjn9UMZfQpZX+RLOV/fuiJz48gy20HQhFRJjKKLjpHE7iNvUcNCfAWpO2Whi4Z2L6M +OuhFLhG6rlrnub+xzI/goP+4s9GFe3lmozm1O2bYQL7Pt2eLSMkZJVX8vY3PXtpOpvJpzv1/THfQ +wUY1mFwjmwJFQ5Ra3bxHrSL+ul4vkSkphnsh3m5kt8sNjzdbowhq6/TdAo9QAwKxuDdollDruF/U +KIqlIgyKhPBZLtU30WHlQnNYKoH3dtvi4k0NX/a3vgW0rk4N3hY9A4GzJl5LuEsAz/+MF7psYC0n +hzck5npgL7XTgwSqT0N1osGDsieYK7EOgLrAhV5Cud+xYJHT6xh+cHiudoO+cVrQkOPKwRYlZ0rw +tnu64ZzZ +-----END CERTIFICATE----- + +OISTE Server Root ECC G1 +======================== +-----BEGIN CERTIFICATE----- +MIICNTCCAbqgAwIBAgIQI/nD1jWvjyhLH/BU6n6XnTAKBggqhkjOPQQDAzBLMQswCQYDVQQGEwJD +SDEZMBcGA1UECgwQT0lTVEUgRm91bmRhdGlvbjEhMB8GA1UEAwwYT0lTVEUgU2VydmVyIFJvb3Qg +RUNDIEcxMB4XDTIzMDUzMTE0NDIyOFoXDTQ4MDUyNDE0NDIyN1owSzELMAkGA1UEBhMCQ0gxGTAX +BgNVBAoMEE9JU1RFIEZvdW5kYXRpb24xITAfBgNVBAMMGE9JU1RFIFNlcnZlciBSb290IEVDQyBH +MTB2MBAGByqGSM49AgEGBSuBBAAiA2IABBcv+hK8rBjzCvRE1nZCnrPoH7d5qVi2+GXROiFPqOuj +vqQycvO2Ackr/XeFblPdreqqLiWStukhEaivtUwL85Zgmjvn6hp4LrQ95SjeHIC6XG4N2xml4z+c +KrhAS93mT6NjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBQ3TYhlz/w9itWj8UnATgwQ +b0K0nDAdBgNVHQ4EFgQUN02IZc/8PYrVo/FJwE4MEG9CtJwwDgYDVR0PAQH/BAQDAgGGMAoGCCqG +SM49BAMDA2kAMGYCMQCpKjAd0MKfkFFRQD6VVCHNFmb3U2wIFjnQEnx/Yxvf4zgAOdktUyBFCxxg +ZzFDJe0CMQCSia7pXGKDYmH5LVerVrkR3SW+ak5KGoJr3M/TvEqzPNcum9v4KGm8ay3sMaE641c= +-----END CERTIFICATE----- + +OISTE Server Root RSA G1 +======================== +-----BEGIN CERTIFICATE----- +MIIFgzCCA2ugAwIBAgIQVaXZZ5Qoxu0M+ifdWwFNGDANBgkqhkiG9w0BAQwFADBLMQswCQYDVQQG +EwJDSDEZMBcGA1UECgwQT0lTVEUgRm91bmRhdGlvbjEhMB8GA1UEAwwYT0lTVEUgU2VydmVyIFJv +b3QgUlNBIEcxMB4XDTIzMDUzMTE0MzcxNloXDTQ4MDUyNDE0MzcxNVowSzELMAkGA1UEBhMCQ0gx +GTAXBgNVBAoMEE9JU1RFIEZvdW5kYXRpb24xITAfBgNVBAMMGE9JU1RFIFNlcnZlciBSb290IFJT +QSBHMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAKqu9KuCz/vlNwvn1ZatkOhLKdxV +YOPMvLO8LZK55KN68YG0nnJyQ98/qwsmtO57Gmn7KNByXEptaZnwYx4M0rH/1ow00O7brEi56rAU +jtgHqSSY3ekJvqgiG1k50SeH3BzN+Puz6+mTeO0Pzjd8JnduodgsIUzkik/HEzxux9UTl7Ko2yRp +g1bTacuCErudG/L4NPKYKyqOBGf244ehHa1uzjZ0Dl4zO8vbUZeUapU8zhhabkvG/AePLhq5Svdk +NCncpo1Q4Y2LS+VIG24ugBA/5J8bZT8RtOpXaZ+0AOuFJJkk9SGdl6r7NH8CaxWQrbueWhl/pIzY ++m0o/DjH40ytas7ZTpOSjswMZ78LS5bOZmdTaMsXEY5Z96ycG7mOaES3GK/m5Q9l3JUJsJMStR8+ +lKXHiHUhsd4JJCpM4rzsTGdHwimIuQq6+cF0zowYJmXa92/GjHtoXAvuY8BeS/FOzJ8vD+HomnqT +8eDI278n5mUpezbgMxVz8p1rhAhoKzYHKyfMeNhqhw5HdPSqoBNdZH702xSu+zrkL8Fl47l6QGzw +Brd7KJvX4V84c5Ss2XCTLdyEr0YconosP4EmQufU2MVshGYRi3drVByjtdgQ8K4p92cIiBdcuJd5 +z+orKu5YM+Vt6SmqZQENghPsJQtdLEByFSnTkCz3GkPVavBpAgMBAAGjYzBhMA8GA1UdEwEB/wQF +MAMBAf8wHwYDVR0jBBgwFoAU8snBDw1jALvsRQ5KH7WxszbNDo0wHQYDVR0OBBYEFPLJwQ8NYwC7 +7EUOSh+1sbM2zQ6NMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQwFAAOCAgEANGd5sjrG5T33 +I3K5Ce+SrScfoE4KsvXaFwyihdJ+klH9FWXXXGtkFu6KRcoMQzZENdl//nk6HOjG5D1rd9QhEOP2 +8yBOqb6J8xycqd+8MDoX0TJD0KqKchxRKEzdNsjkLWd9kYccnbz8qyiWXmFcuCIzGEgWUOrKL+ml +Sdx/PKQZvDatkuK59EvV6wit53j+F8Bdh3foZ3dPAGav9LEDOr4SfEE15fSmG0eLy3n31r8Xbk5l +8PjaV8GUgeV6Vg27Rn9vkf195hfkgSe7BYhW3SCl95gtkRlpMV+bMPKZrXJAlszYd2abtNUOshD+ +FKrDgHGdPY3ofRRsYWSGRqbXVMW215AWRqWFyp464+YTFrYVI8ypKVL9AMb2kI5Wj4kI3Zaq5tNq +qYY19tVFeEJKRvwDyF7YZvZFZSS0vod7VSCd9521Kvy5YhnLbDuv0204bKt7ph6N/Ome/msVuduC +msuY33OhkKCgxeDoAaijFJzIwZqsFVAzje18KotzlUBDJvyBpCpfOZC3J8tRd/iWkx7P8nd9H0aT +olkelUTFLXVksNb54Dxp6gS1HAviRkRNQzuXSXERvSS2wq1yVAb+axj5d9spLFKebXd7Yv0PTY6Y +MjAwcRLWJTXjn/hvnLXrahut6hDTlhZyBiElxky8j3C7DOReIoMt0r7+hVu05L0= +-----END CERTIFICATE----- + +e-Szigno TLS Root CA 2023 +========================= +-----BEGIN CERTIFICATE----- +MIICzzCCAjGgAwIBAgINAOhvGHvWOWuYSkmYCjAKBggqhkjOPQQDBDB1MQswCQYDVQQGEwJIVTER +MA8GA1UEBwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0ZC4xFzAVBgNVBGEMDlZBVEhV +LTIzNTg0NDk3MSIwIAYDVQQDDBllLVN6aWdubyBUTFMgUm9vdCBDQSAyMDIzMB4XDTIzMDcxNzE0 +MDAwMFoXDTM4MDcxNzE0MDAwMFowdTELMAkGA1UEBhMCSFUxETAPBgNVBAcMCEJ1ZGFwZXN0MRYw +FAYDVQQKDA1NaWNyb3NlYyBMdGQuMRcwFQYDVQRhDA5WQVRIVS0yMzU4NDQ5NzEiMCAGA1UEAwwZ +ZS1Temlnbm8gVExTIFJvb3QgQ0EgMjAyMzCBmzAQBgcqhkjOPQIBBgUrgQQAIwOBhgAEAGgP36J8 +PKp0iGEKjcJMpQEiFNT3YHdCnAo4YKGMZz6zY+n6kbCLS+Y53wLCMAFSAL/fjO1ZrTJlqwlZULUZ +wmgcAOAFX9pQJhzDrAQixTpN7+lXWDajwRlTEArRzT/vSzUaQ49CE0y5LBqcvjC2xN7cS53kpDzL +Ltmt3999Cd8ukv+ho2MwYTAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4E +FgQUWYQCYlpGePVd3I8KECgj3NXW+0UwHwYDVR0jBBgwFoAUWYQCYlpGePVd3I8KECgj3NXW+0Uw +CgYIKoZIzj0EAwQDgYsAMIGHAkIBLdqu9S54tma4n7Zwf2Z0z+yOfP7AAXmazlIC58PRDHpty7Ve +7hekm9sEdu4pKeiv+62sUvTXK9Z3hBC9xdIoaDQCQTV2WnXzkoYI9bIeCvZlC9p2x1L/Cx6AcCIw +wzPbGO2E14vs7dOoY4G1VnxHx1YwlGhza9IuqbnZLBwpvQy6uWWL +-----END CERTIFICATE----- + +-----BEGIN CERTIFICATE----- +MIIDdTCCAl2gAwIBAgILBAAAAAABFUtaw5QwDQYJKoZIhvcNAQEFBQAwVzELMAkG +A1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2ExEDAOBgNVBAsTB1Jv +b3QgQ0ExGzAZBgNVBAMTEkdsb2JhbFNpZ24gUm9vdCBDQTAeFw05ODA5MDExMjAw +MDBaFw0yODAxMjgxMjAwMDBaMFcxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9i +YWxTaWduIG52LXNhMRAwDgYDVQQLEwdSb290IENBMRswGQYDVQQDExJHbG9iYWxT +aWduIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDaDuaZ +jc6j40+Kfvvxi4Mla+pIH/EqsLmVEQS98GPR4mdmzxzdzxtIK+6NiY6arymAZavp +xy0Sy6scTHAHoT0KMM0VjU/43dSMUBUc71DuxC73/OlS8pF94G3VNTCOXkNz8kHp +1Wrjsok6Vjk4bwY8iGlbKk3Fp1S4bInMm/k8yuX9ifUSPJJ4ltbcdG6TRGHRjcdG +snUOhugZitVtbNV4FpWi6cgKOOvyJBNPc1STE4U6G7weNLWLBYy5d4ux2x8gkasJ +U26Qzns3dLlwR5EiUWMWea6xrkEmCMgZK9FGqkjWZCrXgzT/LCrBbBlDSgeF59N8 +9iFo7+ryUp9/k5DPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8E +BTADAQH/MB0GA1UdDgQWBBRge2YaRQ2XyolQL30EzTSo//z9SzANBgkqhkiG9w0B +AQUFAAOCAQEA1nPnfE920I2/7LqivjTFKDK1fPxsnCwrvQmeU79rXqoRSLblCKOz +yj1hTdNGCbM+w6DjY1Ub8rrvrTnhQ7k4o+YviiY776BQVvnGCv04zcQLcFGUl5gE +38NflNUVyRRBnMRddWQVDf9VMOyGj/8N7yy5Y0b2qvzfvGn9LhJIZJrglfCm7ymP +AbEVtQwdpf5pLGkkeB6zpxxxYu7KyJesF12KwvhHhm4qxFYxldBniYUr+WymXUad +DKqC5JlR3XC321Y9YeRq4VzW9v493kHMB65jUr9TU/Qr6cf9tveCX4XSQRjbgbME +HMUfpIBvFSDJ3gyICh3WZlXi/EjJKSZp4A== +-----END CERTIFICATE----- diff --git a/certificates/cert_bundle.py b/certificates/cert_bundle.py new file mode 100644 index 00000000..0ba42de3 --- /dev/null +++ b/certificates/cert_bundle.py @@ -0,0 +1,734 @@ +#!/usr/bin/env python3 +# +# OpenShock CA trust store manager. Two subcommands: +# +# generate (default) — download curl's CA bundle, SHA-256-verify it, and write two files: +# cacert-curl.pem (the verbatim verified base) and cacert-merged.pem +# (base + pinned_certs/*.pem + local custom_certs/*.pem). Does NOT +# emit a packed x509_crt_bundle; the firmware build converts and +# packs cacert-merged.pem into flash via ESP-IDF's certificate-bundle step. +# audit — audit every cert already in cacert-merged.pem (base + pinned + custom): +# flag expiry, and for pinned roots probe the live endpoints to +# decide which are still needed. Reports pins no endpoint chains +# through as removable, and raises an alert on a still-needed cert +# that is expired/expiring. Used by CI. +# +# Based on Espressif's original certificate bundle generation utility, extended to +# add SHA-256 verification, deprecation-warning surfacing, atomic writes, pinned/custom +# roots, and the audit subcommand. +# +# Copyright 2018-2019 Espressif Systems (Shanghai) PTE LTD +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from __future__ import annotations + +import base64 +import datetime +import hashlib +import os +import re +import subprocess +import sys +import tempfile +import urllib.request +import ssl +import warnings +from pathlib import Path + +try: + from cryptography import x509 + from cryptography.hazmat.primitives import serialization + from cryptography.utils import CryptographyDeprecationWarning +except ImportError: + print( + 'cert_bundle.py: cryptography package not installed ' '(pip install cryptography)', + file=sys.stderr, + ) + raise + +URL_PEM = 'https://curl.se/ca/cacert.pem' +URL_SHA256 = 'https://curl.se/ca/cacert.pem.sha256' +MANUAL_HASH_PAGE = 'https://curl.se/docs/caextract.html' + +# The verbatim curl/Mozilla CA extract, saved exactly as downloaded so `sha256sum` of +# this file matches curl's published checksum (manual verification, see README). +BASE_PEM_FILE = 'cacert-curl.pem' + +# The merged trust store packed into firmware (sdkconfig.defaults: +# CONFIG_MBEDTLS_CUSTOM_CERTIFICATE_BUNDLE_PATH): the verified curl base followed by every +# pinned (and any local custom) cert. +MERGED_PEM_FILE = 'cacert-merged.pem' + +# Banner placed at the top of the merged bundle, in place of curl's own header, so it is +# never mistaken for the verbatim curl extract (its sha256 intentionally differs). +MERGED_BANNER = ( + b'##\n' + b'## OpenShock firmware TLS trust store -- MERGED / GENERATED, do not edit by hand.\n' + b'##\n' + b'## Produced by certificates/cert_bundle.py. This is what the firmware packs:\n' + b'## the verbatim curl/Mozilla CA extract (see cacert-curl.pem) + certificates/pinned_certs/.\n' + b'## Its sha256 intentionally differs from curl\'s published hash. To change it, edit\n' + b'## pinned_certs/ and re-run `cert_bundle.py generate` -- never hand-edit this file.\n' + b'##\n' +) + +# Pinned certs (tracked, shipped to every device, expiry-monitored). Roots the +# Mozilla/curl base has retired but our infrastructure still chains through -- e.g. +# GlobalSign Root CA R1, which cross-signs GTS Root R4 in the api.openshock.app chain. +PINNED_CERTS_DIR = 'pinned_certs' +PINNED_CERTS_GLOB = '*.pem' + +# Custom certs (self-hosted/internal PKI). Local-only, git-ignored. +CUSTOM_CERTS_DIR = 'custom_certs' # optional; if missing -> ignored +CUSTOM_CERTS_GLOB = '*.pem' # required extension + +# A pinned cert this close to (or past) expiry is a problem: generation hard-fails on an +# already-expired pin; the `audit` subcommand / the CI workflow raise an alert once a +# still-needed pin is within this window. Override with PINNED_CERT_EXPIRY_WARN_DAYS. +EXPIRY_WARN_DAYS_DEFAULT = 90 + +PEM_BLOCK_RE = re.compile( + br'-----BEGIN CERTIFICATE-----\s*[\s\S]+?\s*-----END CERTIFICATE-----\s*', + re.MULTILINE, +) + + +class InputError(RuntimeError): + pass + + +def critical(msg: str) -> None: + sys.stderr.write('cert_bundle.py: ' + msg + '\n') + + +def download_bytes(url: str, timeout: int = 30) -> bytes: + ctx = ssl.create_default_context() + req = urllib.request.Request( + url, + headers={ + 'User-Agent': 'esp32-gen-crt-bundle/strict', + 'Accept': 'text/plain,*/*', + }, + method='GET', + ) + with urllib.request.urlopen(req, context=ctx, timeout=timeout) as resp: + # urllib responses vary; handle both .status and .getcode() + status = getattr(resp, 'status', None) + if status is None: + try: + status = resp.getcode() + except Exception: + status = 200 + if status != 200: + raise InputError(f'Download failed: HTTP {status} for {url}') + data = resp.read() + if not data: + raise InputError(f'Download failed: empty response from {url}') + return data + + +def parse_sha256_file(content: bytes, *, expect_filename: str | None = None) -> str: + """ + Parse a .sha256 file. + Common formats include: + - ' cacert.pem' + - '' + We accept the first line and extract a leading 64-hex hash. + If expect_filename is provided and a filename appears on the line, + we sanity-check that it matches. + """ + text = content.decode('utf-8', errors='replace').strip() + if not text: + raise InputError('Downloaded sha256 file is empty') + + first = text.splitlines()[0].strip() + m = re.match(r'^([0-9a-fA-F]{64})(?:\s+[* ]?(.+))?$', first) + if not m: + raise InputError(f'Could not parse SHA-256 from sha256 file: {first!r}') + + h = m.group(1).lower() + fn = (m.group(2) or '').strip() + + if expect_filename and fn: + # Some sha256sum formats include full paths; compare basename. + if os.path.basename(fn) != expect_filename: + raise InputError(f'SHA-256 file refers to unexpected filename {fn!r} (expected {expect_filename!r})') + + return h + + +def extract_pem_cert_blocks(pem_bytes: bytes, *, source: str) -> list[bytes]: + blocks = PEM_BLOCK_RE.findall(pem_bytes) + if not blocks: + raise InputError(f'No PEM certificate blocks found in {source}') + return blocks + + +def _load_single_pem_cert_block_from_bytes_strict(data: bytes, *, source: str) -> bytes: + # Hard fail if file contains any private key material. + if b'PRIVATE KEY' in data: + raise InputError(f'Custom cert file contains private key material (PRIVATE KEY): {source}') + + begin = b'-----BEGIN CERTIFICATE-----' + end = b'-----END CERTIFICATE-----' + + begin_idx = data.find(begin) + end_idx = data.find(end) + if begin_idx == -1 or end_idx == -1: + raise InputError(f'Custom cert file must contain exactly 1 certificate: {source}') + + end_idx += len(end) + + # Allow leading label / comment lines before the certificate (curl's bundle prefixes + # each cert with a human-readable name), but nothing may follow it. Private-key material + # is rejected globally above, and the single-block checks below reject extra certs. + if data[end_idx:].strip(b' \t\r\n'): + raise InputError(f'Non-whitespace data after certificate in {source}') + + # Exactly one cert block (no extra PEM blocks) + if data.count(begin) != 1 or data.count(end) != 1: + raise InputError(f'Custom cert file must contain exactly 1 certificate: {source}') + tmp = data.replace(begin, b'') + if b'-----BEGIN ' in tmp: + raise InputError(f'Custom cert file contains additional PEM blocks besides the certificate: {source}') + + # Validate base64 payload decodes cleanly + pem = data[begin_idx:end_idx] + lines = pem.splitlines() + if not lines or lines[0].strip() != begin or lines[-1].strip() != end: + raise InputError(f'Malformed CERTIFICATE PEM boundaries in {source}') + + b64_lines = [ln.strip() for ln in lines[1:-1] if ln.strip()] + if not b64_lines: + raise InputError(f'Empty certificate payload in {source}') + + b64_blob = b''.join(b64_lines) + try: + der = base64.b64decode(b64_blob, validate=True) + except Exception as e: + raise InputError(f'Invalid base64 payload in certificate: {source}') from e + if not der or len(der) < 64: + raise InputError(f'Certificate DER payload too small / invalid: {source}') + + # Canonicalize output PEM (stable formatting) + b64_canon = base64.b64encode(der) + out_lines = [b64_canon[i : i + 64] for i in range(0, len(b64_canon), 64)] + return b'-----BEGIN CERTIFICATE-----\n' + b'\n'.join(out_lines) + b'\n-----END CERTIFICATE-----\n' + + +def _load_single_pem_cert_block_from_file(path: Path) -> bytes: + try: + data = path.read_bytes() + except Exception as e: + raise InputError(f'Failed to read custom cert file: {path}') from e + + return _load_single_pem_cert_block_from_bytes_strict(data, source=f'custom cert file: {path}') + + +def load_dir_cert_blocks(dirname: str, glob: str) -> tuple[list[bytes], list[Path]]: + """ + Load one-cert-per-file PEM blocks from a directory. + + Returns: (blocks, paths) + + Behavior: + - If the directory does not exist -> return ([], []) + - Only files matching `glob` are considered + - If the directory exists but contains no matches -> return ([], []) + - Each file must contain exactly one certificate + """ + d = Path(dirname) + if not d.exists(): + return ([], []) + + if not d.is_dir(): + raise InputError(f'{dirname} exists but is not a directory') + + paths = sorted(d.glob(glob)) + if not paths: + return ([], []) + + critical(f'Loading certificates from: {dirname}/ ({len(paths)} file(s))') + + blocks: list[bytes] = [] + for p in paths: + if p.suffix.lower() != '.pem': + # Should not happen due to glob, but keep strict. + raise InputError(f'Cert file must have .pem extension: {p}') + blocks.append(_load_single_pem_cert_block_from_file(p)) + + return (blocks, paths) + + +def load_pinned_cert_blocks() -> tuple[list[bytes], list[Path]]: + """Load tracked, shipped pinned CA certs from pinned_certs/*.pem.""" + return load_dir_cert_blocks(PINNED_CERTS_DIR, PINNED_CERTS_GLOB) + + +def load_custom_cert_blocks() -> tuple[list[bytes], list[Path]]: + """Load local-only custom CA certs from custom_certs/*.pem.""" + return load_dir_cert_blocks(CUSTOM_CERTS_DIR, CUSTOM_CERTS_GLOB) + + +def _cert_not_after_utc(cert: x509.Certificate) -> datetime.datetime: + # cryptography >= 42 exposes tz-aware not_valid_after_utc; older exposes naive UTC. + try: + return cert.not_valid_after_utc + except AttributeError: + return cert.not_valid_after.replace(tzinfo=datetime.timezone.utc) + + +def check_certs_expiry( + certs: list[x509.Certificate], + origins: list[str], + warn_days: int, +) -> tuple[list[str], list[str]]: + """ + Classify certs by expiry against `warn_days`. + + Returns (expired, expiring) as human-readable descriptions: + - expired: notAfter is in the past + - expiring: notAfter is within `warn_days` from now (but not yet expired) + Emits a diagnostic line for each problem cert. + """ + now = datetime.datetime.now(datetime.timezone.utc) + expired: list[str] = [] + expiring: list[str] = [] + + for idx, cert in enumerate(certs): + origin = origins[idx] if idx < len(origins) else '' + not_after = _cert_not_after_utc(cert) + days_left = (not_after - now).days + desc = f'{cert.subject.rfc4514_string()} (origin: {origin}; notAfter: {not_after:%Y-%m-%d}; {days_left}d left)' + + if days_left < 0: + critical(f'EXPIRED certificate: {desc}') + expired.append(desc) + elif days_left <= warn_days: + critical(f'WARNING: certificate expires within {warn_days}d: {desc}') + expiring.append(desc) + + return (expired, expiring) + + +def _expiry_warn_days() -> int: + raw = os.environ.get('PINNED_CERT_EXPIRY_WARN_DAYS') + if not raw: + return EXPIRY_WARN_DAYS_DEFAULT + try: + val = int(raw) + except ValueError: + raise InputError(f'PINNED_CERT_EXPIRY_WARN_DAYS must be an integer, got {raw!r}') + if val < 0: + raise InputError('PINNED_CERT_EXPIRY_WARN_DAYS must be >= 0') + return val + + +def _reject_non_ca(certs: list[x509.Certificate], origins: list[str], *, kind: str) -> None: + non_ca = [(c, o) for (c, o) in zip(certs, origins) if not _is_ca_certificate(c)] + if not non_ca: + return + critical(f'FATAL: One or more {kind} certificates are not CA certificates (BasicConstraints CA=TRUE missing)') + for c, origin in non_ca: + critical(f' Origin : {origin}') + critical(f' Subject: {c.subject.rfc4514_string()}') + critical(f' Issuer : {c.issuer.rfc4514_string()}') + raise InputError(f'Refusing to include non-CA {kind} certificates') + + +# -------------------------------------------------------------------------------------- +# audit — expiry of every shipped cert + live-endpoint relevance for pinned roots +# -------------------------------------------------------------------------------------- + +# Endpoints whose live chains decide whether a pinned root still earns its place. esp_crt_bundle +# does not do path building: it trusts the chain as the server sends it and looks up the issuer +# of the topmost presented cert among the trusted roots. So the anchor an endpoint "requires" is +# precisely the issuer of the last cert in its presented chain. +MONITORED_DOMAINS = ['api.openshock.app', 'api.openshock.dev'] +PROBE_TIMEOUT_S = 20 + + +def _openssl_name(pem_or_der: bytes, field: str) -> str: + """Canonical (rfc2253) -subject/-issuer string for a single cert, via openssl.""" + proc = subprocess.run( + ['openssl', 'x509', '-noout', f'-{field}', '-nameopt', 'rfc2253'], + input=pem_or_der, + capture_output=True, + ) + if proc.returncode != 0: + raise InputError(f'openssl x509 -{field} failed: {proc.stderr.decode(errors="replace")}') + out = proc.stdout.decode(errors='replace').strip() + return out.split('=', 1)[1].strip() if '=' in out else out + + +def _rfc2253_subject(cert: x509.Certificate) -> str: + return _openssl_name(cert.public_bytes(serialization.Encoding.PEM), 'subject') + + +def required_anchor_subject(domain: str) -> str | None: + """ + rfc2253 subject of the anchor `domain` requires (issuer of the topmost cert it + presents), or None if the endpoint is unreachable. + """ + try: + proc = subprocess.run( + ['openssl', 's_client', '-connect', f'{domain}:443', '-servername', domain, '-showcerts'], + input=b'', + capture_output=True, + timeout=PROBE_TIMEOUT_S, + ) + except (subprocess.TimeoutExpired, FileNotFoundError) as e: + critical(f' probe error for {domain}: {e}') + return None + + chain = re.findall(rb'-----BEGIN CERTIFICATE-----.+?-----END CERTIFICATE-----', proc.stdout, re.S) + if not chain: + critical(f' {domain}: no certificates presented (unreachable / handshake failed)') + return None + return _openssl_name(chain[-1], 'issuer') + + +def _emit_gh_outputs(*, removable: list[str], alert: bool, probe_ok: bool) -> None: + gh_out = os.environ.get('GITHUB_OUTPUT') + if not gh_out: + return + with open(gh_out, 'a', encoding='utf-8') as f: + f.write(f'removable={" ".join(removable)}\n') + f.write(f'alert={"true" if alert else "false"}\n') + f.write(f'probe_ok={"true" if probe_ok else "false"}\n') + + +def audit() -> int: + """ + Audit every cert currently in the shipped bundle (cacert-merged.pem = base + pinned + custom): + - expiry of all of them; + - for pinned roots, probe the live endpoints to decide which are still needed. + + Emits GitHub outputs (removable / alert / probe_ok). Returns exit code 1 on a genuine + rotate-now alert (a still-needed cert expired/expiring); removal of unneeded pins is + left to the workflow's PR step keyed on the `removable` output. Fail-safe: if any + monitored endpoint is unreachable, nothing is proposed for removal. + """ + warn_days = _expiry_warn_days() + + # The bundle as actually shipped. + try: + bundle_bytes = Path(MERGED_PEM_FILE).read_bytes() + except OSError as e: + raise InputError(f'Cannot read {MERGED_PEM_FILE}: {e}') from e + bundle_blocks = extract_pem_cert_blocks(bundle_bytes, source=MERGED_PEM_FILE) + bundle_certs = load_all_certs(bundle_blocks, label='bundle') + + # Pinned roots, keyed by canonical subject, so we can tell them apart from the curl + # base within the merged bundle and know which file to retire. + pinned_blocks, pinned_paths = load_pinned_cert_blocks() + pinned_by_subject: dict[str, str] = {} + for blk, path in zip(pinned_blocks, pinned_paths): + pinned_by_subject[_openssl_name(blk, 'subject')] = path.as_posix() + + critical(f'Auditing {len(bundle_certs)} certificate(s) in {MERGED_PEM_FILE} ' + f'({len(pinned_by_subject)} pinned); warn window {warn_days}d') + + # Which anchors do the live endpoints actually require? + required: set[str] = set() + probe_ok = True + for domain in MONITORED_DOMAINS: + subj = required_anchor_subject(domain) + if subj is None: + probe_ok = False + critical(f' {domain}: UNREACHABLE (will not retire any pin this run)') + continue + required.add(subj) + critical(f' {domain}: requires anchor -> {subj}') + + now = datetime.datetime.now(datetime.timezone.utc) + removable: list[str] = [] + alert = False + + for cert in bundle_certs: + subject = _rfc2253_subject(cert) + days_left = (_cert_not_after_utc(cert) - now).days + pinned_path = pinned_by_subject.get(subject) + + if pinned_path is not None: + needed = subject in required + if not needed: + if probe_ok: + critical(f'REMOVABLE pin (no endpoint chains through it): {pinned_path} [{subject}]') + removable.append(pinned_path) + else: + critical(f'unused pin, but a probe failed -> keeping (fail-safe): {pinned_path}') + continue + # Still needed -> expiry is an operational risk. + if days_left < 0: + critical(f'ALERT: pinned root EXPIRED and still required: {pinned_path} [{subject}]') + alert = True + elif days_left <= warn_days: + critical(f'ALERT: pinned root expires in {days_left}d and still required: {pinned_path} [{subject}]') + alert = True + else: + # Curl/base (or committed custom) root. We can't retire these individually, but a + # shipped root that is already expired is still worth failing on. + if days_left < 0: + critical(f'ALERT: shipped root EXPIRED: {subject}') + alert = True + elif days_left <= warn_days: + critical(f'note: base root expires in {days_left}d (curl refresh will handle it): {subject}') + + critical('') + critical(f'Summary: {len(removable)} removable pin(s), alert={alert}, probe_ok={probe_ok}') + _emit_gh_outputs(removable=removable, alert=alert, probe_ok=probe_ok) + return 1 if alert else 0 + + +def _is_ca_certificate(cert: x509.Certificate) -> bool: + """ + Best-effort CA check: + - Requires BasicConstraints CA=TRUE + (We keep this strict to avoid adding leaf/server certs by mistake.) + """ + try: + bc = cert.extensions.get_extension_for_class(x509.BasicConstraints).value + except Exception: + return False + return bool(getattr(bc, 'ca', False)) + + +def load_all_certs( + blocks: list[bytes], + *, + label: str = 'CA', + origins: list[str] | None = None, +) -> list[x509.Certificate]: + certs: list[x509.Certificate] = [] + + for idx, b in enumerate(blocks, start=1): + origin = '' + if origins and 0 <= (idx - 1) < len(origins): + origin = origins[idx - 1] + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter('always', CryptographyDeprecationWarning) + + try: + cert = x509.load_pem_x509_certificate(b) + except Exception as e: + critical(f'FATAL: Invalid certificate at index {idx} ({label})') + if origin: + critical(f' Origin : {origin}') + critical(f' Reason: {e}') + critical(' Offending PEM:') + critical(b.decode('ascii', errors='replace').rstrip()) + raise InputError('Aborting due to invalid CA certificate') from e + + for warn in w: + if issubclass(warn.category, CryptographyDeprecationWarning): + subject = cert.subject.rfc4514_string() + issuer = cert.issuer.rfc4514_string() + + critical('WARNING: CA certificate triggers CryptographyDeprecationWarning') + critical(' This may become fatal in future cryptography releases.') + critical(f' Index : {idx} ({label})') + if origin: + critical(f' Origin : {origin}') + critical(f' Subject : {subject}') + critical(f' Issuer : {issuer}') + critical(f' Reason : {warn.message}') + critical(' Offending PEM:') + critical(b.decode('ascii', errors='replace').rstrip()) + + certs.append(cert) + + return certs + + +def validate_unique_subjects(certs: list[x509.Certificate]) -> None: + """ + esp_crt_bundle (the ESP-IDF build step that packs cacert-merged.pem into flash) looks + certificates up by their DER-encoded subject name, so two certs sharing a subject + would make that lookup ambiguous. Reject duplicates here. + """ + seen: dict[bytes, x509.Certificate] = {} + for crt in certs: + subject_der = crt.subject.public_bytes(serialization.Encoding.DER) + if subject_der in seen: + raise InputError( + 'Duplicate certificate subject name detected; this can make ' + f'esp_crt_bundle subject-name lookup ambiguous: {crt.subject.rfc4514_string()}' + ) + seen[subject_der] = crt + + +def atomic_write(path: str, data: bytes) -> None: + out_dir = os.path.dirname(os.path.abspath(path)) or '.' + os.makedirs(out_dir, exist_ok=True) + + fd = None + tmp_path = None + try: + fd, tmp_path = tempfile.mkstemp(prefix='.tmp_crt_bundle_', dir=out_dir) + with os.fdopen(fd, 'wb') as f: + f.write(data) + f.flush() + os.fsync(f.fileno()) + fd = None + + os.replace(tmp_path, path) + tmp_path = None + finally: + try: + if tmp_path and os.path.exists(tmp_path): + os.remove(tmp_path) + except Exception: + pass + + +def _append_pem_blocks(base: bytes, blocks: list[bytes]) -> bytes: + out = bytearray(base) + for blk in blocks: + if out and not out.endswith(b'\n'): + out += b'\n' + out += b'\n' + blk + if not out.endswith(b'\n'): + out += b'\n' + return bytes(out) + + +def generate() -> int: + warn_days = _expiry_warn_days() + + critical(f'Downloading CA bundle from: {URL_PEM}') + pem = download_bytes(URL_PEM) + + got_hash = hashlib.sha256(pem).hexdigest().lower() + critical(f'Downloaded {len(pem)} bytes') + critical(f'SHA-256(curl download) = {got_hash}') + + critical(f'Downloading published hash from: {URL_SHA256}') + sha_file = download_bytes(URL_SHA256) + expected_hash = parse_sha256_file(sha_file, expect_filename='cacert.pem') + critical(f'Published SHA-256 = {expected_hash}') + + critical('') + critical('MANUAL VERIFICATION REQUIRED') + critical(' Compare the "SHA-256(curl download)" hash above (the upstream bytes, computed') + critical(' BEFORE pinned/custom certs are appended) against:') + critical(f' {MANUAL_HASH_PAGE}') + critical('') + + if got_hash != expected_hash: + raise InputError('SHA-256 mismatch against cacert.pem.sha256 — refusing to proceed') + + critical('Automatic SHA-256 validation passed') + + # Curl/Mozilla base. + blocks = extract_pem_cert_blocks(pem, source='downloaded curl CA bundle') + curl_certs = load_all_certs(blocks, label='CA', origins=['curl bundle'] * len(blocks)) + critical(f'Found {len(blocks)} certificate blocks in curl bundle') + + # Pinned roots (tracked, shipped, appended into cacert-merged.pem, expiry-monitored). + pinned_blocks, pinned_paths = load_pinned_cert_blocks() + pinned_certs: list[x509.Certificate] = [] + if pinned_blocks: + pinned_origins = [f'{PINNED_CERTS_DIR}/{p.name}' for p in pinned_paths] + pinned_certs = load_all_certs(pinned_blocks, label='pinned', origins=pinned_origins) + _reject_non_ca(pinned_certs, pinned_origins, kind='pinned') + + # Never ship an already-dead anchor. + expired, expiring = check_certs_expiry(pinned_certs, pinned_origins, warn_days) + if expired: + raise InputError(f'Refusing to ship {len(expired)} expired pinned certificate(s)') + if expiring: + critical(f'NOTE: {len(expiring)} pinned cert(s) expire within {warn_days}d — rotate soon') + critical(f'Found {len(pinned_blocks)} pinned certificate(s)') + else: + critical(f'No pinned certificates found ({PINNED_CERTS_DIR}/*.pem)') + + # Custom roots (local-only, git-ignored self-hosted PKI). + custom_blocks, custom_paths = load_custom_cert_blocks() + custom_certs: list[x509.Certificate] = [] + if custom_blocks: + custom_origins = [f'custom cert file: {p}' for p in custom_paths] + custom_certs = load_all_certs(custom_blocks, label='custom', origins=custom_origins) + _reject_non_ca(custom_certs, custom_origins, kind='custom') + critical(f'Found {len(custom_blocks)} custom certificate(s)') + else: + critical('No custom certificates found (custom_certs/*.pem)') + + # Deduplicate: drop any pinned/custom cert whose DER is already present (in the base + # or an earlier pin/custom) so the merged output never contains the same cert twice. + seen_der = {c.public_bytes(serialization.Encoding.DER) for c in curl_certs} + extra_blocks: list[bytes] = [] + extra_certs: list[x509.Certificate] = [] + for blk, cert, origin in zip( + list(pinned_blocks) + list(custom_blocks), + pinned_certs + custom_certs, + [f'{PINNED_CERTS_DIR}/{p.name}' for p in pinned_paths] + [f'custom_certs/{p.name}' for p in custom_paths], + ): + der = cert.public_bytes(serialization.Encoding.DER) + if der in seen_der: + critical(f'Skipping duplicate certificate (already in bundle): {cert.subject.rfc4514_string()} [{origin}]') + continue + seen_der.add(der) + extra_blocks.append(blk) + extra_certs.append(cert) + + # Validate the deduplicated set: unique subject names so esp_crt_bundle's subject-name + # lookup stays unambiguous (this also catches two *different* certs sharing a subject). + certs = curl_certs + extra_certs + critical(f'Bundle: {len(curl_certs)} base + {len(extra_certs)} pinned/custom = {len(certs)} certs') + validate_unique_subjects(certs) + + # Save the verbatim curl base (so `sha256sum {BASE_PEM_FILE}` matches curl's published + # hash) and the merged store the firmware actually packs. The firmware build converts + # and packs cacert-merged.pem via ESP-IDF's certificate-bundle step; this script does not emit + # the packed x509 bundle itself. + critical(f'Saving {BASE_PEM_FILE} (verbatim curl base, {len(blocks)} certs, atomic)') + atomic_write(BASE_PEM_FILE, pem) + + # Merged file gets our own banner in place of curl's leading header, then all certs. + base_no_header = pem[pem.find(b'-----BEGIN CERTIFICATE-----') :] + combined = MERGED_BANNER + _append_pem_blocks(base_no_header, extra_blocks) + critical(f'Saving {MERGED_PEM_FILE} ({len(certs)} certs, atomic)') + atomic_write(MERGED_PEM_FILE, combined) + + critical('Done') + return 0 + + +USAGE = 'usage: cert_bundle.py [generate|audit]' + + +def main() -> int: + args = sys.argv[1:] + if not args or args == ['generate']: + return generate() + if args == ['audit']: + return audit() + if args in (['-h'], ['--help']): + critical(USAGE) + critical(' generate (default) download + verify; write cacert-curl.pem (base) + cacert-merged.pem (base+pinned+custom)') + critical(' audit audit expiry of every shipped cert + probe endpoints for pinned') + critical(' relevance; exits 1 on a still-needed expired/expiring cert') + return 0 + raise InputError(USAGE) + + +if __name__ == '__main__': + try: + raise SystemExit(main()) + except InputError as e: + critical(str(e)) + raise SystemExit(2) diff --git a/certificates/gen_crt_bundle.py b/certificates/gen_crt_bundle.py deleted file mode 100644 index b8aed29b..00000000 --- a/certificates/gen_crt_bundle.py +++ /dev/null @@ -1,427 +0,0 @@ -#!/usr/bin/env python3 -# -# ESP32 x509 certificate bundle download and conversion script with SHA-256 Verification -# -# This file is based on Espressif's original certificate bundle -# generation utility, but has been modified to: -# - Automatically download and convert curl's CA certificate bundle -# - Perform automatic SHA-256 integrity verification of downloads -# - Emit warnings for certificates that trigger cryptography deprecation notices -# - Write the output bundle atomically to avoid partial updates -# - Optionally include user-provided custom CA certificates from custom_certs/*.pem -# -# Copyright 2018-2019 Espressif Systems (Shanghai) PTE LTD -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -from __future__ import annotations - -import base64 -import hashlib -import os -import re -import struct -import sys -import tempfile -import urllib.request -import ssl -import warnings -from pathlib import Path - -try: - from cryptography import x509 - from cryptography.hazmat.primitives import serialization - from cryptography.utils import CryptographyDeprecationWarning -except ImportError: - print( - 'gen_crt_bundle.py: cryptography package not installed ' '(pip install cryptography)', - file=sys.stderr, - ) - raise - -URL_PEM = 'https://curl.se/ca/cacert.pem' -URL_SHA256 = 'https://curl.se/ca/cacert.pem.sha256' -MANUAL_HASH_PAGE = 'https://curl.se/docs/caextract.html' -OUTPUT_FILE = 'x509_crt_bundle' -LOCAL_PEM_FILE = 'cacert.pem' - -# Custom certs (self-hosted/internal PKI) -CUSTOM_CERTS_DIR = 'custom_certs' # optional; if missing -> ignored -CUSTOM_CERTS_GLOB = '*.pem' # required extension - -PEM_BLOCK_RE = re.compile( - br'-----BEGIN CERTIFICATE-----\s*[\s\S]+?\s*-----END CERTIFICATE-----\s*', - re.MULTILINE, -) - - -class InputError(RuntimeError): - pass - - -def critical(msg: str) -> None: - sys.stderr.write('gen_crt_bundle.py: ' + msg + '\n') - - -def download_bytes(url: str, timeout: int = 30) -> bytes: - ctx = ssl.create_default_context() - req = urllib.request.Request( - url, - headers={ - 'User-Agent': 'esp32-gen-crt-bundle/strict', - 'Accept': 'text/plain,*/*', - }, - method='GET', - ) - with urllib.request.urlopen(req, context=ctx, timeout=timeout) as resp: - # urllib responses vary; handle both .status and .getcode() - status = getattr(resp, 'status', None) - if status is None: - try: - status = resp.getcode() - except Exception: - status = 200 - if status != 200: - raise InputError(f'Download failed: HTTP {status} for {url}') - data = resp.read() - if not data: - raise InputError(f'Download failed: empty response from {url}') - return data - - -def parse_sha256_file(content: bytes, *, expect_filename: str | None = None) -> str: - """ - Parse a .sha256 file. - Common formats include: - - ' cacert.pem' - - '' - We accept the first line and extract a leading 64-hex hash. - If expect_filename is provided and a filename appears on the line, - we sanity-check that it matches. - """ - text = content.decode('utf-8', errors='replace').strip() - if not text: - raise InputError('Downloaded sha256 file is empty') - - first = text.splitlines()[0].strip() - m = re.match(r'^([0-9a-fA-F]{64})(?:\s+[* ]?(.+))?$', first) - if not m: - raise InputError(f'Could not parse SHA-256 from sha256 file: {first!r}') - - h = m.group(1).lower() - fn = (m.group(2) or '').strip() - - if expect_filename and fn: - # Some sha256sum formats include full paths; compare basename. - if os.path.basename(fn) != expect_filename: - raise InputError(f'SHA-256 file refers to unexpected filename {fn!r} (expected {expect_filename!r})') - - return h - - -def extract_pem_cert_blocks(pem_bytes: bytes, *, source: str) -> list[bytes]: - blocks = PEM_BLOCK_RE.findall(pem_bytes) - if not blocks: - raise InputError(f'No PEM certificate blocks found in {source}') - return blocks - - -def _load_single_pem_cert_block_from_bytes_strict(data: bytes, *, source: str) -> bytes: - # Hard fail if file contains any private key material. - if b'PRIVATE KEY' in data: - raise InputError(f'Custom cert file contains private key material (PRIVATE KEY): {source}') - - begin = b'-----BEGIN CERTIFICATE-----' - end = b'-----END CERTIFICATE-----' - - begin_idx = data.find(begin) - end_idx = data.find(end) - if begin_idx == -1 or end_idx == -1: - raise InputError(f'Custom cert file must contain exactly 1 certificate: {source}') - - end_idx += len(end) - - # Disallow any non-whitespace bytes before/after the public certificate - if data[:begin_idx].strip(b' \t\r\n'): - raise InputError(f'Non-whitespace data before certificate in {source}') - if data[end_idx:].strip(b' \t\r\n'): - raise InputError(f'Non-whitespace data after certificate in {source}') - - # Exactly one cert block (no extra PEM blocks) - if data.count(begin) != 1 or data.count(end) != 1: - raise InputError(f'Custom cert file must contain exactly 1 certificate: {source}') - tmp = data.replace(begin, b'') - if b'-----BEGIN ' in tmp: - raise InputError(f'Custom cert file contains additional PEM blocks besides the certificate: {source}') - - # Validate base64 payload decodes cleanly - pem = data[begin_idx:end_idx] - lines = pem.splitlines() - if not lines or lines[0].strip() != begin or lines[-1].strip() != end: - raise InputError(f'Malformed CERTIFICATE PEM boundaries in {source}') - - b64_lines = [ln.strip() for ln in lines[1:-1] if ln.strip()] - if not b64_lines: - raise InputError(f'Empty certificate payload in {source}') - - b64_blob = b''.join(b64_lines) - try: - der = base64.b64decode(b64_blob, validate=True) - except Exception as e: - raise InputError(f'Invalid base64 payload in certificate: {source}') from e - if not der or len(der) < 64: - raise InputError(f'Certificate DER payload too small / invalid: {source}') - - # Canonicalize output PEM (stable formatting) - b64_canon = base64.b64encode(der) - out_lines = [b64_canon[i : i + 64] for i in range(0, len(b64_canon), 64)] - return b'-----BEGIN CERTIFICATE-----\n' + b'\n'.join(out_lines) + b'\n-----END CERTIFICATE-----\n' - - -def _load_single_pem_cert_block_from_file(path: Path) -> bytes: - try: - data = path.read_bytes() - except Exception as e: - raise InputError(f'Failed to read custom cert file: {path}') from e - - return _load_single_pem_cert_block_from_bytes_strict(data, source=f'custom cert file: {path}') - - -def load_custom_cert_blocks() -> tuple[list[bytes], list[Path]]: - """ - Load user-provided custom CA certificate PEM blocks from custom_certs/*.pem. - - Returns: (blocks, paths) - - Behavior: - - If custom_certs/ does not exist -> return ([], []) - - Only *.pem files are considered - - If the directory exists but contains no *.pem -> return ([], []) - - Non-.pem files are ignored (we do not scan them) - - Each .pem must contain exactly one certificate - """ - d = Path(CUSTOM_CERTS_DIR) - if not d.exists(): - return ([], []) - - if not d.is_dir(): - raise InputError(f'{CUSTOM_CERTS_DIR} exists but is not a directory') - - paths = sorted(d.glob(CUSTOM_CERTS_GLOB)) - if not paths: - return ([], []) - - critical(f'Loading custom certificates from: {CUSTOM_CERTS_DIR}/ ({len(paths)} file(s))') - - blocks: list[bytes] = [] - for p in paths: - if p.suffix.lower() != '.pem': - # Should not happen due to glob, but keep strict. - raise InputError(f'Custom cert file must have .pem extension: {p}') - blocks.append(_load_single_pem_cert_block_from_file(p)) - - return (blocks, paths) - - -def _is_ca_certificate(cert: x509.Certificate) -> bool: - """ - Best-effort CA check: - - Requires BasicConstraints CA=TRUE - (We keep this strict to avoid adding leaf/server certs by mistake.) - """ - try: - bc = cert.extensions.get_extension_for_class(x509.BasicConstraints).value - except Exception: - return False - return bool(getattr(bc, 'ca', False)) - - -def load_all_certs( - blocks: list[bytes], - *, - label: str = 'CA', - origins: list[str] | None = None, -) -> list[x509.Certificate]: - certs: list[x509.Certificate] = [] - - for idx, b in enumerate(blocks, start=1): - origin = '' - if origins and 0 <= (idx - 1) < len(origins): - origin = origins[idx - 1] - - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter('always', CryptographyDeprecationWarning) - - try: - cert = x509.load_pem_x509_certificate(b) - except Exception as e: - critical(f'FATAL: Invalid certificate at index {idx} ({label})') - if origin: - critical(f' Origin : {origin}') - critical(f' Reason: {e}') - critical(' Offending PEM:') - critical(b.decode('ascii', errors='replace').rstrip()) - raise InputError('Aborting due to invalid CA certificate') from e - - for warn in w: - if issubclass(warn.category, CryptographyDeprecationWarning): - subject = cert.subject.rfc4514_string() - issuer = cert.issuer.rfc4514_string() - - critical('WARNING: CA certificate triggers CryptographyDeprecationWarning') - critical(' This may become fatal in future cryptography releases.') - critical(f' Index : {idx} ({label})') - if origin: - critical(f' Origin : {origin}') - critical(f' Subject : {subject}') - critical(f' Issuer : {issuer}') - critical(f' Reason : {warn.message}') - critical(' Offending PEM:') - critical(b.decode('ascii', errors='replace').rstrip()) - - certs.append(cert) - - return certs - - -def create_bundle(certs: list[x509.Certificate]) -> bytes: - def subject_der(c: x509.Certificate) -> bytes: - return c.subject.public_bytes(serialization.Encoding.DER) - - certs_sorted = sorted(certs, key=subject_der) - - for i in range(1, len(certs_sorted)): - if subject_der(certs_sorted[i - 1]) == subject_der(certs_sorted[i]): - raise InputError( - 'Duplicate certificate subject name detected in bundle; ' 'this can make subject-name lookup ambiguous' - ) - - bundle = struct.pack('>H', len(certs_sorted)) - - for crt in certs_sorted: - pub_key_der = crt.public_key().public_bytes( - serialization.Encoding.DER, - serialization.PublicFormat.SubjectPublicKeyInfo, - ) - sub_name_der = subject_der(crt) - - if len(sub_name_der) > 0xFFFF or len(pub_key_der) > 0xFFFF: - raise InputError('Subject name or public key too large for bundle format') - - bundle += struct.pack('>HH', len(sub_name_der), len(pub_key_der)) - bundle += sub_name_der - bundle += pub_key_der - - return bundle - - -def atomic_write(path: str, data: bytes) -> None: - out_dir = os.path.dirname(os.path.abspath(path)) or '.' - os.makedirs(out_dir, exist_ok=True) - - fd = None - tmp_path = None - try: - fd, tmp_path = tempfile.mkstemp(prefix='.tmp_crt_bundle_', dir=out_dir) - with os.fdopen(fd, 'wb') as f: - f.write(data) - f.flush() - os.fsync(f.fileno()) - fd = None - - os.replace(tmp_path, path) - tmp_path = None - finally: - try: - if tmp_path and os.path.exists(tmp_path): - os.remove(tmp_path) - except Exception: - pass - - -def main() -> int: - if len(sys.argv) != 1: - raise InputError('This script takes no arguments') - - critical(f'Downloading CA bundle from: {URL_PEM}') - pem = download_bytes(URL_PEM) - - got_hash = hashlib.sha256(pem).hexdigest().lower() - critical(f'Downloaded {len(pem)} bytes') - critical(f'SHA-256(cacert.pem) = {got_hash}') - - critical(f'Downloading published hash from: {URL_SHA256}') - sha_file = download_bytes(URL_SHA256) - expected_hash = parse_sha256_file(sha_file, expect_filename=os.path.basename(LOCAL_PEM_FILE)) - critical(f'Published SHA-256 = {expected_hash}') - - critical('') - critical('MANUAL VERIFICATION REQUIRED') - critical(' Compare the above SHA-256 hash against:') - critical(f' {MANUAL_HASH_PAGE}') - critical('') - - if got_hash != expected_hash: - raise InputError('SHA-256 mismatch against cacert.pem.sha256 — refusing to proceed') - - critical('Automatic SHA-256 validation passed') - - # Only write cacert.pem after verification passes - critical(f'Saving verified PEM to {LOCAL_PEM_FILE} (atomic)') - atomic_write(LOCAL_PEM_FILE, pem) - - blocks = extract_pem_cert_blocks(pem, source='downloaded curl CA bundle (cacert.pem)') - critical(f'Found {len(blocks)} certificate blocks in curl bundle') - - (custom_blocks, custom_paths) = load_custom_cert_blocks() - custom_certs: list[x509.Certificate] = [] - if custom_blocks: - critical(f'Found {len(custom_blocks)} custom certificate(s)') - custom_origins = [f'custom cert file: {p}' for p in custom_paths] - custom_certs = load_all_certs(custom_blocks, label='custom', origins=custom_origins) - - # Enforce that custom certs are CA certs (best-effort) and prevent accidental leaf addition. - non_ca = [(c, o) for (c, o) in zip(custom_certs, custom_origins) if not _is_ca_certificate(c)] - if non_ca: - critical( - 'FATAL: One or more custom certificates are not CA certificates (BasicConstraints CA=TRUE missing)' - ) - for c, origin in non_ca: - critical(f' Origin : {origin}') - critical(f' Subject: {c.subject.rfc4514_string()}') - critical(f' Issuer : {c.issuer.rfc4514_string()}') - raise InputError('Refusing to include non-CA custom certificates') - else: - critical('No custom certificates found (custom_certs/*.pem)') - - curl_origins = ['curl bundle'] * len(blocks) - curl_certs = load_all_certs(blocks, label='CA', origins=curl_origins) - certs = curl_certs + custom_certs - - critical(f'Parsed {len(certs)} certificates total') - - bundle = create_bundle(certs) - critical(f'Writing bundle to {OUTPUT_FILE} (atomic)') - atomic_write(OUTPUT_FILE, bundle) - - critical('Done') - return 0 - - -if __name__ == '__main__': - try: - raise SystemExit(main()) - except InputError as e: - critical(str(e)) - raise SystemExit(2) diff --git a/certificates/pinned_certs/GlobalSign_Root_CA_R1.pem b/certificates/pinned_certs/GlobalSign_Root_CA_R1.pem new file mode 100644 index 00000000..b82159be --- /dev/null +++ b/certificates/pinned_certs/GlobalSign_Root_CA_R1.pem @@ -0,0 +1,23 @@ +GlobalSign Root CA R1 +===================== +-----BEGIN CERTIFICATE----- +MIIDdTCCAl2gAwIBAgILBAAAAAABFUtaw5QwDQYJKoZIhvcNAQEFBQAwVzELMAkG +A1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2ExEDAOBgNVBAsTB1Jv +b3QgQ0ExGzAZBgNVBAMTEkdsb2JhbFNpZ24gUm9vdCBDQTAeFw05ODA5MDExMjAw +MDBaFw0yODAxMjgxMjAwMDBaMFcxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9i +YWxTaWduIG52LXNhMRAwDgYDVQQLEwdSb290IENBMRswGQYDVQQDExJHbG9iYWxT +aWduIFJvb3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDaDuaZ +jc6j40+Kfvvxi4Mla+pIH/EqsLmVEQS98GPR4mdmzxzdzxtIK+6NiY6arymAZavp +xy0Sy6scTHAHoT0KMM0VjU/43dSMUBUc71DuxC73/OlS8pF94G3VNTCOXkNz8kHp +1Wrjsok6Vjk4bwY8iGlbKk3Fp1S4bInMm/k8yuX9ifUSPJJ4ltbcdG6TRGHRjcdG +snUOhugZitVtbNV4FpWi6cgKOOvyJBNPc1STE4U6G7weNLWLBYy5d4ux2x8gkasJ +U26Qzns3dLlwR5EiUWMWea6xrkEmCMgZK9FGqkjWZCrXgzT/LCrBbBlDSgeF59N8 +9iFo7+ryUp9/k5DPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8E +BTADAQH/MB0GA1UdDgQWBBRge2YaRQ2XyolQL30EzTSo//z9SzANBgkqhkiG9w0B +AQUFAAOCAQEA1nPnfE920I2/7LqivjTFKDK1fPxsnCwrvQmeU79rXqoRSLblCKOz +yj1hTdNGCbM+w6DjY1Ub8rrvrTnhQ7k4o+YviiY776BQVvnGCv04zcQLcFGUl5gE +38NflNUVyRRBnMRddWQVDf9VMOyGj/8N7yy5Y0b2qvzfvGn9LhJIZJrglfCm7ymP +AbEVtQwdpf5pLGkkeB6zpxxxYu7KyJesF12KwvhHhm4qxFYxldBniYUr+WymXUad +DKqC5JlR3XC321Y9YeRq4VzW9v493kHMB65jUr9TU/Qr6cf9tveCX4XSQRjbgbME +HMUfpIBvFSDJ3gyICh3WZlXi/EjJKSZp4A== +-----END CERTIFICATE----- diff --git a/certificates/x509_crt_bundle b/certificates/x509_crt_bundle deleted file mode 100644 index 76e5585c..00000000 Binary files a/certificates/x509_crt_bundle and /dev/null differ diff --git a/chips/ESP32-C3/4MB/merge-image.py b/chips/ESP32-C3/4MB/merge-image.py deleted file mode 100644 index 8e76a390..00000000 --- a/chips/ESP32-C3/4MB/merge-image.py +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/python3 - -import esptool - -# fmt: off -# Note: Bootloader for esp32-s3 starts at 0x0000, unlike several other ESP32 variants that start at 0x1000. -esptool.main([ - '--chip', 'esp32s3', - 'merge_bin', '-o', 'merged.bin', - '--flash_size', '4MB', - '0x0', './bootloader.bin', - '0x8000', './partitions.bin', - '0x10000', './app.bin', - '0x353000', './staticfs.bin' # This is littlefs.bin, the github CI/CD pipeline renames it to staticfs.bin -]) -# fmt: on diff --git a/chips/ESP32-S2/4MB/merge-image.py b/chips/ESP32-S2/4MB/merge-image.py deleted file mode 100644 index bfe06d40..00000000 --- a/chips/ESP32-S2/4MB/merge-image.py +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/python3 - -import esptool - -# fmt: off -esptool.main([ - '--chip', 'esp32', - 'merge_bin', '-o', 'merged.bin', - '--flash_size', '4MB', - '0x1000', './bootloader.bin', - '0x8000', './partitions.bin', - '0x10000', './app.bin', - '0x353000', './staticfs.bin' # This is littlefs.bin, the github CI/CD pipeline renames it to staticfs.bin -]) -# fmt: on diff --git a/chips/ESP32-S3/4MB/merge-image.py b/chips/ESP32-S3/4MB/merge-image.py deleted file mode 100644 index 8e76a390..00000000 --- a/chips/ESP32-S3/4MB/merge-image.py +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/python3 - -import esptool - -# fmt: off -# Note: Bootloader for esp32-s3 starts at 0x0000, unlike several other ESP32 variants that start at 0x1000. -esptool.main([ - '--chip', 'esp32s3', - 'merge_bin', '-o', 'merged.bin', - '--flash_size', '4MB', - '0x0', './bootloader.bin', - '0x8000', './partitions.bin', - '0x10000', './app.bin', - '0x353000', './staticfs.bin' # This is littlefs.bin, the github CI/CD pipeline renames it to staticfs.bin -]) -# fmt: on diff --git a/chips/ESP32/4MB/merge-image.py b/chips/ESP32/4MB/merge-image.py deleted file mode 100644 index bfe06d40..00000000 --- a/chips/ESP32/4MB/merge-image.py +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/python3 - -import esptool - -# fmt: off -esptool.main([ - '--chip', 'esp32', - 'merge_bin', '-o', 'merged.bin', - '--flash_size', '4MB', - '0x1000', './bootloader.bin', - '0x8000', './partitions.bin', - '0x10000', './app.bin', - '0x353000', './staticfs.bin' # This is littlefs.bin, the github CI/CD pipeline renames it to staticfs.bin -]) -# fmt: on diff --git a/components/.gitignore b/components/.gitignore new file mode 100644 index 00000000..59e77771 --- /dev/null +++ b/components/.gitignore @@ -0,0 +1,6 @@ +# IDF build artifacts generated by `idf.py` in per-component host tests +# (e.g. components/*/host_test). Applies to every component subdirectory. +# managed_components/ and dependencies.lock are already ignored repo-wide. +build/ +sdkconfig +sdkconfig.old diff --git a/components/chipset/CMakeLists.txt b/components/chipset/CMakeLists.txt new file mode 100644 index 00000000..316bfdd0 --- /dev/null +++ b/components/chipset/CMakeLists.txt @@ -0,0 +1,16 @@ +# OpenShock chipset utilities — GPIO validation, compile-time pin assertions. +# +# Public surface: +# Chipset.h — OpenShock::IsValidInputPin / IsValidOutputPin per IDF_TARGET +# +# CompatibilityChecks.cpp contains static_asserts that fail the build if any +# board config (CONFIG_OPENSHOCK_ESTOP_PIN, CONFIG_OPENSHOCK_RF_TX_GPIO, etc.) resolves to +# an invalid pin for the current chip. It has to be in the link for the +# asserts to run; no runtime code. + +idf_component_register( + SRCS "src/CompatibilityChecks.cpp" + INCLUDE_DIRS "include" + REQUIRES common # Common.h + esp_driver_gpio # driver/gpio.h +) diff --git a/include/Chipset.h b/components/chipset/include/Chipset.h similarity index 99% rename from include/Chipset.h rename to components/chipset/include/Chipset.h index 61ffbfde..35e3d2cc 100644 --- a/include/Chipset.h +++ b/components/chipset/include/Chipset.h @@ -1,6 +1,6 @@ #pragma once -#include "Common.h" +#include "OpenShock.h" #include diff --git a/components/chipset/src/CompatibilityChecks.cpp b/components/chipset/src/CompatibilityChecks.cpp new file mode 100644 index 00000000..9d62d518 --- /dev/null +++ b/components/chipset/src/CompatibilityChecks.cpp @@ -0,0 +1,18 @@ +#include "Chipset.h" +#include "OpenShock.h" + +// Board pins are Kconfig options (CONFIG_OPENSHOCK_*) and always have a value +// (-1 == OPENSHOCK_GPIO_INVALID when the board has no such pin). A pin is valid +// if it's a usable GPIO or explicitly bypassed with INVALID; anything else is a +// misconfiguration and fails the build. +const bool kIsValidOrUndefinedRfTxPin = OpenShock::IsValidOutputPin(CONFIG_OPENSHOCK_RF_TX_GPIO) || CONFIG_OPENSHOCK_RF_TX_GPIO == OPENSHOCK_GPIO_INVALID; +static_assert(kIsValidOrUndefinedRfTxPin, "CONFIG_OPENSHOCK_RF_TX_GPIO is not a valid output GPIO, and is not declared as bypassed by board specific definitions, refusing to compile"); + +const bool kIsValidOrUndefinedEStopPin = OpenShock::IsValidInputPin(CONFIG_OPENSHOCK_ESTOP_PIN) || CONFIG_OPENSHOCK_ESTOP_PIN == OPENSHOCK_GPIO_INVALID; +static_assert(kIsValidOrUndefinedEStopPin, "CONFIG_OPENSHOCK_ESTOP_PIN is not a valid input GPIO, and is not declared as bypassed by board specific definitions, refusing to compile"); + +const bool kIsValidOrUndefinedLedPin = OpenShock::IsValidOutputPin(CONFIG_OPENSHOCK_LED_GPIO) || CONFIG_OPENSHOCK_LED_GPIO == OPENSHOCK_GPIO_INVALID; +static_assert(kIsValidOrUndefinedLedPin, "CONFIG_OPENSHOCK_LED_GPIO is not a valid output GPIO, and is not declared as bypassed by board specific definitions, refusing to compile"); + +const bool kIsValidOrUndefinedWs2812bPin = OpenShock::IsValidOutputPin(CONFIG_OPENSHOCK_LED_WS2812B) || CONFIG_OPENSHOCK_LED_WS2812B == OPENSHOCK_GPIO_INVALID; +static_assert(kIsValidOrUndefinedWs2812bPin, "CONFIG_OPENSHOCK_LED_WS2812B is not a valid output GPIO, and is not declared as bypassed by board specific definitions, refusing to compile"); diff --git a/components/common/CMakeLists.txt b/components/common/CMakeLists.txt new file mode 100644 index 00000000..e682584a --- /dev/null +++ b/components/common/CMakeLists.txt @@ -0,0 +1,25 @@ +# OpenShock foundation utilities — language helpers, format conversions, +# containers, string helpers, and the FreeRTOS-backed concurrency primitives. +# Absorbed the former `util` and `concurrency` components. The mbedTLS-dependent +# helpers (Hashing.h, Base64) live in the separate `crypto` component. +# +# Public surface (include/): +# Common.h / FormatHelpers.h / OpenShock.h — basic defines and macros +# enums/*.h — shared enums (consumers include with the enums/ prefix) +# Convert.h, SemVer.h, Checksum.h, RateLimiter.h, TinyVec.h, +# StringHelpers.h, SimpleMutex.h, ReadWriteMutex.h, util/*.h + +idf_component_register( + SRCS "src/Convert.cpp" + "src/DigitCounter.cpp" + "src/RateLimiter.cpp" + "src/ReadWriteMutex.cpp" + "src/SemVer.cpp" + "src/SimpleMutex.cpp" + "src/StringHelpers.cpp" + "src/TaskUtils.cpp" + INCLUDE_DIRS "include" + REQUIRES logging # Logging.h / OS_PANIC* (TinyVec.h) in public headers + temporal # Temporal.h (RateLimiter.h) in public header + freertos # semphr.h in SimpleMutex.h / ReadWriteMutex.h public headers +) diff --git a/components/common/host_test/CMakeLists.txt b/components/common/host_test/CMakeLists.txt new file mode 100644 index 00000000..c30d26bb --- /dev/null +++ b/components/common/host_test/CMakeLists.txt @@ -0,0 +1,12 @@ +cmake_minimum_required(VERSION 3.16) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) + +# Only build 'main'; the common headers are pulled in by include path (they are +# header-only, so no firmware component needs to be linked). +set(COMPONENTS main) + +# linux-target freertos mock, so unity's IDF integration has its dependencies. +list(APPEND EXTRA_COMPONENT_DIRS "$ENV{IDF_PATH}/tools/mocks/freertos/") + +project(common_host_test) diff --git a/components/common/host_test/main/CMakeLists.txt b/components/common/host_test/main/CMakeLists.txt new file mode 100644 index 00000000..a07236ce --- /dev/null +++ b/components/common/host_test/main/CMakeLists.txt @@ -0,0 +1,36 @@ +# Compile common's PURE sources directly with a stub Logging.h (stubs/) so we avoid +# the firmware-only transitive deps that the real Logging.h pulls in (esp_system / +# esp_ota_ops / serial). Absorbed the former `util` host test after util was merged +# into common. +idf_component_register( + SRCS "test_main.cpp" + "test_enums.cpp" + "test_stringhelpers.cpp" + "test_stringutils.cpp" + "test_convert.cpp" + "test_hexutils.cpp" + "test_digitcounter.cpp" + "test_semver.cpp" + "test_checksum.cpp" + "test_tinyvec.cpp" + "test_fnproxy.cpp" + "../../src/StringHelpers.cpp" # StringSplit/StringIContains/FormatToString (out-of-line) + "../../src/Convert.cpp" + "../../src/DigitCounter.cpp" + "../../src/SemVer.cpp" + INCLUDE_DIRS "." + "stubs" # stub Logging.h (shadows the firmware one) + "../../include" # common's headers (enums/*, Convert.h, SemVer.h, util/*.h) + REQUIRES unity + esp_hal_gpio # Convert.h -> hal/gpio_types.h (moved out of `hal` in IDF 6.0) + WHOLE_ARCHIVE # keep the auto-registered TEST_CASEs from being stripped +) + +# TinyVec.h -> OpenShock.h requires these firmware build-time defines (used as +# string literals). Supply dummies so the pure logic compiles on the host. +target_compile_definitions(${COMPONENT_LIB} PRIVATE + "OPENSHOCK_API_DOMAIN=\"test\"" + "OPENSHOCK_FW_CDN_DOMAIN=\"test\"" + "OPENSHOCK_FW_VERSION=\"0.0.0-test\"" + "OPENSHOCK_FW_USERAGENT=\"host-test\"" +) diff --git a/components/common/host_test/main/stubs/Logging.h b/components/common/host_test/main/stubs/Logging.h new file mode 100644 index 00000000..7389d9dc --- /dev/null +++ b/components/common/host_test/main/stubs/Logging.h @@ -0,0 +1,18 @@ +#pragma once + +// Host-test stub for the firmware Logging.h. The real one pulls in esp_system / +// esp_ota_ops / freertos (for the OS_PANIC* macros) and the `serial` transport, +// none of which exist on the linux host target. common's pure sources only use +// these macros for diagnostics, so no-op the logging ones and abort on panic - +// the logic under test does not depend on logging. +#include +#define OS_LOGV(tag, ...) ((void)0) +#define OS_LOGD(tag, ...) ((void)0) +#define OS_LOGI(tag, ...) ((void)0) +#define OS_LOGW(tag, ...) ((void)0) +#define OS_LOGE(tag, ...) ((void)0) +#define OS_LOGN(tag, ...) ((void)0) +#define OS_LOGWTF(tag, ...) ((void)0) +#define OS_PANIC(tag, ...) ::abort() +#define OS_PANIC_OTA(tag, ...) ::abort() +#define OS_PANIC_INSTANT(tag, ...) ::abort() diff --git a/components/common/host_test/main/test_checksum.cpp b/components/common/host_test/main/test_checksum.cpp new file mode 100644 index 00000000..73951877 --- /dev/null +++ b/components/common/host_test/main/test_checksum.cpp @@ -0,0 +1,50 @@ +// Checksum::Sum8 (byte sum) backs the RF protocol frames; ReverseNibble / +// ReverseInverseNibble are lookup-table bit reversals used by some encoders. +#include "unity.h" + +#include "Checksum.h" + +#include + +using namespace OpenShock; + +// Independent 4-bit reversal for cross-checking the table-driven versions. +static uint8_t rev4(uint8_t b) +{ + return ((b & 1) << 3) | ((b & 2) << 1) | ((b & 4) >> 1) | ((b & 8) >> 3); +} + +TEST_CASE("Sum8 over a byte span wraps at 8 bits", "[util][checksum]") +{ + const uint8_t d[] = {1, 2, 3, 4, 250}; // 260 -> 0x04 + TEST_ASSERT_EQUAL_UINT8(4, Checksum::Sum8(d, sizeof(d))); +} + +TEST_CASE("Sum8 over an integral sums its bytes", "[util][checksum]") +{ + TEST_ASSERT_EQUAL_UINT8(10, Checksum::Sum8(static_cast(0x01020304))); // 1+2+3+4 + TEST_ASSERT_EQUAL_UINT8(0xFF, Checksum::Sum8(static_cast(0xFF))); + TEST_ASSERT_EQUAL_UINT8(0, Checksum::Sum8(static_cast(0))); +} + +TEST_CASE("Sum8 over a trivially copyable struct", "[util][checksum]") +{ + struct Packed { + uint8_t a, b, c, d; + } p{10, 20, 30, 40}; + TEST_ASSERT_EQUAL_UINT8(100, Checksum::Sum8(p)); +} + +TEST_CASE("ReverseNibble reverses the low nibble bit order", "[util][checksum]") +{ + for (uint8_t b = 0; b < 16; b++) { + TEST_ASSERT_EQUAL_UINT8(rev4(b), Checksum::ReverseNibble(b)); + } +} + +TEST_CASE("ReverseInverseNibble reverses the inverted nibble", "[util][checksum]") +{ + for (uint8_t b = 0; b < 16; b++) { + TEST_ASSERT_EQUAL_UINT8(rev4((~b) & 0xF), Checksum::ReverseInverseNibble(b)); + } +} diff --git a/components/common/host_test/main/test_convert.cpp b/components/common/host_test/main/test_convert.cpp new file mode 100644 index 00000000..8efbf4dc --- /dev/null +++ b/components/common/host_test/main/test_convert.cpp @@ -0,0 +1,77 @@ +// Convert::To* string -> integer/bool parsers (used for serial args, config, +// backend responses). Range enforcement matters: an out-of-range shocker id or +// intensity must be rejected, not truncated. +#include "unity.h" + +#include "Convert.h" + +#include + +using namespace OpenShock; + +TEST_CASE("Convert::ToUint8 range", "[util][convert]") +{ + uint8_t v = 0; + TEST_ASSERT_TRUE(Convert::ToUint8("0", v)); + TEST_ASSERT_EQUAL_UINT8(0, v); + TEST_ASSERT_TRUE(Convert::ToUint8("255", v)); + TEST_ASSERT_EQUAL_UINT8(255, v); + TEST_ASSERT_TRUE(Convert::ToUint8("42", v)); + TEST_ASSERT_EQUAL_UINT8(42, v); + + TEST_ASSERT_FALSE(Convert::ToUint8("256", v)); // overflow + TEST_ASSERT_FALSE(Convert::ToUint8("-1", v)); // negative + TEST_ASSERT_FALSE(Convert::ToUint8("", v)); // empty + TEST_ASSERT_FALSE(Convert::ToUint8("abc", v)); // not a number + TEST_ASSERT_FALSE(Convert::ToUint8("12x", v)); // trailing junk +} + +TEST_CASE("Convert::ToInt8 signed range", "[util][convert]") +{ + int8_t v = 0; + TEST_ASSERT_TRUE(Convert::ToInt8("-128", v)); + TEST_ASSERT_EQUAL_INT8(-128, v); + TEST_ASSERT_TRUE(Convert::ToInt8("127", v)); + TEST_ASSERT_EQUAL_INT8(127, v); + + TEST_ASSERT_FALSE(Convert::ToInt8("128", v)); + TEST_ASSERT_FALSE(Convert::ToInt8("-129", v)); +} + +TEST_CASE("Convert::ToUint16 / ToInt32 boundaries", "[util][convert]") +{ + uint16_t u16 = 0; + TEST_ASSERT_TRUE(Convert::ToUint16("65535", u16)); + TEST_ASSERT_EQUAL_UINT16(65535, u16); + TEST_ASSERT_FALSE(Convert::ToUint16("65536", u16)); + + int32_t i32 = 0; + TEST_ASSERT_TRUE(Convert::ToInt32("-2147483648", i32)); + TEST_ASSERT_EQUAL_INT32(INT32_MIN, i32); + TEST_ASSERT_TRUE(Convert::ToInt32("2147483647", i32)); + TEST_ASSERT_EQUAL_INT32(INT32_MAX, i32); + TEST_ASSERT_FALSE(Convert::ToInt32("2147483648", i32)); +} + +TEST_CASE("Convert::ToUint64 large values", "[util][convert]") +{ + uint64_t v = 0; + TEST_ASSERT_TRUE(Convert::ToUint64("18446744073709551615", v)); // UINT64_MAX + TEST_ASSERT_EQUAL_UINT64(UINT64_MAX, v); + TEST_ASSERT_FALSE(Convert::ToUint64("18446744073709551616", v)); // overflow +} + +TEST_CASE("Convert::ToBool", "[util][convert]") +{ + bool b = false; + TEST_ASSERT_TRUE(Convert::ToBool("true", b)); + TEST_ASSERT_TRUE(b); + TEST_ASSERT_TRUE(Convert::ToBool("false", b)); + TEST_ASSERT_FALSE(b); + TEST_ASSERT_TRUE(Convert::ToBool("TRUE", b)); // case-insensitive + TEST_ASSERT_TRUE(b); + + TEST_ASSERT_FALSE(Convert::ToBool("yes", b)); + TEST_ASSERT_FALSE(Convert::ToBool("1", b)); + TEST_ASSERT_FALSE(Convert::ToBool("", b)); +} diff --git a/components/common/host_test/main/test_digitcounter.cpp b/components/common/host_test/main/test_digitcounter.cpp new file mode 100644 index 00000000..f178e3af --- /dev/null +++ b/components/common/host_test/main/test_digitcounter.cpp @@ -0,0 +1,27 @@ +// Util::Digits10Count - base-10 digit count (incl. sign) used to size buffers. +#include "unity.h" + +#include "util/DigitCounter.h" + +#include + +using namespace OpenShock; + +TEST_CASE("Digits10Count unsigned", "[util][digits]") +{ + TEST_ASSERT_EQUAL_size_t(1, Util::Digits10Count(0)); + TEST_ASSERT_EQUAL_size_t(1, Util::Digits10Count(9)); + TEST_ASSERT_EQUAL_size_t(2, Util::Digits10Count(10)); + TEST_ASSERT_EQUAL_size_t(3, Util::Digits10Count(999)); + TEST_ASSERT_EQUAL_size_t(4, Util::Digits10Count(1000)); + TEST_ASSERT_EQUAL_size_t(20, Util::Digits10Count(UINT64_MAX)); +} + +TEST_CASE("Digits10Count signed counts the sign", "[util][digits]") +{ + TEST_ASSERT_EQUAL_size_t(1, Util::Digits10Count(0)); + TEST_ASSERT_EQUAL_size_t(1, Util::Digits10Count(7)); + TEST_ASSERT_EQUAL_size_t(2, Util::Digits10Count(-1)); // '-' + '1' + TEST_ASSERT_EQUAL_size_t(4, Util::Digits10Count(-123)); + TEST_ASSERT_EQUAL_size_t(11, Util::Digits10Count(INT32_MIN)); // -2147483648 +} diff --git a/components/common/host_test/main/test_enums.cpp b/components/common/host_test/main/test_enums.cpp new file mode 100644 index 00000000..990188fe --- /dev/null +++ b/components/common/host_test/main/test_enums.cpp @@ -0,0 +1,163 @@ +// Parser coverage for the shared enum headers. These decide how user/backend +// strings map to shocker models, commands, OTA channels/steps and boot types - +// getting them wrong misroutes real commands, so pin every accepted spelling. +#include "unity.h" + +#include "enums/FirmwareBootType.h" +#include "enums/OtaUpdateChannel.h" +#include "enums/OtaUpdateStep.h" +#include "enums/ShockerCommandType.h" +#include "enums/ShockerModelType.h" + +using namespace OpenShock; + +// ---- ShockerModelType ------------------------------------------------------ + +TEST_CASE("ShockerModelType: all accepted spellings", "[common][enums]") +{ + ShockerModelType m; + TEST_ASSERT_TRUE(TryParseShockerModelType(m, "caixianlin")); + TEST_ASSERT_EQUAL(ShockerModelType::CaiXianlin, m); + TEST_ASSERT_TRUE(TryParseShockerModelType(m, "cai-xianlin")); + TEST_ASSERT_EQUAL(ShockerModelType::CaiXianlin, m); + TEST_ASSERT_TRUE(TryParseShockerModelType(m, "petrainer")); + TEST_ASSERT_EQUAL(ShockerModelType::Petrainer, m); + TEST_ASSERT_TRUE(TryParseShockerModelType(m, "petrainer998dr")); + TEST_ASSERT_EQUAL(ShockerModelType::Petrainer998DR, m); + TEST_ASSERT_TRUE(TryParseShockerModelType(m, "wellturnt330")); + TEST_ASSERT_EQUAL(ShockerModelType::WellturnT330, m); + TEST_ASSERT_TRUE(TryParseShockerModelType(m, "t330")); + TEST_ASSERT_EQUAL(ShockerModelType::WellturnT330, m); + TEST_ASSERT_TRUE(TryParseShockerModelType(m, "d80")); + TEST_ASSERT_EQUAL(ShockerModelType::D80, m); +} + +TEST_CASE("ShockerModelType: case-insensitive", "[common][enums]") +{ + ShockerModelType m; + TEST_ASSERT_TRUE(TryParseShockerModelType(m, "CaiXianlin")); + TEST_ASSERT_EQUAL(ShockerModelType::CaiXianlin, m); + TEST_ASSERT_TRUE(TryParseShockerModelType(m, "PETRAINER")); + TEST_ASSERT_EQUAL(ShockerModelType::Petrainer, m); + TEST_ASSERT_TRUE(TryParseShockerModelType(m, "T330")); + TEST_ASSERT_EQUAL(ShockerModelType::WellturnT330, m); +} + +TEST_CASE("ShockerModelType: the 'pettrainer' typo only with allowTypo", "[common][enums]") +{ + ShockerModelType m; + TEST_ASSERT_FALSE(TryParseShockerModelType(m, "pettrainer")); // default: rejected + TEST_ASSERT_FALSE(TryParseShockerModelType(m, "pettrainer998dr")); + + TEST_ASSERT_TRUE(TryParseShockerModelType(m, "pettrainer", true)); // allowTypo: accepted + TEST_ASSERT_EQUAL(ShockerModelType::Petrainer, m); + TEST_ASSERT_TRUE(TryParseShockerModelType(m, "pettrainer998dr", true)); + TEST_ASSERT_EQUAL(ShockerModelType::Petrainer998DR, m); +} + +TEST_CASE("ShockerModelType: rejects unknown and empty", "[common][enums]") +{ + ShockerModelType m; + TEST_ASSERT_FALSE(TryParseShockerModelType(m, "")); + TEST_ASSERT_FALSE(TryParseShockerModelType(m, "nope")); + TEST_ASSERT_FALSE(TryParseShockerModelType(m, "cai")); // prefix, not a full match +} + +// ---- ShockerCommandType ---------------------------------------------------- + +TEST_CASE("ShockerCommandType: all commands + case-insensitive", "[common][enums]") +{ + ShockerCommandType c; + TEST_ASSERT_TRUE(TryParseShockerCommandType(c, "stop")); + TEST_ASSERT_EQUAL(ShockerCommandType::Stop, c); + TEST_ASSERT_TRUE(TryParseShockerCommandType(c, "shock")); + TEST_ASSERT_EQUAL(ShockerCommandType::Shock, c); + TEST_ASSERT_TRUE(TryParseShockerCommandType(c, "vibrate")); + TEST_ASSERT_EQUAL(ShockerCommandType::Vibrate, c); + TEST_ASSERT_TRUE(TryParseShockerCommandType(c, "sound")); + TEST_ASSERT_EQUAL(ShockerCommandType::Sound, c); + TEST_ASSERT_TRUE(TryParseShockerCommandType(c, "LIGHT")); + TEST_ASSERT_EQUAL(ShockerCommandType::Light, c); +} + +TEST_CASE("ShockerCommandType: rejects unknown/empty", "[common][enums]") +{ + ShockerCommandType c; + TEST_ASSERT_FALSE(TryParseShockerCommandType(c, "")); + TEST_ASSERT_FALSE(TryParseShockerCommandType(c, "zap")); +} + +// ---- OtaUpdateChannel (string_view API) ------------------------------------ + +TEST_CASE("OtaUpdateChannel: stable/beta/develop/dev + case", "[common][enums]") +{ + OtaUpdateChannel ch; + TEST_ASSERT_TRUE(TryParseOtaUpdateChannel(ch, "stable")); + TEST_ASSERT_EQUAL(OtaUpdateChannel::Stable, ch); + TEST_ASSERT_TRUE(TryParseOtaUpdateChannel(ch, "Beta")); + TEST_ASSERT_EQUAL(OtaUpdateChannel::Beta, ch); + TEST_ASSERT_TRUE(TryParseOtaUpdateChannel(ch, "develop")); + TEST_ASSERT_EQUAL(OtaUpdateChannel::Develop, ch); + TEST_ASSERT_TRUE(TryParseOtaUpdateChannel(ch, "DEV")); + TEST_ASSERT_EQUAL(OtaUpdateChannel::Develop, ch); + + TEST_ASSERT_FALSE(TryParseOtaUpdateChannel(ch, "")); + TEST_ASSERT_FALSE(TryParseOtaUpdateChannel(ch, "nightly")); + TEST_ASSERT_FALSE(TryParseOtaUpdateChannel(ch, "stabl")); // prefix, not full +} + +// ---- OtaUpdateStep --------------------------------------------------------- + +TEST_CASE("OtaUpdateStep: every step + rejects unknown", "[common][enums]") +{ + OtaUpdateStep s; + TEST_ASSERT_TRUE(TryParseOtaUpdateStep(s, "none")); + TEST_ASSERT_EQUAL(OtaUpdateStep::None, s); + TEST_ASSERT_TRUE(TryParseOtaUpdateStep(s, "updating")); + TEST_ASSERT_EQUAL(OtaUpdateStep::Updating, s); + TEST_ASSERT_TRUE(TryParseOtaUpdateStep(s, "updated")); + TEST_ASSERT_EQUAL(OtaUpdateStep::Updated, s); + TEST_ASSERT_TRUE(TryParseOtaUpdateStep(s, "validating")); + TEST_ASSERT_EQUAL(OtaUpdateStep::Validating, s); + TEST_ASSERT_TRUE(TryParseOtaUpdateStep(s, "validated")); + TEST_ASSERT_EQUAL(OtaUpdateStep::Validated, s); + TEST_ASSERT_TRUE(TryParseOtaUpdateStep(s, "RollingBack")); + TEST_ASSERT_EQUAL(OtaUpdateStep::RollingBack, s); + + TEST_ASSERT_FALSE(TryParseOtaUpdateStep(s, "")); + TEST_ASSERT_FALSE(TryParseOtaUpdateStep(s, "done")); +} + +// ---- FirmwareBootType (const char* API) ------------------------------------ + +TEST_CASE("FirmwareBootType: normal/newfirmware/new_firmware/rollback", "[common][enums]") +{ + FirmwareBootType b; + TEST_ASSERT_TRUE(TryParseFirmwareBootType(b, "normal")); + TEST_ASSERT_EQUAL(FirmwareBootType::Normal, b); + TEST_ASSERT_TRUE(TryParseFirmwareBootType(b, "newfirmware")); + TEST_ASSERT_EQUAL(FirmwareBootType::NewFirmware, b); + TEST_ASSERT_TRUE(TryParseFirmwareBootType(b, "new_firmware")); + TEST_ASSERT_EQUAL(FirmwareBootType::NewFirmware, b); + TEST_ASSERT_TRUE(TryParseFirmwareBootType(b, "ROLLBACK")); + TEST_ASSERT_EQUAL(FirmwareBootType::Rollback, b); + + TEST_ASSERT_FALSE(TryParseFirmwareBootType(b, "")); + TEST_ASSERT_FALSE(TryParseFirmwareBootType(b, "reboot")); +} + +TEST_CASE("InferFirmwareBootType: maps every OTA step to a boot type", "[common][enums]") +{ + // Only 'Updated' means we just flashed new firmware this boot. + TEST_ASSERT_EQUAL(FirmwareBootType::NewFirmware, InferFirmwareBootType(OtaUpdateStep::Updated)); + + // A crash while validating, or an explicit rolling-back step, means this boot is + // the previous image after a rollback. + TEST_ASSERT_EQUAL(FirmwareBootType::Rollback, InferFirmwareBootType(OtaUpdateStep::Validating)); + TEST_ASSERT_EQUAL(FirmwareBootType::Rollback, InferFirmwareBootType(OtaUpdateStep::RollingBack)); + + // Everything else is a normal boot. + TEST_ASSERT_EQUAL(FirmwareBootType::Normal, InferFirmwareBootType(OtaUpdateStep::None)); + TEST_ASSERT_EQUAL(FirmwareBootType::Normal, InferFirmwareBootType(OtaUpdateStep::Updating)); + TEST_ASSERT_EQUAL(FirmwareBootType::Normal, InferFirmwareBootType(OtaUpdateStep::Validated)); +} diff --git a/components/common/host_test/main/test_fnproxy.cpp b/components/common/host_test/main/test_fnproxy.cpp new file mode 100644 index 00000000..548faa39 --- /dev/null +++ b/components/common/host_test/main/test_fnproxy.cpp @@ -0,0 +1,43 @@ +// FnProxy<&Class::method> yields a plain R(*)(void*, Ts...) trampoline used to +// hand C++ member functions to C-style callback APIs (FreeRTOS tasks, esp_event). +// Verify it forwards args and returns for the const/non-const overloads. +#include "unity.h" + +#include "util/FnProxy.h" + +#include + +using OpenShock::Util::FnProxy; + +namespace { + struct Counter { + int value = 0; + int add(int x) { return value += x; } + int get() const noexcept { return value; } + }; +} // namespace + +// noexcept propagates onto the function-pointer type; plain methods stay plain. +static_assert(std::is_same_v)>, + int (*)(void*, int)>); +static_assert(std::is_same_v)>, + int (*)(void*) noexcept>); + +TEST_CASE("FnProxy forwards to a non-const member", "[util][fnproxy]") +{ + Counter c; + auto fn = FnProxy<&Counter::add>; // int(*)(void*, int) + + TEST_ASSERT_EQUAL_INT(5, fn(&c, 5)); + TEST_ASSERT_EQUAL_INT(8, fn(&c, 3)); + TEST_ASSERT_EQUAL_INT(8, c.value); +} + +TEST_CASE("FnProxy forwards to a const noexcept member", "[util][fnproxy]") +{ + Counter c; + c.value = 42; + auto fn = FnProxy<&Counter::get>; // int(*)(void*) + + TEST_ASSERT_EQUAL_INT(42, fn(&c)); +} diff --git a/components/common/host_test/main/test_hexutils.cpp b/components/common/host_test/main/test_hexutils.cpp new file mode 100644 index 00000000..f6957696 --- /dev/null +++ b/components/common/host_test/main/test_hexutils.cpp @@ -0,0 +1,67 @@ +// HexUtils: byte<->hex conversions used for BSSID, partition hashes, etc. +#include "unity.h" + +#include "util/HexUtils.h" + +#include +#include +#include + +using namespace OpenShock; + +TEST_CASE("ToHex single byte, upper and lower", "[util][hex]") +{ + char buf[3] = {0}; + HexUtils::ToHex(0xAB, buf); + TEST_ASSERT_EQUAL_STRING("AB", buf); + HexUtils::ToHex(0x0F, buf); + TEST_ASSERT_EQUAL_STRING("0F", buf); + HexUtils::ToHex(0xAB, buf, false); // lower + TEST_ASSERT_EQUAL_STRING("ab", buf); + HexUtils::ToHex(0x00, buf); + TEST_ASSERT_EQUAL_STRING("00", buf); +} + +TEST_CASE("ToHex array -> array", "[util][hex]") +{ + const uint8_t mac[6] = {0xDE, 0xAD, 0xBE, 0xEF, 0x01, 0x23}; + auto out = HexUtils::ToHex<6>(mac); + TEST_ASSERT_EQUAL_STRING("DEADBEEF0123", out.data()); +} + +TEST_CASE("TryParseHex round-trips", "[util][hex]") +{ + uint8_t out[3] = {0}; + TEST_ASSERT_EQUAL_size_t(3, HexUtils::TryParseHex("aBcDeF", 6, out, 3)); + TEST_ASSERT_EQUAL_HEX8(0xAB, out[0]); + TEST_ASSERT_EQUAL_HEX8(0xCD, out[1]); + TEST_ASSERT_EQUAL_HEX8(0xEF, out[2]); + + // odd length / bad chars / small buffer -> 0 + TEST_ASSERT_EQUAL_size_t(0, HexUtils::TryParseHex("ABC", 3, out, 3)); // odd + TEST_ASSERT_EQUAL_size_t(0, HexUtils::TryParseHex("ABGH", 4, out, 3)); // non-hex + TEST_ASSERT_EQUAL_size_t(0, HexUtils::TryParseHex("ABCDEF", 6, out, 2)); // buffer too small +} + +TEST_CASE("TryParseHexPair", "[util][hex]") +{ + uint8_t v = 0; + TEST_ASSERT_TRUE(HexUtils::TryParseHexPair('A', 'b', v)); + TEST_ASSERT_EQUAL_HEX8(0xAB, v); + TEST_ASSERT_TRUE(HexUtils::TryParseHexPair('0', '0', v)); + TEST_ASSERT_EQUAL_HEX8(0x00, v); + TEST_ASSERT_FALSE(HexUtils::TryParseHexPair('G', '0', v)); // invalid high nibble + TEST_ASSERT_FALSE(HexUtils::TryParseHexPair('0', 'Z', v)); // invalid low nibble +} + +TEST_CASE("TryParseHexMac", "[util][hex]") +{ + uint8_t out[6] = {0}; + const char* mac = "DE:AD:BE:EF:01:23"; + TEST_ASSERT_EQUAL_size_t(6, HexUtils::TryParseHexMac(mac, std::strlen(mac), out, 6)); + TEST_ASSERT_EQUAL_HEX8(0xDE, out[0]); + TEST_ASSERT_EQUAL_HEX8(0x23, out[5]); + + const char* bad = "DE-AD-BE"; // wrong separator + TEST_ASSERT_EQUAL_size_t(0, HexUtils::TryParseHexMac(bad, std::strlen(bad), out, 6)); +} diff --git a/components/common/host_test/main/test_main.cpp b/components/common/host_test/main/test_main.cpp new file mode 100644 index 00000000..bc9d814b --- /dev/null +++ b/components/common/host_test/main/test_main.cpp @@ -0,0 +1,10 @@ +// Host-only (linux target) test binary for the common component's header-only +// logic. FreeRTOS is mocked, so there is no ESP startup - drive Unity directly. +#include "unity.h" + +extern "C" int main(void) +{ + UNITY_BEGIN(); + unity_run_all_tests(); + return UNITY_END(); +} diff --git a/components/common/host_test/main/test_semver.cpp b/components/common/host_test/main/test_semver.cpp new file mode 100644 index 00000000..ccd2f88d --- /dev/null +++ b/components/common/host_test/main/test_semver.cpp @@ -0,0 +1,104 @@ +// SemVer parsing + ordering - used for OTA/firmware version comparisons. +#include "unity.h" + +#include "SemVer.h" + +#include + +using namespace OpenShock; + +TEST_CASE("TryParseSemVer basic", "[util][semver]") +{ + SemVer v; + TEST_ASSERT_TRUE(TryParseSemVer("1.2.3", v)); + TEST_ASSERT_EQUAL_UINT16(1, v.major); + TEST_ASSERT_EQUAL_UINT16(2, v.minor); + TEST_ASSERT_EQUAL_UINT16(3, v.patch); + TEST_ASSERT_TRUE(v.prerelease.empty()); + TEST_ASSERT_TRUE(v.build.empty()); + TEST_ASSERT_TRUE(v.isValid()); +} + +TEST_CASE("TryParseSemVer with dotted prerelease and build", "[util][semver]") +{ + // OpenShock's own RC tag format, e.g. 1.6.0-rc.1 / 1.5.0-rc.7. + SemVer v; + TEST_ASSERT_TRUE(TryParseSemVer("1.5.0-rc.7+build.9", v)); + TEST_ASSERT_EQUAL_UINT16(1, v.major); + TEST_ASSERT_EQUAL_UINT16(5, v.minor); + TEST_ASSERT_EQUAL_UINT16(0, v.patch); + TEST_ASSERT_TRUE(v.prerelease == "rc.7"); + TEST_ASSERT_TRUE(v.build == "build.9"); +} + +TEST_CASE("TryParseSemVer rejects malformed", "[util][semver]") +{ + SemVer v; + TEST_ASSERT_FALSE(TryParseSemVer("1.2", v)); + TEST_ASSERT_FALSE(TryParseSemVer("", v)); + TEST_ASSERT_FALSE(TryParseSemVer("abc", v)); + TEST_ASSERT_FALSE(TryParseSemVer("1.2.x", v)); + TEST_ASSERT_FALSE(TryParseSemVer("1.2.3.4", v)); // core has too many parts +} + +TEST_CASE("SemVer ordering: core precedence", "[util][semver]") +{ + SemVer a, b; + TEST_ASSERT_TRUE(TryParseSemVer("1.2.3", a)); + TEST_ASSERT_TRUE(TryParseSemVer("1.2.4", b)); + TEST_ASSERT_TRUE(a < b); + TEST_ASSERT_TRUE(b > a); + TEST_ASSERT_TRUE(a != b); + + SemVer major1, major2; + TEST_ASSERT_TRUE(TryParseSemVer("2.0.0", major2)); + TEST_ASSERT_TRUE(TryParseSemVer("1.99.99", major1)); + TEST_ASSERT_TRUE(major1 < major2); +} + +TEST_CASE("SemVer ordering: a prerelease is below its release", "[util][semver]") +{ + SemVer rel, pre; + TEST_ASSERT_TRUE(TryParseSemVer("1.0.0", rel)); + TEST_ASSERT_TRUE(TryParseSemVer("1.0.0-rc1", pre)); + TEST_ASSERT_TRUE(pre < rel); + TEST_ASSERT_TRUE(rel > pre); +} + +TEST_CASE("SemVer ordering: dotted RCs sort numerically", "[util][semver]") +{ + // The bug this replaces: 1.5.0-rc.6 and 1.5.0-rc.7 used to parse equal. + SemVer rc6, rc7; + TEST_ASSERT_TRUE(TryParseSemVer("1.5.0-rc.6", rc6)); + TEST_ASSERT_TRUE(TryParseSemVer("1.5.0-rc.7", rc7)); + TEST_ASSERT_TRUE(rc6 < rc7); + TEST_ASSERT_TRUE(rc6 != rc7); + + // numeric identifiers rank below alphanumeric (rc.9 < rc.beta) + SemVer rc9, rcbeta; + TEST_ASSERT_TRUE(TryParseSemVer("1.0.0-rc.9", rc9)); + TEST_ASSERT_TRUE(TryParseSemVer("1.0.0-rc.beta", rcbeta)); + TEST_ASSERT_TRUE(rc9 < rcbeta); + + // more identifiers outrank fewer when the prefix is equal (rc < rc.1) + SemVer rc, rc1; + TEST_ASSERT_TRUE(TryParseSemVer("1.0.0-rc", rc)); + TEST_ASSERT_TRUE(TryParseSemVer("1.0.0-rc.1", rc1)); + TEST_ASSERT_TRUE(rc < rc1); +} + +TEST_CASE("SemVer: build metadata is ignored for precedence", "[util][semver]") +{ + SemVer a, b; + TEST_ASSERT_TRUE(TryParseSemVer("1.0.0+build1", a)); + TEST_ASSERT_TRUE(TryParseSemVer("1.0.0+build2", b)); + TEST_ASSERT_FALSE(a < b); + TEST_ASSERT_FALSE(b < a); +} + +TEST_CASE("SemVer toString round-trips with dots", "[util][semver]") +{ + SemVer v; + TEST_ASSERT_TRUE(TryParseSemVer("3.1.4-beta.2+build.9", v)); + TEST_ASSERT_TRUE(v.toString() == "3.1.4-beta.2+build.9"); +} diff --git a/components/common/host_test/main/test_stringhelpers.cpp b/components/common/host_test/main/test_stringhelpers.cpp new file mode 100644 index 00000000..fcf4bff4 --- /dev/null +++ b/components/common/host_test/main/test_stringhelpers.cpp @@ -0,0 +1,43 @@ +// StringHelpers::StringIEquals - the shared case-insensitive compare used by the +// enum parsers and util. Length-aware, so it must be correct on non-terminated +// views too. +#include "unity.h" + +#include "StringHelpers.h" + +#include + +using namespace OpenShock; + +TEST_CASE("StringIEquals: equal regardless of case", "[common][strhelpers]") +{ + TEST_ASSERT_TRUE(StringIEquals("hello", "hello")); + TEST_ASSERT_TRUE(StringIEquals("Hello", "hELLo")); + TEST_ASSERT_TRUE(StringIEquals("ABC123", "abc123")); + TEST_ASSERT_TRUE(StringIEquals("", "")); +} + +TEST_CASE("StringIEquals: unequal content or length", "[common][strhelpers]") +{ + TEST_ASSERT_FALSE(StringIEquals("hello", "world")); + TEST_ASSERT_FALSE(StringIEquals("hello", "hell")); // different length + TEST_ASSERT_FALSE(StringIEquals("hell", "hello")); + TEST_ASSERT_FALSE(StringIEquals("", "x")); + TEST_ASSERT_FALSE(StringIEquals("x", "")); + TEST_ASSERT_FALSE(StringIEquals("abc", "abd")); +} + +TEST_CASE("StringIEquals: only ASCII letters are folded", "[common][strhelpers]") +{ + // digits/punctuation compare exactly; case folding is A-Z / a-z only + TEST_ASSERT_TRUE(StringIEquals("a-b_c", "A-B_C")); + TEST_ASSERT_FALSE(StringIEquals("a-b", "a_b")); +} + +TEST_CASE("StringIEquals: works on non-NUL-terminated sub-views", "[common][strhelpers]") +{ + std::string_view full = "prefixHELLOsuffix"; + std::string_view mid = full.substr(6, 5); // "HELLO", not NUL-terminated + TEST_ASSERT_TRUE(StringIEquals(mid, "hello")); + TEST_ASSERT_FALSE(StringIEquals(mid, "hell")); +} diff --git a/components/common/host_test/main/test_stringutils.cpp b/components/common/host_test/main/test_stringutils.cpp new file mode 100644 index 00000000..9623942e --- /dev/null +++ b/components/common/host_test/main/test_stringutils.cpp @@ -0,0 +1,80 @@ +// StringHelpers: trimming, prefix/suffix, splitting. All std::string_view based. +#include "unity.h" + +#include "StringHelpers.h" + +#include +#include + +using namespace OpenShock; + +TEST_CASE("StringTrim variants", "[common][strhelpers]") +{ + TEST_ASSERT_TRUE(StringTrim(" hi ") == "hi"); + TEST_ASSERT_TRUE(StringTrimLeft(" hi ") == "hi "); + TEST_ASSERT_TRUE(StringTrimRight(" hi ") == " hi"); + TEST_ASSERT_TRUE(StringTrim("") == ""); + TEST_ASSERT_TRUE(StringTrim(" ") == ""); + TEST_ASSERT_TRUE(StringTrim("nospace") == "nospace"); + TEST_ASSERT_TRUE(StringTrim("\t\n x \r\n") == "x"); +} + +TEST_CASE("StringHasPrefix / HasSuffix", "[common][strhelpers]") +{ + TEST_ASSERT_TRUE(StringHasPrefix("hello", 'h')); + TEST_ASSERT_FALSE(StringHasPrefix("hello", 'x')); + TEST_ASSERT_TRUE(StringHasPrefix("hello", std::string_view("hel"))); + TEST_ASSERT_FALSE(StringHasPrefix("hello", std::string_view("world"))); + TEST_ASSERT_FALSE(StringHasPrefix("hi", std::string_view("hello"))); // longer than view +} + +TEST_CASE("StringSplitByFirst / ByLast", "[common][strhelpers]") +{ + auto [a, b] = StringSplitByFirst("key=value=extra", '='); + TEST_ASSERT_TRUE(a == "key"); + TEST_ASSERT_TRUE(b == "value=extra"); + + auto [c, d] = StringSplitByLast("a.b.c", '.'); + TEST_ASSERT_TRUE(c == "a.b"); + TEST_ASSERT_TRUE(d == "c"); + + // no delimiter -> first half is the whole string + auto [e, f] = StringSplitByFirst("nodelim", '='); + TEST_ASSERT_TRUE(e == "nodelim"); +} + +TEST_CASE("TryStringSplit into a fixed array", "[common][strhelpers]") +{ + std::string_view parts[4]; + TEST_ASSERT_TRUE(TryStringSplit("192.168.0.1", '.', parts)); + TEST_ASSERT_TRUE(parts[0] == "192"); + TEST_ASSERT_TRUE(parts[1] == "168"); + TEST_ASSERT_TRUE(parts[2] == "0"); + TEST_ASSERT_TRUE(parts[3] == "1"); + + std::string_view three[4]; + TEST_ASSERT_FALSE(TryStringSplit("1.2.3", '.', three)); // too few -> false + + // "too many": the last slot keeps the whole remainder (nothing dropped), so a + // caller like IPAddressUtils then rejects it via Convert::ToUint8("4.5"). + std::string_view five[4]; + TEST_ASSERT_TRUE(TryStringSplit("1.2.3.4.5", '.', five)); + TEST_ASSERT_TRUE(five[3] == "4.5"); +} + +TEST_CASE("StringSplit into a vector", "[common][strhelpers]") +{ + auto v = StringSplit("a,b,,c", ','); + TEST_ASSERT_EQUAL_size_t(4, v.size()); + TEST_ASSERT_TRUE(v[0] == "a"); + TEST_ASSERT_TRUE(v[2] == ""); // empty field preserved + TEST_ASSERT_TRUE(v[3] == "c"); +} + +TEST_CASE("StringIContains / StringHasPrefixIC", "[common][strhelpers]") +{ + TEST_ASSERT_TRUE(StringIContains("Hello World", "LO WO")); + TEST_ASSERT_FALSE(StringIContains("Hello", "xyz")); + TEST_ASSERT_TRUE(StringHasPrefixIC("HELLO", "hel")); + TEST_ASSERT_FALSE(StringHasPrefixIC("hello", "world")); +} diff --git a/components/common/host_test/main/test_tinyvec.cpp b/components/common/host_test/main/test_tinyvec.cpp new file mode 100644 index 00000000..a6c526a1 --- /dev/null +++ b/components/common/host_test/main/test_tinyvec.cpp @@ -0,0 +1,82 @@ +// TinyVec is the firmware's POD-only dynamic array (manual malloc/realloc growth). +// It underpins buffer assembly across the codebase, so exercise its growth, +// zero-fill, replace and move semantics. +#include "unity.h" + +#include "TinyVec.h" + +#include +#include +#include + +TEST_CASE("TinyVec append grows and preserves data", "[util][tinyvec]") +{ + TinyVec v; + TEST_ASSERT_TRUE(v.empty()); + + const uint16_t a[] = {1, 2, 3}; + v.append(a, 3); + TEST_ASSERT_EQUAL_UINT32(3, v.size()); + TEST_ASSERT_GREATER_OR_EQUAL_UINT32(3, v.capacity()); + + const uint16_t b[] = {4, 5}; + v.append(b, 2); + TEST_ASSERT_EQUAL_UINT32(5, v.size()); + for (uint16_t i = 0; i < 5; i++) { + TEST_ASSERT_EQUAL_UINT16(i + 1, v[i]); + } +} + +TEST_CASE("TinyVec resize zero-fills the grown region", "[util][tinyvec]") +{ + TinyVec v; + const uint8_t a[] = {0xAA, 0xBB}; + v.append(a, 2); + + v.resize(5); + TEST_ASSERT_EQUAL_UINT32(5, v.size()); + TEST_ASSERT_EQUAL_UINT8(0xAA, v[0]); + TEST_ASSERT_EQUAL_UINT8(0xBB, v[1]); + TEST_ASSERT_EQUAL_UINT8(0, v[2]); + TEST_ASSERT_EQUAL_UINT8(0, v[4]); +} + +TEST_CASE("TinyVec assign replaces the contents", "[util][tinyvec]") +{ + TinyVec v; + const int a[] = {7, 8, 9, 10}; + v.append(a, 4); + + const int b[] = {1, 2}; + v.assign(b, 2); + TEST_ASSERT_EQUAL_UINT32(2, v.size()); + TEST_ASSERT_EQUAL_INT(1, v[0]); + TEST_ASSERT_EQUAL_INT(2, v[1]); + + v.clear(); + TEST_ASSERT_TRUE(v.empty()); +} + +TEST_CASE("TinyVec move transfers ownership", "[util][tinyvec]") +{ + TinyVec v; + const uint32_t a[] = {100, 200}; + v.append(a, 2); + + TinyVec w(std::move(v)); + TEST_ASSERT_EQUAL_UINT32(2, w.size()); + TEST_ASSERT_EQUAL_UINT32(100, w[0]); + TEST_ASSERT_EQUAL_UINT32(0, v.size()); // moved-from is emptied +} + +TEST_CASE("TinyVec constructs from a buffer and converts to span", "[util][tinyvec]") +{ + const uint8_t a[] = {5, 6, 7}; + TinyVec v(a, 3); + TEST_ASSERT_EQUAL_UINT32(3, v.size()); + TEST_ASSERT_EQUAL_UINT8(6, v[1]); + + std::span s = v; + TEST_ASSERT_EQUAL_UINT32(3, s.size()); + TEST_ASSERT_EQUAL_UINT8(7, s[2]); +} diff --git a/components/common/host_test/sdkconfig.defaults b/components/common/host_test/sdkconfig.defaults new file mode 100644 index 00000000..c3d7cf2c --- /dev/null +++ b/components/common/host_test/sdkconfig.defaults @@ -0,0 +1,4 @@ +CONFIG_IDF_TARGET="linux" +CONFIG_IDF_TARGET_LINUX=y +CONFIG_COMPILER_CXX_EXCEPTIONS=y +CONFIG_UNITY_ENABLE_IDF_TEST_RUNNER=y diff --git a/components/common/include/Checksum.h b/components/common/include/Checksum.h new file mode 100644 index 00000000..36161bad --- /dev/null +++ b/components/common/include/Checksum.h @@ -0,0 +1,71 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace OpenShock::Checksum { + + // ------------------------------------------------------------ + // Raw byte span checksum + // ------------------------------------------------------------ + constexpr uint8_t Sum8(const uint8_t* data, std::size_t size) + { + uint8_t checksum = 0; + for (std::size_t i = 0; i < size; ++i) { + checksum += data[i]; + } + return checksum; + } + + // ------------------------------------------------------------ + // Generic integral overload (C++20 concepts) + // ------------------------------------------------------------ + template + constexpr uint8_t Sum8(T value) + { + // Shift on the unsigned representation; shifting signed values is implementation-defined for negatives + std::make_unsigned_t bits = static_cast>(value); + + uint8_t result = 0; + + for (std::size_t i = 0; i < sizeof(T); ++i) { + result += static_cast((bits >> (i * 8)) & 0xFF); + } + + return result; + } + + // ------------------------------------------------------------ + // Generic trivially copyable object overload + // ------------------------------------------------------------ + template + requires(!std::integral && std::is_trivially_copyable_v) + constexpr uint8_t Sum8(const T& data) + { + static_assert(std::is_trivially_copyable_v, "Sum8 only supports trivially copyable types"); + + return Sum8(reinterpret_cast(std::addressof(data)), sizeof(T)); + } + + /** + * Make sure the uint8 only has its high bits (0x0F) set before using this function + */ + constexpr uint8_t ReverseNibble(uint8_t b) + { + return (0xF7B3D591E6A2C480ull >> (b * 4)) & 0xF; // Trust me bro + } + + /** + * Make sure the uint8 only has its high bits (0x0F) set before using this function + */ + constexpr uint8_t ReverseInverseNibble(uint8_t b) + { + return (0x084C2A6E195D3B7Full >> (b * 4)) & 0xF; // Trust me bro + } +} // namespace OpenShock::Checksum + +#undef SUM8_INT_FN diff --git a/include/Convert.h b/components/common/include/Convert.h similarity index 93% rename from include/Convert.h rename to components/common/include/Convert.h index 26d9834e..a552a0f4 100644 --- a/include/Convert.h +++ b/components/common/include/Convert.h @@ -6,7 +6,7 @@ #include #include -namespace OpenShock::Convert { // TODO: C++23 make this use std::from_chars instead +namespace OpenShock::Convert { void FromInt8(int8_t val, std::string& str); void FromUint8(uint8_t val, std::string& str); void FromInt16(int16_t val, std::string& str); diff --git a/include/FormatHelpers.h b/components/common/include/FormatHelpers.h similarity index 100% rename from include/FormatHelpers.h rename to components/common/include/FormatHelpers.h diff --git a/components/common/include/OpenShock.h b/components/common/include/OpenShock.h new file mode 100644 index 00000000..aa445a2d --- /dev/null +++ b/components/common/include/OpenShock.h @@ -0,0 +1,39 @@ +#pragma once + +#define DISABLE_DEFAULT(TypeName) TypeName() = delete +#define DISABLE_COPY(TypeName) \ + TypeName(const TypeName&) = delete; \ + TypeName& operator=(const TypeName&) = delete +#define DISABLE_MOVE(TypeName) \ + TypeName(TypeName&&) = delete; \ + TypeName& operator=(TypeName&&) = delete + +// Config comes from two places: Kconfig (CONFIG_OPENSHOCK_*, via sdkconfig.h - +// domains, hostname, AP prefix, pins, buffer sizes; see src/Kconfig.projbuild) +// and the generated env header force-included by the build (OPENSHOCK_FW_VERSION, +// OPENSHOCK_FW_BOARD, OPENSHOCK_FW_CHIP, ...; see scripts/gen_env_header.py). +#include "sdkconfig.h" + +#ifndef OPENSHOCK_FW_VERSION +#error "OPENSHOCK_FW_VERSION must be defined (generated by scripts/gen_env_header.py)" +#endif +#ifndef OPENSHOCK_FW_BOARD +#error "OPENSHOCK_FW_BOARD must be defined (generated by scripts/gen_env_header.py)" +#endif +#ifndef OPENSHOCK_FW_CHIP +#error "OPENSHOCK_FW_CHIP must be defined (generated by scripts/gen_env_header.py)" +#endif + +#define OPENSHOCK_FW_CDN_URL(path) "https://" CONFIG_OPENSHOCK_FW_CDN_DOMAIN path + +#define OPENSHOCK_GPIO_INVALID -1 + +// Allow OPENSHOCK_FW_USERAGENT to be overridden through compiler flags; otherwise +// generate a default user agent from the hostname, version, board and chip. +#ifndef OPENSHOCK_FW_USERAGENT +#define OPENSHOCK_FW_USERAGENT CONFIG_OPENSHOCK_FW_HOSTNAME "/" OPENSHOCK_FW_VERSION " (ESP-IDF; " OPENSHOCK_FW_BOARD "; " OPENSHOCK_FW_CHIP "; Espressif)" +#endif + +namespace OpenShock::Constants { + const char* const FW_USERAGENT = OPENSHOCK_FW_USERAGENT; +} // namespace OpenShock::Constants diff --git a/include/RateLimiter.h b/components/common/include/RateLimiter.h similarity index 96% rename from include/RateLimiter.h rename to components/common/include/RateLimiter.h index 906943c7..d25461df 100644 --- a/include/RateLimiter.h +++ b/components/common/include/RateLimiter.h @@ -1,6 +1,6 @@ #pragma once -#include "Common.h" +#include "OpenShock.h" #include "SimpleMutex.h" #include diff --git a/include/ReadWriteMutex.h b/components/common/include/ReadWriteMutex.h similarity index 98% rename from include/ReadWriteMutex.h rename to components/common/include/ReadWriteMutex.h index 70768e45..3eb79f40 100644 --- a/include/ReadWriteMutex.h +++ b/components/common/include/ReadWriteMutex.h @@ -2,7 +2,7 @@ #include -#include "Common.h" +#include "OpenShock.h" namespace OpenShock { class ReadWriteMutex { diff --git a/include/SemVer.h b/components/common/include/SemVer.h similarity index 100% rename from include/SemVer.h rename to components/common/include/SemVer.h diff --git a/include/SimpleMutex.h b/components/common/include/SimpleMutex.h similarity index 98% rename from include/SimpleMutex.h rename to components/common/include/SimpleMutex.h index bd6df741..08cfc45d 100644 --- a/include/SimpleMutex.h +++ b/components/common/include/SimpleMutex.h @@ -2,7 +2,7 @@ #include -#include "Common.h" +#include "OpenShock.h" namespace OpenShock { class SimpleMutex { diff --git a/include/util/StringUtils.h b/components/common/include/StringHelpers.h similarity index 85% rename from include/util/StringUtils.h rename to components/common/include/StringHelpers.h index 022689fe..4d263c7d 100644 --- a/include/util/StringUtils.h +++ b/components/common/include/StringHelpers.h @@ -1,9 +1,10 @@ #pragma once -#include - +#include +#include #include #include +#include #include #include #include @@ -130,23 +131,28 @@ namespace OpenShock { return view.substr(pos + delimiter.length()); } + // Splits `view` into N parts on `delimiter`. The first N-1 parts are the + // delimited fields; the last part is the entire remainder (delimiters and + // all), so nothing is silently dropped. Returns false only if there are too + // few parts (fewer than N-1 delimiters). Callers that need each part to be a + // single field validate the remainder themselves (e.g. Convert::ToUint8). template constexpr bool TryStringSplit(std::string_view view, char delimiter, std::string_view (&out)[N]) { + static_assert(N > 0, "TryStringSplit needs at least one output slot"); + std::size_t pos = 0; - std::size_t idx = 0; - while (pos < view.size() && idx < N) { + for (std::size_t idx = 0; idx < N - 1; ++idx) { std::size_t nextPos = view.find(delimiter, pos); if (nextPos == std::string_view::npos) { - nextPos = view.size(); + return false; // too few parts } - out[idx] = view.substr(pos, nextPos - pos); pos = nextPos + 1; - ++idx; } - return idx == N; + out[N - 1] = view.substr(pos); // remainder (may still contain delimiters) + return true; } std::vector StringSplit(std::string_view view, char delimiter, std::size_t maxSplits = std::numeric_limits::max()); std::vector StringSplit(std::string_view view, bool (*predicate)(char delimiter), std::size_t maxSplits = std::numeric_limits::max()); @@ -173,9 +179,11 @@ namespace OpenShock { return std::make_pair(view.substr(0, pos), pos == std::string_view::npos ? std::string_view() : view.substr(pos + delimiter.length())); } - bool StringIEquals(std::string_view a, std::string_view b); - bool StringIContains(std::string_view haystack, std::string_view needle); - bool StringHasPrefixIC(std::string_view view, std::string_view prefix); - - String StringToArduinoString(std::string_view view); + // Case-insensitive ASCII equality for two string views. Length-aware, so it is + // safe on non-NUL-terminated views (e.g. a value straight out of a JSON/jsmn + // parser). Header-only and constexpr so the foundation `common` component can + // expose it without pulling in `util`. + bool StringIEquals(std::string_view a, std::string_view b) noexcept; + bool StringIContains(std::string_view haystack, std::string_view needle) noexcept; + bool StringHasPrefixIC(std::string_view view, std::string_view prefix) noexcept; } // namespace OpenShock diff --git a/include/TinyVec.h b/components/common/include/TinyVec.h similarity index 88% rename from include/TinyVec.h rename to components/common/include/TinyVec.h index 54027e3a..8d573138 100644 --- a/include/TinyVec.h +++ b/components/common/include/TinyVec.h @@ -1,14 +1,14 @@ #pragma once -#include "Common.h" #include "Logging.h" +#include "OpenShock.h" #include #include #include #include #include -#include +#include #include #include @@ -68,6 +68,14 @@ class TinyVec { T& operator[](SizeType i) noexcept { return _data[i]; } const T& operator[](SizeType i) const noexcept { return _data[i]; } + T* begin() noexcept { return _data; } + const T* begin() const noexcept { return _data; } + T* end() noexcept { return _data + _len; } + const T* end() const noexcept { return _data + _len; } + + operator std::span() noexcept { return {_data, static_cast(_len)}; } + operator std::span() const noexcept { return {_data, static_cast(_len)}; } + void reserve(SizeType new_cap) { if (new_cap <= _cap) return; diff --git a/include/AccountLinkResultCode.h b/components/common/include/enums/AccountLinkResultCode.h similarity index 100% rename from include/AccountLinkResultCode.h rename to components/common/include/enums/AccountLinkResultCode.h diff --git a/components/common/include/enums/FirmwareBootType.h b/components/common/include/enums/FirmwareBootType.h new file mode 100644 index 00000000..2482c7e0 --- /dev/null +++ b/components/common/include/enums/FirmwareBootType.h @@ -0,0 +1,52 @@ +#pragma once + +#include "StringHelpers.h" +#include "enums/OtaUpdateStep.h" + +#include +#include + +namespace OpenShock { + enum class FirmwareBootType : uint8_t { + Normal, + NewFirmware, + Rollback + }; + + inline bool TryParseFirmwareBootType(FirmwareBootType& bootType, std::string_view str) + { + if (StringIEquals(str, "normal")) { + bootType = FirmwareBootType::Normal; + return true; + } + + if (StringIEquals(str, "newfirmware") || StringIEquals(str, "new_firmware")) { + bootType = FirmwareBootType::NewFirmware; + return true; + } + + if (StringIEquals(str, "rollback")) { + bootType = FirmwareBootType::Rollback; + return true; + } + + return false; + } + + // Maps the OTA update step persisted in config (read once at boot) to the boot + // type this startup represents. + constexpr FirmwareBootType InferFirmwareBootType(OtaUpdateStep step) + { + switch (step) { + case OtaUpdateStep::Updated: + return FirmwareBootType::NewFirmware; + // Validating means we crashed mid-validation of the new firmware, so this + // boot is the rollback to the previous image. + case OtaUpdateStep::Validating: + case OtaUpdateStep::RollingBack: + return FirmwareBootType::Rollback; + default: + return FirmwareBootType::Normal; + } + } +} // namespace OpenShock diff --git a/include/GatewayClientState.h b/components/common/include/enums/GatewayClientState.h similarity index 100% rename from include/GatewayClientState.h rename to components/common/include/enums/GatewayClientState.h diff --git a/include/OtaUpdateChannel.h b/components/common/include/enums/OtaUpdateChannel.h similarity index 53% rename from include/OtaUpdateChannel.h rename to components/common/include/enums/OtaUpdateChannel.h index 3ab942b3..9172b27d 100644 --- a/include/OtaUpdateChannel.h +++ b/components/common/include/enums/OtaUpdateChannel.h @@ -1,26 +1,30 @@ #pragma once -#include "serialization/_fbs/HubConfig_generated.h" +#include "StringHelpers.h" #include -#include +#include namespace OpenShock { - typedef OpenShock::Serialization::Configuration::OtaUpdateChannel OtaUpdateChannel; + enum class OtaUpdateChannel : uint8_t { + Stable, + Beta, + Develop + }; - inline bool TryParseOtaUpdateChannel(OtaUpdateChannel& channel, const char* str) + inline bool TryParseOtaUpdateChannel(OtaUpdateChannel& channel, std::string_view str) { - if (strcasecmp(str, "stable") == 0) { + if (StringIEquals(str, "stable")) { channel = OtaUpdateChannel::Stable; return true; } - if (strcasecmp(str, "beta") == 0) { + if (StringIEquals(str, "beta")) { channel = OtaUpdateChannel::Beta; return true; } - if (strcasecmp(str, "develop") == 0 || strcasecmp(str, "dev") == 0) { + if (StringIEquals(str, "develop") || StringIEquals(str, "dev")) { channel = OtaUpdateChannel::Develop; return true; } diff --git a/include/OtaUpdateStep.h b/components/common/include/enums/OtaUpdateStep.h similarity index 50% rename from include/OtaUpdateStep.h rename to components/common/include/enums/OtaUpdateStep.h index 8fae996f..68ef3908 100644 --- a/include/OtaUpdateStep.h +++ b/components/common/include/enums/OtaUpdateStep.h @@ -1,41 +1,48 @@ #pragma once -#include "serialization/_fbs/HubConfig_generated.h" +#include "StringHelpers.h" #include -#include +#include namespace OpenShock { - typedef OpenShock::Serialization::Configuration::OtaUpdateStep OtaUpdateStep; - - inline bool TryParseOtaUpdateStep(OtaUpdateStep& channel, const char* str) + enum class OtaUpdateStep : uint8_t { + None, + Updating, + Updated, + Validating, + Validated, + RollingBack + }; + + inline bool TryParseOtaUpdateStep(OtaUpdateStep& channel, std::string_view str) { - if (strcasecmp(str, "none") == 0) { + if (StringIEquals(str, "none")) { channel = OtaUpdateStep::None; return true; } - if (strcasecmp(str, "updating") == 0) { + if (StringIEquals(str, "updating")) { channel = OtaUpdateStep::Updating; return true; } - if (strcasecmp(str, "updated") == 0) { + if (StringIEquals(str, "updated")) { channel = OtaUpdateStep::Updated; return true; } - if (strcasecmp(str, "validating") == 0) { + if (StringIEquals(str, "validating")) { channel = OtaUpdateStep::Validating; return true; } - if (strcasecmp(str, "validated") == 0) { + if (StringIEquals(str, "validated")) { channel = OtaUpdateStep::Validated; return true; } - if (strcasecmp(str, "rollingback") == 0) { + if (StringIEquals(str, "rollingback")) { channel = OtaUpdateStep::RollingBack; return true; } diff --git a/include/SetGPIOResultCode.h b/components/common/include/enums/SetGPIOResultCode.h similarity index 100% rename from include/SetGPIOResultCode.h rename to components/common/include/enums/SetGPIOResultCode.h diff --git a/include/ShockerCommandType.h b/components/common/include/enums/ShockerCommandType.h similarity index 59% rename from include/ShockerCommandType.h rename to components/common/include/enums/ShockerCommandType.h index f23f34a6..d1f9dd23 100644 --- a/include/ShockerCommandType.h +++ b/components/common/include/enums/ShockerCommandType.h @@ -1,7 +1,9 @@ #pragma once +#include "StringHelpers.h" + #include -#include +#include namespace OpenShock { enum class ShockerCommandType : uint8_t { @@ -12,21 +14,21 @@ namespace OpenShock { Light }; - inline bool ShockerCommandTypeFromString(const char* str, ShockerCommandType& out) + inline bool TryParseShockerCommandType(ShockerCommandType& out, std::string_view str) { - if (strcasecmp(str, "stop") == 0) { + if (StringIEquals(str, "stop")) { out = ShockerCommandType::Stop; return true; - } else if (strcasecmp(str, "shock") == 0) { + } else if (StringIEquals(str, "shock")) { out = ShockerCommandType::Shock; return true; - } else if (strcasecmp(str, "vibrate") == 0) { + } else if (StringIEquals(str, "vibrate")) { out = ShockerCommandType::Vibrate; return true; - } else if (strcasecmp(str, "sound") == 0) { + } else if (StringIEquals(str, "sound")) { out = ShockerCommandType::Sound; return true; - } else if (strcasecmp(str, "light") == 0) { + } else if (StringIEquals(str, "light")) { out = ShockerCommandType::Light; return true; } else { diff --git a/include/ShockerModelType.h b/components/common/include/enums/ShockerModelType.h similarity index 55% rename from include/ShockerModelType.h rename to components/common/include/enums/ShockerModelType.h index 016d52a0..2bccaf6d 100644 --- a/include/ShockerModelType.h +++ b/components/common/include/enums/ShockerModelType.h @@ -1,7 +1,9 @@ #pragma once +#include "StringHelpers.h" + #include -#include +#include namespace OpenShock { enum class ShockerModelType : uint8_t { @@ -12,39 +14,39 @@ namespace OpenShock { D80 }; - inline bool ShockerModelTypeFromString(const char* str, ShockerModelType& out, bool allowTypo = false) + inline bool TryParseShockerModelType(ShockerModelType& out, std::string_view str, bool allowTypo = false) { - if (strcasecmp(str, "caixianlin") == 0 || strcasecmp(str, "cai-xianlin") == 0) { + if (StringIEquals(str, "caixianlin") || StringIEquals(str, "cai-xianlin")) { out = ShockerModelType::CaiXianlin; return true; } - if (strcasecmp(str, "petrainer") == 0) { + if (StringIEquals(str, "petrainer")) { out = ShockerModelType::Petrainer; return true; } - if (allowTypo && strcasecmp(str, "pettrainer") == 0) { + if (allowTypo && StringIEquals(str, "pettrainer")) { out = ShockerModelType::Petrainer; return true; } - if (strcasecmp(str, "petrainer998dr") == 0) { + if (StringIEquals(str, "petrainer998dr")) { out = ShockerModelType::Petrainer998DR; return true; } - if (allowTypo && strcasecmp(str, "pettrainer998dr") == 0) { + if (allowTypo && StringIEquals(str, "pettrainer998dr")) { out = ShockerModelType::Petrainer998DR; return true; } - if (strcasecmp(str, "wellturnt330") == 0 || strcasecmp(str, "t330") == 0) { + if (StringIEquals(str, "wellturnt330") || StringIEquals(str, "t330")) { out = ShockerModelType::WellturnT330; return true; } - if (strcasecmp(str, "d80") == 0) { + if (StringIEquals(str, "d80")) { out = ShockerModelType::D80; return true; } diff --git a/include/WebSocketMessageType.h b/components/common/include/enums/WebSocketMessageType.h similarity index 100% rename from include/WebSocketMessageType.h rename to components/common/include/enums/WebSocketMessageType.h diff --git a/components/common/include/util/DigitCounter.h b/components/common/include/util/DigitCounter.h new file mode 100644 index 00000000..cf4665ce --- /dev/null +++ b/components/common/include/util/DigitCounter.h @@ -0,0 +1,39 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace OpenShock::Util { + // Maximum number of base-10 characters T can render to: its digit count plus a '-' for signed types. + template + inline constexpr int Digits10CountMax = std::numeric_limits::digits10 + (std::is_signed_v ? 2 : 1); + + // Number of base-10 characters val renders to, counting a leading '-' when negative. + template + constexpr std::size_t Digits10Count(T val) + { + std::size_t count = 1; + std::make_unsigned_t mag; + + if constexpr (std::is_signed_v) { + if (val < 0) { + ++count; // the leading '-' + mag = std::make_unsigned_t(0) - static_cast>(val); // magnitude via modular negation + } else { + mag = static_cast>(val); + } + } else { + mag = val; + } + + while (mag >= 10) { + mag /= 10; + ++count; + } + + return count; + } +} // namespace OpenShock::Util diff --git a/components/common/include/util/FnProxy.h b/components/common/include/util/FnProxy.h new file mode 100644 index 00000000..93ce60d3 --- /dev/null +++ b/components/common/include/util/FnProxy.h @@ -0,0 +1,45 @@ +#pragma once + +#include + +namespace OpenShock::Util { + + namespace detail { + // NX carries the source method's noexcept-ness onto the trampoline (and thus + // onto the resulting function-pointer type); the const specializations bind C + // as `const C` so the cast matches the method's constness. + template + struct Body { + static R call(void* self, A... args) noexcept(NX) + { return (static_cast(self)->*MF)(std::forward(args)...); } + }; + template struct Deduce; + template + struct Deduce : Body {}; + template + struct Deduce : Body {}; + template + struct Deduce : Body {}; + template + struct Deduce : Body {}; + // (ref-qualified &/&& members omitted, meaningless for a void* callback.) + } // namespace detail + + /// Variable template: FnProxy<&Class::method> is a plain function pointer, + /// R(*)(void*, Args...), that casts the void* back to Class* and calls the + /// method. It exists to hand C++ member functions to C callback APIs that pass + /// context as a void* (FreeRTOS tasks, esp_event handlers). + /// + /// Usage: + /// auto cb = FnProxy<&Sensor::read>; // R(*)(void*, float) + /// Sensor s; + /// cb(&s, 1.5f); + /// + /// Works for any non-ref-qualified member function. The four Deduce + /// specializations cover the const/noexcept combinations, each forwarding the + /// deduced signature to the single Body::call trampoline whose address becomes + /// the resulting function pointer. A noexcept method yields a noexcept function + /// pointer, which still converts to a plain one for C APIs. + template + constexpr auto FnProxy = &detail::Deduce::call; +} // namespace OpenShock::Util diff --git a/include/util/HexUtils.h b/components/common/include/util/HexUtils.h similarity index 100% rename from include/util/HexUtils.h rename to components/common/include/util/HexUtils.h diff --git a/include/util/TaskUtils.h b/components/common/include/util/TaskUtils.h similarity index 100% rename from include/util/TaskUtils.h rename to components/common/include/util/TaskUtils.h diff --git a/src/Convert.cpp b/components/common/src/Convert.cpp similarity index 99% rename from src/Convert.cpp rename to components/common/src/Convert.cpp index 8aa694d6..82a9c3cb 100644 --- a/src/Convert.cpp +++ b/components/common/src/Convert.cpp @@ -1,7 +1,7 @@ #include "Convert.h" #include "util/DigitCounter.h" -#include "util/StringUtils.h" +#include "StringHelpers.h" #include #include diff --git a/src/util/DigitCounter.cpp b/components/common/src/DigitCounter.cpp similarity index 100% rename from src/util/DigitCounter.cpp rename to components/common/src/DigitCounter.cpp diff --git a/src/RateLimiter.cpp b/components/common/src/RateLimiter.cpp similarity index 99% rename from src/RateLimiter.cpp rename to components/common/src/RateLimiter.cpp index 594fd97d..c5fd7fce 100644 --- a/src/RateLimiter.cpp +++ b/components/common/src/RateLimiter.cpp @@ -2,7 +2,7 @@ #include "RateLimiter.h" -#include "Core.h" +#include "Temporal.h" #include diff --git a/src/ReadWriteMutex.cpp b/components/common/src/ReadWriteMutex.cpp similarity index 100% rename from src/ReadWriteMutex.cpp rename to components/common/src/ReadWriteMutex.cpp diff --git a/src/SemVer.cpp b/components/common/src/SemVer.cpp similarity index 65% rename from src/SemVer.cpp rename to components/common/src/SemVer.cpp index 1ac4a064..3b0b5386 100644 --- a/src/SemVer.cpp +++ b/components/common/src/SemVer.cpp @@ -5,7 +5,7 @@ const char* const TAG = "SemVer"; #include "Convert.h" #include "Logging.h" #include "util/DigitCounter.h" -#include "util/StringUtils.h" +#include "StringHelpers.h" using namespace OpenShock; @@ -268,37 +268,71 @@ bool SemVer::operator==(const SemVer& other) const return major == other.major && minor == other.minor && patch == other.patch && prerelease == other.prerelease && build == other.build; } -bool SemVer::operator<(const SemVer& other) const +static bool semverIdentifierIsNumeric(std::string_view id) { - if (major < other.major) { - return true; - } - if (major > other.major) { + if (id.empty()) { return false; } - - if (minor < other.minor) { - return true; - } - if (minor > other.minor) { - return false; + for (char c : id) { + if (c < '0' || c > '9') { + return false; + } } + return true; +} - if (patch < other.patch) { - return true; - } - if (patch > other.patch) { - return false; +// Compare two non-empty dot-separated prerelease strings per semver 11.4: +// numeric identifiers compare numerically and rank below alphanumeric ones; +// alphanumeric compare in ASCII order; a larger set of identifiers outranks a +// smaller one when all preceding identifiers are equal. Returns <0 / 0 / >0. +static int semverComparePrerelease(std::string_view a, std::string_view b) +{ + while (!a.empty() || !b.empty()) { + if (a.empty()) return -1; // a ran out first -> fewer identifiers -> lower + if (b.empty()) return 1; + + size_t ad = a.find('.'); + size_t bd = b.find('.'); + std::string_view aid = a.substr(0, ad); + std::string_view bid = b.substr(0, bd); + + bool an = semverIdentifierIsNumeric(aid); + bool bn = semverIdentifierIsNumeric(bid); + if (an && bn) { + if (aid.length() != bid.length()) return aid.length() < bid.length() ? -1 : 1; // no leading zeros + int c = aid.compare(bid); + if (c != 0) return c < 0 ? -1 : 1; + } else if (an != bn) { + return an ? -1 : 1; // numeric ranks below alphanumeric + } else { + int c = aid.compare(bid); + if (c != 0) return c < 0 ? -1 : 1; + } + + a = (ad == std::string_view::npos) ? std::string_view {} : a.substr(ad + 1); + b = (bd == std::string_view::npos) ? std::string_view {} : b.substr(bd + 1); } + return 0; +} - if (prerelease < other.prerelease) { - return true; +bool SemVer::operator<(const SemVer& other) const +{ + if (major != other.major) return major < other.major; + if (minor != other.minor) return minor < other.minor; + if (patch != other.patch) return patch < other.patch; + + // Prerelease precedence (semver 11.3): a version WITH a prerelease is lower + // than the associated normal version. Build metadata is ignored (semver 10). + bool thisPre = !prerelease.empty(); + bool otherPre = !other.prerelease.empty(); + if (thisPre != otherPre) { + return thisPre; // this has a prerelease, other is the release -> this < other } - if (prerelease > other.prerelease) { - return false; + if (!thisPre) { + return false; // both normal versions of the same core -> equal precedence } - return build < other.build; + return semverComparePrerelease(prerelease, other.prerelease) < 0; } bool SemVer::operator==(std::string_view other) const @@ -323,58 +357,47 @@ bool SemVer::operator<(std::string_view other) const bool OpenShock::TryParseSemVer(std::string_view semverStr, SemVer& semver) { - std::string_view parts[3]; - if (!OpenShock::TryStringSplit(semverStr, '.', parts)) { - OS_LOGE(TAG, "Failed to split version string: %.*s", semverStr.length(), semverStr.data()); - return false; - } - - std::string_view majorStr = parts[0], minorStr = parts[1], patchStr = parts[2]; + // Peel off build (after the first '+') then prerelease (after the first '-'), + // leaving the bare major.minor.patch core. Splitting the whole string by '.' + // up front would corrupt a dotted prerelease/build such as "-rc.7". + std::string_view rest = semverStr; - size_t plusIdx = patchStr.find('+'); - size_t dashIdx = patchStr.find('-'); + std::string_view build; + size_t plusIdx = rest.find('+'); + if (plusIdx != std::string_view::npos) { + build = rest.substr(plusIdx + 1); + rest = rest.substr(0, plusIdx); + } - std::string_view restStr; - if (dashIdx != std::string_view::npos || plusIdx != std::string_view::npos) { - restStr = patchStr.substr(std::min(dashIdx, plusIdx)); - patchStr.remove_suffix(restStr.length()); + std::string_view prerelease; + size_t dashIdx = rest.find('-'); + if (dashIdx != std::string_view::npos) { + prerelease = rest.substr(dashIdx + 1); + rest = rest.substr(0, dashIdx); } - if (!Convert::ToUint16(majorStr, semver.major)) { - OS_LOGE(TAG, "Invalid major version: %.*s", majorStr.length(), majorStr.data()); + std::string_view parts[3]; + if (!OpenShock::TryStringSplit(rest, '.', parts)) { + OS_LOGE(TAG, "Failed to split version core: %.*s", static_cast(rest.length()), rest.data()); return false; } - if (!Convert::ToUint16(minorStr, semver.minor)) { - OS_LOGE(TAG, "Invalid minor version: %.*s", minorStr.length(), minorStr.data()); + if (!Convert::ToUint16(parts[0], semver.major) || !Convert::ToUint16(parts[1], semver.minor) || !Convert::ToUint16(parts[2], semver.patch)) { + OS_LOGE(TAG, "Invalid version core: %.*s", static_cast(rest.length()), rest.data()); return false; } - if (!Convert::ToUint16(patchStr, semver.patch)) { - OS_LOGE(TAG, "Invalid patch version: %.*s", patchStr.length(), patchStr.data()); + if (!prerelease.empty() && !semverIsPrerelease(prerelease)) { + OS_LOGE(TAG, "Invalid prerelease: %.*s", static_cast(prerelease.length()), prerelease.data()); return false; } - - if (!restStr.empty()) { - if (plusIdx != std::string_view::npos) { - semver.build = restStr.substr((plusIdx - patchStr.length()) + 1); - restStr.remove_suffix(semver.build.length() + 1); - - if (!semver.build.empty() && !semverIsBuild(semver.build)) { - OS_LOGE(TAG, "Invalid build: %s", semver.build.c_str()); - return false; - } - } - - if (dashIdx != std::string_view::npos) { - semver.prerelease = restStr.substr(1); - - if (!semver.prerelease.empty() && !semverIsPrerelease(semver.prerelease)) { - OS_LOGE(TAG, "Invalid prerelease: %s", semver.prerelease.c_str()); - return false; - } - } + if (!build.empty() && !semverIsBuild(build)) { + OS_LOGE(TAG, "Invalid build: %.*s", static_cast(build.length()), build.data()); + return false; } + semver.prerelease.assign(prerelease); + semver.build.assign(build); + return true; } diff --git a/src/SimpleMutex.cpp b/components/common/src/SimpleMutex.cpp similarity index 100% rename from src/SimpleMutex.cpp rename to components/common/src/SimpleMutex.cpp diff --git a/src/util/StringUtils.cpp b/components/common/src/StringHelpers.cpp similarity index 82% rename from src/util/StringUtils.cpp rename to components/common/src/StringHelpers.cpp index ed5d8bc9..a395b975 100644 --- a/src/util/StringUtils.cpp +++ b/components/common/src/StringHelpers.cpp @@ -1,8 +1,4 @@ -#include "util/StringUtils.h" - -const char* const TAG = "StringUtils"; - -#include "Logging.h" +#include "StringHelpers.h" #include #include @@ -24,7 +20,6 @@ bool OpenShock::FormatToString(std::string& out, const char* format, ...) // If result is negative, something went wrong. if (result < 0) { - OS_LOGE(TAG, "Failed to format string"); return false; } @@ -44,7 +39,6 @@ bool OpenShock::FormatToString(std::string& out, const char* format, ...) // Free heap buffer and return false. if (result < 0) { delete[] bufferPtr; - OS_LOGE(TAG, "Failed to format string"); return false; } } @@ -125,25 +119,23 @@ std::vector OpenShock::StringSplitWhiteSpace(std::string_view return StringSplit(view, [](char c) { return isspace(c) != 0; }, maxSplits); } -bool OpenShock::StringIEquals(std::string_view a, std::string_view b) +static bool lowercaseEqual(char a, char b) { + return tolower(a) == tolower(b); +} + +bool OpenShock::StringIEquals(std::string_view a, std::string_view b) noexcept { - if (a.size() != b.size()) return false; - return strncasecmp(a.data(), b.data(), a.size()) == 0; + return std::ranges::equal(a, b, lowercaseEqual); } -bool OpenShock::StringIContains(std::string_view haystack, std::string_view needle) +bool OpenShock::StringIContains(std::string_view haystack, std::string_view needle) noexcept { if (haystack.size() < needle.size()) return false; - if (haystack.size() == needle.size()) return StringIEquals(haystack, needle); + if (haystack.size() == needle.size()) return std::ranges::equal(haystack, needle, lowercaseEqual); - return std::search(haystack.begin(), haystack.end(), needle.begin(), needle.end(), [](char hc, char nc) { return tolower(hc) == tolower(nc); }) != haystack.end(); + return std::search(haystack.begin(), haystack.end(), needle.begin(), needle.end(), lowercaseEqual) != haystack.end(); } -bool OpenShock::StringHasPrefixIC(std::string_view view, std::string_view prefix) +bool OpenShock::StringHasPrefixIC(std::string_view view, std::string_view prefix) noexcept { if (view.size() < prefix.size()) return false; - return StringIEquals(view.substr(0, prefix.size()), prefix); -} - -String OpenShock::StringToArduinoString(std::string_view view) -{ - return String(view.data(), view.size()); + return std::ranges::equal(view.substr(0, prefix.size()), prefix, lowercaseEqual); } diff --git a/src/util/TaskUtils.cpp b/components/common/src/TaskUtils.cpp similarity index 100% rename from src/util/TaskUtils.cpp rename to components/common/src/TaskUtils.cpp diff --git a/components/config/CMakeLists.txt b/components/config/CMakeLists.txt new file mode 100644 index 00000000..888f6a18 --- /dev/null +++ b/components/config/CMakeLists.txt @@ -0,0 +1,20 @@ +# OpenShock configuration model — typed config structs with JSON + flatbuffer +# (de)serialization, persisted to littlefs via the fs component. +# +# Extracted out of `core` into a low-layer component: it depends only on the +# generated wire schema (serialization), JSON, the config filesystem and a few +# foundation utilities — nothing in core. This is what lets device_control (estop/ +# radio/visual/CommandHandler, which read Config::) leave core in a later step +# without a config <-> core cycle. +FILE(GLOB_RECURSE config_sources + ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp) + +idf_component_register( + SRCS ${config_sources} + INCLUDE_DIRS "include" + REQUIRES common osjson serialization flatbuffers # in public headers (enums, json/Json.h, HubConfig fbs, TinyVec, flatbuffers::Offset) + esp_wifi esp_hal_gpio # WiFiCredentials.h -> esp_wifi_types.h (wifi_auth_mode_t); RFConfig.h -> hal/gpio_types.h (gpio_num_t) + PRIV_REQUIRES chipset fs logging # .cpp only (GPIO validation, ConfigFs, OS_LOG*/OS_PANIC; ReadWriteMutex via common) +) + +target_compile_options(${COMPONENT_LIB} PRIVATE -Wno-error) diff --git a/include/config/BackendConfig.h b/components/config/include/config/BackendConfig.h similarity index 82% rename from include/config/BackendConfig.h rename to components/config/include/config/BackendConfig.h index 629e90ba..0b312985 100644 --- a/include/config/BackendConfig.h +++ b/components/config/include/config/BackendConfig.h @@ -18,7 +18,7 @@ namespace OpenShock::Config { bool FromFlatbuffers(const Serialization::Configuration::BackendConfig* config) override; [[nodiscard]] flatbuffers::Offset ToFlatbuffers(flatbuffers::FlatBufferBuilder& builder, bool withSensitiveData) const override; - bool FromJSON(const cJSON* json) override; - [[nodiscard]] cJSON* ToJSON(bool withSensitiveData) const override; + bool FromJSON(JSON::JsonView json) override; + void ToJSON(json_gen_str_t* gen, const char* name, bool withSensitiveData) const override; }; } // namespace OpenShock::Config diff --git a/include/config/CaptivePortalConfig.h b/components/config/include/config/CaptivePortalConfig.h similarity index 81% rename from include/config/CaptivePortalConfig.h rename to components/config/include/config/CaptivePortalConfig.h index 359321ca..75105c56 100644 --- a/include/config/CaptivePortalConfig.h +++ b/components/config/include/config/CaptivePortalConfig.h @@ -14,7 +14,7 @@ namespace OpenShock::Config { bool FromFlatbuffers(const Serialization::Configuration::CaptivePortalConfig* config) override; [[nodiscard]] flatbuffers::Offset ToFlatbuffers(flatbuffers::FlatBufferBuilder& builder, bool withSensitiveData) const override; - bool FromJSON(const cJSON* json) override; - [[nodiscard]] cJSON* ToJSON(bool withSensitiveData) const override; + bool FromJSON(JSON::JsonView json) override; + void ToJSON(json_gen_str_t* gen, const char* name, bool withSensitiveData) const override; }; } // namespace OpenShock::Config diff --git a/include/config/Config.h b/components/config/include/config/Config.h similarity index 94% rename from include/config/Config.h rename to components/config/include/config/Config.h index 3f70db73..63eb3869 100644 --- a/include/config/Config.h +++ b/components/config/include/config/Config.h @@ -55,7 +55,9 @@ namespace OpenShock::Config { bool SetEStop(const EStopConfig& config); bool GetWiFiCredentials(std::vector& out); - bool GetWiFiCredentials(cJSON* array, bool withSensitiveData); + // Emits each stored credential as an object into an already-open JSON array + // (the caller opens/closes it with json_gen_{start,end}_array). + bool GetWiFiCredentials(json_gen_str_t* gen, bool withSensitiveData); bool SetWiFiCredentials(const std::vector& credentials); bool GetRFConfigTxPin(gpio_num_t& out); diff --git a/include/config/ConfigBase.h b/components/config/include/config/ConfigBase.h similarity index 53% rename from include/config/ConfigBase.h rename to components/config/include/config/ConfigBase.h index 3bd7abc7..3b43e0f5 100644 --- a/include/config/ConfigBase.h +++ b/components/config/include/config/ConfigBase.h @@ -2,7 +2,9 @@ #include "serialization/_fbs/HubConfig_generated.h" -#include +#include "json/Json.h" + +#include namespace OpenShock::Config { template @@ -12,8 +14,11 @@ namespace OpenShock::Config { virtual bool FromFlatbuffers(const T* config) = 0; [[nodiscard]] virtual flatbuffers::Offset ToFlatbuffers(flatbuffers::FlatBufferBuilder& builder, bool withSensitiveData) const = 0; - virtual bool FromJSON(const cJSON* json) = 0; - [[nodiscard]] virtual cJSON* ToJSON(bool withSensitiveData) const = 0; + virtual bool FromJSON(JSON::JsonView json) = 0; + // Emits this config as its own JSON object. When `name` is non-null the object + // is added as a named member of the enclosing object; when null it is anonymous + // (document root or array element). See JSON::objBegin/objEnd. + virtual void ToJSON(json_gen_str_t* gen, const char* name, bool withSensitiveData) const = 0; }; } // namespace OpenShock::Config diff --git a/include/config/EStopConfig.h b/components/config/include/config/EStopConfig.h similarity index 81% rename from include/config/EStopConfig.h rename to components/config/include/config/EStopConfig.h index de726734..b585f2ef 100644 --- a/include/config/EStopConfig.h +++ b/components/config/include/config/EStopConfig.h @@ -17,7 +17,7 @@ namespace OpenShock::Config { bool FromFlatbuffers(const Serialization::Configuration::EStopConfig* config) override; flatbuffers::Offset ToFlatbuffers(flatbuffers::FlatBufferBuilder& builder, bool withSensitiveData) const override; - bool FromJSON(const cJSON* json) override; - cJSON* ToJSON(bool withSensitiveData) const override; + bool FromJSON(JSON::JsonView json) override; + void ToJSON(json_gen_str_t* gen, const char* name, bool withSensitiveData) const override; }; } // namespace OpenShock::Config diff --git a/include/config/OtaUpdateConfig.h b/components/config/include/config/OtaUpdateConfig.h similarity index 81% rename from include/config/OtaUpdateConfig.h rename to components/config/include/config/OtaUpdateConfig.h index 26fb5f87..d81241b6 100644 --- a/include/config/OtaUpdateConfig.h +++ b/components/config/include/config/OtaUpdateConfig.h @@ -1,9 +1,9 @@ #pragma once #include "config/ConfigBase.h" -#include "FirmwareBootType.h" -#include "OtaUpdateChannel.h" -#include "OtaUpdateStep.h" +#include "enums/FirmwareBootType.h" +#include "enums/OtaUpdateChannel.h" +#include "enums/OtaUpdateStep.h" #include @@ -30,7 +30,7 @@ namespace OpenShock::Config { bool FromFlatbuffers(const Serialization::Configuration::OtaUpdateConfig* config) override; [[nodiscard]] flatbuffers::Offset ToFlatbuffers(flatbuffers::FlatBufferBuilder& builder, bool withSensitiveData) const override; - bool FromJSON(const cJSON* json) override; - [[nodiscard]] cJSON* ToJSON(bool withSensitiveData) const override; + bool FromJSON(JSON::JsonView json) override; + void ToJSON(json_gen_str_t* gen, const char* name, bool withSensitiveData) const override; }; } // namespace OpenShock::Config diff --git a/include/config/RFConfig.h b/components/config/include/config/RFConfig.h similarity index 81% rename from include/config/RFConfig.h rename to components/config/include/config/RFConfig.h index cf766e69..49cf370d 100644 --- a/include/config/RFConfig.h +++ b/components/config/include/config/RFConfig.h @@ -17,7 +17,7 @@ namespace OpenShock::Config { bool FromFlatbuffers(const Serialization::Configuration::RFConfig* config) override; [[nodiscard]] flatbuffers::Offset ToFlatbuffers(flatbuffers::FlatBufferBuilder& builder, bool withSensitiveData) const override; - bool FromJSON(const cJSON* json) override; - [[nodiscard]] cJSON* ToJSON(bool withSensitiveData) const override; + bool FromJSON(JSON::JsonView json) override; + void ToJSON(json_gen_str_t* gen, const char* name, bool withSensitiveData) const override; }; } // namespace OpenShock::Config diff --git a/include/config/RootConfig.h b/components/config/include/config/RootConfig.h similarity index 88% rename from include/config/RootConfig.h rename to components/config/include/config/RootConfig.h index d182a66c..946f3078 100644 --- a/include/config/RootConfig.h +++ b/components/config/include/config/RootConfig.h @@ -26,7 +26,7 @@ namespace OpenShock::Config { bool FromFlatbuffers(const Serialization::Configuration::HubConfig* config) override; [[nodiscard]] flatbuffers::Offset ToFlatbuffers(flatbuffers::FlatBufferBuilder& builder, bool withSensitiveData) const override; - bool FromJSON(const cJSON* json) override; - [[nodiscard]] cJSON* ToJSON(bool withSensitiveData) const override; + bool FromJSON(JSON::JsonView json) override; + void ToJSON(json_gen_str_t* gen, const char* name, bool withSensitiveData) const override; }; } // namespace OpenShock::Config diff --git a/include/config/SerialInputConfig.h b/components/config/include/config/SerialInputConfig.h similarity index 81% rename from include/config/SerialInputConfig.h rename to components/config/include/config/SerialInputConfig.h index a9ac7746..8742aa8d 100644 --- a/include/config/SerialInputConfig.h +++ b/components/config/include/config/SerialInputConfig.h @@ -14,7 +14,7 @@ namespace OpenShock::Config { bool FromFlatbuffers(const Serialization::Configuration::SerialInputConfig* config) override; [[nodiscard]] flatbuffers::Offset ToFlatbuffers(flatbuffers::FlatBufferBuilder& builder, bool withSensitiveData) const override; - bool FromJSON(const cJSON* json) override; - [[nodiscard]] cJSON* ToJSON(bool withSensitiveData) const override; + bool FromJSON(JSON::JsonView json) override; + void ToJSON(json_gen_str_t* gen, const char* name, bool withSensitiveData) const override; }; } // namespace OpenShock::Config diff --git a/include/config/WiFiConfig.h b/components/config/include/config/WiFiConfig.h similarity index 85% rename from include/config/WiFiConfig.h rename to components/config/include/config/WiFiConfig.h index f3671608..ca16b5b7 100644 --- a/include/config/WiFiConfig.h +++ b/components/config/include/config/WiFiConfig.h @@ -21,7 +21,7 @@ namespace OpenShock::Config { bool FromFlatbuffers(const Serialization::Configuration::WiFiConfig* config) override; [[nodiscard]] flatbuffers::Offset ToFlatbuffers(flatbuffers::FlatBufferBuilder& builder, bool withSensitiveData) const override; - bool FromJSON(const cJSON* json) override; - [[nodiscard]] cJSON* ToJSON(bool withSensitiveData) const override; + bool FromJSON(JSON::JsonView json) override; + void ToJSON(json_gen_str_t* gen, const char* name, bool withSensitiveData) const override; }; } // namespace OpenShock::Config diff --git a/include/config/WiFiCredentials.h b/components/config/include/config/WiFiCredentials.h similarity index 72% rename from include/config/WiFiCredentials.h rename to components/config/include/config/WiFiCredentials.h index cc545a1d..b79b64e9 100644 --- a/include/config/WiFiCredentials.h +++ b/components/config/include/config/WiFiCredentials.h @@ -2,8 +2,8 @@ #include "config/ConfigBase.h" +#include #include -#include #include #include @@ -18,8 +18,8 @@ namespace OpenShock::Config { uint8_t id; std::string ssid; std::string password; - wifi_auth_mode_t authMode; // Auth mode when saved, WIFI_AUTH_MAX = unknown - std::array bssid; // Pinned BSSID, all zeros = no pin + wifi_auth_mode_t authMode; // Auth mode when saved, WIFI_AUTH_MAX = unknown + std::array bssid; // Pinned BSSID, all zeros = no pin bool HasPinnedBSSID() const; @@ -28,7 +28,7 @@ namespace OpenShock::Config { bool FromFlatbuffers(const Serialization::Configuration::WiFiCredentials* config) override; [[nodiscard]] flatbuffers::Offset ToFlatbuffers(flatbuffers::FlatBufferBuilder& builder, bool withSensitiveData) const override; - bool FromJSON(const cJSON* json) override; - [[nodiscard]] cJSON* ToJSON(bool withSensitiveData) const override; + bool FromJSON(JSON::JsonView json) override; + void ToJSON(json_gen_str_t* gen, const char* name, bool withSensitiveData) const override; }; } // namespace OpenShock::Config diff --git a/components/config/include/config/internal/utils.h b/components/config/include/config/internal/utils.h new file mode 100644 index 00000000..92ca6a72 --- /dev/null +++ b/components/config/include/config/internal/utils.h @@ -0,0 +1,73 @@ +#pragma once + +#include "config/ConfigBase.h" + +#include "json/Json.h" + +#include + +#include +#include + +namespace OpenShock::Config::Internal::Utils { + bool FromU8GpioNum(gpio_num_t& val, uint8_t u8Val); + + void FromFbsStr(std::string& str, const flatbuffers::String* fbsStr, const char* defaultStr); + + bool FromJsonGpioNum(gpio_num_t& val, JSON::JsonView json, std::string_view name); + + template // T inherits from ConfigBase + void FromFbsVec(std::vector& vec, const flatbuffers::Vector>* fbsVec) + { + vec.clear(); + + if (fbsVec == nullptr) { + return; + } + + for (auto fbsItem : *fbsVec) { + T item; + if (item.FromFlatbuffers(fbsItem)) { + vec.push_back(std::move(item)); + } + } + } + template // T inherits from ConfigBase + bool FromJsonStrParsed(T& val, JSON::JsonView json, std::string_view name, bool (*StringParser)(T&, std::string_view), T defaultVal) + { + JSON::JsonView jsonVal = json[name]; + if (!jsonVal.valid()) { + val = defaultVal; + return true; + } + + std::string_view view; + if (!jsonVal.tryGetStr(view)) { + return false; + } + + if (!StringParser(val, view)) { + return false; + } + + return true; + } + template // T inherits from ConfigBase + bool FromJsonArray(std::vector& vec, JSON::JsonView jsonArray) + { + vec.clear(); + if (!jsonArray.isArray()) { + return true; + } + + const int count = jsonArray.count(); + for (int i = 0; i < count; ++i) { + T item; + if (item.FromJSON(jsonArray.at(i))) { + vec.push_back(std::move(item)); + } + } + + return true; + } +} // namespace OpenShock::Config::Internal::Utils diff --git a/src/config/BackendConfig.cpp b/components/config/src/BackendConfig.cpp similarity index 65% rename from src/config/BackendConfig.cpp rename to components/config/src/BackendConfig.cpp index ac49a2e2..2281dbc5 100644 --- a/src/config/BackendConfig.cpp +++ b/components/config/src/BackendConfig.cpp @@ -8,7 +8,7 @@ const char* const TAG = "Config::BackendConfig"; using namespace OpenShock::Config; BackendConfig::BackendConfig() - : domain(OPENSHOCK_API_DOMAIN) + : domain(CONFIG_OPENSHOCK_API_DOMAIN) , authToken() { } @@ -21,7 +21,7 @@ BackendConfig::BackendConfig(std::string_view domain, std::string_view authToken void BackendConfig::ToDefault() { - domain = OPENSHOCK_API_DOMAIN; + domain = CONFIG_OPENSHOCK_API_DOMAIN; authToken.clear(); } @@ -33,7 +33,7 @@ bool BackendConfig::FromFlatbuffers(const Serialization::Configuration::BackendC return true; } - Internal::Utils::FromFbsStr(domain, config->domain(), OPENSHOCK_API_DOMAIN); + Internal::Utils::FromFbsStr(domain, config->domain(), CONFIG_OPENSHOCK_API_DOMAIN); Internal::Utils::FromFbsStr(authToken, config->auth_token(), ""); return true; @@ -53,34 +53,41 @@ flatbuffers::Offset Back return Serialization::Configuration::CreateBackendConfig(builder, domainOffset, authTokenOffset); } -bool BackendConfig::FromJSON(const cJSON* json) +bool BackendConfig::FromJSON(JSON::JsonView json) { - if (json == nullptr) { + if (!json.valid()) { OS_LOGW(TAG, "Config is null, setting to default"); ToDefault(); return true; } - if (cJSON_IsObject(json) == 0) { + if (!json.isObject()) { OS_LOGE(TAG, "json is not an object"); return false; } - Internal::Utils::FromJsonStr(domain, json, "domain", OPENSHOCK_API_DOMAIN); - Internal::Utils::FromJsonStr(authToken, json, "authToken", ""); + if (std::string_view sv; json["domain"].tryGetStr(sv)) { + domain = sv; + } else { + domain = CONFIG_OPENSHOCK_API_DOMAIN; + } + + if (std::string_view sv; json["authToken"].tryGetStr(sv)) { + authToken = sv; + } else { + authToken.clear(); + } return true; } -cJSON* BackendConfig::ToJSON(bool withSensitiveData) const +void BackendConfig::ToJSON(json_gen_str_t* gen, const char* name, bool withSensitiveData) const { - cJSON* root = cJSON_CreateObject(); - - cJSON_AddStringToObject(root, "domain", domain.c_str()); + JSON::objBegin(gen, name); + JSON::objSetString(gen, "domain", domain); if (withSensitiveData) { - cJSON_AddStringToObject(root, "authToken", authToken.c_str()); + JSON::objSetString(gen, "authToken", authToken); } - - return root; + JSON::objEnd(gen, name); } diff --git a/src/config/CaptivePortalConfig.cpp b/components/config/src/CaptivePortalConfig.cpp similarity index 74% rename from src/config/CaptivePortalConfig.cpp rename to components/config/src/CaptivePortalConfig.cpp index eefe1730..e31f8a23 100644 --- a/src/config/CaptivePortalConfig.cpp +++ b/components/config/src/CaptivePortalConfig.cpp @@ -40,29 +40,27 @@ flatbuffers::Offset -#include +#include "json/Json.h" -#include +#include "fs/ConfigFs.h" #include +#include +#include +#include using namespace OpenShock; -static fs::LittleFSFS _configFS; +// littlefs partition holding the persisted config blob. The Arduino LittleFSFS +// stored it as the file "config" at the root of the partition labelled "config" +// (VFS "/config/config" resolved to lfs path "/config"); the raw-lfs ConfigFs opens +// that same path so existing devices keep loading their config after the migration. +static constexpr const char* CONFIG_PARTITION_LABEL = "config"; +static constexpr const char* CONFIG_FILE_PATH = "/config"; + +static OpenShock::ConfigFs _configFs; static Config::RootConfig _configData; static ReadWriteMutex _configMutex; @@ -72,26 +81,17 @@ static bool tryDeserializeConfig(const uint8_t* buffer, std::size_t bufferLen, O } static bool tryLoadConfig(TinyVec& buffer) { - File file = _configFS.open("/config", "rb"); - if (!file) { - OS_LOGE(TAG, "Failed to open config file for reading"); + std::vector data; + if (!_configFs.read(CONFIG_FILE_PATH, data)) { + OS_LOGE(TAG, "Failed to read config file"); return false; } - // Get file size - std::size_t size = file.size(); - - // Resize buffer - buffer.resize(size); - - // Read file - if (file.read(buffer.data(), buffer.size()) != buffer.size()) { - OS_LOGE(TAG, "Failed to read config file, size mismatch"); - return false; + buffer.resize(data.size()); + if (!data.empty()) { + memcpy(buffer.data(), data.data(), data.size()); } - file.close(); - return true; } static bool tryLoadConfig() @@ -105,20 +105,11 @@ static bool tryLoadConfig() } static bool trySaveConfig(const uint8_t* data, std::size_t dataLen) { - File file = _configFS.open("/config", "wb"); - if (!file) { - OS_LOGE(TAG, "Failed to open config file for writing"); - return false; - } - - // Write file - if (file.write(data, dataLen) != dataLen) { + if (!_configFs.write(CONFIG_FILE_PATH, std::span(data, dataLen))) { OS_LOGE(TAG, "Failed to write config file"); return false; } - file.close(); - return true; } static bool trySaveConfig() @@ -136,7 +127,12 @@ void Config::Init() { CONFIG_LOCK_WRITE(); - if (!_configFS.begin(true, "/config", 3, "config")) { + const esp_partition_t* partition = esp_partition_find_first(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_SPIFFS, CONFIG_PARTITION_LABEL); + if (partition == nullptr) { + OS_PANIC(TAG, "Unable to find config partition!"); + } + + if (!_configFs.mount(partition)) { OS_PANIC(TAG, "Unable to mount config LittleFS partition!"); } @@ -153,46 +149,28 @@ void Config::Init() } } -static cJSON* getAsCJSON(bool withSensitiveData) -{ - CONFIG_LOCK_READ(nullptr); - - return _configData.ToJSON(withSensitiveData); -} - std::string Config::GetAsJSON(bool withSensitiveData) { - cJSON* root = getAsCJSON(withSensitiveData); - if (root == nullptr) { - OS_LOGE(TAG, "Failed to get config as JSON"); - return {}; - } - - char* json = cJSON_PrintUnformatted(root); + CONFIG_LOCK_READ({}); - std::string result(json); + JSON::StringWriter writer; + json_gen_str_t* gen = writer.gen(); - free(json); + _configData.ToJSON(gen, nullptr, withSensitiveData); - cJSON_Delete(root); - - return result; + return writer.finish(); } bool Config::SaveFromJSON(std::string_view json) { - cJSON* root = cJSON_ParseWithLength(json.data(), json.size()); - if (root == nullptr) { - OS_LOGE(TAG, "Failed to parse JSON: %s", cJSON_GetErrorPtr()); + JSON::JsonDocument doc; + if (!doc.parse(json)) { + OS_LOGE(TAG, "Failed to parse JSON"); return false; } - CONFIG_LOCK_WRITE_ACTION(false, cJSON_Delete(root)); - - bool result = _configData.FromJSON(root); - - cJSON_Delete(root); + CONFIG_LOCK_WRITE(false); - if (!result) { + if (!_configData.FromJSON(doc.root())) { OS_LOGE(TAG, "Failed to read JSON"); return false; } @@ -245,7 +223,7 @@ void Config::FactoryReset() _configData.ToDefault(); - if (!_configFS.remove("/config") && _configFS.exists("/config")) { + if (!_configFs.remove(CONFIG_FILE_PATH) && _configFs.exists(CONFIG_FILE_PATH)) { OS_PANIC(TAG, "Failed to remove existing config file for factory reset. Reccomend formatting microcontroller and re-flashing firmware"); } @@ -384,14 +362,12 @@ bool Config::GetWiFiCredentials(std::vector& out) return true; } -bool Config::GetWiFiCredentials(cJSON* array, bool withSensitiveData) +bool Config::GetWiFiCredentials(json_gen_str_t* gen, bool withSensitiveData) { CONFIG_LOCK_READ(false); for (auto& creds : _configData.wifi.credentialsList) { - cJSON* jsonCreds = creds.ToJSON(withSensitiveData); - - cJSON_AddItemToArray(array, jsonCreds); + creds.ToJSON(gen, nullptr, withSensitiveData); } return true; diff --git a/src/config/EStopConfig.cpp b/components/config/src/EStopConfig.cpp similarity index 54% rename from src/config/EStopConfig.cpp rename to components/config/src/EStopConfig.cpp index df78b092..1d9b1b0b 100644 --- a/src/config/EStopConfig.cpp +++ b/components/config/src/EStopConfig.cpp @@ -1,17 +1,17 @@ #include "config/EStopConfig.h" #include "Chipset.h" -#include "Common.h" #include "config/internal/utils.h" #include "Logging.h" +#include "OpenShock.h" const char* const TAG = "Config::EStopConfig"; using namespace OpenShock::Config; EStopConfig::EStopConfig() - : enabled(OpenShock::IsValidInputPin(OPENSHOCK_ESTOP_PIN)) - , gpioPin(static_cast(OPENSHOCK_ESTOP_PIN)) + : enabled(OpenShock::IsValidInputPin(CONFIG_OPENSHOCK_ESTOP_PIN)) + , gpioPin(static_cast(CONFIG_OPENSHOCK_ESTOP_PIN)) { } @@ -23,8 +23,8 @@ EStopConfig::EStopConfig(bool enabled, gpio_num_t gpioPin) void EStopConfig::ToDefault() { - enabled = OpenShock::IsValidInputPin(OPENSHOCK_ESTOP_PIN); - gpioPin = static_cast(OPENSHOCK_ESTOP_PIN); + enabled = OpenShock::IsValidInputPin(CONFIG_OPENSHOCK_ESTOP_PIN); + gpioPin = static_cast(CONFIG_OPENSHOCK_ESTOP_PIN); } bool EStopConfig::FromFlatbuffers(const Serialization::Configuration::EStopConfig* config) @@ -50,34 +50,38 @@ flatbuffers::Offset EStopC return Serialization::Configuration::CreateEStopConfig(builder, enabled, gpioPin); } -bool EStopConfig::FromJSON(const cJSON* json) +bool EStopConfig::FromJSON(JSON::JsonView json) { - if (json == nullptr) { + if (!json.valid()) { ToDefault(); // Set to default if config is null return true; } - if (cJSON_IsObject(json) == 0) { + if (!json.isObject()) { OS_LOGE(TAG, "json is not an object"); return false; } - Internal::Utils::FromJsonGpioNum(gpioPin, json, "gpioPin", static_cast(OPENSHOCK_ESTOP_PIN)); + if (!Internal::Utils::FromJsonGpioNum(gpioPin, json, "gpioPin")) { + gpioPin = static_cast(CONFIG_OPENSHOCK_ESTOP_PIN); + } - if (!Internal::Utils::FromJsonBool(enabled, json, "enabled", OpenShock::IsValidInputPin(gpioPin))) { - OS_LOGE(TAG, "Failed to parse enabled"); - return false; + if (JSON::JsonView enabledJson = json["enabled"]; enabledJson.valid()) { + if (!enabledJson.tryGetBool(enabled)) { + OS_LOGE(TAG, "Failed to parse enabled"); + return false; + } + } else { + enabled = OpenShock::IsValidInputPin(gpioPin); } return true; } -cJSON* EStopConfig::ToJSON(bool withSensitiveData) const +void EStopConfig::ToJSON(json_gen_str_t* gen, const char* name, bool withSensitiveData) const { - cJSON* root = cJSON_CreateObject(); - - cJSON_AddBoolToObject(root, "enabled", enabled); - cJSON_AddNumberToObject(root, "gpioPin", gpioPin); - - return root; + JSON::objBegin(gen, name); + json_gen_obj_set_bool(gen, "enabled", enabled); + json_gen_obj_set_int(gen, "gpioPin", gpioPin); + JSON::objEnd(gen, name); } diff --git a/components/config/src/OtaUpdateConfig.cpp b/components/config/src/OtaUpdateConfig.cpp new file mode 100644 index 00000000..2a23896b --- /dev/null +++ b/components/config/src/OtaUpdateConfig.cpp @@ -0,0 +1,143 @@ +#include "config/OtaUpdateConfig.h" + +const char* const TAG = "Config::OtaUpdateConfig"; + +#include "config/internal/utils.h" +#include "Logging.h" + +using namespace OpenShock::Config; +using namespace std::string_view_literals; + +OtaUpdateConfig::OtaUpdateConfig() + : isEnabled(true) + , cdnDomain(CONFIG_OPENSHOCK_FW_CDN_DOMAIN) + , updateChannel(OtaUpdateChannel::Stable) + , checkOnStartup(false) + , checkPeriodically(false) + , checkInterval(30) + , allowBackendManagement(true) + , requireManualApproval(false) + , updateId(0) + , updateStep(OtaUpdateStep::None) +{ +} + +OtaUpdateConfig::OtaUpdateConfig( + bool isEnabled, std::string cdnDomain, OtaUpdateChannel updateChannel, bool checkOnStartup, bool checkPeriodically, uint16_t checkInterval, bool allowBackendManagement, bool requireManualApproval, int32_t updateId, OtaUpdateStep updateStep +) + : isEnabled(isEnabled) + , cdnDomain(std::move(cdnDomain)) + , updateChannel(updateChannel) + , checkOnStartup(checkOnStartup) + , checkPeriodically(checkPeriodically) + , checkInterval(checkInterval) + , allowBackendManagement(allowBackendManagement) + , requireManualApproval(requireManualApproval) + , updateId(updateId) + , updateStep(updateStep) +{ +} + +void OtaUpdateConfig::ToDefault() +{ + isEnabled = true; + cdnDomain = CONFIG_OPENSHOCK_FW_CDN_DOMAIN; + updateChannel = OtaUpdateChannel::Stable; + checkOnStartup = false; + checkPeriodically = false; + checkInterval = 30; // 30 minutes + allowBackendManagement = true; + requireManualApproval = false; + updateId = 0; + updateStep = OtaUpdateStep::None; +} + +bool OtaUpdateConfig::FromFlatbuffers(const Serialization::Configuration::OtaUpdateConfig* config) +{ + if (config == nullptr) { + OS_LOGW(TAG, "Config is null, setting to default"); + ToDefault(); + return true; + } + + isEnabled = config->is_enabled(); + Internal::Utils::FromFbsStr(cdnDomain, config->cdn_domain(), CONFIG_OPENSHOCK_FW_CDN_DOMAIN); + updateChannel = static_cast(config->update_channel()); + checkOnStartup = config->check_on_startup(); + checkPeriodically = config->check_periodically(); + checkInterval = config->check_interval(); + allowBackendManagement = config->allow_backend_management(); + requireManualApproval = config->require_manual_approval(); + updateId = config->update_id(); + updateStep = static_cast(config->update_step()); + + return true; +} + +flatbuffers::Offset OtaUpdateConfig::ToFlatbuffers(flatbuffers::FlatBufferBuilder& builder, bool withSensitiveData) const +{ + return Serialization::Configuration::CreateOtaUpdateConfig( + builder, + isEnabled, + builder.CreateString(cdnDomain), + static_cast(updateChannel), + checkOnStartup, + checkPeriodically, + checkInterval, + allowBackendManagement, + requireManualApproval, + updateId, + static_cast(updateStep) + ); +} + +bool OtaUpdateConfig::FromJSON(JSON::JsonView json) +{ + if (!json.valid()) { + OS_LOGW(TAG, "Config is null, setting to default"); + ToDefault(); + return true; + } + + if (!json.isObject()) { + OS_LOGE(TAG, "json is not an object"); + return false; + } + + if (!json["isEnabled"].tryGetBool(isEnabled)) isEnabled = true; + + if (std::string_view sv; json["cdnDomain"].tryGetStr(sv)) { + cdnDomain = sv; + } else { + cdnDomain = CONFIG_OPENSHOCK_FW_CDN_DOMAIN; + } + + Internal::Utils::FromJsonStrParsed(updateChannel, json, "updateChannel"sv, OpenShock::TryParseOtaUpdateChannel, OpenShock::OtaUpdateChannel::Stable); + + if (!json["checkOnStartup"].tryGetBool(checkOnStartup)) checkOnStartup = false; + if (!json["checkPeriodically"].tryGetBool(checkPeriodically)) checkPeriodically = false; + if (!json["checkInterval"].tryGetU16(checkInterval)) checkInterval = 30; + if (!json["allowBackendManagement"].tryGetBool(allowBackendManagement)) allowBackendManagement = true; + if (!json["requireManualApproval"].tryGetBool(requireManualApproval)) requireManualApproval = false; + if (!json["updateId"].tryGetI32(updateId)) updateId = 0; + + Internal::Utils::FromJsonStrParsed(updateStep, json, "updateStep"sv, OpenShock::TryParseOtaUpdateStep, OpenShock::OtaUpdateStep::None); + + return true; +} + +void OtaUpdateConfig::ToJSON(json_gen_str_t* gen, const char* name, bool withSensitiveData) const +{ + JSON::objBegin(gen, name); + json_gen_obj_set_bool(gen, "isEnabled", isEnabled); + JSON::objSetString(gen, "cdnDomain", cdnDomain); + JSON::objSetString(gen, "updateChannel", OpenShock::Serialization::Configuration::EnumNameOtaUpdateChannel(static_cast(updateChannel))); + json_gen_obj_set_bool(gen, "checkOnStartup", checkOnStartup); + json_gen_obj_set_bool(gen, "checkPeriodically", checkPeriodically); + json_gen_obj_set_int(gen, "checkInterval", checkInterval); + json_gen_obj_set_bool(gen, "allowBackendManagement", allowBackendManagement); + json_gen_obj_set_bool(gen, "requireManualApproval", requireManualApproval); + json_gen_obj_set_int(gen, "updateId", updateId); + JSON::objSetString(gen, "updateStep", OpenShock::Serialization::Configuration::EnumNameOtaUpdateStep(static_cast(updateStep))); + JSON::objEnd(gen, name); +} diff --git a/src/config/RFConfig.cpp b/components/config/src/RFConfig.cpp similarity index 55% rename from src/config/RFConfig.cpp rename to components/config/src/RFConfig.cpp index 81d5080b..bc698e64 100644 --- a/src/config/RFConfig.cpp +++ b/components/config/src/RFConfig.cpp @@ -2,14 +2,14 @@ const char* const TAG = "Config::RFConfig"; -#include "Common.h" #include "config/internal/utils.h" #include "Logging.h" +#include "OpenShock.h" using namespace OpenShock::Config; RFConfig::RFConfig() - : txPin(static_cast(OPENSHOCK_RF_TX_GPIO)) + : txPin(static_cast(CONFIG_OPENSHOCK_RF_TX_GPIO)) , keepAliveEnabled(true) { } @@ -22,7 +22,7 @@ RFConfig::RFConfig(gpio_num_t txPin, bool keepAliveEnabled) void RFConfig::ToDefault() { - txPin = static_cast(OPENSHOCK_RF_TX_GPIO); + txPin = static_cast(CONFIG_OPENSHOCK_RF_TX_GPIO); keepAliveEnabled = true; } @@ -34,7 +34,9 @@ bool RFConfig::FromFlatbuffers(const Serialization::Configuration::RFConfig* con return true; } - Internal::Utils::FromU8GpioNum(txPin, config->tx_pin(), static_cast(OPENSHOCK_RF_TX_GPIO)); + if (!Internal::Utils::FromU8GpioNum(txPin, config->tx_pin())) { + txPin = static_cast(CONFIG_OPENSHOCK_RF_TX_GPIO); + } keepAliveEnabled = config->keepalive_enabled(); return true; @@ -45,31 +47,31 @@ flatbuffers::Offset RFConfig: return Serialization::Configuration::CreateRFConfig(builder, txPin, keepAliveEnabled); } -bool RFConfig::FromJSON(const cJSON* json) +bool RFConfig::FromJSON(JSON::JsonView json) { - if (json == nullptr) { + if (!json.valid()) { OS_LOGW(TAG, "Config is null, setting to default"); ToDefault(); return true; } - if (cJSON_IsObject(json) == 0) { + if (!json.isObject()) { OS_LOGE(TAG, "json is not an object"); return false; } - Internal::Utils::FromJsonGpioNum(txPin, json, "txPin", static_cast(OPENSHOCK_RF_TX_GPIO)); - Internal::Utils::FromJsonBool(keepAliveEnabled, json, "keepAliveEnabled", true); + if (!Internal::Utils::FromJsonGpioNum(txPin, json, "txPin")) { + txPin = static_cast(CONFIG_OPENSHOCK_RF_TX_GPIO); + } + if (!json["keepAliveEnabled"].tryGetBool(keepAliveEnabled)) keepAliveEnabled = true; return true; } -cJSON* RFConfig::ToJSON(bool withSensitiveData) const +void RFConfig::ToJSON(json_gen_str_t* gen, const char* name, bool withSensitiveData) const { - cJSON* root = cJSON_CreateObject(); - - cJSON_AddNumberToObject(root, "txPin", static_cast(txPin)); //-V2564 - cJSON_AddBoolToObject(root, "keepAliveEnabled", keepAliveEnabled); - - return root; + JSON::objBegin(gen, name); + json_gen_obj_set_int(gen, "txPin", static_cast(txPin)); + json_gen_obj_set_bool(gen, "keepAliveEnabled", keepAliveEnabled); + JSON::objEnd(gen, name); } diff --git a/src/config/RootConfig.cpp b/components/config/src/RootConfig.cpp similarity index 71% rename from src/config/RootConfig.cpp rename to components/config/src/RootConfig.cpp index 9babc0b9..97440612 100644 --- a/src/config/RootConfig.cpp +++ b/components/config/src/RootConfig.cpp @@ -87,50 +87,50 @@ flatbuffers::Offset RootConf return Serialization::Configuration::CreateHubConfig(builder, rfOffset, wifiOffset, captivePortalOffset, backendOffset, serialInputOffset, otaUpdateOffset, estopOffset); } -bool RootConfig::FromJSON(const cJSON* json) +bool RootConfig::FromJSON(JSON::JsonView json) { - if (json == nullptr) { + if (!json.valid()) { OS_LOGW(TAG, "Config is null, setting to default"); ToDefault(); return true; } - if (cJSON_IsObject(json) == 0) { + if (!json.isObject()) { OS_LOGE(TAG, "json is not an object"); return false; } - if (!rf.FromJSON(cJSON_GetObjectItemCaseSensitive(json, "rf"))) { + if (!rf.FromJSON(json["rf"])) { OS_LOGE(TAG, "Unable to load rf config"); return false; } - if (!wifi.FromJSON(cJSON_GetObjectItemCaseSensitive(json, "wifi"))) { + if (!wifi.FromJSON(json["wifi"])) { OS_LOGE(TAG, "Unable to load wifi config"); return false; } - if (!captivePortal.FromJSON(cJSON_GetObjectItemCaseSensitive(json, "captivePortal"))) { + if (!captivePortal.FromJSON(json["captivePortal"])) { OS_LOGE(TAG, "Unable to load captive portal config"); return false; } - if (!backend.FromJSON(cJSON_GetObjectItemCaseSensitive(json, "backend"))) { + if (!backend.FromJSON(json["backend"])) { OS_LOGE(TAG, "Unable to load backend config"); return false; } - if (!serialInput.FromJSON(cJSON_GetObjectItemCaseSensitive(json, "serialInput"))) { + if (!serialInput.FromJSON(json["serialInput"])) { OS_LOGE(TAG, "Unable to load serial input config"); return false; } - if (!otaUpdate.FromJSON(cJSON_GetObjectItemCaseSensitive(json, "otaUpdate"))) { + if (!otaUpdate.FromJSON(json["otaUpdate"])) { OS_LOGE(TAG, "Unable to load ota update config"); return false; } - if (!estop.FromJSON(cJSON_GetObjectItemCaseSensitive(json, "estop"))) { + if (!estop.FromJSON(json["estop"])) { OS_LOGE(TAG, "Unable to load estop config"); return false; } @@ -138,17 +138,15 @@ bool RootConfig::FromJSON(const cJSON* json) return true; } -cJSON* RootConfig::ToJSON(bool withSensitiveData) const +void RootConfig::ToJSON(json_gen_str_t* gen, const char* name, bool withSensitiveData) const { - cJSON* root = cJSON_CreateObject(); - - cJSON_AddItemToObject(root, "rf", rf.ToJSON(withSensitiveData)); - cJSON_AddItemToObject(root, "wifi", wifi.ToJSON(withSensitiveData)); - cJSON_AddItemToObject(root, "captivePortal", captivePortal.ToJSON(withSensitiveData)); - cJSON_AddItemToObject(root, "backend", backend.ToJSON(withSensitiveData)); - cJSON_AddItemToObject(root, "serialInput", serialInput.ToJSON(withSensitiveData)); - cJSON_AddItemToObject(root, "otaUpdate", otaUpdate.ToJSON(withSensitiveData)); - cJSON_AddItemToObject(root, "estop", estop.ToJSON(withSensitiveData)); - - return root; + JSON::objBegin(gen, name); + rf.ToJSON(gen, "rf", withSensitiveData); + wifi.ToJSON(gen, "wifi", withSensitiveData); + captivePortal.ToJSON(gen, "captivePortal", withSensitiveData); + backend.ToJSON(gen, "backend", withSensitiveData); + serialInput.ToJSON(gen, "serialInput", withSensitiveData); + otaUpdate.ToJSON(gen, "otaUpdate", withSensitiveData); + estop.ToJSON(gen, "estop", withSensitiveData); + JSON::objEnd(gen, name); } diff --git a/src/config/SerialInputConfig.cpp b/components/config/src/SerialInputConfig.cpp similarity index 74% rename from src/config/SerialInputConfig.cpp rename to components/config/src/SerialInputConfig.cpp index b851e665..ade9bfb4 100644 --- a/src/config/SerialInputConfig.cpp +++ b/components/config/src/SerialInputConfig.cpp @@ -40,29 +40,27 @@ flatbuffers::Offset return Serialization::Configuration::CreateSerialInputConfig(builder, echoEnabled); } -bool SerialInputConfig::FromJSON(const cJSON* json) +bool SerialInputConfig::FromJSON(JSON::JsonView json) { - if (json == nullptr) { + if (!json.valid()) { OS_LOGW(TAG, "Config is null, setting to default"); ToDefault(); return true; } - if (cJSON_IsObject(json) == 0) { + if (!json.isObject()) { OS_LOGE(TAG, "json is not an object"); return false; } - Internal::Utils::FromJsonBool(echoEnabled, json, "echoEnabled", true); + if (!json["echoEnabled"].tryGetBool(echoEnabled)) echoEnabled = true; return true; } -cJSON* SerialInputConfig::ToJSON(bool withSensitiveData) const +void SerialInputConfig::ToJSON(json_gen_str_t* gen, const char* name, bool withSensitiveData) const { - cJSON* root = cJSON_CreateObject(); - - cJSON_AddBoolToObject(root, "echoEnabled", echoEnabled); - - return root; + JSON::objBegin(gen, name); + json_gen_obj_set_bool(gen, "echoEnabled", echoEnabled); + JSON::objEnd(gen, name); } diff --git a/src/config/WiFiConfig.cpp b/components/config/src/WiFiConfig.cpp similarity index 61% rename from src/config/WiFiConfig.cpp rename to components/config/src/WiFiConfig.cpp index 725f6c9e..3e6ec532 100644 --- a/src/config/WiFiConfig.cpp +++ b/components/config/src/WiFiConfig.cpp @@ -8,8 +8,8 @@ const char* const TAG = "Config::WiFiConfig"; using namespace OpenShock::Config; WiFiConfig::WiFiConfig() - : accessPointSSID(OPENSHOCK_FW_AP_PREFIX) - , hostname(OPENSHOCK_FW_HOSTNAME) + : accessPointSSID(CONFIG_OPENSHOCK_FW_AP_PREFIX) + , hostname(CONFIG_OPENSHOCK_FW_HOSTNAME) , credentialsList() { } @@ -23,8 +23,8 @@ WiFiConfig::WiFiConfig(std::string_view accessPointSSID, std::string_view hostna void WiFiConfig::ToDefault() { - accessPointSSID = OPENSHOCK_FW_AP_PREFIX; - hostname = OPENSHOCK_FW_HOSTNAME; + accessPointSSID = CONFIG_OPENSHOCK_FW_AP_PREFIX; + hostname = CONFIG_OPENSHOCK_FW_HOSTNAME; credentialsList.clear(); } @@ -36,8 +36,8 @@ bool WiFiConfig::FromFlatbuffers(const Serialization::Configuration::WiFiConfig* return true; } - Internal::Utils::FromFbsStr(accessPointSSID, config->ap_ssid(), OPENSHOCK_FW_AP_PREFIX); - Internal::Utils::FromFbsStr(hostname, config->hostname(), OPENSHOCK_FW_HOSTNAME); + Internal::Utils::FromFbsStr(accessPointSSID, config->ap_ssid(), CONFIG_OPENSHOCK_FW_AP_PREFIX); + Internal::Utils::FromFbsStr(hostname, config->hostname(), CONFIG_OPENSHOCK_FW_HOSTNAME); Internal::Utils::FromFbsVec(credentialsList, config->credentials()); return true; @@ -55,29 +55,38 @@ flatbuffers::Offset WiFiCon return Serialization::Configuration::CreateWiFiConfig(builder, builder.CreateString(accessPointSSID), builder.CreateString(hostname), builder.CreateVector(fbsCredentialsList)); } -bool WiFiConfig::FromJSON(const cJSON* json) +bool WiFiConfig::FromJSON(JSON::JsonView json) { - if (json == nullptr) { + if (!json.valid()) { OS_LOGW(TAG, "Config is null, setting to default"); ToDefault(); return true; } - if (cJSON_IsObject(json) == 0) { + if (!json.isObject()) { OS_LOGE(TAG, "json is not an object"); return false; } - Internal::Utils::FromJsonStr(accessPointSSID, json, "accessPointSSID", OPENSHOCK_FW_AP_PREFIX); - Internal::Utils::FromJsonStr(hostname, json, "hostname", OPENSHOCK_FW_HOSTNAME); + if (std::string_view sv; json["accessPointSSID"].tryGetStr(sv)) { + accessPointSSID = sv; + } else { + accessPointSSID = CONFIG_OPENSHOCK_FW_AP_PREFIX; + } + + if (std::string_view sv; json["hostname"].tryGetStr(sv)) { + hostname = sv; + } else { + hostname = CONFIG_OPENSHOCK_FW_HOSTNAME; + } - const cJSON* credentialsListJson = cJSON_GetObjectItemCaseSensitive(json, "credentials"); - if (credentialsListJson == nullptr) { + JSON::JsonView credentialsListJson = json["credentials"]; + if (!credentialsListJson.valid()) { OS_LOGE(TAG, "credentials is null"); return false; } - if (cJSON_IsArray(credentialsListJson) == 0) { + if (!credentialsListJson.isArray()) { OS_LOGE(TAG, "credentials is not an array"); return false; } @@ -87,20 +96,16 @@ bool WiFiConfig::FromJSON(const cJSON* json) return true; } -cJSON* WiFiConfig::ToJSON(bool withSensitiveData) const +void WiFiConfig::ToJSON(json_gen_str_t* gen, const char* name, bool withSensitiveData) const { - cJSON* root = cJSON_CreateObject(); - - cJSON_AddStringToObject(root, "accessPointSSID", accessPointSSID.c_str()); - cJSON_AddStringToObject(root, "hostname", hostname.c_str()); - - cJSON* credentialsListJson = cJSON_CreateArray(); + JSON::objBegin(gen, name); + JSON::objSetString(gen, "accessPointSSID", accessPointSSID); + JSON::objSetString(gen, "hostname", hostname); + json_gen_push_array(gen, "credentials"); for (auto& credentials : credentialsList) { - cJSON_AddItemToArray(credentialsListJson, credentials.ToJSON(withSensitiveData)); + credentials.ToJSON(gen, nullptr, withSensitiveData); } - - cJSON_AddItemToObject(root, "credentials", credentialsListJson); - - return root; + json_gen_pop_array(gen); + JSON::objEnd(gen, name); } diff --git a/src/config/WiFiCredentials.cpp b/components/config/src/WiFiCredentials.cpp similarity index 80% rename from src/config/WiFiCredentials.cpp rename to components/config/src/WiFiCredentials.cpp index 3b825640..a0669518 100644 --- a/src/config/WiFiCredentials.cpp +++ b/components/config/src/WiFiCredentials.cpp @@ -4,6 +4,8 @@ const char* const TAG = "Config::WiFiCredentials"; #include "config/internal/utils.h" #include "Logging.h" + +#include #include "util/HexUtils.h" using namespace OpenShock::Config; @@ -145,31 +147,41 @@ flatbuffers::Offset Wi return Serialization::Configuration::CreateWiFiCredentials(builder, id, ssidOffset, passwordOffset, toFbsAuthMode(authMode), bssidPtr); } -bool WiFiCredentials::FromJSON(const cJSON* json) +bool WiFiCredentials::FromJSON(JSON::JsonView json) { - if (json == nullptr) { + if (!json.valid()) { OS_LOGW(TAG, "Config is null, setting to default"); ToDefault(); return true; } - if (cJSON_IsObject(json) == 0) { + if (!json.isObject()) { OS_LOGE(TAG, "json is not an object"); return false; } - Internal::Utils::FromJsonU8(id, json, "id", 0); - Internal::Utils::FromJsonStr(ssid, json, "ssid", ""); - Internal::Utils::FromJsonStr(password, json, "password", ""); + if (!json["id"].tryGetU8(id)) id = 0; + + if (std::string_view sv; json["ssid"].tryGetStr(sv)) { + ssid = sv; + } else { + ssid.clear(); + } + + if (std::string_view sv; json["password"].tryGetStr(sv)) { + password = sv; + } else { + password.clear(); + } uint8_t authModeVal = static_cast(FbsAuthMode::UNKNOWN); - Internal::Utils::FromJsonU8(authModeVal, json, "authMode", static_cast(FbsAuthMode::UNKNOWN)); + if (!json["authMode"].tryGetU8(authModeVal)) authModeVal = static_cast(FbsAuthMode::UNKNOWN); authMode = fromFbsAuthMode(static_cast(authModeVal)); bssid.fill(0); - const cJSON* bssidJson = cJSON_GetObjectItemCaseSensitive(json, "bssid"); - if (cJSON_IsString(bssidJson) && bssidJson->valuestring != nullptr && strlen(bssidJson->valuestring) == 12) { - HexUtils::TryParseHex(bssidJson->valuestring, 12, bssid.data(), bssid.size()); + std::string_view bssidStr; + if (json["bssid"].tryGetStr(bssidStr) && bssidStr.size() == 12) { + HexUtils::TryParseHex(bssidStr.data(), bssidStr.size(), bssid.data(), bssid.size()); } if (ssid.empty()) { @@ -180,24 +192,22 @@ bool WiFiCredentials::FromJSON(const cJSON* json) return true; } -cJSON* WiFiCredentials::ToJSON(bool withSensitiveData) const +void WiFiCredentials::ToJSON(json_gen_str_t* gen, const char* name, bool withSensitiveData) const { - cJSON* root = cJSON_CreateObject(); - - cJSON_AddNumberToObject(root, "id", id); //-V2564 - cJSON_AddStringToObject(root, "ssid", ssid.c_str()); + JSON::objBegin(gen, name); + json_gen_obj_set_int(gen, "id", id); + JSON::objSetString(gen, "ssid", ssid); if (withSensitiveData) { - cJSON_AddStringToObject(root, "password", password.c_str()); + JSON::objSetString(gen, "password", password); } - cJSON_AddNumberToObject(root, "authMode", static_cast(toFbsAuthMode(authMode))); + json_gen_obj_set_int(gen, "authMode", static_cast(toFbsAuthMode(authMode))); if (HasPinnedBSSID()) { char hex[13]; for (std::size_t i = 0; i < bssid.size(); ++i) { HexUtils::ToHex(bssid[i], &hex[i * 2]); } hex[12] = '\0'; - cJSON_AddStringToObject(root, "bssid", hex); + JSON::objSetString(gen, "bssid", hex); } - - return root; + JSON::objEnd(gen, name); } diff --git a/components/config/src/internal/utils.cpp b/components/config/src/internal/utils.cpp new file mode 100644 index 00000000..89516a0a --- /dev/null +++ b/components/config/src/internal/utils.cpp @@ -0,0 +1,41 @@ +#include "config/internal/utils.h" + +const char* const TAG = "Config::Internal::Utils"; + +#include "Chipset.h" +#include "Logging.h" + +#include + +using namespace OpenShock; + +bool Config::Internal::Utils::FromU8GpioNum(gpio_num_t& val, uint8_t u8Val) +{ + if (u8Val >= GPIO_NUM_MAX || !GPIO_IS_VALID_GPIO(u8Val)) { + OS_LOGE(TAG, "invalid GPIO number"); + return false; + } + + val = static_cast(u8Val); + + return true; +} + +void Config::Internal::Utils::FromFbsStr(std::string& str, const flatbuffers::String* fbsStr, const char* defaultStr) +{ + if (fbsStr != nullptr) { + str = fbsStr->c_str(); + } else { + str = defaultStr; + } +} + +bool Config::Internal::Utils::FromJsonGpioNum(gpio_num_t& val, JSON::JsonView json, std::string_view name) +{ + uint8_t u8Val; + if (!json[name].tryGetU8(u8Val)) { + return false; + } + + return FromU8GpioNum(val, u8Val); +} diff --git a/components/core/CMakeLists.txt b/components/core/CMakeLists.txt new file mode 100644 index 00000000..a993b63c --- /dev/null +++ b/components/core/CMakeLists.txt @@ -0,0 +1,27 @@ +# OpenShock application core. +# +# The tightly-interconnected heart of the firmware: config, serialization, wifi, +# captive portal, gateway/OTA, radio, visual, estop, command handling and the +# websocket message handlers. These form a single dependency cycle +# (config <-> serialization, wifi <-> captive portal, gateway <-> ota, ...) and +# therefore cannot be split into separate components — they live here together. +FILE(GLOB_RECURSE core_sources + ${CMAKE_CURRENT_SOURCE_DIR}/src/*.c + ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/*.S) + +idf_component_register( + SRCS ${core_sources} + INCLUDE_DIRS "include" + PRIV_INCLUDE_DIRS "src" # internal headers (CaptivePortalInstance.h, GatewayClient.h, message_handlers/impl/*, ...) + REQUIRES common crypto temporal logging hwutil http chipset led_drivers protocols osjson events esp_driver_rmt esp_driver_gpio esp_netif esp_event esp_wifi esp_timer app_update nvs_flash mbedtls flatbuffers serialization config + # Internal-only now that CaptivePortalInstance.h + GatewayClient.h are private: + PRIV_REQUIRES esp_http_server fs dns_server rfc8908 espressif__esp_websocket_client + device_control # estop/CommandHandler/visual used only in core .cpp +) + +# IDF compiles application components with -Werror. The legacy PlatformIO build +# applied -Wno-error to our own sources (build_src_flags), so partial designated +# initializers and similar intentional patterns don't fail the build. Match that +# here; can be tightened later by fixing the individual warnings. +target_compile_options(${COMPONENT_LIB} PRIVATE -Wno-error) diff --git a/components/core/Kconfig.projbuild b/components/core/Kconfig.projbuild new file mode 100644 index 00000000..f5bd95bf --- /dev/null +++ b/components/core/Kconfig.projbuild @@ -0,0 +1,66 @@ +menu "OpenShock Board" + + config OPENSHOCK_RF_TX_GPIO + int "RF transmitter GPIO" + default -1 + help + GPIO driving the 433 MHz RF transmitter. Leave at -1 to ship without a + fixed pin; the firmware then uses the pin configured at runtime. + + config OPENSHOCK_ESTOP_PIN + int "E-Stop input GPIO" + default -1 + help + GPIO for a physical emergency-stop button. -1 disables the built-in E-Stop. + + config OPENSHOCK_LED_GPIO + int "Status LED GPIO (single-colour)" + default -1 + help + GPIO for a plain on/off status LED. -1 if the board has none. + + config OPENSHOCK_LED_WS2812B + int "Status LED GPIO (WS2812B RGB)" + default -1 + help + GPIO for an addressable WS2812B RGB status LED. -1 if the board has none. + + config OPENSHOCK_LED_SWAP_RG_CHANNELS + bool "Swap red/green channels on the RGB LED" + default n + help + Some WS2812B-alike parts wire the red and green channels swapped. + Enable to correct the colour order. + + config OPENSHOCK_API_DOMAIN + string "OpenShock API domain" + default "api.openshock.app" + help + Default backend API domain the hub connects to. Overridable at runtime. + + config OPENSHOCK_FW_CDN_DOMAIN + string "Firmware CDN domain" + default "firmware.openshock.org" + help + Default CDN domain used for OTA firmware/version lookups. Overridable at runtime. + + config OPENSHOCK_FW_HOSTNAME + string "Default hostname" + default "OpenShock" + help + Default network hostname; also the base of the default user agent. + + config OPENSHOCK_FW_AP_PREFIX + string "SoftAP SSID prefix" + default "OpenShock-" + help + Prefix for the captive-portal SoftAP SSID (the device MAC is appended). + + config OPENSHOCK_URI_BUFFER_SIZE + int "HTTP URI buffer size (bytes)" + default 256 + help + Stack buffer size for composed request URIs. Increase if a configured + domain plus path can exceed this. + +endmenu diff --git a/components/core/idf_component.yml b/components/core/idf_component.yml new file mode 100644 index 00000000..9fde9e8a --- /dev/null +++ b/components/core/idf_component.yml @@ -0,0 +1,8 @@ +# esp_websocket_client backs the gateway (LCG) WebSocket client (src/GatewayClient.cpp) +# now that we no longer use the Arduino BadWebSockets library. In IDF 5.x it is a +# managed component rather than a bundled one, so declare it here. +# +# The captive portal's static assets are read from a littlefs partition via the raw +# vendored littlefs core (components/littlefs) — no VFS, no joltwallet. +dependencies: + espressif/esp_websocket_client: "^1.4.0" diff --git a/include/GatewayConnectionManager.h b/components/core/include/GatewayConnectionManager.h similarity index 79% rename from include/GatewayConnectionManager.h rename to components/core/include/GatewayConnectionManager.h index 060c9228..5ced56ac 100644 --- a/include/GatewayConnectionManager.h +++ b/components/core/include/GatewayConnectionManager.h @@ -1,10 +1,10 @@ #pragma once -#include "AccountLinkResultCode.h" -#include "span.h" +#include "enums/AccountLinkResultCode.h" #include #include +#include #include namespace OpenShock::GatewayConnectionManager { @@ -17,7 +17,7 @@ namespace OpenShock::GatewayConnectionManager { void UnLink(); bool SendMessageTXT(std::string_view data); - bool SendMessageBIN(tcb::span data); + bool SendMessageBIN(std::span data); void MarkPingReceived(); diff --git a/include/OtaUpdateManager.h b/components/core/include/OtaUpdateManager.h similarity index 66% rename from include/OtaUpdateManager.h rename to components/core/include/OtaUpdateManager.h index 2d319488..7067b087 100644 --- a/include/OtaUpdateManager.h +++ b/components/core/include/OtaUpdateManager.h @@ -1,7 +1,7 @@ #pragma once -#include "FirmwareBootType.h" -#include "OtaUpdateChannel.h" +#include "enums/FirmwareBootType.h" +#include "enums/OtaUpdateChannel.h" #include "SemVer.h" #include @@ -9,6 +9,10 @@ #include #include +namespace OpenShock::HTTP { + class Client; +} + namespace OpenShock::OtaUpdateManager { [[nodiscard]] bool Init(); @@ -19,9 +23,9 @@ namespace OpenShock::OtaUpdateManager { uint8_t filesystemBinaryHash[32]; }; - bool TryGetFirmwareVersion(OtaUpdateChannel channel, OpenShock::SemVer& version); + bool TryGetFirmwareVersion(HTTP::Client& client, OtaUpdateChannel channel, OpenShock::SemVer& version); bool TryGetFirmwareBoards(const OpenShock::SemVer& version, std::vector& boards); - bool TryGetFirmwareRelease(const OpenShock::SemVer& version, FirmwareRelease& release); + bool TryGetFirmwareRelease(HTTP::Client& client, const OpenShock::SemVer& version, FirmwareRelease& release); bool TryStartFirmwareUpdate(const OpenShock::SemVer& version); diff --git a/include/captiveportal/Manager.h b/components/core/include/captiveportal/Manager.h similarity index 68% rename from include/captiveportal/Manager.h rename to components/core/include/captiveportal/Manager.h index 37d338ad..59be1c30 100644 --- a/include/captiveportal/Manager.h +++ b/components/core/include/captiveportal/Manager.h @@ -1,10 +1,9 @@ #pragma once #include +#include #include -#include "span.h" - namespace OpenShock::CaptivePortal { [[nodiscard]] bool Init(); @@ -18,9 +17,12 @@ namespace OpenShock::CaptivePortal { bool IsRunning(); + /// @brief The fixed IPv4 address the captive portal AP serves from (e.g. "4.3.2.1"). + const char* ApIPv4String(); + bool SendMessageTXT(uint8_t socketId, std::string_view data); - bool SendMessageBIN(uint8_t socketId, tcb::span data); + bool SendMessageBIN(uint8_t socketId, std::span data); bool BroadcastMessageTXT(std::string_view data); - bool BroadcastMessageBIN(tcb::span data); + bool BroadcastMessageBIN(std::span data); } // namespace OpenShock::CaptivePortal diff --git a/include/http/JsonAPI.h b/components/core/include/http/JsonAPI.h similarity index 100% rename from include/http/JsonAPI.h rename to components/core/include/http/JsonAPI.h diff --git a/components/core/include/message_handlers/WebSocket.h b/components/core/include/message_handlers/WebSocket.h new file mode 100644 index 00000000..51fd9a3f --- /dev/null +++ b/components/core/include/message_handlers/WebSocket.h @@ -0,0 +1,9 @@ +#pragma once + +#include +#include + +namespace OpenShock::MessageHandlers::WebSocket { + void HandleGatewayBinary(std::span data); + void HandleLocalBinary(uint8_t socketId, std::span data); +} diff --git a/include/serialization/CallbackFn.h b/components/core/include/serialization/CallbackFn.h similarity index 60% rename from include/serialization/CallbackFn.h rename to components/core/include/serialization/CallbackFn.h index 9d677c94..664b0305 100644 --- a/include/serialization/CallbackFn.h +++ b/components/core/include/serialization/CallbackFn.h @@ -2,9 +2,8 @@ #include #include - -#include "span.h" +#include namespace OpenShock::Serialization::Common { - typedef std::function data)> SerializationCallbackFn; + typedef std::function data)> SerializationCallbackFn; } diff --git a/include/serialization/JsonAPI.h b/components/core/include/serialization/JsonAPI.h similarity index 61% rename from include/serialization/JsonAPI.h rename to components/core/include/serialization/JsonAPI.h index 322e8c8b..180dd7c1 100644 --- a/include/serialization/JsonAPI.h +++ b/components/core/include/serialization/JsonAPI.h @@ -1,8 +1,7 @@ #pragma once -#include "ShockerModelType.h" - -#include +#include "enums/ShockerModelType.h" +#include "json/Json.h" #include #include @@ -41,9 +40,9 @@ namespace OpenShock::Serialization::JsonAPI { std::string country; }; - bool ParseLcgInstanceDetailsJsonResponse(int code, const cJSON* root, LcgInstanceDetailsResponse& out); - bool ParseBackendVersionJsonResponse(int code, const cJSON* root, BackendVersionResponse& out); - bool ParseAccountLinkJsonResponse(int code, const cJSON* root, AccountLinkResponse& out); - bool ParseHubInfoJsonResponse(int code, const cJSON* root, HubInfoResponse& out); - bool ParseAssignLcgJsonResponse(int code, const cJSON* root, AssignLcgResponse& out); + bool ParseLcgInstanceDetailsJsonResponse(int code, JSON::JsonView root, LcgInstanceDetailsResponse& out); + bool ParseBackendVersionJsonResponse(int code, JSON::JsonView root, BackendVersionResponse& out); + bool ParseAccountLinkJsonResponse(int code, JSON::JsonView root, AccountLinkResponse& out); + bool ParseHubInfoJsonResponse(int code, JSON::JsonView root, HubInfoResponse& out); + bool ParseAssignLcgJsonResponse(int code, JSON::JsonView root, AssignLcgResponse& out); } // namespace OpenShock::Serialization::JsonAPI diff --git a/include/serialization/JsonSerial.h b/components/core/include/serialization/JsonSerial.h similarity index 65% rename from include/serialization/JsonSerial.h rename to components/core/include/serialization/JsonSerial.h index 9348a8d3..3bbef5b4 100644 --- a/include/serialization/JsonSerial.h +++ b/components/core/include/serialization/JsonSerial.h @@ -1,9 +1,8 @@ #pragma once -#include "ShockerCommandType.h" -#include "ShockerModelType.h" - -#include +#include "enums/ShockerCommandType.h" +#include "enums/ShockerModelType.h" +#include "json/Json.h" #include @@ -16,5 +15,5 @@ namespace OpenShock::Serialization::JsonSerial { uint16_t durationMs; }; - bool ParseShockerCommand(const cJSON* root, ShockerCommand& out); + bool ParseShockerCommand(JSON::JsonView root, ShockerCommand& out); } // namespace OpenShock::Serialization::JsonSerial diff --git a/include/serialization/WSGateway.h b/components/core/include/serialization/WSGateway.h similarity index 95% rename from include/serialization/WSGateway.h rename to components/core/include/serialization/WSGateway.h index 768d7665..8bf83a33 100644 --- a/include/serialization/WSGateway.h +++ b/components/core/include/serialization/WSGateway.h @@ -1,6 +1,6 @@ #pragma once -#include "FirmwareBootType.h" +#include "enums/FirmwareBootType.h" #include "SemVer.h" #include "serialization/CallbackFn.h" diff --git a/include/serialization/WSLocal.h b/components/core/include/serialization/WSLocal.h similarity index 97% rename from include/serialization/WSLocal.h rename to components/core/include/serialization/WSLocal.h index d6f48b86..57f034a4 100644 --- a/include/serialization/WSLocal.h +++ b/components/core/include/serialization/WSLocal.h @@ -5,8 +5,6 @@ #include "serialization/_fbs/WifiNetworkEventType_generated.h" -#include - #include namespace OpenShock { diff --git a/include/wifi/WiFiManager.h b/components/core/include/wifi/WiFiManager.h similarity index 91% rename from include/wifi/WiFiManager.h rename to components/core/include/wifi/WiFiManager.h index 1e90549d..05294773 100644 --- a/include/wifi/WiFiManager.h +++ b/components/core/include/wifi/WiFiManager.h @@ -55,11 +55,6 @@ namespace OpenShock::WiFiManager { /// @return True if the hub is connected to a network bool GetIPAddress(char* ipAddress); - /// @brief Gets the hubs IP address if it's connected to a network (IPv6) - /// @param ipAddress Variable to store the IP address in - /// @return True if the hub is connected to a network - bool GetIPv6Address(char* ipAddress); - /// @brief Gets a copy of the vector of discovered WiFi networks /// @return Vector of discovered WiFiNetworks std::vector GetDiscoveredWiFiNetworks(); diff --git a/include/wifi/WiFiNetwork.h b/components/core/include/wifi/WiFiNetwork.h similarity index 100% rename from include/wifi/WiFiNetwork.h rename to components/core/include/wifi/WiFiNetwork.h diff --git a/include/wifi/WiFiScanManager.h b/components/core/include/wifi/WiFiScanManager.h similarity index 97% rename from include/wifi/WiFiScanManager.h rename to components/core/include/wifi/WiFiScanManager.h index 691c03ba..c6fc4327 100644 --- a/include/wifi/WiFiScanManager.h +++ b/components/core/include/wifi/WiFiScanManager.h @@ -6,6 +6,7 @@ #include #include +#include namespace OpenShock::WiFiScanManager { [[nodiscard]] bool Init(); diff --git a/include/wifi/WiFiScanStatus.h b/components/core/include/wifi/WiFiScanStatus.h similarity index 100% rename from include/wifi/WiFiScanStatus.h rename to components/core/include/wifi/WiFiScanStatus.h diff --git a/components/core/src/GatewayClient.cpp b/components/core/src/GatewayClient.cpp new file mode 100644 index 00000000..f76645b9 --- /dev/null +++ b/components/core/src/GatewayClient.cpp @@ -0,0 +1,255 @@ +#include "GatewayClient.h" + +const char* const TAG = "GatewayClient"; + +#include "config/Config.h" +#include "events/Events.h" +#include "Logging.h" +#include "message_handlers/WebSocket.h" +#include "OpenShock.h" +#include "OtaUpdateManager.h" +#include "serialization/WSGateway.h" +#include "Temporal.h" +#include "visual/VisualStateManager.h" + +#include + +#include + +using namespace OpenShock; + +const int64_t GATEWAY_PING_TIMEOUT = 90'000; + +// WebSocket opcodes (RFC 6455) as delivered by esp_websocket_client event data. +static constexpr int WS_OP_TEXT = 0x01; +static constexpr int WS_OP_BINARY = 0x02; + +static bool s_bootStatusSent = false; + +GatewayClient::GatewayClient(const std::string& authToken) + : m_headers() + , m_client(nullptr) + , m_state(GatewayClientState::Disconnected) + , m_lastPingTimestamp(0) + , m_binReasm() +{ + OS_LOGD(TAG, "Creating GatewayClient"); + + m_headers = "Firmware-Version: " OPENSHOCK_FW_VERSION "\r\n" + "Device-Token: " + + authToken + "\r\n"; +} +GatewayClient::~GatewayClient() +{ + OS_LOGD(TAG, "Destroying GatewayClient"); + + _setState(GatewayClientState::Disconnected); + + if (m_client != nullptr) { + esp_websocket_client_close(m_client, pdMS_TO_TICKS(1000)); + esp_websocket_client_destroy(m_client); + m_client = nullptr; + } +} + +void GatewayClient::connect(const std::string& host, uint16_t port, const std::string& path) +{ + if (m_state != GatewayClientState::Disconnected) { + return; + } + + _setState(GatewayClientState::Connecting); + + esp_websocket_client_config_t config = {}; + config.host = host.c_str(); + config.port = port; + config.path = path.c_str(); + config.transport = WEBSOCKET_TRANSPORT_OVER_SSL; + config.user_agent = OpenShock::Constants::FW_USERAGENT; + config.headers = m_headers.c_str(); + config.disable_auto_reconnect = true; // GatewayConnectionManager owns reconnection + config.crt_bundle_attach = esp_crt_bundle_attach; // verify server against the compiled-in CA bundle + + m_client = esp_websocket_client_init(&config); + if (m_client == nullptr) { + OS_LOGE(TAG, "Failed to initialize WebSocket client"); + _setState(GatewayClientState::Disconnected); + return; + } + + esp_websocket_register_events(m_client, WEBSOCKET_EVENT_ANY, &GatewayClient::_eventHandler, this); + + esp_err_t err = esp_websocket_client_start(m_client); + if (err != ESP_OK) { + OS_LOGE(TAG, "Failed to start WebSocket client: %s", esp_err_to_name(err)); + esp_websocket_client_destroy(m_client); + m_client = nullptr; + _setState(GatewayClientState::Disconnected); + return; + } +} + +void GatewayClient::disconnect() +{ + if (m_state != GatewayClientState::Connected) { + return; + } + _setState(GatewayClientState::Disconnecting); + if (m_client != nullptr) { + esp_websocket_client_close(m_client, pdMS_TO_TICKS(1000)); + } +} + +bool GatewayClient::sendMessageTXT(std::string_view data) +{ + if (m_state != GatewayClientState::Connected || m_client == nullptr) { + return false; + } + + return esp_websocket_client_send_text(m_client, data.data(), data.length(), pdMS_TO_TICKS(10'000)) >= 0; +} + +bool GatewayClient::sendMessageBIN(std::span data) +{ + if (m_state != GatewayClientState::Connected || m_client == nullptr) { + return false; + } + + return esp_websocket_client_send_bin(m_client, reinterpret_cast(data.data()), data.size(), pdMS_TO_TICKS(10'000)) >= 0; +} + +void GatewayClient::markPingReceived() +{ + m_lastPingTimestamp = OpenShock::millis(); +} + +bool GatewayClient::loop() +{ + if (m_state == GatewayClientState::Disconnected) { + return false; + } + + // esp_websocket_client runs its own task; we only enforce the app-level ping timeout. + if (m_state != GatewayClientState::Connected) { + // Still connecting or disconnecting + return true; + } + + if (m_lastPingTimestamp != 0 && (OpenShock::millis() - m_lastPingTimestamp) > GATEWAY_PING_TIMEOUT) { + OS_LOGW(TAG, "No ping received from gateway for %lld ms, forcing reconnect", GATEWAY_PING_TIMEOUT); + if (m_client != nullptr) { + esp_websocket_client_close(m_client, pdMS_TO_TICKS(1000)); + } + _setState(GatewayClientState::Disconnected); + return false; + } + + return true; +} + +void GatewayClient::_setState(GatewayClientState state) +{ + if (m_state == state) { + return; + } + + m_state = state; + + ESP_ERROR_CHECK(esp_event_post(OPENSHOCK_EVENTS, OPENSHOCK_EVENT_GATEWAY_CLIENT_STATE_CHANGED, &m_state, sizeof(m_state), portMAX_DELAY)); +} + +void GatewayClient::_sendBootStatus() +{ + if (s_bootStatusSent) return; + + OS_LOGV(TAG, "Sending Gateway boot status message"); + + int32_t updateId; + if (!Config::GetOtaUpdateId(updateId)) { + OS_LOGE(TAG, "Failed to get OTA update ID"); + return; + } + + OpenShock::OtaUpdateStep updateStep; + if (!Config::GetOtaUpdateStep(updateStep)) { + OS_LOGE(TAG, "Failed to get OTA firmware boot type"); + return; + } + + using namespace std::string_view_literals; + + OpenShock::SemVer version; + if (!OpenShock::TryParseSemVer(OPENSHOCK_FW_VERSION ""sv, version)) { + OS_LOGE(TAG, "Failed to parse firmware version"); + return; + } + + s_bootStatusSent = Serialization::Gateway::SerializeBootStatusMessage(updateId, OtaUpdateManager::GetFirmwareBootType(), [this](std::span data) { return sendMessageBIN(data); }); + + if (s_bootStatusSent && updateStep != OpenShock::OtaUpdateStep::None) { + if (!Config::SetOtaUpdateStep(OpenShock::OtaUpdateStep::None)) { + OS_LOGE(TAG, "Failed to reset firmware boot type to normal"); + } + } +} + +void GatewayClient::_eventHandler(void* arg, esp_event_base_t /*base*/, int32_t eventId, void* eventData) +{ + auto* self = static_cast(arg); + const auto* data = static_cast(eventData); + + switch (eventId) { + case WEBSOCKET_EVENT_CONNECTED: + self->m_lastPingTimestamp = 0; + self->_setState(GatewayClientState::Connected); + self->_sendBootStatus(); + break; + case WEBSOCKET_EVENT_DISCONNECTED: + case WEBSOCKET_EVENT_CLOSED: + self->_setState(GatewayClientState::Disconnected); + break; + case WEBSOCKET_EVENT_ERROR: + OS_LOGE(TAG, "Received error from API"); + break; + case WEBSOCKET_EVENT_DATA: + self->_handleData(data); + break; + default: + break; + } +} + +void GatewayClient::_handleData(const esp_websocket_event_data_t* data) +{ + if (data == nullptr) { + return; + } + + // Control frames (ping/pong/close) and empty frames carry no application payload. + if (data->op_code == WS_OP_TEXT) { + OS_LOGW(TAG, "Received text from API, JSON parsing is not supported anymore :D"); + return; + } + if (data->op_code != WS_OP_BINARY && data->op_code != 0x00 /* continuation */) { + return; + } + + // esp_websocket_client may deliver a message in chunks (payload_offset/payload_len). + // Fast path: a complete single-frame message. + if (data->payload_offset == 0 && data->data_len == data->payload_len) { + MessageHandlers::WebSocket::HandleGatewayBinary(std::span(reinterpret_cast(data->data_ptr), data->data_len)); + return; + } + + // Fragmented / chunked message: accumulate until complete. + if (data->payload_offset == 0) { + m_binReasm.clear(); + m_binReasm.reserve(data->payload_len); + } + m_binReasm.insert(m_binReasm.end(), reinterpret_cast(data->data_ptr), reinterpret_cast(data->data_ptr) + data->data_len); + + if (m_binReasm.size() >= static_cast(data->payload_len)) { + MessageHandlers::WebSocket::HandleGatewayBinary(std::span(m_binReasm.data(), m_binReasm.size())); + m_binReasm.clear(); + } +} diff --git a/include/GatewayClient.h b/components/core/src/GatewayClient.h similarity index 57% rename from include/GatewayClient.h rename to components/core/src/GatewayClient.h index 49458438..c9a44c1c 100644 --- a/include/GatewayClient.h +++ b/components/core/src/GatewayClient.h @@ -1,15 +1,16 @@ #pragma once -#include "Common.h" -#include "GatewayClientState.h" -#include "span.h" +#include "enums/GatewayClientState.h" +#include "OpenShock.h" -#include +#include +#include #include +#include #include #include -#include +#include namespace OpenShock { class GatewayClient { @@ -26,7 +27,7 @@ namespace OpenShock { void disconnect(); bool sendMessageTXT(std::string_view data); - bool sendMessageBIN(tcb::span data); + bool sendMessageBIN(std::span data); void markPingReceived(); @@ -35,10 +36,14 @@ namespace OpenShock { private: void _setState(GatewayClientState state); void _sendBootStatus(); - void _handleEvent(WStype_t type, uint8_t* payload, std::size_t length); - WebSocketsClient m_webSocket; + static void _eventHandler(void* arg, esp_event_base_t base, int32_t eventId, void* eventData); + void _handleData(const esp_websocket_event_data_t* data); + + std::string m_headers; + esp_websocket_client_handle_t m_client; GatewayClientState m_state; int64_t m_lastPingTimestamp; + std::vector m_binReasm; // reassembly buffer for fragmented binary frames }; } // namespace OpenShock diff --git a/src/GatewayConnectionManager.cpp b/components/core/src/GatewayConnectionManager.cpp similarity index 83% rename from src/GatewayConnectionManager.cpp rename to components/core/src/GatewayConnectionManager.cpp index e6640378..2aa2dabd 100644 --- a/src/GatewayConnectionManager.cpp +++ b/components/core/src/GatewayConnectionManager.cpp @@ -6,11 +6,12 @@ const char* const TAG = "GatewayConnectionManager"; #include "captiveportal/Manager.h" #include "config/Config.h" -#include "Core.h" +#include "events/Events.h" #include "GatewayClient.h" #include "http/JsonAPI.h" #include "Logging.h" #include "serialization/WSLocal.h" +#include "Temporal.h" #include "SimpleMutex.h" @@ -18,20 +19,6 @@ const char* const TAG = "GatewayConnectionManager"; #include #include -// -// ###### ######## ###### ## ## ######## #### ######## ## ## ######## #### ###### ## ## -// ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## -// ## ## ## ## ## ## ## ## ## #### ## ## ## ## ## ## -// ###### ###### ## ## ## ######## ## ## ## ######## ## ###### ##### -// ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## -// ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## -// ###### ######## ###### ####### ## ## #### ## ## ## ## #### ###### ## ## -// -// TODO: Fix loading CA Certificate bundles, currently fails with "[esp_crt_bundle.c:161] esp_crt_bundle_init(): Unable to allocate memory for bundle" -// This is probably due to the fact that the bundle is too large for the ESP32's heap or the bundle is incorrectly packedy them -// -#warning SSL certificate verification is currently not implemented, by RFC definition this is a security risk, and allows for MITM attacks, but the realistic risk is low - const char* const AUTH_TOKEN_FILE = "/authToken"; const uint8_t FLAG_NONE = 0; @@ -63,21 +50,25 @@ static void DestroyClient() s_wsClient = nullptr; } -static void evh_gotIP(arduino_event_t* event) -{ - (void)event; - - s_flags.fetch_or(FLAG_HAS_IP, std::memory_order_relaxed); - OS_LOGD(TAG, "Got IP address"); -} - -static void evh_wiFiDisconnected(arduino_event_t* event) +static void handleWiFiStateChanged(void* arg, esp_event_base_t base, int32_t id, void* data) { - (void)event; - - s_flags.store(FLAG_NONE, std::memory_order_relaxed); - DestroyClient(); - OS_LOGD(TAG, "Lost IP address"); + (void)arg; + (void)base; + (void)id; + + switch (*static_cast(data)) { + case OPENSHOCK_WIFI_STATE_GOT_IP: + s_flags.fetch_or(FLAG_HAS_IP, std::memory_order_relaxed); + OS_LOGD(TAG, "Got IP address"); + break; + case OPENSHOCK_WIFI_STATE_DISCONNECTED: + s_flags.store(FLAG_NONE, std::memory_order_relaxed); + DestroyClient(); + OS_LOGD(TAG, "Lost IP address"); + break; + default: + break; + } } static bool checkIsDeAuthRateLimited(int64_t millis) @@ -94,9 +85,11 @@ namespace JsonAPI = OpenShock::Serialization::JsonAPI; bool GatewayConnectionManager::Init() { - WiFi.onEvent(evh_gotIP, ARDUINO_EVENT_WIFI_STA_GOT_IP); - WiFi.onEvent(evh_gotIP, ARDUINO_EVENT_WIFI_STA_GOT_IP6); - WiFi.onEvent(evh_wiFiDisconnected, ARDUINO_EVENT_WIFI_STA_DISCONNECTED); + esp_err_t err = esp_event_handler_register(OPENSHOCK_EVENTS, OPENSHOCK_EVENT_WIFI_STATE_CHANGED, handleWiFiStateChanged, nullptr); + if (err != ESP_OK) { + OS_LOGE(TAG, "Failed to register WiFi state event handler: %s", esp_err_to_name(err)); + return false; + } return true; } @@ -124,7 +117,7 @@ AccountLinkResultCode GatewayConnectionManager::Link(std::string_view linkCode) DestroyClient(); - OS_LOGD(TAG, "Attempting to link to account using code %.*s", linkCode.length(), linkCode.data()); + OS_LOGD(TAG, "Attempting to link to account using code %.*s", static_cast(linkCode.length()), linkCode.data()); if (linkCode.length() != LINK_CODE_LENGTH) { OS_LOGE(TAG, "Invalid link code length: expected %zu, got %zu", static_cast(LINK_CODE_LENGTH), linkCode.length()); @@ -195,7 +188,7 @@ bool GatewayConnectionManager::SendMessageTXT(std::string_view data) return client->sendMessageTXT(data); } -bool GatewayConnectionManager::SendMessageBIN(tcb::span data) +bool GatewayConnectionManager::SendMessageBIN(std::span data) { auto client = GetClient(); if (client == nullptr) { diff --git a/src/OtaUpdateManager.cpp b/components/core/src/OtaUpdateManager.cpp similarity index 85% rename from src/OtaUpdateManager.cpp rename to components/core/src/OtaUpdateManager.cpp index e4e467e4..9da8676d 100644 --- a/src/OtaUpdateManager.cpp +++ b/components/core/src/OtaUpdateManager.cpp @@ -3,27 +3,29 @@ const char* const TAG = "OtaUpdateManager"; #include "captiveportal/Manager.h" -#include "Common.h" #include "config/Config.h" -#include "Core.h" +#include "fs/FsCheck.h" #include "GatewayConnectionManager.h" #include "Hashing.h" #include "http/HTTPRequestManager.h" +#include "hwutil/PartitionUtils.h" #include "Logging.h" +#include "OpenShock.h" #include "SemVer.h" #include "serialization/WSGateway.h" #include "SimpleMutex.h" +#include "Temporal.h" #include "util/HexUtils.h" -#include "util/PartitionUtils.h" -#include "util/StringUtils.h" +#include "StringHelpers.h" #include "util/TaskUtils.h" #include "wifi/WiFiManager.h" +#include +#include #include +#include #include - -#include -#include +#include #include #include @@ -45,16 +47,6 @@ using namespace std::string_view_literals; #define OPENSHOCK_FW_CDN_FILESYSTEM_URL_FORMAT OPENSHOCK_FW_CDN_VERSION_BASE_URL_FORMAT "/staticfs.bin" #define OPENSHOCK_FW_CDN_SHA256_HASHES_URL_FORMAT OPENSHOCK_FW_CDN_VERSION_BASE_URL_FORMAT "/hashes.sha256.txt" -/// @brief Stops initArduino() from handling OTA rollbacks -/// @todo Get rid of Arduino entirely. >:( -/// -/// @see .platformio/packages/framework-arduinoespressif32/cores/esp32/esp32-hal-misc.c -/// @return true -bool verifyRollbackLater() -{ - return true; -} - enum OtaTaskEventFlag : uint32_t { OTA_TASK_EVENT_UPDATE_REQUESTED = 1 << 0, OTA_TASK_EVENT_WIFI_DISCONNECTED = 1 << 1, // If both connected and disconnected are set, disconnected takes priority. @@ -174,7 +166,7 @@ static bool _sendFailureMessage(std::string_view message, bool fatal = false) return true; } -static bool otaum_flash_app_partition(const esp_partition_t* partition, std::string_view remoteUrl, const uint8_t (&remoteHash)[32]) +static bool otaum_flash_app_partition(OpenShock::HTTP::Client& client, const esp_partition_t* partition, std::string_view remoteUrl, const uint8_t (&remoteHash)[32]) { OS_LOGD(TAG, "Flashing app partition"); @@ -190,7 +182,7 @@ static bool otaum_flash_app_partition(const esp_partition_t* partition, std::str return true; }; - if (!OpenShock::FlashPartitionFromUrl(partition, remoteUrl, remoteHash, onProgress)) { + if (!OpenShock::FlashPartitionFromUrl(client, partition, remoteUrl, remoteHash, onProgress)) { OS_LOGE(TAG, "Failed to flash app partition"); _sendFailureMessage("Failed to flash app partition"sv); return false; @@ -210,7 +202,7 @@ static bool otaum_flash_app_partition(const esp_partition_t* partition, std::str return true; } -static bool otaum_flash_fs_partition(const esp_partition_t* parition, std::string_view remoteUrl, const uint8_t (&remoteHash)[32]) +static bool otaum_flash_fs_partition(OpenShock::HTTP::Client& client, const esp_partition_t* parition, std::string_view remoteUrl, const uint8_t (&remoteHash)[32]) { if (!otaum_send_progress_msg(Serialization::Types::OtaUpdateProgressTask::PreparingForUpdate, 0.0f)) { return false; @@ -237,7 +229,7 @@ static bool otaum_flash_fs_partition(const esp_partition_t* parition, std::strin return true; }; - if (!OpenShock::FlashPartitionFromUrl(parition, remoteUrl, remoteHash, onProgress)) { + if (!OpenShock::FlashPartitionFromUrl(client, parition, remoteUrl, remoteHash, onProgress)) { OS_LOGE(TAG, "Failed to flash filesystem partition"); _sendFailureMessage("Failed to flash filesystem partition"sv); return false; @@ -247,21 +239,38 @@ static bool otaum_flash_fs_partition(const esp_partition_t* parition, std::strin return false; } - // Attempt to mount filesystem. - fs::LittleFSFS test; - if (!test.begin(false, "/static", 10, "static0")) { + // Verify the freshly flashed image mounts cleanly. + if (!OpenShock::TestFilesystem(parition)) { OS_LOGE(TAG, "Failed to mount filesystem"); _sendFailureMessage("Failed to mount filesystem"sv); return false; } - test.end(); return true; } +static esp_err_t otaum_set_wdt_timeout(uint32_t timeoutMs) +{ + esp_task_wdt_config_t twdt_config = { + .timeout_ms = timeoutMs, + .idle_core_mask = (1 << CONFIG_FREERTOS_NUMBER_OF_CORES) - 1, // Bitmask of all cores + .trigger_panic = true, + }; + // ESP-IDF initializes the TWDT at startup (CONFIG_ESP_TASK_WDT_INIT, default y), so + // reconfigure it instead of re-initializing. esp_task_wdt_reconfigure() returns + // ESP_ERR_INVALID_STATE if the TWDT isn't initialized yet, in which case we fall back to init. + esp_err_t err = esp_task_wdt_reconfigure(&twdt_config); + if (err == ESP_ERR_INVALID_STATE) { + // Not yet initialized, fall back to init + return esp_task_wdt_init(&twdt_config); + } + + return err; +} + static void otaum_restore_wdt_timeout() { - if (esp_task_wdt_init(5, true) != ESP_OK) { + if (otaum_set_wdt_timeout(5) != ESP_OK) { OS_LOGE(TAG, "Failed to restore task watchdog timeout"); } }; @@ -334,6 +343,10 @@ static void otaum_updatetask(void* arg) continue; } + // Reuse a single connection for this update's sequential CDN requests + // (version -> release -> filesystem binary -> app binary). + OpenShock::HTTP::Client client; + OpenShock::SemVer version; if (updateRequested) { updateRequested = false; @@ -346,7 +359,7 @@ static void otaum_updatetask(void* arg) OS_LOGD(TAG, "Checking for updates"); // Fetch current version. - if (!OtaUpdateManager::TryGetFirmwareVersion(config.updateChannel, version)) { + if (!OtaUpdateManager::TryGetFirmwareVersion(client, config.updateChannel, version)) { OS_LOGE(TAG, "Failed to fetch firmware version"); continue; } @@ -359,7 +372,7 @@ static void otaum_updatetask(void* arg) continue; } - OS_LOGD(TAG, "Updating to version: %.*s", versionStr.length(), versionStr.data()); + OS_LOGD(TAG, "Updating to version: %.*s", static_cast(versionStr.length()), versionStr.data()); // Generate random int32_t for this update. int32_t updateId = static_cast(esp_random()); @@ -383,7 +396,7 @@ static void otaum_updatetask(void* arg) // Fetch current release. OtaUpdateManager::FirmwareRelease release; - if (!OtaUpdateManager::TryGetFirmwareRelease(version, release)) { + if (!OtaUpdateManager::TryGetFirmwareRelease(client, version, release)) { OS_LOGE(TAG, "Failed to fetch firmware release"); // TODO: Send error message to server _sendFailureMessage("Failed to fetch firmware release"sv); continue; @@ -391,10 +404,10 @@ static void otaum_updatetask(void* arg) // Print release. OS_LOGD(TAG, "Firmware release:"); - OS_LOGD(TAG, " Version: %.*s", versionStr.length(), versionStr.data()); - OS_LOGD(TAG, " App binary URL: %.*s", release.appBinaryUrl.length(), release.appBinaryUrl.data()); + OS_LOGD(TAG, " Version: %.*s", static_cast(versionStr.length()), versionStr.data()); + OS_LOGD(TAG, " App binary URL: %.*s", static_cast(release.appBinaryUrl.length()), release.appBinaryUrl.data()); OS_LOGD(TAG, " App binary hash: %s", HexUtils::ToHex<32>(release.appBinaryHash).data()); - OS_LOGD(TAG, " Filesystem binary URL: %.*s", release.filesystemBinaryUrl.length(), release.filesystemBinaryUrl.data()); + OS_LOGD(TAG, " Filesystem binary URL: %.*s", static_cast(release.filesystemBinaryUrl.length()), release.filesystemBinaryUrl.data()); OS_LOGD(TAG, " Filesystem binary hash: %s", HexUtils::ToHex<32>(release.filesystemBinaryHash).data()); // Get available app update partition. @@ -415,18 +428,18 @@ static void otaum_updatetask(void* arg) // Increase task watchdog timeout. // Prevents panics on some ESP32s when clearing large partitions. - if (esp_task_wdt_init(15, true) != ESP_OK) { + if (otaum_set_wdt_timeout(15) != ESP_OK) { OS_LOGE(TAG, "Failed to increase task watchdog timeout"); _sendFailureMessage("Failed to increase task watchdog timeout"sv); continue; } // Flash app and filesystem partitions. - if (!otaum_flash_fs_partition(filesystemPartition, release.filesystemBinaryUrl, release.filesystemBinaryHash)) { + if (!otaum_flash_fs_partition(client, filesystemPartition, release.filesystemBinaryUrl, release.filesystemBinaryHash)) { otaum_restore_wdt_timeout(); continue; } - if (!otaum_flash_app_partition(appPartition, release.appBinaryUrl, release.appBinaryHash)) { + if (!otaum_flash_app_partition(client, appPartition, release.appBinaryUrl, release.appBinaryHash)) { otaum_restore_wdt_timeout(); continue; } @@ -519,18 +532,7 @@ bool OtaUpdateManager::Init() } // Infer boot type from update step. - switch (updateStep) { - case OtaUpdateStep::Updated: - _bootType = FirmwareBootType::NewFirmware; - break; - case OtaUpdateStep::Validating: // If the update step is validating, we have failed in the middle of validating the new firmware, meaning this is a rollback. - case OtaUpdateStep::RollingBack: - _bootType = FirmwareBootType::Rollback; - break; - default: - _bootType = FirmwareBootType::Normal; - break; - } + _bootType = OpenShock::InferFirmwareBootType(updateStep); if (updateStep == OtaUpdateStep::Updated) { if (!Config::SetOtaUpdateStep(OtaUpdateStep::Validating)) { @@ -559,7 +561,7 @@ bool OtaUpdateManager::Init() return true; } -bool OtaUpdateManager::TryGetFirmwareVersion(OtaUpdateChannel channel, OpenShock::SemVer& version) +bool OtaUpdateManager::TryGetFirmwareVersion(HTTP::Client& client, OtaUpdateChannel channel, OpenShock::SemVer& version) { std::string_view channelIndexUrl; switch (channel) { @@ -579,7 +581,7 @@ bool OtaUpdateManager::TryGetFirmwareVersion(OtaUpdateChannel channel, OpenShock OS_LOGD(TAG, "Fetching firmware version from %s", channelIndexUrl); - auto response = OpenShock::HTTP::GetString( + auto response = client.GetString( channelIndexUrl, { {"Accept", "text/plain"} @@ -592,7 +594,7 @@ bool OtaUpdateManager::TryGetFirmwareVersion(OtaUpdateChannel channel, OpenShock } if (!OpenShock::TryParseSemVer(response.data, version)) { - OS_LOGE(TAG, "Failed to parse firmware version: %.*s", response.data.size(), response.data.data()); + OS_LOGE(TAG, "Failed to parse firmware version: %.*s", static_cast(response.data.size()), response.data.data()); return false; } @@ -620,14 +622,14 @@ bool OtaUpdateManager::TryGetFirmwareBoards(const OpenShock::SemVer& version, st static bool _tryParseIntoHash(std::string_view hash, uint8_t (&hashBytes)[32]) { if (HexUtils::TryParseHex(hash.data(), hash.size(), hashBytes, 32) != 32) { - OS_LOGE(TAG, "Failed to parse hash: %.*s", hash.size(), hash.data()); + OS_LOGE(TAG, "Failed to parse hash: %.*s", static_cast(hash.size()), hash.data()); return false; } return true; } -bool OtaUpdateManager::TryGetFirmwareRelease(const OpenShock::SemVer& version, FirmwareRelease& release) +bool OtaUpdateManager::TryGetFirmwareRelease(HTTP::Client& client, const OpenShock::SemVer& version, FirmwareRelease& release) { auto versionStr = version.toString(); // TODO: This is abusing the SemVer::toString() method causing alot of string copies, fix this @@ -649,7 +651,7 @@ bool OtaUpdateManager::TryGetFirmwareRelease(const OpenShock::SemVer& version, F } // Fetch hashes. - auto sha256HashesResponse = OpenShock::HTTP::GetString( + auto sha256HashesResponse = client.GetString( sha256HashesUrl, { {"Accept", "text/plain"} @@ -668,7 +670,7 @@ bool OtaUpdateManager::TryGetFirmwareRelease(const OpenShock::SemVer& version, F for (std::string_view line : hashesLines) { auto parts = OpenShock::StringSplitWhiteSpace(line); if (parts.size() != 2) { - OS_LOGE(TAG, "Invalid hashes entry: %.*s", line.size(), line.data()); + OS_LOGE(TAG, "Invalid hashes entry: %.*s", static_cast(line.size()), line.data()); return false; } @@ -678,7 +680,7 @@ bool OtaUpdateManager::TryGetFirmwareRelease(const OpenShock::SemVer& version, F file = OpenShock::StringRemovePrefix(file, "./"sv); if (hash.size() != 64) { - OS_LOGE(TAG, "Invalid hash: %.*s", hash.size(), hash.data()); + OS_LOGE(TAG, "Invalid hash: %.*s", static_cast(hash.size()), hash.data()); return false; } diff --git a/components/core/src/captiveportal/CaptivePortalInstance.cpp b/components/core/src/captiveportal/CaptivePortalInstance.cpp new file mode 100644 index 00000000..60c4d7f4 --- /dev/null +++ b/components/core/src/captiveportal/CaptivePortalInstance.cpp @@ -0,0 +1,1087 @@ +#include +#include + +#include "captiveportal/CaptivePortalInstance.h" + +const char* const TAG = "CaptivePortalInstance"; + +#include "captiveportal/Manager.h" +#include "Chipset.h" +#include "CommandHandler.h" +#include "config/Config.h" +#include "enums/OtaUpdateChannel.h" +#include "estop/EStopManager.h" +#include "GatewayConnectionManager.h" +#include "http/ContentTypes.h" +#include "hwutil/PartitionUtils.h" +#include "Logging.h" +#include "message_handlers/WebSocket.h" +#include "RateLimiter.h" +#include "rfc8908/RFC8908Handler.h" +#include "serialization/WSLocal.h" +#include "wifi/WiFiManager.h" +#include "wifi/WiFiScanManager.h" + +#include "serialization/_fbs/HubToLocalMessage_generated.h" + +#include "json/Json.h" + +#include +#include +#include +#include + +const uint16_t HTTP_PORT = 80; + +// Largest inbound WebSocket message we'll accept (local FlatBuffer commands are tiny). +static constexpr size_t MAX_WS_MSG = 8 * 1024; + +// HTTP status lines (esp_http_server needs the full "code reason" string, and stores +// the pointer rather than copying — so these must have static storage duration). +static constexpr const char* S200 = "200 OK"; +static constexpr const char* S304 = "304 Not Modified"; +static constexpr const char* S400 = "400 Bad Request"; +static constexpr const char* S429 = "429 Too Many Requests"; +static constexpr const char* S500 = "500 Internal Server Error"; + +static const char* const JSON_ERR_INTERNAL = "{\"error\":\"InternalError\"}"; +static const char* const JSON_ERR_MISSING_PARAM = "{\"error\":\"MissingParam\"}"; +static const char* const JSON_ERR_INVALID_PIN = "{\"error\":\"InvalidPin\"}"; +static const char* const JSON_ERR_MISSING_SSID = "{\"error\":\"MissingSsid\"}"; +static const char* const JSON_ERR_INVALID_SSID = "{\"error\":\"InvalidSsid\"}"; +static const char* const JSON_ERR_PASSWORD_SHORT = "{\"error\":\"PasswordTooShort\"}"; +static const char* const JSON_ERR_PASSWORD_LONG = "{\"error\":\"PasswordTooLong\"}"; +static const char* const JSON_ERR_CODE_REQUIRED = "{\"error\":\"CodeRequired\"}"; +static const char* const JSON_ERR_INVALID_CHANNEL = "{\"error\":\"InvalidChannel\"}"; +static const char* const JSON_ERR_RATE_LIMITED = "{\"error\":\"RateLimited\"}"; + +using namespace OpenShock; + +static OpenShock::RateLimiter& getAccountLinkRateLimiter() +{ + static OpenShock::RateLimiter* rl = nullptr; + if (rl == nullptr) { + rl = new OpenShock::RateLimiter(); + rl->addLimit(60'000, 5); // 5 attempts per minute + rl->addLimit(300'000, 10); // 10 attempts per 5 minutes + } + return *rl; +} + +static const esp_partition_t* getStaticPartition() +{ + const esp_partition_t* partition = esp_partition_find_first(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_SPIFFS, "static0"); + if (partition != nullptr) { + return partition; + } + + partition = esp_partition_find_first(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_SPIFFS, "static1"); + if (partition != nullptr) { + return partition; + } + + return nullptr; +} + +static const char* getPartitionHash() +{ + const esp_partition_t* partition = getStaticPartition(); + if (partition == nullptr) { + return nullptr; + } + + static char hash[65]; + if (!OpenShock::TryGetPartitionHash(partition, hash)) { + return nullptr; + } + + return hash; +} + +// --------------------------------------------------------------------------- +// Small HTTP helpers (replace the ESPAsyncWebServer request API) +// --------------------------------------------------------------------------- + +static esp_err_t sendResp(httpd_req_t* req, const char* status, const char* type, std::string_view body) +{ + httpd_resp_set_status(req, status); + if (type != nullptr) { + httpd_resp_set_type(req, type); + } + return httpd_resp_send(req, body.data(), body.size()); +} + +static std::string urlDecode(const char* s) +{ + auto hexVal = [](char h) -> int { + if (h >= '0' && h <= '9') return h - '0'; + h = static_cast(h | 0x20); + return h - 'a' + 10; + }; + + std::string out; + for (size_t i = 0; s[i] != '\0'; ++i) { + char c = s[i]; + if (c == '+') { + out += ' '; + } else if (c == '%' && isxdigit(static_cast(s[i + 1])) && isxdigit(static_cast(s[i + 2]))) { + out += static_cast((hexVal(s[i + 1]) << 4) | hexVal(s[i + 2])); + i += 2; + } else { + out += c; + } + } + return out; +} + +// Read and URL-decode a query-string parameter. httpd does not decode for us. +static bool getQueryParam(httpd_req_t* req, const char* key, std::string& out) +{ + size_t qlen = httpd_req_get_url_query_len(req); + if (qlen == 0) { + return false; + } + + std::string query; + query.resize(qlen + 1); + if (httpd_req_get_url_query_str(req, query.data(), query.size()) != ESP_OK) { + return false; + } + + char val[256]; + if (httpd_query_key_value(query.c_str(), key, val, sizeof(val)) != ESP_OK) { + return false; + } + + out = urlDecode(val); + return true; +} + +// Read a urlencoded request body (application/x-www-form-urlencoded). +static bool recvBody(httpd_req_t* req, std::string& out) +{ + int total = req->content_len; + if (total <= 0 || total > 4096) { + return false; + } + + out.resize(total); + int off = 0; + while (off < total) { + int r = httpd_req_recv(req, out.data() + off, total - off); + if (r == HTTPD_SOCK_ERR_TIMEOUT) { + continue; + } + if (r <= 0) { + return false; + } + off += r; + } + return true; +} + +static bool getFormParam(const std::string& body, const char* key, std::string& out) +{ + char val[256]; + if (httpd_query_key_value(body.c_str(), key, val, sizeof(val)) != ESP_OK) { + return false; + } + out = urlDecode(val); + return true; +} + +static const char* contentTypeForPath(const std::string& path) +{ + auto endsWith = [&](const char* ext) { + size_t n = strlen(ext); + return path.size() >= n && path.compare(path.size() - n, n, ext) == 0; + }; + + if (endsWith(".html")) return HTTP::ContentType::TextHTML; + if (endsWith(".js")) return "text/javascript"; + if (endsWith(".css")) return "text/css"; + if (endsWith(".svg")) return "image/svg+xml"; + if (endsWith(".png")) return "image/png"; + if (endsWith(".ico")) return "image/x-icon"; + if (endsWith(".json")) return HTTP::ContentType::JSON; + if (endsWith(".woff2")) return "font/woff2"; + if (endsWith(".txt")) return HTTP::ContentType::TextPlain; + return "application/octet-stream"; +} + +// --------------------------------------------------------------------------- +// API handlers (ported 1:1 from the ESPAsyncWebServer lambdas) +// --------------------------------------------------------------------------- + +static esp_err_t apiBoard(httpd_req_t* req) +{ + bool hasPredefinedPins = CONFIG_OPENSHOCK_RF_TX_GPIO != OPENSHOCK_GPIO_INVALID; + return sendResp(req, S200, HTTP::ContentType::JSON, hasPredefinedPins ? "{\"has_predefined_pins\":true}" : "{\"has_predefined_pins\":false}"); +} + +static esp_err_t apiPortalClose(httpd_req_t* req) +{ + CaptivePortal::SetUserDone(); + return sendResp(req, S200, nullptr, {}); +} + +static esp_err_t apiWifiScan(httpd_req_t* req) +{ + bool run = true; + std::string runStr; + if (getQueryParam(req, "run", runStr)) { + run = atoi(runStr.c_str()) != 0; + } + if (run) { + WiFiScanManager::StartScan(); + } else { + WiFiScanManager::AbortScan(); + } + return sendResp(req, S200, nullptr, {}); +} + +static esp_err_t apiWifiNetworksDelete(httpd_req_t* req) +{ + std::string ssid; + if (!getQueryParam(req, "ssid", ssid)) { + return sendResp(req, S400, HTTP::ContentType::JSON, JSON_ERR_MISSING_SSID); + } + if (!WiFiManager::Forget(ssid.c_str())) { + return sendResp(req, S500, HTTP::ContentType::JSON, JSON_ERR_INTERNAL); + } + return sendResp(req, S200, nullptr, {}); +} + +static esp_err_t apiAccountLink(httpd_req_t* req) +{ + if (!getAccountLinkRateLimiter().tryRequest()) { + return sendResp(req, S429, HTTP::ContentType::JSON, JSON_ERR_RATE_LIMITED); + } + std::string code; + if (!getQueryParam(req, "code", code)) { + return sendResp(req, S400, HTTP::ContentType::JSON, JSON_ERR_CODE_REQUIRED); + } + auto result = GatewayConnectionManager::Link(std::string_view(code)); + using ResultCode = OpenShock::AccountLinkResultCode; + if (result == ResultCode::Success) { + return sendResp(req, S200, nullptr, {}); + } + const char* error; + switch (result) { + case ResultCode::CodeRequired: + error = "CodeRequired"; + break; + case ResultCode::InvalidCodeLength: + error = "InvalidCodeLength"; + break; + case ResultCode::NoInternetConnection: + error = "NoInternetConnection"; + break; + case ResultCode::InvalidCode: + error = "InvalidCode"; + break; + case ResultCode::RateLimited: + error = "RateLimited"; + break; + case ResultCode::RequestFailed: + error = "RequestFailed"; + break; + case ResultCode::RequestTimedOut: + error = "RequestTimedOut"; + break; + case ResultCode::ServerError: + error = "ServerError"; + break; + case ResultCode::InvalidResponse: + error = "InvalidResponse"; + break; + case ResultCode::ConfigSaveFailed: + error = "ConfigSaveFailed"; + break; + default: + error = "InternalError"; + break; + } + OpenShock::JSON::StringWriter writer; + json_gen_str_t* gen = writer.gen(); + json_gen_start_object(gen); + OpenShock::JSON::objSetString(gen, "error", error); + json_gen_end_object(gen); + std::string json = writer.finish(); + return sendResp(req, S400, HTTP::ContentType::JSON, json); +} + +static esp_err_t apiAccountDelete(httpd_req_t* req) +{ + GatewayConnectionManager::UnLink(); + return sendResp(req, S200, nullptr, {}); +} + +static esp_err_t apiConfigRfPin(httpd_req_t* req) +{ + std::string pinStr; + if (!getQueryParam(req, "pin", pinStr)) { + return sendResp(req, S400, HTTP::ContentType::JSON, JSON_ERR_INVALID_PIN); + } + int pin = atoi(pinStr.c_str()); + auto result = CommandHandler::SetRfTxPin(static_cast(pin)); + using ResultCode = OpenShock::SetGPIOResultCode; + if (result != ResultCode::Success) { + return sendResp(req, S400, HTTP::ContentType::JSON, (result == ResultCode::InvalidPin) ? JSON_ERR_INVALID_PIN : JSON_ERR_INTERNAL); + } + OpenShock::JSON::StringWriter writer; + json_gen_str_t* gen = writer.gen(); + json_gen_start_object(gen); + json_gen_obj_set_int(gen, "pin", pin); + json_gen_end_object(gen); + std::string json = writer.finish(); + return sendResp(req, S200, HTTP::ContentType::JSON, json); +} + +static esp_err_t apiConfigEstopPin(httpd_req_t* req) +{ + std::string pinStr; + if (!getQueryParam(req, "pin", pinStr)) { + return sendResp(req, S400, HTTP::ContentType::JSON, JSON_ERR_INVALID_PIN); + } + int8_t pin = static_cast(atoi(pinStr.c_str())); + if (!IsValidInputPin(pin)) { + return sendResp(req, S400, HTTP::ContentType::JSON, JSON_ERR_INVALID_PIN); + } + if (!EStopManager::SetEStopPin(static_cast(pin)) || !Config::SetEStopGpioPin(static_cast(pin))) { + return sendResp(req, S500, HTTP::ContentType::JSON, JSON_ERR_INTERNAL); + } + OpenShock::JSON::StringWriter writer; + json_gen_str_t* gen = writer.gen(); + json_gen_start_object(gen); + json_gen_obj_set_int(gen, "pin", pin); + json_gen_end_object(gen); + std::string json = writer.finish(); + return sendResp(req, S200, HTTP::ContentType::JSON, json); +} + +static esp_err_t apiConfigEstopEnabled(httpd_req_t* req) +{ + std::string enabledStr; + if (!getQueryParam(req, "enabled", enabledStr)) { + return sendResp(req, S400, HTTP::ContentType::JSON, JSON_ERR_INTERNAL); + } + bool enabled = atoi(enabledStr.c_str()) != 0; + bool success = EStopManager::SetEStopEnabled(enabled) && Config::SetEStopEnabled(enabled); + if (success) { + return sendResp(req, S200, nullptr, {}); + } + return sendResp(req, S500, HTTP::ContentType::JSON, JSON_ERR_INTERNAL); +} + +static esp_err_t apiWifiNetworksAdd(httpd_req_t* req) +{ + std::string body; + if (!recvBody(req, body)) { + return sendResp(req, S400, HTTP::ContentType::JSON, JSON_ERR_MISSING_SSID); + } + + std::string ssid; + if (!getFormParam(body, "ssid", ssid) || ssid.empty() || ssid.length() > 31) { + return sendResp(req, S400, HTTP::ContentType::JSON, ssid.empty() ? JSON_ERR_MISSING_SSID : JSON_ERR_INVALID_SSID); + } + + std::string password; + if (getFormParam(body, "password", password) && !password.empty()) { + if (password.length() < 8) { + return sendResp(req, S400, HTTP::ContentType::JSON, JSON_ERR_PASSWORD_SHORT); + } + if (password.length() > 63) { + return sendResp(req, S400, HTTP::ContentType::JSON, JSON_ERR_PASSWORD_LONG); + } + } + + bool connect = true; + std::string connectStr; + if (getFormParam(body, "connect", connectStr)) { + connect = atoi(connectStr.c_str()) != 0; + } + + wifi_auth_mode_t authMode = WIFI_AUTH_MAX; + std::string securityStr; + if (getFormParam(body, "security", securityStr)) { + int sec = atoi(securityStr.c_str()); + if (sec >= 0 && sec <= static_cast(WIFI_AUTH_MAX)) { + authMode = static_cast(sec); + } + } + + if (!WiFiManager::Save(ssid.c_str(), std::string_view(password), connect, authMode)) { + return sendResp(req, S500, HTTP::ContentType::JSON, JSON_ERR_INTERNAL); + } + return sendResp(req, S200, nullptr, {}); +} + +static esp_err_t apiWifiConnect(httpd_req_t* req) +{ + std::string body; + std::string ssid; + if (!recvBody(req, body) || !getFormParam(body, "ssid", ssid)) { + return sendResp(req, S400, HTTP::ContentType::JSON, JSON_ERR_MISSING_SSID); + } + if (!WiFiManager::Connect(ssid.c_str())) { + return sendResp(req, S500, HTTP::ContentType::JSON, JSON_ERR_INTERNAL); + } + return sendResp(req, S200, nullptr, {}); +} + +static esp_err_t apiWifiDisconnect(httpd_req_t* req) +{ + WiFiManager::Disconnect(); + return sendResp(req, S200, nullptr, {}); +} + +static esp_err_t apiOtaEnabled(httpd_req_t* req) +{ + std::string enabledStr; + if (!getQueryParam(req, "enabled", enabledStr)) { + return sendResp(req, S400, HTTP::ContentType::JSON, JSON_ERR_MISSING_PARAM); + } + bool enabled = atoi(enabledStr.c_str()) != 0; + Config::OtaUpdateConfig cfg; + if (!Config::GetOtaUpdateConfig(cfg)) { + return sendResp(req, S500, HTTP::ContentType::JSON, JSON_ERR_INTERNAL); + } + cfg.isEnabled = enabled; + if (!Config::SetOtaUpdateConfig(cfg)) { + return sendResp(req, S500, HTTP::ContentType::JSON, JSON_ERR_INTERNAL); + } + return sendResp(req, S200, nullptr, {}); +} + +static esp_err_t apiOtaDomain(httpd_req_t* req) +{ + std::string domain; + if (!getQueryParam(req, "domain", domain)) { + return sendResp(req, S400, HTTP::ContentType::JSON, JSON_ERR_MISSING_PARAM); + } + Config::OtaUpdateConfig cfg; + if (!Config::GetOtaUpdateConfig(cfg)) { + return sendResp(req, S500, HTTP::ContentType::JSON, JSON_ERR_INTERNAL); + } + cfg.cdnDomain = domain; + if (!Config::SetOtaUpdateConfig(cfg)) { + return sendResp(req, S500, HTTP::ContentType::JSON, JSON_ERR_INTERNAL); + } + return sendResp(req, S200, nullptr, {}); +} + +static esp_err_t apiOtaChannel(httpd_req_t* req) +{ + std::string channelStr; + if (!getQueryParam(req, "channel", channelStr)) { + return sendResp(req, S400, HTTP::ContentType::JSON, JSON_ERR_MISSING_PARAM); + } + OtaUpdateChannel channel; + if (!TryParseOtaUpdateChannel(channel, channelStr.c_str())) { + return sendResp(req, S400, HTTP::ContentType::JSON, JSON_ERR_INVALID_CHANNEL); + } + Config::OtaUpdateConfig cfg; + if (!Config::GetOtaUpdateConfig(cfg)) { + return sendResp(req, S500, HTTP::ContentType::JSON, JSON_ERR_INTERNAL); + } + cfg.updateChannel = channel; + if (!Config::SetOtaUpdateConfig(cfg)) { + return sendResp(req, S500, HTTP::ContentType::JSON, JSON_ERR_INTERNAL); + } + return sendResp(req, S200, nullptr, {}); +} + +static esp_err_t apiOtaCheckInterval(httpd_req_t* req) +{ + std::string intervalStr; + if (!getQueryParam(req, "interval", intervalStr)) { + return sendResp(req, S400, HTTP::ContentType::JSON, JSON_ERR_MISSING_PARAM); + } + uint16_t interval = static_cast(atoi(intervalStr.c_str())); + Config::OtaUpdateConfig cfg; + if (!Config::GetOtaUpdateConfig(cfg)) { + return sendResp(req, S500, HTTP::ContentType::JSON, JSON_ERR_INTERNAL); + } + cfg.checkInterval = interval; + if (!Config::SetOtaUpdateConfig(cfg)) { + return sendResp(req, S500, HTTP::ContentType::JSON, JSON_ERR_INTERNAL); + } + return sendResp(req, S200, nullptr, {}); +} + +static esp_err_t apiOtaAllowBackendManagement(httpd_req_t* req) +{ + std::string allowStr; + if (!getQueryParam(req, "allow", allowStr)) { + return sendResp(req, S400, HTTP::ContentType::JSON, JSON_ERR_MISSING_PARAM); + } + bool allow = atoi(allowStr.c_str()) != 0; + Config::OtaUpdateConfig cfg; + if (!Config::GetOtaUpdateConfig(cfg)) { + return sendResp(req, S500, HTTP::ContentType::JSON, JSON_ERR_INTERNAL); + } + cfg.allowBackendManagement = allow; + if (!Config::SetOtaUpdateConfig(cfg)) { + return sendResp(req, S500, HTTP::ContentType::JSON, JSON_ERR_INTERNAL); + } + return sendResp(req, S200, nullptr, {}); +} + +static esp_err_t apiOtaRequireManualApproval(httpd_req_t* req) +{ + std::string requireStr; + if (!getQueryParam(req, "require", requireStr)) { + return sendResp(req, S400, HTTP::ContentType::JSON, JSON_ERR_MISSING_PARAM); + } + bool require = atoi(requireStr.c_str()) != 0; + Config::OtaUpdateConfig cfg; + if (!Config::GetOtaUpdateConfig(cfg)) { + return sendResp(req, S500, HTTP::ContentType::JSON, JSON_ERR_INTERNAL); + } + cfg.requireManualApproval = require; + if (!Config::SetOtaUpdateConfig(cfg)) { + return sendResp(req, S500, HTTP::ContentType::JSON, JSON_ERR_INTERNAL); + } + return sendResp(req, S200, nullptr, {}); +} + +static esp_err_t apiOtaCheck(httpd_req_t* req) +{ + // TODO: trigger OTA check - OtaUpdateManager does not yet expose a CheckForUpdates method + return sendResp(req, S200, nullptr, {}); +} + +// --------------------------------------------------------------------------- +// Static file serving (gzipped assets from the raw littlefs static0 partition) +// --------------------------------------------------------------------------- + +esp_err_t CaptivePortal::CaptivePortalInstance::staticFileHandler(httpd_req_t* req) +{ + auto* self = static_cast(req->user_ctx); + + if (!self->m_staticFs.isMounted()) { + // Filesystem image was never uploaded — serve the help page for any request. + return sendResp( + req, + S200, + HTTP::ContentType::TextPlain, + // Raw string literal (1+ to remove the first newline) + 1 + R"( +You probably forgot to upload the Filesystem with PlatformIO! +Go to PlatformIO -> Platform -> Upload Filesystem Image! +If this happened with a file we provided or you just need help, come to the Discord! + +discord.gg/OpenShock +)" + ); + } + + std::string uri(req->uri); + auto qpos = uri.find('?'); + if (qpos != std::string::npos) { + uri.resize(qpos); + } + + // Reject path traversal, then fall through to the captive redirect. + if (uri.find("..") != std::string::npos) { + return RFC8908::EmitRedirect(req); + } + + if (uri == "/") { + uri = "/index.html"; + } + + // All assets are pre-gzipped, stored at /www/.gz on the static0 partition. + std::string path = "/www" + uri + ".gz"; + + if (!self->m_staticFs.exists(path.c_str())) { + // Unknown path → captive redirect (matches serveStatic default-file fallthrough). + return RFC8908::EmitRedirect(req); + } + + // ETag / conditional request. Keep the quoted hash alive until after send. + std::string etag; + if (self->m_fsHash != nullptr) { + etag = std::string("\"") + self->m_fsHash + "\""; + char inm[80]; + if (httpd_req_get_hdr_value_str(req, "If-None-Match", inm, sizeof(inm)) == ESP_OK && etag == inm) { + httpd_resp_set_status(req, S304); + return httpd_resp_send(req, nullptr, 0); + } + } + + httpd_resp_set_type(req, contentTypeForPath(uri)); + httpd_resp_set_hdr(req, "Content-Encoding", "gzip"); + httpd_resp_set_hdr(req, "Cache-Control", "max-age=3600"); + if (!etag.empty()) { + httpd_resp_set_hdr(req, "ETag", etag.c_str()); + } + + bool ok = self->m_staticFs.readFile(path.c_str(), [req](std::span chunk) { return httpd_resp_send_chunk(req, reinterpret_cast(chunk.data()), chunk.size()) == ESP_OK; }); + if (!ok) { + // Headers/chunks may already be on the wire — can't cleanly redirect now. + return ESP_FAIL; + } + return httpd_resp_send_chunk(req, nullptr, 0); +} + +// --------------------------------------------------------------------------- +// WebSocket +// --------------------------------------------------------------------------- + +namespace { + // Per-session context so httpd's free callback can reap the fd->id slot on close. + struct WsSessCtx { + OpenShock::CaptivePortal::CaptivePortalInstance* self; + int fd; + }; + + // Deferred WS send marshalled onto the httpd task via httpd_queue_work. + struct WsSendJob { + httpd_handle_t hd; + int fd; + bool broadcast; + bool binary; + std::vector data; + }; + + void wsSendWork(void* arg) + { + auto* job = static_cast(arg); + + httpd_ws_frame_t frame = {}; + frame.final = true; + frame.type = job->binary ? HTTPD_WS_TYPE_BINARY : HTTPD_WS_TYPE_TEXT; + frame.payload = job->data.data(); + frame.len = job->data.size(); + + if (job->broadcast) { + size_t count = 8; + int fds[8]; + if (httpd_get_client_list(job->hd, &count, fds) == ESP_OK) { + for (size_t i = 0; i < count; ++i) { + if (httpd_ws_get_fd_info(job->hd, fds[i]) == HTTPD_WS_CLIENT_WEBSOCKET) { + httpd_ws_send_frame_async(job->hd, fds[i], &frame); + } + } + } + } else { + httpd_ws_send_frame_async(job->hd, job->fd, &frame); + } + + delete job; + } +} // namespace + +int CaptivePortal::CaptivePortalInstance::idForFd(int fd) +{ + ScopedLock lock__(&m_clientsMutex); + for (uint8_t i = 0; i < MAX_WS_CLIENTS; ++i) { + if (m_clients[i].used && m_clients[i].fd == fd) { + return i; + } + } + return -1; +} + +int CaptivePortal::CaptivePortalInstance::fdForId(uint8_t socketId) +{ + ScopedLock lock__(&m_clientsMutex); + if (socketId < MAX_WS_CLIENTS && m_clients[socketId].used) { + return m_clients[socketId].fd; + } + return -1; +} + +uint8_t CaptivePortal::CaptivePortalInstance::onWsOpen(int fd) +{ + ScopedLock lock__(&m_clientsMutex); + + // Idempotent: an fd already tracked keeps its id. + for (uint8_t i = 0; i < MAX_WS_CLIENTS; ++i) { + if (m_clients[i].used && m_clients[i].fd == fd) { + return i; + } + } + + for (uint8_t i = 0; i < MAX_WS_CLIENTS; ++i) { + if (!m_clients[i].used) { + m_clients[i].used = true; + m_clients[i].fd = fd; + m_clients[i].reasmType = WebSocketMessageType::Binary; + m_clients[i].reasm.clear(); + return i; + } + } + + return 0xFF; // full +} + +void CaptivePortal::CaptivePortalInstance::onWsClose(int fd) +{ + int id = -1; + { + ScopedLock lock__(&m_clientsMutex); + for (uint8_t i = 0; i < MAX_WS_CLIENTS; ++i) { + if (m_clients[i].used && m_clients[i].fd == fd) { + m_clients[i].used = false; + m_clients[i].reasm.clear(); + id = i; + break; + } + } + } + + if (id >= 0) { + handleWebSocketClientDisconnected(static_cast(id)); + } +} + +void CaptivePortal::CaptivePortalInstance::wsSessCtxFree(void* ctx) +{ + auto* c = static_cast(ctx); + if (c == nullptr) { + return; + } + c->self->onWsClose(c->fd); + delete c; +} + +esp_err_t CaptivePortal::CaptivePortalInstance::wsHandler(httpd_req_t* req) +{ + auto* self = static_cast(req->user_ctx); + int fd = httpd_req_to_sockfd(req); + + // Initial GET = handshake just completed → allocate id, arm disconnect hook, greet. + if (req->method == HTTP_GET) { + uint8_t id = self->onWsOpen(fd); + if (id == 0xFF) { + OS_LOGW(TAG, "WebSocket client table full, rejecting fd %d", fd); + httpd_sess_trigger_close(self->m_server, fd); + return ESP_OK; + } + + auto* sessCtx = new WsSessCtx {self, fd}; + httpd_sess_set_ctx(self->m_server, fd, sessCtx, &CaptivePortalInstance::wsSessCtxFree); + + self->handleWebSocketClientConnected(req, id); + return ESP_OK; + } + + // Data frame: two-call recv (length first, then payload). + httpd_ws_frame_t frame = {}; + esp_err_t ret = httpd_ws_recv_frame(req, &frame, 0); + if (ret != ESP_OK) { + return ret; + } + + if (frame.len == 0) { + self->onWsFrame(fd, frame.type, frame.final, {}); + return ESP_OK; + } + + if (frame.len > MAX_WS_MSG) { + OS_LOGE(TAG, "WebSocket frame too large (%u bytes), dropping", static_cast(frame.len)); + return ESP_FAIL; + } + + std::vector buf(frame.len); + frame.payload = buf.data(); + ret = httpd_ws_recv_frame(req, &frame, frame.len); + if (ret != ESP_OK) { + return ret; + } + + self->onWsFrame(fd, frame.type, frame.final, {buf.data(), frame.len}); + return ESP_OK; +} + +void CaptivePortal::CaptivePortalInstance::onWsFrame(int fd, httpd_ws_type_t opcode, bool final, std::span payload) +{ + int idx = idForFd(fd); + if (idx < 0) { + // Lazy fallback in case the handshake GET call didn't fire on this IDF build. + uint8_t id = onWsOpen(fd); + if (id == 0xFF) { + return; + } + idx = id; + } + uint8_t socketId = static_cast(idx); + + switch (opcode) { + case HTTPD_WS_TYPE_TEXT: + case HTTPD_WS_TYPE_BINARY: + { + WebSocketMessageType type = (opcode == HTTPD_WS_TYPE_TEXT) ? WebSocketMessageType::Text : WebSocketMessageType::Binary; + if (final) { + dispatchWsMessage(socketId, type, payload); + } else { + ScopedLock lock__(&m_clientsMutex); + m_clients[idx].reasmType = type; + m_clients[idx].reasm.assign(payload.begin(), payload.end()); + } + break; + } + case HTTPD_WS_TYPE_CONTINUE: + { + WebSocketMessageType type; + std::vector full; + { + ScopedLock lock__(&m_clientsMutex); + m_clients[idx].reasm.insert(m_clients[idx].reasm.end(), payload.begin(), payload.end()); + if (!final) { + break; + } + type = m_clients[idx].reasmType; + full = std::move(m_clients[idx].reasm); + m_clients[idx].reasm.clear(); + } + dispatchWsMessage(socketId, type, full); + break; + } + default: + // PING/PONG/CLOSE are handled by httpd (handle_ws_control_frames = false). + break; + } +} + +void CaptivePortal::CaptivePortalInstance::dispatchWsMessage(uint8_t socketId, WebSocketMessageType type, std::span payload) +{ + switch (type) { + case WebSocketMessageType::Binary: + MessageHandlers::WebSocket::HandleLocalBinary(socketId, payload); + break; + case WebSocketMessageType::Text: + OS_LOGE(TAG, "Text messages are not supported"); + break; + default: + break; + } +} + +void CaptivePortal::CaptivePortalInstance::handleWebSocketClientConnected(httpd_req_t* req, uint8_t socketId) +{ + OS_LOGD(TAG, "WebSocket client #%u connected (fd %d)", socketId, httpd_req_to_sockfd(req)); + + // We're on the httpd task with a live req — send directly, no copy/queue needed. + auto sendBin = [req](std::span data) -> bool { + httpd_ws_frame_t frame = {}; + frame.final = true; + frame.type = HTTPD_WS_TYPE_BINARY; + frame.payload = const_cast(data.data()); + frame.len = data.size(); + return httpd_ws_send_frame(req, &frame) == ESP_OK; + }; + + WiFiNetwork connectedNetwork; + WiFiNetwork* connectedNetworkPtr = nullptr; + if (WiFiManager::GetConnectedNetwork(connectedNetwork)) { + connectedNetworkPtr = &connectedNetwork; + } + + Serialization::Local::SerializeReadyMessage(connectedNetworkPtr, GatewayConnectionManager::IsLinked(), sendBin); + + // Send all previously scanned wifi networks + auto networks = OpenShock::WiFiManager::GetDiscoveredWiFiNetworks(); + Serialization::Local::SerializeWiFiNetworksEvent(Serialization::Types::WifiNetworkEventType::Discovered, networks, sendBin); +} + +void CaptivePortal::CaptivePortalInstance::handleWebSocketClientDisconnected(uint8_t socketId) +{ + OS_LOGD(TAG, "WebSocket client #%u disconnected", socketId); +} + +// --------------------------------------------------------------------------- +// Public WS send API (called from arbitrary tasks → must copy + queue) +// --------------------------------------------------------------------------- + +bool CaptivePortal::CaptivePortalInstance::queueSend(int fd, bool broadcast, bool binary, std::span data) +{ + if (m_server == nullptr) { + return false; + } + + auto* job = new WsSendJob; + job->hd = m_server; + job->fd = fd; + job->broadcast = broadcast; + job->binary = binary; + job->data.assign(data.begin(), data.end()); + + if (httpd_queue_work(m_server, wsSendWork, job) != ESP_OK) { + delete job; + return false; + } + return true; +} + +bool CaptivePortal::CaptivePortalInstance::sendMessageTXT(uint8_t socketId, std::string_view data) +{ + int fd = fdForId(socketId); + if (fd < 0) { + return false; + } + return queueSend(fd, false, false, {reinterpret_cast(data.data()), data.size()}); +} + +bool CaptivePortal::CaptivePortalInstance::sendMessageBIN(uint8_t socketId, std::span data) +{ + int fd = fdForId(socketId); + if (fd < 0) { + return false; + } + return queueSend(fd, false, true, data); +} + +bool CaptivePortal::CaptivePortalInstance::broadcastMessageTXT(std::string_view data) +{ + return queueSend(-1, true, false, {reinterpret_cast(data.data()), data.size()}); +} + +bool CaptivePortal::CaptivePortalInstance::broadcastMessageBIN(std::span data) +{ + return queueSend(-1, true, true, data); +} + +bool CaptivePortal::CaptivePortalInstance::hasClients() +{ + ScopedLock lock__(&m_clientsMutex); + for (uint8_t i = 0; i < MAX_WS_CLIENTS; ++i) { + if (m_clients[i].used) { + return true; + } + } + return false; +} + +// --------------------------------------------------------------------------- +// HTTP server setup +// --------------------------------------------------------------------------- + +void CaptivePortal::CaptivePortalInstance::registerHandlers() +{ + auto reg = [this](const char* uri, httpd_method_t method, esp_err_t (*handler)(httpd_req_t*)) { + httpd_uri_t def = {}; + def.uri = uri; + def.method = method; + def.handler = handler; + def.user_ctx = this; + esp_err_t err = httpd_register_uri_handler(m_server, &def); + if (err != ESP_OK) { + OS_LOGE(TAG, "Failed to register handler %s: %s", uri, esp_err_to_name(err)); + } + }; + + // API endpoints (registered before the static wildcard so they take priority). + reg("/api/board", HTTP_GET, apiBoard); + reg("/api/portal/close", HTTP_POST, apiPortalClose); + reg("/api/wifi/scan", HTTP_POST, apiWifiScan); + reg("/api/wifi/networks", HTTP_DELETE, apiWifiNetworksDelete); + reg("/api/wifi/networks", HTTP_POST, apiWifiNetworksAdd); + reg("/api/wifi/connect", HTTP_POST, apiWifiConnect); + reg("/api/wifi/disconnect", HTTP_POST, apiWifiDisconnect); + reg("/api/account/link", HTTP_POST, apiAccountLink); + reg("/api/account", HTTP_DELETE, apiAccountDelete); + reg("/api/config/rf/pin", HTTP_PUT, apiConfigRfPin); + reg("/api/config/estop/pin", HTTP_PUT, apiConfigEstopPin); + reg("/api/config/estop/enabled", HTTP_PUT, apiConfigEstopEnabled); + reg("/api/ota/enabled", HTTP_PUT, apiOtaEnabled); + reg("/api/ota/domain", HTTP_PUT, apiOtaDomain); + reg("/api/ota/channel", HTTP_PUT, apiOtaChannel); + reg("/api/ota/check-interval", HTTP_PUT, apiOtaCheckInterval); + reg("/api/ota/allow-backend-management", HTTP_PUT, apiOtaAllowBackendManagement); + reg("/api/ota/require-manual-approval", HTTP_PUT, apiOtaRequireManualApproval); + reg("/api/ota/check", HTTP_POST, apiOtaCheck); + + // OS captive-detection probes + RFC 8908 endpoint + 404 redirect (rfc8908 component). + // Registered before the static wildcard so the specific probe paths take priority. + RFC8908::RegisterProbeHandlers(m_server, CaptivePortal::ApIPv4String()); + + // WebSocket. + httpd_uri_t ws = {}; + ws.uri = "/ws"; + ws.method = HTTP_GET; + ws.handler = &CaptivePortalInstance::wsHandler; + ws.user_ctx = this; + ws.is_websocket = true; + ws.handle_ws_control_frames = false; + ws.supported_subprotocol = "flatbuffers"; + if (httpd_register_uri_handler(m_server, &ws) != ESP_OK) { + OS_LOGE(TAG, "Failed to register WebSocket handler"); + } + + // Static files — catch-all, registered LAST so specific routes win. + reg("/*", HTTP_GET, &CaptivePortalInstance::staticFileHandler); +} + +bool CaptivePortal::CaptivePortalInstance::startHttpServer() +{ + httpd_config_t config = HTTPD_DEFAULT_CONFIG(); + config.server_port = HTTP_PORT; + config.max_uri_handlers = 40; + config.max_open_sockets = 4; + config.lru_purge_enable = true; + config.stack_size = 8192; + config.recv_wait_timeout = 10; + config.send_wait_timeout = 10; + config.uri_match_fn = httpd_uri_match_wildcard; + + esp_err_t err = httpd_start(&m_server, &config); + if (err != ESP_OK) { + OS_LOGE(TAG, "Failed to start HTTP server: %s", esp_err_to_name(err)); + m_server = nullptr; + return false; + } + + registerHandlers(); + return true; +} + +// --------------------------------------------------------------------------- +// Lifecycle +// --------------------------------------------------------------------------- + +CaptivePortal::CaptivePortalInstance::CaptivePortalInstance() + : m_server(nullptr) + , m_staticFs() + , m_fsHash(nullptr) + , m_dnsServer() + , m_clients {} + , m_clientsMutex() +{ + // Mount the static filesystem (gzipped portal assets) read-only via raw littlefs. + const esp_partition_t* partition = getStaticPartition(); + if (partition == nullptr) { + OS_LOGE(TAG, "Failed to find static filesystem partition"); + } else if (!m_staticFs.mount(partition)) { + OS_LOGE(TAG, "Failed to mount static filesystem"); + } else if (!m_staticFs.exists("/www/index.html.gz")) { + OS_LOGE(TAG, "/www/index.html.gz not found — serving error page"); + } else { + m_fsHash = getPartitionHash(); + OS_LOGI(TAG, "Serving files from littlefs (hash: %s)", m_fsHash != nullptr ? m_fsHash : "?"); + } + + // Start the combined HTTP + WebSocket server on port 80. + if (!startHttpServer()) { + return; + } + + // Start the wildcard DNS responder (all A queries → the portal AP IP). + m_dnsServer.start(CaptivePortal::ApIPv4String()); +} + +CaptivePortal::CaptivePortalInstance::~CaptivePortalInstance() +{ + m_dnsServer.stop(); + + // Stop the server (closes all WS sockets, firing the session free callbacks). + if (m_server != nullptr) { + httpd_stop(m_server); + m_server = nullptr; + } + + m_staticFs.unmount(); +} diff --git a/components/core/src/captiveportal/CaptivePortalInstance.h b/components/core/src/captiveportal/CaptivePortalInstance.h new file mode 100644 index 00000000..aac8f104 --- /dev/null +++ b/components/core/src/captiveportal/CaptivePortalInstance.h @@ -0,0 +1,78 @@ +#pragma once + +#include +#include + +#include "dns_server/DNSServer.h" +#include "enums/WebSocketMessageType.h" +#include "fs/StaticFs.h" +#include "OpenShock.h" +#include "SimpleMutex.h" + +#include + +#include +#include +#include +#include + +namespace OpenShock::CaptivePortal { + class CaptivePortalInstance { + DISABLE_COPY(CaptivePortalInstance); + DISABLE_MOVE(CaptivePortalInstance); + + public: + CaptivePortalInstance(); + ~CaptivePortalInstance(); + + // WS sends invoked from arbitrary tasks (WiFiManager, GatewayConnectionManager, + // the captive-portal manager task). httpd frames may only be written from the + // httpd task, so these copy the payload and marshal it via httpd_queue_work. + bool sendMessageTXT(uint8_t socketId, std::string_view data); + bool sendMessageBIN(uint8_t socketId, std::span data); + bool broadcastMessageTXT(std::string_view data); + bool broadcastMessageBIN(std::span data); + bool hasClients(); + + private: + static constexpr uint8_t MAX_WS_CLIENTS = 4; // matches AP max_connection + + struct WsClient { + bool used; + int fd; + WebSocketMessageType reasmType; // opcode of an in-progress fragmented message + std::vector reasm; // reassembly buffer for continuation frames + }; + + // --- HTTP handlers that need instance state (recovered via req->user_ctx) --- + static esp_err_t wsHandler(httpd_req_t* req); + static esp_err_t staticFileHandler(httpd_req_t* req); + + bool startHttpServer(); + void registerHandlers(); + + // --- WebSocket plumbing --- + uint8_t onWsOpen(int fd); + void onWsClose(int fd); + void onWsFrame(int fd, httpd_ws_type_t opcode, bool final, std::span payload); + void dispatchWsMessage(uint8_t socketId, WebSocketMessageType type, std::span payload); + void handleWebSocketClientConnected(httpd_req_t* req, uint8_t socketId); + void handleWebSocketClientDisconnected(uint8_t socketId); + + int fdForId(uint8_t socketId); + int idForFd(int fd); // -1 if not found + + bool queueSend(int fd, bool broadcast, bool binary, std::span data); + + // Per-session free callback fired by httpd on socket close/LRU/timeout. + static void wsSessCtxFree(void* ctx); + + httpd_handle_t m_server; + StaticFs m_staticFs; + const char* m_fsHash; + DNSServer m_dnsServer; + + WsClient m_clients[MAX_WS_CLIENTS]; + SimpleMutex m_clientsMutex; + }; +} // namespace OpenShock::CaptivePortal diff --git a/src/captiveportal/Manager.cpp b/components/core/src/captiveportal/Manager.cpp similarity index 62% rename from src/captiveportal/Manager.cpp rename to components/core/src/captiveportal/Manager.cpp index ea21ed5e..3de4f0f5 100644 --- a/src/captiveportal/Manager.cpp +++ b/components/core/src/captiveportal/Manager.cpp @@ -1,4 +1,5 @@ #include +#include #include "captiveportal/Manager.h" @@ -7,19 +8,21 @@ const char* const TAG = "CaptivePortal"; #include "captiveportal/CaptivePortalInstance.h" #include "CommandHandler.h" #include "config/Config.h" -#include "Core.h" #include "GatewayConnectionManager.h" #include "Logging.h" +#include "Temporal.h" -#include -#include -#include - +#include +#include #include +#include +#include #include "SimpleMutex.h" +#include "util/TaskUtils.h" #include +#include #include using namespace OpenShock; @@ -28,15 +31,21 @@ static std::atomic s_alwaysEnabled = false; static std::atomic s_forceClosed = false; static std::atomic s_userDone = false; static esp_timer_handle_t s_captivePortalUpdateLoopTimer = nullptr; +static TaskHandle_t s_managerTask = nullptr; static SimpleMutex s_instanceMutex; static std::shared_ptr s_instance = nullptr; +static esp_netif_t* s_apNetif = nullptr; + +// The captive portal AP always serves from this fixed address; the DNS server and +// RFC8908 handler reference it too (see CaptivePortal::ApIPv4String()). +static const char* const CAPTIVE_PORTAL_AP_IP = "4.3.2.1"; // Absolute esp_timer timestamps (microseconds). 0 = not armed. -static std::atomic s_startupGraceExpiry = 0; // Don't open portal until this time passes -static std::atomic s_autoCloseExpiry = 0; // Auto-close AP when no clients connected and device is online +static std::atomic s_startupGraceExpiry = 0; // Don't open portal until this time passes +static std::atomic s_autoCloseExpiry = 0; // Auto-close AP when no clients connected and device is online -static constexpr int64_t STARTUP_GRACE_PERIOD_US = 30LL * 1'000'000; // 30 seconds -static constexpr int64_t AUTO_CLOSE_DELAY_US = 5LL * 60 * 1'000'000; // 5 minutes +static constexpr int64_t STARTUP_GRACE_PERIOD_US = 30LL * 1'000'000; // 30 seconds +static constexpr int64_t AUTO_CLOSE_DELAY_US = 5LL * 60 * 1'000'000; // 5 minutes static bool isDeviceFullyConfigured() { @@ -72,28 +81,48 @@ static bool captiveportal_start() OS_LOGI(TAG, "Starting captive portal"); - if (!WiFi.enableAP(true)) { - OS_LOGE(TAG, "Failed to enable AP mode"); - return false; - } + // esp_wifi is already initialized by WiFiManager; create the AP netif once and + // pin it to a fixed IP so the DNS/HTTP portal has a stable address. + if (s_apNetif == nullptr) { + s_apNetif = esp_netif_create_default_wifi_ap(); + if (s_apNetif == nullptr) { + OS_LOGE(TAG, "Failed to create AP netif"); + return false; + } - if (!WiFi.softAP((OPENSHOCK_FW_AP_PREFIX + WiFi.macAddress()).c_str())) { - OS_LOGE(TAG, "Failed to start AP"); - WiFi.enableAP(false); - return false; + esp_netif_ip_info_t ipInfo = {}; + esp_netif_str_to_ip4(CAPTIVE_PORTAL_AP_IP, &ipInfo.ip); + esp_netif_str_to_ip4(CAPTIVE_PORTAL_AP_IP, &ipInfo.gw); + esp_netif_str_to_ip4("255.255.255.0", &ipInfo.netmask); + + esp_netif_dhcps_stop(s_apNetif); + esp_netif_set_ip_info(s_apNetif, &ipInfo); + esp_netif_dhcps_start(s_apNetif); } - IPAddress apIP(4, 3, 2, 1); - if (!WiFi.softAPConfig(apIP, apIP, IPAddress(255, 255, 255, 0))) { - OS_LOGE(TAG, "Failed to configure AP"); - WiFi.softAPdisconnect(true); + // AP SSID = prefix + this device's STA MAC (matches the old Arduino naming). + uint8_t mac[6]; + esp_read_mac(mac, ESP_MAC_WIFI_STA); + + wifi_config_t apConfig = {}; + snprintf(reinterpret_cast(apConfig.ap.ssid), sizeof(apConfig.ap.ssid), "%s%02X:%02X:%02X:%02X:%02X:%02X", CONFIG_OPENSHOCK_FW_AP_PREFIX, mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]); + apConfig.ap.ssid_len = strlen(reinterpret_cast(apConfig.ap.ssid)); + apConfig.ap.channel = 1; + apConfig.ap.authmode = WIFI_AUTH_OPEN; + apConfig.ap.max_connection = 4; + apConfig.ap.beacon_interval = 100; + + esp_err_t err = esp_wifi_set_mode(WIFI_MODE_APSTA); + if (err != ESP_OK) { + OS_LOGE(TAG, "Failed to enable AP mode: %s", esp_err_to_name(err)); return false; } - std::string hostname; - if (!Config::GetWiFiHostname(hostname)) { - OS_LOGE(TAG, "Failed to get WiFi hostname, reverting to default"); - hostname = OPENSHOCK_FW_HOSTNAME; + err = esp_wifi_set_config(WIFI_IF_AP, &apConfig); + if (err != ESP_OK) { + OS_LOGE(TAG, "Failed to configure AP: %s", esp_err_to_name(err)); + esp_wifi_set_mode(WIFI_MODE_STA); + return false; } CreateInstance(); @@ -112,10 +141,15 @@ static void captiveportal_stop() DestroyInstance(); s_userDone = false; - WiFi.softAPdisconnect(true); + // Drop the AP but keep the STA connection alive. + esp_wifi_set_mode(WIFI_MODE_STA); } -static void captiveportal_updateloop(void*) +// Runs the captive-portal state machine once. This does heavy work (starting the +// portal constructs CaptivePortalInstance, which mounts LittleFS and spins up the +// HTTP/WS/DNS servers), so it MUST run on the dedicated manager task below — never +// on the esp_timer task, whose 3.5KB stack would overflow. +static void captiveportal_tick() { int64_t now = esp_timer_get_time(); @@ -184,6 +218,25 @@ static void captiveportal_updateloop(void*) } } +// Dedicated worker task. The 500ms timer only notifies us; all the heavy tick work +// (portal start/stop, instance construction) runs here on a generous stack. +static void captiveportal_managertask(void*) +{ + for (;;) { + ulTaskNotifyTake(pdTRUE, portMAX_DELAY); + captiveportal_tick(); + } +} + +// esp_timer callback (runs on the esp_timer task, 3.5KB stack). Must stay trivial — +// it only kicks the manager task, which does the real work on its own stack. +static void captiveportal_timernotify(void*) +{ + if (s_managerTask != nullptr) { + xTaskNotifyGive(s_managerTask); + } +} + bool CaptivePortal::Init() { // If device is already fully configured, set a startup grace period before opening portal @@ -192,8 +245,13 @@ bool CaptivePortal::Init() OS_LOGI(TAG, "Device fully configured, startup grace period of 30s before opening portal"); } + if (TaskUtils::TaskCreateExpensive(captiveportal_managertask, "CaptivePortalManager", 6144, nullptr, 1, &s_managerTask) != pdPASS) { + OS_LOGE(TAG, "Failed to create captive portal manager task"); + return false; + } + esp_timer_create_args_t args = { - .callback = captiveportal_updateloop, + .callback = captiveportal_timernotify, .arg = nullptr, .dispatch_method = ESP_TIMER_TASK, .name = "captive_portal_update", @@ -225,9 +283,7 @@ void CaptivePortal::SetUserDone() void CaptivePortal::SetAlwaysEnabled(bool alwaysEnabled) { s_alwaysEnabled = alwaysEnabled; - Config::SetCaptivePortalConfig({ - .alwaysEnabled = alwaysEnabled, - }); + Config::SetCaptivePortalConfig(Config::CaptivePortalConfig(alwaysEnabled)); } bool CaptivePortal::IsAlwaysEnabled() { @@ -258,6 +314,11 @@ bool CaptivePortal::IsRunning() return GetInstance() != nullptr; } +const char* CaptivePortal::ApIPv4String() +{ + return CAPTIVE_PORTAL_AP_IP; +} + bool CaptivePortal::SendMessageTXT(uint8_t socketId, std::string_view data) { auto instance = GetInstance(); @@ -267,7 +328,7 @@ bool CaptivePortal::SendMessageTXT(uint8_t socketId, std::string_view data) return true; } -bool CaptivePortal::SendMessageBIN(uint8_t socketId, tcb::span data) +bool CaptivePortal::SendMessageBIN(uint8_t socketId, std::span data) { auto instance = GetInstance(); if (instance == nullptr) return false; @@ -286,7 +347,7 @@ bool CaptivePortal::BroadcastMessageTXT(std::string_view data) return true; } -bool CaptivePortal::BroadcastMessageBIN(tcb::span data) +bool CaptivePortal::BroadcastMessageBIN(std::span data) { auto instance = GetInstance(); if (instance == nullptr) return false; diff --git a/src/http/JsonAPI.cpp b/components/core/src/http/JsonAPI.cpp similarity index 84% rename from src/http/JsonAPI.cpp rename to components/core/src/http/JsonAPI.cpp index e2ab0610..c2b34f74 100644 --- a/src/http/JsonAPI.cpp +++ b/components/core/src/http/JsonAPI.cpp @@ -2,10 +2,10 @@ const char* const TAG = "JsonAPI"; -#include "Common.h" -#include "Logging.h" #include "config/Config.h" -#include "util/StringUtils.h" +#include "Logging.h" +#include "OpenShock.h" +#include "StringHelpers.h" using namespace OpenShock; @@ -16,7 +16,7 @@ HTTP::Response HTTP::JsonAPI::LinkA return {HTTP::RequestResult::InternalError, 0, {}}; } - char uri[OPENSHOCK_URI_BUFFER_SIZE]; + char uri[CONFIG_OPENSHOCK_URI_BUFFER_SIZE]; int written = snprintf(uri, sizeof(uri), "https://%s/1/device/pair/%.*s", domain.c_str(), static_cast(accountLinkCode.length()), accountLinkCode.data()); if (written < 0 || static_cast(written) >= sizeof(uri)) { OS_LOGE(TAG, "URI truncated for LinkAccount"); @@ -40,7 +40,7 @@ HTTP::Response HTTP::JsonAPI::GetHubInf return {HTTP::RequestResult::InternalError, 0, {}}; } - char uri[OPENSHOCK_URI_BUFFER_SIZE]; + char uri[CONFIG_OPENSHOCK_URI_BUFFER_SIZE]; int written = snprintf(uri, sizeof(uri), "https://%s/1/device/self", domain.c_str()); if (written < 0 || static_cast(written) >= sizeof(uri)) { OS_LOGE(TAG, "URI truncated for GetHubInfo"); @@ -50,8 +50,8 @@ HTTP::Response HTTP::JsonAPI::GetHubInf return HTTP::GetJSON( uri, { - { "Accept", "application/json"}, - {"DeviceToken", OpenShock::StringToArduinoString(hubToken)} + { "Accept", "application/json"}, + {"DeviceToken", std::string(hubToken)} }, Serialization::JsonAPI::ParseHubInfoJsonResponse, std::array {200} @@ -65,7 +65,7 @@ HTTP::Response HTTP::JsonAPI::AssignL return {HTTP::RequestResult::InternalError, 0, {}}; } - char uri[OPENSHOCK_URI_BUFFER_SIZE]; + char uri[CONFIG_OPENSHOCK_URI_BUFFER_SIZE]; int written = snprintf(uri, sizeof(uri), "https://%s/2/device/assignLCG?version=2", domain.c_str()); if (written < 0 || static_cast(written) >= sizeof(uri)) { OS_LOGE(TAG, "URI truncated for AssignLcg"); @@ -75,8 +75,8 @@ HTTP::Response HTTP::JsonAPI::AssignL return HTTP::GetJSON( uri, { - { "Accept", "application/json"}, - {"DeviceToken", OpenShock::StringToArduinoString(hubToken)} + { "Accept", "application/json"}, + {"DeviceToken", std::string(hubToken)} }, Serialization::JsonAPI::ParseAssignLcgJsonResponse, std::array {200} diff --git a/src/message_handlers/ShockerCommandList.cpp b/components/core/src/message_handlers/ShockerCommandList.cpp similarity index 98% rename from src/message_handlers/ShockerCommandList.cpp rename to components/core/src/message_handlers/ShockerCommandList.cpp index 561f50c7..0183c81e 100644 --- a/src/message_handlers/ShockerCommandList.cpp +++ b/components/core/src/message_handlers/ShockerCommandList.cpp @@ -3,8 +3,8 @@ const char* const TAG = "ShockerCommandHandler"; #include "CommandHandler.h" +#include "enums/ShockerModelType.h" #include "Logging.h" -#include "ShockerModelType.h" #include diff --git a/include/message_handlers/ShockerCommandList.h b/components/core/src/message_handlers/ShockerCommandList.h similarity index 100% rename from include/message_handlers/ShockerCommandList.h rename to components/core/src/message_handlers/ShockerCommandList.h diff --git a/include/message_handlers/impl/WSGateway.h b/components/core/src/message_handlers/impl/WSGateway.h similarity index 100% rename from include/message_handlers/impl/WSGateway.h rename to components/core/src/message_handlers/impl/WSGateway.h diff --git a/include/message_handlers/impl/WSLocal.h b/components/core/src/message_handlers/impl/WSLocal.h similarity index 100% rename from include/message_handlers/impl/WSLocal.h rename to components/core/src/message_handlers/impl/WSLocal.h diff --git a/src/message_handlers/websocket/Gateway.cpp b/components/core/src/message_handlers/websocket/Gateway.cpp similarity index 95% rename from src/message_handlers/websocket/Gateway.cpp rename to components/core/src/message_handlers/websocket/Gateway.cpp index 711604ca..76d75379 100644 --- a/src/message_handlers/websocket/Gateway.cpp +++ b/components/core/src/message_handlers/websocket/Gateway.cpp @@ -8,8 +8,6 @@ const char* const TAG = "ServerMessageHandlers"; #include "serialization/_fbs/GatewayToHubMessage_generated.h" -#include - #include #include @@ -37,7 +35,7 @@ static std::array s_serverHandlers = []() return handlers; }(); -void MessageHandlers::WebSocket::HandleGatewayBinary(tcb::span data) +void MessageHandlers::WebSocket::HandleGatewayBinary(std::span data) { if (data.size() < sizeof(flatbuffers::uoffset_t)) { OS_LOGE(TAG, "Message too small to be a valid FlatBuffer"); diff --git a/src/message_handlers/websocket/Local.cpp b/components/core/src/message_handlers/websocket/Local.cpp similarity index 96% rename from src/message_handlers/websocket/Local.cpp rename to components/core/src/message_handlers/websocket/Local.cpp index 8b9cebac..3b079ae3 100644 --- a/src/message_handlers/websocket/Local.cpp +++ b/components/core/src/message_handlers/websocket/Local.cpp @@ -7,8 +7,6 @@ const char* const TAG = "LocalMessageHandlers"; #include "serialization/_fbs/LocalToHubMessage_generated.h" -#include - #include #include @@ -31,7 +29,7 @@ static std::array s_localHandlers = []() { return handlers; }(); -void MessageHandlers::WebSocket::HandleLocalBinary(uint8_t socketId, tcb::span data) +void MessageHandlers::WebSocket::HandleLocalBinary(uint8_t socketId, std::span data) { if (data.size() < sizeof(flatbuffers::uoffset_t)) { OS_LOGE(TAG, "Message too small to be a valid FlatBuffer"); diff --git a/src/message_handlers/websocket/gateway/OtaUpdateRequest.cpp b/components/core/src/message_handlers/websocket/gateway/OtaUpdateRequest.cpp similarity index 100% rename from src/message_handlers/websocket/gateway/OtaUpdateRequest.cpp rename to components/core/src/message_handlers/websocket/gateway/OtaUpdateRequest.cpp diff --git a/src/message_handlers/websocket/gateway/Ping.cpp b/components/core/src/message_handlers/websocket/gateway/Ping.cpp similarity index 100% rename from src/message_handlers/websocket/gateway/Ping.cpp rename to components/core/src/message_handlers/websocket/gateway/Ping.cpp diff --git a/src/message_handlers/websocket/gateway/ShockerCommandList.cpp b/components/core/src/message_handlers/websocket/gateway/ShockerCommandList.cpp similarity index 100% rename from src/message_handlers/websocket/gateway/ShockerCommandList.cpp rename to components/core/src/message_handlers/websocket/gateway/ShockerCommandList.cpp diff --git a/src/message_handlers/websocket/gateway/Trigger.cpp b/components/core/src/message_handlers/websocket/gateway/Trigger.cpp similarity index 100% rename from src/message_handlers/websocket/gateway/Trigger.cpp rename to components/core/src/message_handlers/websocket/gateway/Trigger.cpp diff --git a/src/message_handlers/websocket/gateway/_InvalidMessage.cpp b/components/core/src/message_handlers/websocket/gateway/_InvalidMessage.cpp similarity index 100% rename from src/message_handlers/websocket/gateway/_InvalidMessage.cpp rename to components/core/src/message_handlers/websocket/gateway/_InvalidMessage.cpp diff --git a/src/message_handlers/websocket/local/Common_ShockerCommandList.cpp b/components/core/src/message_handlers/websocket/local/Common_ShockerCommandList.cpp similarity index 100% rename from src/message_handlers/websocket/local/Common_ShockerCommandList.cpp rename to components/core/src/message_handlers/websocket/local/Common_ShockerCommandList.cpp diff --git a/src/message_handlers/websocket/local/_InvalidMessage.cpp b/components/core/src/message_handlers/websocket/local/_InvalidMessage.cpp similarity index 100% rename from src/message_handlers/websocket/local/_InvalidMessage.cpp rename to components/core/src/message_handlers/websocket/local/_InvalidMessage.cpp diff --git a/components/core/src/serialization/JsonAPI.cpp b/components/core/src/serialization/JsonAPI.cpp new file mode 100644 index 00000000..f553dc4e --- /dev/null +++ b/components/core/src/serialization/JsonAPI.cpp @@ -0,0 +1,251 @@ +#include "serialization/JsonAPI.h" + +const char* const TAG = "JsonAPI"; + +#include "Logging.h" + +#include + +// Log an invalid-response error together with the offending JSON, zero-copy +// straight out of the parse buffer (jsmn views are not NUL-terminated). +#define ESP_LOGJSONE(err, view) OS_LOGE(TAG, "Invalid JSON response (" err "): %.*s", static_cast((view).raw().size()), (view).raw().data()) + +using namespace OpenShock::Serialization; + +bool JsonAPI::ParseLcgInstanceDetailsJsonResponse(int code, JSON::JsonView root, JsonAPI::LcgInstanceDetailsResponse& out) +{ + (void)code; + + if (!root.isObject()) { + ESP_LOGJSONE("not an object", root); + return false; + } + + out = {}; + + std::string_view name; + if (!root["name"].tryGetStr(name)) { + ESP_LOGJSONE("value at 'data.name' is not a string", root); + return false; + } + + std::string_view version; + if (!root["version"].tryGetStr(version)) { + ESP_LOGJSONE("value at 'data.version' is not a string", root); + return false; + } + + std::string_view currentTime; + if (!root["currentTime"].tryGetStr(currentTime)) { + ESP_LOGJSONE("value at 'data.currentTime' is not a string", root); + return false; + } + + std::string_view countryCode; + if (!root["countryCode"].tryGetStr(countryCode)) { + ESP_LOGJSONE("value at 'data.countryCode' is not a string", root); + return false; + } + + std::string_view fqdn; + if (!root["fqdn"].tryGetStr(fqdn)) { + ESP_LOGJSONE("value at 'data.fqdn' is not a string", root); + return false; + } + + out.name.assign(name); + out.version.assign(version); + out.currentTime.assign(currentTime); + out.countryCode.assign(countryCode); + out.fqdn.assign(fqdn); + + return true; +} +bool JsonAPI::ParseBackendVersionJsonResponse(int code, JSON::JsonView root, JsonAPI::BackendVersionResponse& out) +{ + (void)code; + + if (!root.isObject()) { + ESP_LOGJSONE("not an object", root); + return false; + } + + JSON::JsonView data = root["data"]; + if (!data.isObject()) { + ESP_LOGJSONE("value at 'data' is not an object", root); + return false; + } + + out = {}; + + std::string_view version; + if (!data["version"].tryGetStr(version)) { + ESP_LOGJSONE("value at 'data.version' is not a string", root); + return false; + } + + std::string_view commit; + if (!data["commit"].tryGetStr(commit)) { + ESP_LOGJSONE("value at 'data.commit' is not a string", root); + return false; + } + + std::string_view currentTime; + if (!data["currentTime"].tryGetStr(currentTime)) { + ESP_LOGJSONE("value at 'data.currentTime' is not a string", root); + return false; + } + + out.version.assign(version); + out.commit.assign(commit); + out.currentTime.assign(currentTime); + + return true; +} + +bool JsonAPI::ParseAccountLinkJsonResponse(int code, JSON::JsonView root, JsonAPI::AccountLinkResponse& out) +{ + (void)code; + + if (!root.isObject()) { + ESP_LOGJSONE("not an object", root); + return false; + } + + std::string_view data; + if (!root["data"].tryGetStr(data)) { + ESP_LOGJSONE("value at 'data' is not a string", root); + return false; + } + + out = {}; + + out.authToken.assign(data); + + return true; +} +bool JsonAPI::ParseHubInfoJsonResponse(int code, JSON::JsonView root, JsonAPI::HubInfoResponse& out) +{ + (void)code; + + if (!root.isObject()) { + ESP_LOGJSONE("not an object", root); + return false; + } + + JSON::JsonView data = root["data"]; + if (!data.isObject()) { + ESP_LOGJSONE("value at 'data' is not an object", root); + return false; + } + + std::string_view hubId; + if (!data["id"].tryGetStr(hubId)) { + ESP_LOGJSONE("value at 'data.id' is not a string", root); + return false; + } + + std::string_view hubName; + if (!data["name"].tryGetStr(hubName)) { + ESP_LOGJSONE("value at 'data.name' is not a string", root); + return false; + } + + JSON::JsonView hubShockers = data["shockers"]; + if (!hubShockers.isArray()) { + ESP_LOGJSONE("value at 'data.shockers' is not an array", root); + return false; + } + + out = {}; + + out.hubId.assign(hubId); + out.hubName.assign(hubName); + + if (out.hubId.empty() || out.hubName.empty()) { + ESP_LOGJSONE("value at 'data.id' or 'data.name' is empty", root); + return false; + } + + const int shockerCount = hubShockers.count(); + for (int i = 0; i < shockerCount; ++i) { + JSON::JsonView shocker = hubShockers.at(i); + + std::string_view shockerId; + if (!shocker["id"].tryGetStr(shockerId)) { + ESP_LOGJSONE("value at 'shocker.id' is not a string", shocker); + return false; + } + if (shockerId.empty()) { + ESP_LOGJSONE("value at 'shocker.id' is empty", shocker); + return false; + } + + int64_t shockerRfId; + if (!shocker["rfId"].tryGetI64(shockerRfId)) { + ESP_LOGJSONE("value at 'shocker.rfId' is not a number", shocker); + return false; + } + if (shockerRfId < 0 || shockerRfId > UINT16_MAX) { + ESP_LOGJSONE("value at 'shocker.rfId' is not a valid uint16_t", shocker); + return false; + } + uint16_t shockerRfIdU16 = static_cast(shockerRfId); + + std::string_view shockerModel; + if (!shocker["model"].tryGetStr(shockerModel)) { + ESP_LOGJSONE("value at 'shocker.model' is not a string", shocker); + return false; + } + if (shockerModel.empty()) { + ESP_LOGJSONE("value at 'shocker.model' is empty", shocker); + return false; + } + + OpenShock::ShockerModelType shockerModelType; + if (!OpenShock::TryParseShockerModelType(shockerModelType, shockerModel, true)) { // PetTrainer is a typo in the API, we pass true to allow it + ESP_LOGJSONE("value at 'shocker.model' is not a valid shocker model", shocker); + return false; + } + + out.shockers.push_back({.id = std::string(shockerId), .rfId = shockerRfIdU16, .model = shockerModelType}); + } + + return true; +} +bool JsonAPI::ParseAssignLcgJsonResponse(int code, JSON::JsonView root, JsonAPI::AssignLcgResponse& out) +{ + (void)code; + + if (!root.isObject()) { + ESP_LOGJSONE("not an object", root); + return false; + } + + std::string_view host; + std::string_view path; + std::string_view country; + if (!root["host"].tryGetStr(host) || !root["path"].tryGetStr(path) || !root["country"].tryGetStr(country)) { + ESP_LOGJSONE("value at 'host', 'path' or 'country' is not a string", root); + return false; + } + + int64_t port; + if (!root["port"].tryGetI64(port)) { + ESP_LOGJSONE("value at 'port' is not a number", root); + return false; + } + if (port < 0 || port > UINT16_MAX) { + ESP_LOGJSONE("value at 'port' is outside UINT16 bounds", root); + return false; + } + + out = {}; + + out.host.assign(host); + out.port = static_cast(port); + out.path.assign(path); + out.country.assign(country); + + return true; +} diff --git a/src/serialization/JsonSerial.cpp b/components/core/src/serialization/JsonSerial.cpp similarity index 62% rename from src/serialization/JsonSerial.cpp rename to components/core/src/serialization/JsonSerial.cpp index 237b6f1a..865a34e4 100644 --- a/src/serialization/JsonSerial.cpp +++ b/components/core/src/serialization/JsonSerial.cpp @@ -4,91 +4,96 @@ const char* const TAG = "JsonSerial"; #include "Logging.h" +#include + using namespace OpenShock::Serialization; -bool JsonSerial::ParseShockerCommand(const cJSON* root, JsonSerial::ShockerCommand& out) +bool JsonSerial::ParseShockerCommand(JSON::JsonView root, JsonSerial::ShockerCommand& out) { - if (cJSON_IsObject(root) == 0) { + if (!root.isObject()) { OS_LOGE(TAG, "not an object"); return false; } - const cJSON* model = cJSON_GetObjectItemCaseSensitive(root, "model"); - if (model == nullptr) { + JSON::JsonView model = root["model"]; + if (!model.valid()) { OS_LOGE(TAG, "missing 'model' field"); return false; } - if (cJSON_IsString(model) == 0) { + std::string_view modelStr; + if (!model.tryGetStr(modelStr)) { OS_LOGE(TAG, "value at 'model' is not a string"); return false; } ShockerModelType modelType = ShockerModelType::CaiXianlin; - if (!ShockerModelTypeFromString(model->valuestring, modelType)) { + if (!TryParseShockerModelType(modelType, modelStr)) { OS_LOGE(TAG, "value at 'model' is not a valid shocker model (caixianlin, petrainer, petrainer998dr, wellturnt330, d80)"); return false; } - const cJSON* id = cJSON_GetObjectItemCaseSensitive(root, "id"); - if (id == nullptr) { + JSON::JsonView id = root["id"]; + if (!id.valid()) { OS_LOGE(TAG, "missing 'id' field"); return false; } - if (cJSON_IsNumber(id) == 0) { + int64_t idInt; + if (!id.tryGetI64(idInt)) { OS_LOGE(TAG, "value at 'id' is not a number"); return false; } - int idInt = id->valueint; if (idInt < 0 || idInt > UINT16_MAX) { OS_LOGE(TAG, "value at 'id' is out of range (0-65535)"); return false; } uint16_t idU16 = static_cast(idInt); - const cJSON* command = cJSON_GetObjectItemCaseSensitive(root, "type"); - if (command == nullptr) { + JSON::JsonView command = root["type"]; + if (!command.valid()) { OS_LOGE(TAG, "missing 'type' field"); return false; } - if (cJSON_IsString(command) == 0) { + std::string_view commandStr; + if (!command.tryGetStr(commandStr)) { OS_LOGE(TAG, "value at 'type' is not a string"); return false; } ShockerCommandType commandType = ShockerCommandType::Stop; - if (!ShockerCommandTypeFromString(command->valuestring, commandType)) { + if (!TryParseShockerCommandType(commandType, commandStr)) { OS_LOGE(TAG, "value at 'type' is not a valid shocker command (shock, vibrate, sound, light, stop)"); return false; } - const cJSON* intensity = cJSON_GetObjectItemCaseSensitive(root, "intensity"); - if (intensity == nullptr) { + JSON::JsonView intensity = root["intensity"]; + if (!intensity.valid()) { OS_LOGE(TAG, "missing 'intensity' field"); return false; } - if (cJSON_IsNumber(intensity) == 0) { + int64_t intensityInt; + if (!intensity.tryGetI64(intensityInt)) { OS_LOGE(TAG, "value at 'intensity' is not a number"); return false; } - int intensityInt = intensity->valueint; if (intensityInt < 0 || intensityInt > UINT8_MAX) { OS_LOGE(TAG, "value at 'intensity' is out of range (0-255)"); return false; } uint8_t intensityU8 = static_cast(intensityInt); - const cJSON* durationMs = cJSON_GetObjectItemCaseSensitive(root, "durationMs"); - if (durationMs == nullptr) { + JSON::JsonView durationMs = root["durationMs"]; + if (!durationMs.valid()) { OS_LOGE(TAG, "missing 'durationMs' field"); return false; } - if (cJSON_IsNumber(durationMs) == 0) { + int64_t durationMsInt; + if (!durationMs.tryGetI64(durationMsInt)) { OS_LOGE(TAG, "value at 'durationMs' is not a number"); return false; } - if (durationMs->valueint < 0 || durationMs->valueint > UINT16_MAX) { + if (durationMsInt < 0 || durationMsInt > UINT16_MAX) { OS_LOGE(TAG, "value at 'durationMs' is out of range (0-65535)"); return false; } - uint16_t durationMsU16 = static_cast(durationMs->valueint); + uint16_t durationMsU16 = static_cast(durationMsInt); out = { .model = modelType, diff --git a/src/serialization/WSGateway.cpp b/components/core/src/serialization/WSGateway.cpp similarity index 96% rename from src/serialization/WSGateway.cpp rename to components/core/src/serialization/WSGateway.cpp index cb83e3cf..0d23bf17 100644 --- a/src/serialization/WSGateway.cpp +++ b/components/core/src/serialization/WSGateway.cpp @@ -4,9 +4,8 @@ const char* const TAG = "WSGateway"; #include -#include "config/Config.h" -#include "Core.h" #include "Logging.h" +#include "Temporal.h" #ifndef OPENSHOCK_FW_VERSION_MAJOR #define OPENSHOCK_FW_VERSION_MAJOR 0 @@ -58,7 +57,7 @@ bool Gateway::SerializeBootStatusMessage(int32_t updateId, OpenShock::FirmwareBo auto fbsVersion = Types::CreateSemVerDirect(builder, OPENSHOCK_FW_VERSION_MAJOR, OPENSHOCK_FW_VERSION_MINOR, OPENSHOCK_FW_VERSION_PATCH, OPENSHOCK_FW_VERSION_PRERELEASE, OPENSHOCK_FW_VERSION_BUILD); - auto fbsBootStatus = Gateway::CreateBootStatus(builder, bootType, fbsVersion, updateId); + auto fbsBootStatus = Gateway::CreateBootStatus(builder, static_cast(bootType), fbsVersion, updateId); auto msg = Gateway::CreateHubToGatewayMessage(builder, Gateway::HubToGatewayMessagePayload::BootStatus, fbsBootStatus.Union()); diff --git a/src/serialization/WSLocal.cpp b/components/core/src/serialization/WSLocal.cpp similarity index 100% rename from src/serialization/WSLocal.cpp rename to components/core/src/serialization/WSLocal.cpp diff --git a/src/wifi/WiFiManager.cpp b/components/core/src/wifi/WiFiManager.cpp similarity index 76% rename from src/wifi/WiFiManager.cpp rename to components/core/src/wifi/WiFiManager.cpp index c9407788..dba20fcc 100644 --- a/src/wifi/WiFiManager.cpp +++ b/components/core/src/wifi/WiFiManager.cpp @@ -1,27 +1,30 @@ +#include + #include "wifi/WiFiManager.h" const char* const TAG = "WiFiManager"; #include "captiveportal/Manager.h" #include "config/Config.h" -#include "Core.h" +#include "events/Events.h" #include "FormatHelpers.h" #include "Logging.h" #include "serialization/WSLocal.h" +#include "Temporal.h" #include "util/TaskUtils.h" -#include "visual/VisualStateManager.h" #include "wifi/WiFiNetwork.h" #include "wifi/WiFiScanManager.h" -#include - +#include #include -#include +#include #include "SimpleMutex.h" +#include #include #include +#include #include using namespace OpenShock; @@ -67,7 +70,7 @@ static const char* wifiDisconnectReason(uint8_t reason) return "Switching access points"; // Timeouts - case WIFI_REASON_ASSOC_EXPIRE: + case WIFI_REASON_DISASSOC_DUE_TO_INACTIVITY: // formerly WIFI_REASON_ASSOC_EXPIRE (renamed in ESP-IDF 6) case WIFI_REASON_TIMEOUT: case WIFI_REASON_MISSING_ACKS: return "Connection timed out"; @@ -84,6 +87,7 @@ enum class WiFiState : uint8_t { Connected = 1 << 1, }; +static esp_netif_t* s_staNetif = nullptr; static std::atomic s_wifiState {WiFiState::Disconnected}; static uint8_t s_connectedBSSID[6] = {0}; static std::atomic s_connectedCredentialsID = 0; @@ -91,6 +95,17 @@ static std::atomic s_preferredCredentialsID = 0; static OpenShock::SimpleMutex s_networksMutex; static std::vector s_wifiNetworks; +// Broadcast a coarse connectivity change on the OpenShock event bus. Posted with a +// short timeout because this runs on the default event loop task (posting to the +// same loop must never block on a full queue). +static void postWiFiState(OpenShockWiFiState state) +{ + esp_err_t err = esp_event_post(OPENSHOCK_EVENTS, OPENSHOCK_EVENT_WIFI_STATE_CHANGED, &state, sizeof(state), pdMS_TO_TICKS(100)); + if (err != ESP_OK) { + OS_LOGE(TAG, "Failed to post WiFi state event: %s", esp_err_to_name(err)); + } +} + static bool attractivityComparer(const WiFiNetwork& a, const WiFiNetwork& b) { // Networks with credentials sort before those without @@ -186,9 +201,34 @@ static bool connectWiFi(const std::string& ssid, const std::string& password, wi it->lastConnectAttempt = OpenShock::millis(); } + wifi_config_t config = {}; + std::strncpy(reinterpret_cast(config.sta.ssid), ssid.c_str(), sizeof(config.sta.ssid)); + std::strncpy(reinterpret_cast(config.sta.password), password.c_str(), sizeof(config.sta.password)); + // Let esp_wifi pick the strongest matching BSSID and retry a few times before giving up. + config.sta.scan_method = WIFI_ALL_CHANNEL_SCAN; + config.sta.sort_method = WIFI_CONNECT_AP_BY_SIGNAL; + config.sta.failure_retry_cnt = 3; + if (pinnedBssid != nullptr) { + config.sta.bssid_set = true; + std::memcpy(config.sta.bssid, pinnedBssid, sizeof(config.sta.bssid)); + } + s_wifiState.store(WiFiState::Connecting, std::memory_order_relaxed); - if (WiFi.begin(ssid.c_str(), password.c_str(), 0, pinnedBssid, true) == WL_CONNECT_FAILED) { + postWiFiState(OPENSHOCK_WIFI_STATE_CONNECTING); + + esp_err_t err = esp_wifi_set_config(WIFI_IF_STA, &config); + if (err != ESP_OK) { + OS_LOGE(TAG, "esp_wifi_set_config failed: %s", esp_err_to_name(err)); + s_wifiState.store(WiFiState::Disconnected, std::memory_order_relaxed); + postWiFiState(OPENSHOCK_WIFI_STATE_DISCONNECTED); + return false; + } + + err = esp_wifi_connect(); + if (err != ESP_OK) { + OS_LOGE(TAG, "esp_wifi_connect failed: %s", esp_err_to_name(err)); s_wifiState.store(WiFiState::Disconnected, std::memory_order_relaxed); + postWiFiState(OPENSHOCK_WIFI_STATE_DISCONNECTED); return false; } @@ -208,12 +248,17 @@ static bool authenticate(const WiFiNetwork& net, std::string_view password) return connectWiFi(net.ssid, std::string(password)); } -static void evWiFiConnected(arduino_event_t* event) +static void evWiFiConnected(const wifi_event_sta_connected_t& info) { - auto& info = event->event_info.wifi_sta_connected; - s_wifiState.store(WiFiState::Connected, std::memory_order_relaxed); memcpy(s_connectedBSSID, info.bssid, sizeof(s_connectedBSSID)); + postWiFiState(OPENSHOCK_WIFI_STATE_CONNECTED); + + // info.ssid is not guaranteed null-terminated; bound by ssid_len. + char ssid[33]; + size_t ssidLen = std::min(static_cast(info.ssid_len), sizeof(ssid) - 1); + memcpy(ssid, info.ssid, ssidLen); + ssid[ssidLen] = '\0'; ScopedLock lock__(&s_networksMutex); @@ -221,10 +266,10 @@ static void evWiFiConnected(arduino_event_t* event) if (it == s_wifiNetworks.end()) { s_connectedCredentialsID.store(0, std::memory_order_relaxed); - OS_LOGW(TAG, "Connected to unscanned network \"%s\", BSSID: " BSSID_FMT, reinterpret_cast(info.ssid), BSSID_ARG(info.bssid)); + OS_LOGW(TAG, "Connected to unscanned network \"%s\", BSSID: " BSSID_FMT, ssid, BSSID_ARG(info.bssid)); Config::WiFiCredentials creds; - if (Config::TryGetWiFiCredentialsBySSID(reinterpret_cast(info.ssid), creds)) { + if (Config::TryGetWiFiCredentialsBySSID(ssid, creds)) { s_connectedCredentialsID.store(creds.id, std::memory_order_relaxed); } @@ -233,39 +278,43 @@ static void evWiFiConnected(arduino_event_t* event) s_connectedCredentialsID.store(it->credentialsID, std::memory_order_relaxed); - OS_LOGI(TAG, "Connected to network %s (" BSSID_FMT ")", reinterpret_cast(info.ssid), BSSID_ARG(info.bssid)); + OS_LOGI(TAG, "Connected to network %s (" BSSID_FMT ")", ssid, BSSID_ARG(info.bssid)); Serialization::Local::SerializeWiFiNetworkEvent(Serialization::Types::WifiNetworkEventType::Connected, *it, CaptivePortal::BroadcastMessageBIN); } -static void evWiFiGotIP(arduino_event_t* event) +static void evWiFiGotIP(const ip_event_got_ip_t& info) { - const auto& info = event->event_info.got_ip; - uint8_t ip[4]; memcpy(ip, &info.ip_info.ip.addr, sizeof(ip)); OS_LOGI(TAG, "Got IP address " IPV4ADDR_FMT " from network " BSSID_FMT, IPV4ADDR_ARG(ip), BSSID_ARG(s_connectedBSSID)); + postWiFiState(OPENSHOCK_WIFI_STATE_GOT_IP); + char ipStr[16]; snprintf(ipStr, sizeof(ipStr), IPV4ADDR_FMT, IPV4ADDR_ARG(ip)); Serialization::Local::SerializeWiFiGotIpEvent(ipStr, CaptivePortal::BroadcastMessageBIN); } -static void evWiFiGotIP6(arduino_event_t* event) +static void evWiFiGotIP6(const ip_event_got_ip6_t& info) { - auto& info = event->event_info.got_ip6; - - uint8_t* ip6 = reinterpret_cast(&info.ip6_info.ip.addr); + const uint8_t* ip6 = reinterpret_cast(&info.ip6_info.ip.addr); OS_LOGI(TAG, "Got IPv6 address " IPV6ADDR_FMT " from network " BSSID_FMT, IPV6ADDR_ARG(ip6), BSSID_ARG(s_connectedBSSID)); + + postWiFiState(OPENSHOCK_WIFI_STATE_GOT_IP); } -static void evWiFiDisconnected(arduino_event_t* event) +static void evWiFiDisconnected(const wifi_event_sta_disconnected_t& info) { s_wifiState.store(WiFiState::Disconnected, std::memory_order_relaxed); s_connectedCredentialsID.store(0, std::memory_order_relaxed); + postWiFiState(OPENSHOCK_WIFI_STATE_DISCONNECTED); - auto& info = event->event_info.wifi_sta_disconnected; + char ssid[33]; + size_t ssidLen = std::min(static_cast(info.ssid_len), sizeof(ssid) - 1); + memcpy(ssid, info.ssid, ssidLen); + ssid[ssidLen] = '\0'; - OS_LOGI(TAG, "Disconnected from network %s (" BSSID_FMT ")", info.ssid, BSSID_ARG(info.bssid)); + OS_LOGI(TAG, "Disconnected from network %s (" BSSID_FMT ")", ssid, BSSID_ARG(info.bssid)); // Notify the frontend ScopedLock lock__(&s_networksMutex); @@ -276,7 +325,7 @@ static void evWiFiDisconnected(arduino_event_t* event) // Network not in scan results (forgotten or hidden) — send minimal event WiFiNetwork net; memset(&net, 0, sizeof(net)); - strncpy(net.ssid, reinterpret_cast(info.ssid), sizeof(net.ssid) - 1); + strncpy(net.ssid, ssid, sizeof(net.ssid) - 1); memcpy(net.bssid, info.bssid, sizeof(net.bssid)); Serialization::Local::SerializeWiFiNetworkEvent(Serialization::Types::WifiNetworkEventType::Disconnected, net, CaptivePortal::BroadcastMessageBIN); } @@ -293,9 +342,40 @@ static void evWiFiDisconnected(arduino_event_t* event) } } } -static void evWiFiScanStarted() + +static void wifiEventHandler(void* arg, esp_event_base_t base, int32_t id, void* data) +{ + (void)arg; + (void)base; + + switch (id) { + case WIFI_EVENT_STA_CONNECTED: + evWiFiConnected(*static_cast(data)); + break; + case WIFI_EVENT_STA_DISCONNECTED: + evWiFiDisconnected(*static_cast(data)); + break; + default: + break; + } +} +static void ipEventHandler(void* arg, esp_event_base_t base, int32_t id, void* data) { + (void)arg; + (void)base; + + switch (id) { + case IP_EVENT_STA_GOT_IP: + evWiFiGotIP(*static_cast(data)); + break; + case IP_EVENT_GOT_IP6: + evWiFiGotIP6(*static_cast(data)); + break; + default: + break; + } } + static void evWiFiScanStatusChanged(OpenShock::WiFiScanStatus status) { ScopedLock lock__(&s_networksMutex); @@ -365,8 +445,6 @@ static void evWiFiNetworksDiscovery(const std::vector& } } -esp_err_t set_esp_interface_dns(esp_interface_t interface, IPAddress main_dns, IPAddress backup_dns, IPAddress fallback_dns); - static bool tryConnect() { Config::WiFiCredentials creds; @@ -410,10 +488,43 @@ static void wifimanagerUpdateTask(void*) bool WiFiManager::Init() { - WiFi.onEvent(evWiFiConnected, ARDUINO_EVENT_WIFI_STA_CONNECTED); - WiFi.onEvent(evWiFiGotIP, ARDUINO_EVENT_WIFI_STA_GOT_IP); - WiFi.onEvent(evWiFiGotIP6, ARDUINO_EVENT_WIFI_STA_GOT_IP6); - WiFi.onEvent(evWiFiDisconnected, ARDUINO_EVENT_WIFI_STA_DISCONNECTED); + esp_err_t err; + + // Bring up the network stack that Arduino's WiFi class used to init implicitly. + // The default event loop already exists (Events::Init runs earlier in main). + err = esp_netif_init(); + if (err != ESP_OK) { + OS_LOGE(TAG, "esp_netif_init failed: %s", esp_err_to_name(err)); + return false; + } + + s_staNetif = esp_netif_create_default_wifi_sta(); + if (s_staNetif == nullptr) { + OS_LOGE(TAG, "Failed to create default STA netif"); + return false; + } + + wifi_init_config_t initConfig = WIFI_INIT_CONFIG_DEFAULT(); + err = esp_wifi_init(&initConfig); + if (err != ESP_OK) { + OS_LOGE(TAG, "esp_wifi_init failed: %s", esp_err_to_name(err)); + return false; + } + + // Config owns our credentials; don't let esp_wifi persist/auto-connect from NVS. + esp_wifi_set_storage(WIFI_STORAGE_RAM); + + err = esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, wifiEventHandler, nullptr); + if (err != ESP_OK) { + OS_LOGE(TAG, "Failed to register WIFI_EVENT handler: %s", esp_err_to_name(err)); + return false; + } + err = esp_event_handler_register(IP_EVENT, ESP_EVENT_ANY_ID, ipEventHandler, nullptr); + if (err != ESP_OK) { + OS_LOGE(TAG, "Failed to register IP_EVENT handler: %s", esp_err_to_name(err)); + return false; + } + WiFiScanManager::RegisterStatusChangedHandler(evWiFiScanStatusChanged); WiFiScanManager::RegisterNetworksDiscoveredHandler(evWiFiNetworksDiscovery); @@ -425,29 +536,23 @@ bool WiFiManager::Init() std::string hostname; if (!Config::GetWiFiHostname(hostname)) { OS_LOGE(TAG, "Failed to get WiFi hostname, reverting to default"); - hostname = OPENSHOCK_FW_HOSTNAME; + hostname = CONFIG_OPENSHOCK_FW_HOSTNAME; } - WiFi.setAutoConnect(false); - WiFi.setAutoReconnect(false); - WiFi.enableSTA(true); - WiFi.setHostname(hostname.c_str()); - - // If we recognize the network in the ESP's WiFi cache, try to connect to it - wifi_config_t current_conf; - if (esp_wifi_get_config(static_cast(ESP_IF_WIFI_STA), ¤t_conf) == ESP_OK) { - if (current_conf.sta.ssid[0] != '\0') { - if (Config::GetWiFiCredentialsIDbySSID(reinterpret_cast(current_conf.sta.ssid)) != 0) { - WiFi.begin(); - } - } + err = esp_wifi_set_mode(WIFI_MODE_STA); + if (err != ESP_OK) { + OS_LOGE(TAG, "esp_wifi_set_mode failed: %s", esp_err_to_name(err)); + return false; } - if (set_esp_interface_dns(ESP_IF_WIFI_STA, IPAddress(1, 1, 1, 1), IPAddress(8, 8, 8, 8), IPAddress(9, 9, 9, 9)) != ESP_OK) { - OS_LOGE(TAG, "Failed to set DNS servers"); + err = esp_wifi_start(); + if (err != ESP_OK) { + OS_LOGE(TAG, "esp_wifi_start failed: %s", esp_err_to_name(err)); return false; } + esp_netif_set_hostname(s_staNetif, hostname.c_str()); + if (TaskUtils::TaskCreateUniversal(wifimanagerUpdateTask, TAG, 4096, nullptr, 5, nullptr, 1) != pdPASS) { // TODO: Re-profile stack usage OS_LOGE(TAG, "Failed to create WiFiManager update task"); return false; @@ -604,7 +709,7 @@ bool WiFiManager::Connect(const char* ssid) void WiFiManager::Disconnect() { - WiFi.disconnect(false); + esp_wifi_disconnect(); } bool WiFiManager::IsConnected() @@ -618,16 +723,18 @@ bool WiFiManager::GetConnectedNetwork(OpenShock::WiFiNetwork& network) if (connectedId == 0) { if (IsConnected()) { // We connected without a scan, so populate the network with the current connection info manually - network.credentialsID = 0; - { - auto ssid = WiFi.SSID(); - size_t len = std::min(static_cast(ssid.length()), sizeof(network.ssid) - 1); - memcpy(network.ssid, ssid.c_str(), len); - network.ssid[len] = '\0'; + wifi_ap_record_t apInfo; + if (esp_wifi_sta_get_ap_info(&apInfo) != ESP_OK) { + return false; } - memcpy(network.bssid, WiFi.BSSID(), sizeof(network.bssid)); - network.channel = WiFi.channel(); - network.rssi = WiFi.RSSI(); + + network.credentialsID = 0; + size_t len = std::min(strlen(reinterpret_cast(apInfo.ssid)), sizeof(network.ssid) - 1); + memcpy(network.ssid, apInfo.ssid, len); + network.ssid[len] = '\0'; + memcpy(network.bssid, apInfo.bssid, sizeof(network.bssid)); + network.channel = apInfo.primary; + network.rssi = apInfo.rssi; return true; } return false; @@ -651,21 +758,14 @@ bool WiFiManager::GetIPAddress(char* ipAddress) return false; } - IPAddress ip = WiFi.localIP(); - snprintf(ipAddress, IPV4ADDR_FMT_LEN + 1, IPV4ADDR_FMT, IPV4ADDR_ARG(ip)); - - return true; -} - -bool WiFiManager::GetIPv6Address(char* ipAddress) -{ - if (!IsConnected()) { + esp_netif_ip_info_t ipInfo; + if (s_staNetif == nullptr || esp_netif_get_ip_info(s_staNetif, &ipInfo) != ESP_OK) { return false; } - IPv6Address ip = WiFi.localIPv6(); - const uint8_t* ipPtr = ip; // Using the implicit conversion operator of IPv6Address - snprintf(ipAddress, IPV6ADDR_FMT_LEN + 1, IPV6ADDR_FMT, IPV6ADDR_ARG(ipPtr)); + uint8_t ip[4]; + memcpy(ip, &ipInfo.ip.addr, sizeof(ip)); + snprintf(ipAddress, 16, IPV4ADDR_FMT, IPV4ADDR_ARG(ip)); // "255.255.255.255" + NUL return true; } diff --git a/src/wifi/WiFiNetwork.cpp b/components/core/src/wifi/WiFiNetwork.cpp similarity index 100% rename from src/wifi/WiFiNetwork.cpp rename to components/core/src/wifi/WiFiNetwork.cpp diff --git a/components/core/src/wifi/WiFiScanManager.cpp b/components/core/src/wifi/WiFiScanManager.cpp new file mode 100644 index 00000000..d94868e6 --- /dev/null +++ b/components/core/src/wifi/WiFiScanManager.cpp @@ -0,0 +1,201 @@ +#include + +#include "wifi/WiFiScanManager.h" + +const char* const TAG = "WiFiScanManager"; + +#include "Logging.h" +#include "SimpleMutex.h" + +#include + +#include +#include +#include + +// Per-channel active-scan dwell time. esp_wifi walks every channel itself, so +// this is the only knob that affects scan duration / result quality. +const uint32_t OPENSHOCK_WIFI_SCAN_MIN_MS_PER_CHANNEL = 100; +const uint32_t OPENSHOCK_WIFI_SCAN_MAX_MS_PER_CHANNEL = 300; + +static bool s_initialized = false; +static std::atomic s_scanning = false; +static OpenShock::SimpleMutex s_handlersMutex = {}; +static std::map s_statusChangedHandlers; +static std::map s_networksDiscoveredHandlers; + +using namespace OpenShock; + +static void notifyStatusChangedHandlers(WiFiScanStatus status) +{ + ScopedLock lock__(&s_handlersMutex); + for (auto& it : s_statusChangedHandlers) { + it.second(status); + } +} + +static void notifyNetworksDiscoveredHandlers(const std::vector& records) +{ + ScopedLock lock__(&s_handlersMutex); + for (auto& it : s_networksDiscoveredHandlers) { + it.second(records); + } +} + +// Fetch the scan results esp_wifi buffered for us and fan them out. Runs in the +// esp_event loop task (WIFI_EVENT_SCAN_DONE). +static void handleScanDone() +{ + uint16_t apCount = 0; + esp_err_t err = esp_wifi_scan_get_ap_num(&apCount); + if (err != ESP_OK) { + OS_LOGE(TAG, "esp_wifi_scan_get_ap_num failed: %s", esp_err_to_name(err)); + s_scanning.store(false, std::memory_order_relaxed); + notifyStatusChangedHandlers(WiFiScanStatus::Error); + return; + } + + std::vector records(apCount); + if (apCount > 0) { + err = esp_wifi_scan_get_ap_records(&apCount, records.data()); + if (err != ESP_OK) { + OS_LOGE(TAG, "esp_wifi_scan_get_ap_records failed: %s", esp_err_to_name(err)); + s_scanning.store(false, std::memory_order_relaxed); + notifyStatusChangedHandlers(WiFiScanStatus::Error); + return; + } + records.resize(apCount); + } + + // The handlers expect stable pointers; records[] outlives every synchronous call below. + std::vector recordPtrs; + recordPtrs.reserve(records.size()); + for (const auto& record : records) { + recordPtrs.push_back(&record); + } + + notifyNetworksDiscoveredHandlers(recordPtrs); + + s_scanning.store(false, std::memory_order_relaxed); + notifyStatusChangedHandlers(WiFiScanStatus::Completed); +} + +static void wifiEventHandler(void* arg, esp_event_base_t base, int32_t id, void* data) +{ + (void)arg; + (void)base; + (void)data; + + switch (id) { + case WIFI_EVENT_SCAN_DONE: + // Ignore spurious scan-done events that we didn't initiate (or already handled). + if (s_scanning.load(std::memory_order_relaxed)) { + handleScanDone(); + } + break; + case WIFI_EVENT_STA_STOP: + if (s_scanning.exchange(false, std::memory_order_relaxed)) { + OS_LOGW(TAG, "STA stopped mid-scan, aborting"); + notifyStatusChangedHandlers(WiFiScanStatus::Aborted); + } + break; + default: + break; + } +} + +bool WiFiScanManager::Init() +{ + if (s_initialized) { + OS_LOGW(TAG, "WiFiScanManager is already initialized"); + return true; + } + + esp_err_t err = esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, wifiEventHandler, nullptr); + if (err != ESP_OK) { + OS_LOGE(TAG, "Failed to register WIFI_EVENT handler: %s", esp_err_to_name(err)); + return false; + } + + s_initialized = true; + + return true; +} + +bool WiFiScanManager::IsScanning() +{ + return s_scanning.load(std::memory_order_relaxed); +} + +bool WiFiScanManager::StartScan() +{ + // Claim the scan slot; bail if one is already running. + bool expected = false; + if (!s_scanning.compare_exchange_strong(expected, true, std::memory_order_relaxed)) { + OS_LOGW(TAG, "Cannot start scan: a scan is already running"); + return false; + } + + notifyStatusChangedHandlers(WiFiScanStatus::Started); + + wifi_scan_config_t config = {}; + config.show_hidden = true; + config.scan_type = WIFI_SCAN_TYPE_ACTIVE; + config.scan_time.active.min = OPENSHOCK_WIFI_SCAN_MIN_MS_PER_CHANNEL; + config.scan_time.active.max = OPENSHOCK_WIFI_SCAN_MAX_MS_PER_CHANNEL; + + esp_err_t err = esp_wifi_scan_start(&config, false); // async: WIFI_EVENT_SCAN_DONE fans out results + if (err != ESP_OK) { + OS_LOGE(TAG, "esp_wifi_scan_start failed: %s", esp_err_to_name(err)); + s_scanning.store(false, std::memory_order_relaxed); + notifyStatusChangedHandlers(WiFiScanStatus::Error); + return false; + } + + return true; +} + +bool WiFiScanManager::AbortScan() +{ + if (!s_scanning.exchange(false, std::memory_order_relaxed)) { + OS_LOGW(TAG, "Cannot abort scan: no scan is in progress"); + return false; + } + + esp_err_t err = esp_wifi_scan_stop(); + if (err != ESP_OK) { + OS_LOGE(TAG, "esp_wifi_scan_stop failed: %s", esp_err_to_name(err)); + } + + notifyStatusChangedHandlers(WiFiScanStatus::Aborted); + + return true; +} + +uint64_t WiFiScanManager::RegisterStatusChangedHandler(const WiFiScanManager::StatusChangedHandler& handler) +{ + static uint64_t nextHandle = 0; + ScopedLock lock__(&s_handlersMutex); + uint64_t handle = nextHandle++; + s_statusChangedHandlers[handle] = handler; + return handle; +} +void WiFiScanManager::UnregisterStatusChangedHandler(uint64_t handle) +{ + ScopedLock lock__(&s_handlersMutex); + s_statusChangedHandlers.erase(handle); +} + +uint64_t WiFiScanManager::RegisterNetworksDiscoveredHandler(const WiFiScanManager::NetworksDiscoveredHandler& handler) +{ + static uint64_t nextHandle = 0; + ScopedLock lock__(&s_handlersMutex); + uint64_t handle = nextHandle++; + s_networksDiscoveredHandlers[handle] = handler; + return handle; +} +void WiFiScanManager::UnregisterNetworksDiscoveredHandler(uint64_t handle) +{ + ScopedLock lock__(&s_handlersMutex); + s_networksDiscoveredHandlers.erase(handle); +} diff --git a/components/crypto/CMakeLists.txt b/components/crypto/CMakeLists.txt new file mode 100644 index 00000000..64ff9069 --- /dev/null +++ b/components/crypto/CMakeLists.txt @@ -0,0 +1,14 @@ +# OpenShock crypto helpers — the utilities that pull in mbedTLS, split out of +# `common` so common stays free of the mbedTLS dependency. +# +# Public surface: +# Hashing.h — MD5 / SHA1 / SHA256 hashers (mbedtls/md.h) +# Base64.h — base64 encode/decode (mbedtls/base64.h in the .cpp) + +idf_component_register( + SRCS "src/Base64.cpp" + INCLUDE_DIRS "include" + REQUIRES common # OpenShock.h (Hashing.h), TinyVec.h (Base64.h) in public headers + mbedtls # mbedtls/md.h in Hashing.h public header + PRIV_REQUIRES logging # Logging.h (Base64.cpp) +) diff --git a/components/crypto/host_test/CMakeLists.txt b/components/crypto/host_test/CMakeLists.txt new file mode 100644 index 00000000..87cc4ba7 --- /dev/null +++ b/components/crypto/host_test/CMakeLists.txt @@ -0,0 +1,11 @@ +cmake_minimum_required(VERSION 3.16) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) + +# Only build 'main'; the crypto/common headers are pulled in by include path. +set(COMPONENTS main) + +# linux-target freertos mock, so unity's IDF integration has its dependencies. +list(APPEND EXTRA_COMPONENT_DIRS "$ENV{IDF_PATH}/tools/mocks/freertos/") + +project(crypto_host_test) diff --git a/components/crypto/host_test/main/CMakeLists.txt b/components/crypto/host_test/main/CMakeLists.txt new file mode 100644 index 00000000..879ae646 --- /dev/null +++ b/components/crypto/host_test/main/CMakeLists.txt @@ -0,0 +1,24 @@ +# Compile crypto's PURE sources directly with a stub Logging.h (stubs/) so we +# avoid the firmware-only transitive deps the real Logging.h pulls in. Base64 +# pulls in common headers (TinyVec.h -> OpenShock.h), included by path below. +idf_component_register( + SRCS "test_main.cpp" + "test_base64.cpp" + "../../src/Base64.cpp" + INCLUDE_DIRS "." + "stubs" # stub Logging.h (shadows the firmware one) + "../../include" # crypto headers (Hashing.h, Base64.h) + "../../../common/include" # TinyVec.h, OpenShock.h + REQUIRES unity + mbedtls # Base64.cpp -> mbedtls/base64.h + WHOLE_ARCHIVE # keep the auto-registered TEST_CASEs from being stripped +) + +# TinyVec.h -> OpenShock.h requires these firmware build-time defines (used as +# string literals). Supply dummies so the pure logic compiles on the host. +target_compile_definitions(${COMPONENT_LIB} PRIVATE + "OPENSHOCK_API_DOMAIN=\"test\"" + "OPENSHOCK_FW_CDN_DOMAIN=\"test\"" + "OPENSHOCK_FW_VERSION=\"0.0.0-test\"" + "OPENSHOCK_FW_USERAGENT=\"host-test\"" +) diff --git a/components/crypto/host_test/main/stubs/Logging.h b/components/crypto/host_test/main/stubs/Logging.h new file mode 100644 index 00000000..d7b203c5 --- /dev/null +++ b/components/crypto/host_test/main/stubs/Logging.h @@ -0,0 +1,17 @@ +#pragma once + +// Host-test stub for the firmware Logging.h. The real one pulls in esp_system / +// esp_ota_ops / freertos (for the OS_PANIC* macros) and the `serial` transport, +// none of which exist on the linux host target. Base64 only uses these +// macros for diagnostics, so no-op the logging ones and abort on panic. +#include +#define OS_LOGV(tag, ...) ((void)0) +#define OS_LOGD(tag, ...) ((void)0) +#define OS_LOGI(tag, ...) ((void)0) +#define OS_LOGW(tag, ...) ((void)0) +#define OS_LOGE(tag, ...) ((void)0) +#define OS_LOGN(tag, ...) ((void)0) +#define OS_LOGWTF(tag, ...) ((void)0) +#define OS_PANIC(tag, ...) ::abort() +#define OS_PANIC_OTA(tag, ...) ::abort() +#define OS_PANIC_INSTANT(tag, ...) ::abort() diff --git a/components/crypto/host_test/main/test_base64.cpp b/components/crypto/host_test/main/test_base64.cpp new file mode 100644 index 00000000..4f3d8e8e --- /dev/null +++ b/components/crypto/host_test/main/test_base64.cpp @@ -0,0 +1,58 @@ +// Base64 encode/decode (mbedtls-backed) - used for auth tokens etc. +#include "unity.h" + +#include "TinyVec.h" +#include "Base64.h" + +#include +#include +#include + +using namespace OpenShock; + +TEST_CASE("Base64 encode known vectors", "[util][base64]") +{ + std::string out; + + const uint8_t man[3] = {'M', 'a', 'n'}; + TEST_ASSERT_TRUE(Base64::Encode(std::span(man, 3), out)); + TEST_ASSERT_TRUE(out == "TWFu"); + + // padding cases + const uint8_t m[1] = {'M'}; + TEST_ASSERT_TRUE(Base64::Encode(std::span(m, 1), out)); + TEST_ASSERT_TRUE(out == "TQ=="); + const uint8_t ma[2] = {'M', 'a'}; + TEST_ASSERT_TRUE(Base64::Encode(std::span(ma, 2), out)); + TEST_ASSERT_TRUE(out == "TWE="); +} + +TEST_CASE("Base64 decode known vector", "[util][base64]") +{ + TinyVec out; + TEST_ASSERT_TRUE(Base64::Decode("TWFu", out)); + TEST_ASSERT_EQUAL_size_t(3, out.size()); + TEST_ASSERT_EQUAL_UINT8('M', out[0]); + TEST_ASSERT_EQUAL_UINT8('a', out[1]); + TEST_ASSERT_EQUAL_UINT8('n', out[2]); +} + +TEST_CASE("Base64 decode rejects invalid input", "[util][base64]") +{ + TinyVec out; + TEST_ASSERT_FALSE(Base64::Decode("!!!!", out)); // invalid chars +} + +TEST_CASE("Base64 round-trip", "[util][base64]") +{ + const uint8_t data[5] = {0x00, 0xFF, 0x10, 0x80, 0x7F}; + std::string encoded; + TEST_ASSERT_TRUE(Base64::Encode(std::span(data, 5), encoded)); + + TinyVec decoded; + TEST_ASSERT_TRUE(Base64::Decode(encoded, decoded)); + TEST_ASSERT_EQUAL_size_t(5, decoded.size()); + for (int i = 0; i < 5; ++i) { + TEST_ASSERT_EQUAL_UINT8(data[i], decoded[i]); + } +} diff --git a/components/crypto/host_test/main/test_main.cpp b/components/crypto/host_test/main/test_main.cpp new file mode 100644 index 00000000..bfbd91ec --- /dev/null +++ b/components/crypto/host_test/main/test_main.cpp @@ -0,0 +1,10 @@ +// Host-only (linux target) test binary for the crypto component. FreeRTOS is +// mocked, so there is no ESP startup - drive Unity directly. +#include "unity.h" + +extern "C" int main(void) +{ + UNITY_BEGIN(); + unity_run_all_tests(); + return UNITY_END(); +} diff --git a/components/crypto/host_test/sdkconfig.defaults b/components/crypto/host_test/sdkconfig.defaults new file mode 100644 index 00000000..c3d7cf2c --- /dev/null +++ b/components/crypto/host_test/sdkconfig.defaults @@ -0,0 +1,4 @@ +CONFIG_IDF_TARGET="linux" +CONFIG_IDF_TARGET_LINUX=y +CONFIG_COMPILER_CXX_EXCEPTIONS=y +CONFIG_UNITY_ENABLE_IDF_TEST_RUNNER=y diff --git a/include/util/Base64Utils.h b/components/crypto/include/Base64.h similarity index 81% rename from include/util/Base64Utils.h rename to components/crypto/include/Base64.h index cb94cc29..fc1f5598 100644 --- a/include/util/Base64Utils.h +++ b/components/crypto/include/Base64.h @@ -2,19 +2,19 @@ #include #include +#include #include #include -#include "span.h" #include "TinyVec.h" -namespace OpenShock::Base64Utils { +namespace OpenShock::Base64 { /// @brief Encodes a byte array to base64. /// @param data The data to encode. /// @param dataLen The size of the data to encode. /// @param output The output string to write to. /// @return The amount of bytes written to the output buffer. - bool Encode(tcb::span data, std::string& output); + bool Encode(std::span data, std::string& output); /// @brief Decodes a base64 string. /// @param data The data to decode. @@ -22,4 +22,4 @@ namespace OpenShock::Base64Utils { /// @param output The output buffer to write to. /// @return The amount of bytes written to the output buffer. bool Decode(std::string_view data, TinyVec& output) noexcept; -} // namespace OpenShock::Base64Utils +} // namespace OpenShock::Base64 diff --git a/components/crypto/include/Hashing.h b/components/crypto/include/Hashing.h new file mode 100644 index 00000000..0acedffa --- /dev/null +++ b/components/crypto/include/Hashing.h @@ -0,0 +1,80 @@ +#pragma once + +#include "OpenShock.h" + +// The low-level per-algorithm headers (mbedtls/md5.h, sha1.h, sha256.h) and their +// mbedtls_md5_* / mbedtls_sha1_* / mbedtls_sha256_* APIs became private in mbedTLS +// 3.6 (shipped with ESP-IDF 6.0). The generic message-digest interface in +// mbedtls/md.h is the public, stable replacement, so all three hashers are now +// built on top of it. +#include + +#include // TODO: When we use C++20, change this to +#include +#include + +namespace OpenShock { + class MD5 { + DISABLE_COPY(MD5); + DISABLE_MOVE(MD5); + + public: + // mbedtls_md_info_from_type never returns null for a compiled-in algorithm + // (MD5/SHA1/SHA256 are all enabled by default); a setup failure surfaces as + // begin() returning false. + MD5() + { + mbedtls_md_init(&ctx); + mbedtls_md_setup(&ctx, mbedtls_md_info_from_type(MBEDTLS_MD_MD5), 0); + } + ~MD5() { mbedtls_md_free(&ctx); } + + inline bool begin() { return mbedtls_md_starts(&ctx) == 0; } + inline bool update(const uint8_t* data, std::size_t dataLen) { return mbedtls_md_update(&ctx, data, dataLen) == 0; } + inline bool update(std::string_view data) { return update(reinterpret_cast(data.data()), data.length()); } + inline bool finish(std::array& hash) { return mbedtls_md_finish(&ctx, hash.data()) == 0; } + + private: + mbedtls_md_context_t ctx; + }; + class SHA1 { + DISABLE_COPY(SHA1); + DISABLE_MOVE(SHA1); + + public: + SHA1() + { + mbedtls_md_init(&ctx); + mbedtls_md_setup(&ctx, mbedtls_md_info_from_type(MBEDTLS_MD_SHA1), 0); + } + ~SHA1() { mbedtls_md_free(&ctx); } + + inline bool begin() { return mbedtls_md_starts(&ctx) == 0; } + inline bool update(const uint8_t* data, std::size_t dataLen) { return mbedtls_md_update(&ctx, data, dataLen) == 0; } + inline bool update(std::string_view data) { return update(reinterpret_cast(data.data()), data.length()); } + inline bool finish(std::array& hash) { return mbedtls_md_finish(&ctx, hash.data()) == 0; } + + private: + mbedtls_md_context_t ctx; + }; + class SHA256 { + DISABLE_COPY(SHA256); + DISABLE_MOVE(SHA256); + + public: + SHA256() + { + mbedtls_md_init(&ctx); + mbedtls_md_setup(&ctx, mbedtls_md_info_from_type(MBEDTLS_MD_SHA256), 0); + } + ~SHA256() { mbedtls_md_free(&ctx); } + + inline bool begin() { return mbedtls_md_starts(&ctx) == 0; } + inline bool update(const uint8_t* data, std::size_t dataLen) { return mbedtls_md_update(&ctx, data, dataLen) == 0; } + inline bool update(std::string_view data) { return update(reinterpret_cast(data.data()), data.length()); } + inline bool finish(std::array& hash) { return mbedtls_md_finish(&ctx, hash.data()) == 0; } + + private: + mbedtls_md_context_t ctx; + }; +} // namespace OpenShock diff --git a/src/util/Base64Utils.cpp b/components/crypto/src/Base64.cpp similarity index 88% rename from src/util/Base64Utils.cpp rename to components/crypto/src/Base64.cpp index 027350f5..bae55ced 100644 --- a/src/util/Base64Utils.cpp +++ b/components/crypto/src/Base64.cpp @@ -1,6 +1,6 @@ -#include "util/Base64Utils.h" +#include "Base64.h" -const char* const TAG = "Base64Utils"; +const char* const TAG = "Base64"; #include "Logging.h" @@ -18,7 +18,7 @@ constexpr std::size_t CalculateDecodedSize(std::size_t size) noexcept return ((size / 4) * 3) + 3; // +3 guards against missing padding variants } -bool Base64Utils::Encode(tcb::span data, std::string& output) +bool Base64::Encode(std::span data, std::string& output) { std::size_t requiredLen = CalculateEncodedSize(data.size()); @@ -42,7 +42,7 @@ bool Base64Utils::Encode(tcb::span data, std::string& output) return true; } -bool Base64Utils::Decode(std::string_view data, TinyVec& output) noexcept +bool Base64::Decode(std::string_view data, TinyVec& output) noexcept { std::size_t requiredLen = CalculateDecodedSize(data.size()); diff --git a/components/device_control/CMakeLists.txt b/components/device_control/CMakeLists.txt new file mode 100644 index 00000000..3a2d1078 --- /dev/null +++ b/components/device_control/CMakeLists.txt @@ -0,0 +1,19 @@ +# OpenShock device control — the hardware-facing actuators and inputs: estop +# (EStopManager), radio (RFTransmitter), visual status LEDs (VisualStateManager) +# and the shocker CommandHandler that drives the radio. +# +# Extracted out of `core`: its only upstream-core dependency was config, which is +# now its own low-layer component, so device_control depends only on config + the +# foundation/driver layer and nothing else in core. core PRIV_REQUIRES it one-way. +FILE(GLOB_RECURSE device_control_sources + ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp) + +idf_component_register( + SRCS ${device_control_sources} + INCLUDE_DIRS "include" + REQUIRES common esp_hal_gpio esp_driver_rmt freertos # public headers (enums, gpio_types, rmt_tx/encoder, TaskHandle/QueueHandle) + PRIV_REQUIRES config chipset events led_drivers logging temporal protocols # concurrency/util folded into common (public REQUIRE) + esp_driver_gpio esp_wifi esp_event esp_netif soc +) + +target_compile_options(${COMPONENT_LIB} PRIVATE -Wno-error) diff --git a/include/CommandHandler.h b/components/device_control/include/CommandHandler.h similarity index 81% rename from include/CommandHandler.h rename to components/device_control/include/CommandHandler.h index 93f751ab..0baef0a2 100644 --- a/include/CommandHandler.h +++ b/components/device_control/include/CommandHandler.h @@ -1,8 +1,8 @@ #pragma once -#include "SetGPIOResultCode.h" -#include "ShockerCommandType.h" -#include "ShockerModelType.h" +#include "enums/SetGPIOResultCode.h" +#include "enums/ShockerCommandType.h" +#include "enums/ShockerModelType.h" #include diff --git a/include/estop/EStopManager.h b/components/device_control/include/estop/EStopManager.h similarity index 100% rename from include/estop/EStopManager.h rename to components/device_control/include/estop/EStopManager.h diff --git a/include/estop/EStopState.h b/components/device_control/include/estop/EStopState.h similarity index 100% rename from include/estop/EStopState.h rename to components/device_control/include/estop/EStopState.h diff --git a/include/radio/RFTransmitter.h b/components/device_control/include/radio/RFTransmitter.h similarity index 73% rename from include/radio/RFTransmitter.h rename to components/device_control/include/radio/RFTransmitter.h index af0f3198..4fb49dcd 100644 --- a/include/radio/RFTransmitter.h +++ b/components/device_control/include/radio/RFTransmitter.h @@ -1,10 +1,11 @@ #pragma once -#include "Common.h" -#include "ShockerCommandType.h" -#include "ShockerModelType.h" +#include "enums/ShockerCommandType.h" +#include "enums/ShockerModelType.h" +#include "OpenShock.h" -#include +#include +#include #include #include @@ -23,7 +24,7 @@ namespace OpenShock { inline gpio_num_t GetTxPin() const { return m_txPin; } - inline bool ok() const { return m_rmtHandle != nullptr && m_queueHandle != nullptr && m_taskHandle != nullptr; } + inline bool ok() const { return m_txPin != GPIO_NUM_NC && m_queueHandle != nullptr && m_taskHandle != nullptr; } bool SendCommand(ShockerModelType model, uint16_t shockerId, ShockerCommandType type, uint8_t intensity, uint16_t durationMs, bool overwriteExisting = true); void ClearPendingCommands(); @@ -35,8 +36,9 @@ namespace OpenShock { struct Command; gpio_num_t m_txPin; - rmt_obj_t* m_rmtHandle; QueueHandle_t m_queueHandle; TaskHandle_t m_taskHandle; + rmt_channel_handle_t m_rmtChannel; + rmt_encoder_handle_t m_rmtEncoder; }; } // namespace OpenShock diff --git a/include/visual/VisualStateManager.h b/components/device_control/include/visual/VisualStateManager.h similarity index 90% rename from include/visual/VisualStateManager.h rename to components/device_control/include/visual/VisualStateManager.h index 041ac279..445fc85b 100644 --- a/include/visual/VisualStateManager.h +++ b/components/device_control/include/visual/VisualStateManager.h @@ -1,6 +1,5 @@ #pragma once -#include "estop/EStopManager.h" #include namespace OpenShock::VisualStateManager { diff --git a/src/CommandHandler.cpp b/components/device_control/src/CommandHandler.cpp similarity index 96% rename from src/CommandHandler.cpp rename to components/device_control/src/CommandHandler.cpp index deec3e23..3d0aa658 100644 --- a/src/CommandHandler.cpp +++ b/components/device_control/src/CommandHandler.cpp @@ -5,15 +5,17 @@ const char* const TAG = "CommandHandler"; #include "Chipset.h" -#include "Common.h" #include "config/Config.h" -#include "Core.h" #include "estop/EStopManager.h" #include "estop/EStopState.h" #include "events/Events.h" #include "Logging.h" + +#include +#include "OpenShock.h" #include "radio/RFTransmitter.h" #include "SimpleMutex.h" +#include "Temporal.h" #include "util/TaskUtils.h" #include @@ -205,15 +207,15 @@ bool CommandHandler::Init() gpio_num_t txPin = rfConfig.txPin; if (!OpenShock::IsValidOutputPin(txPin)) { - if (!OpenShock::IsValidOutputPin(OPENSHOCK_RF_TX_GPIO)) { - OS_LOGE(TAG, "Configured RF TX pin (%hhi) is invalid, and default pin (%hhi) is invalid. Unable to initialize RF transmitter", txPin, OPENSHOCK_RF_TX_GPIO); + if (!OpenShock::IsValidOutputPin(CONFIG_OPENSHOCK_RF_TX_GPIO)) { + OS_LOGE(TAG, "Configured RF TX pin (%hhi) is invalid, and default pin (%hhi) is invalid. Unable to initialize RF transmitter", txPin, CONFIG_OPENSHOCK_RF_TX_GPIO); OS_LOGD(TAG, "Setting RF TX pin to GPIO_INVALID"); return Config::SetRFConfigTxPin(static_cast(OPENSHOCK_GPIO_INVALID)); // This is not a error yet, unless we are unable to save the RF TX Pin as invalid } - OS_LOGW(TAG, "Configured RF TX pin (%hhi) is invalid, using default pin (%hhi)", txPin, OPENSHOCK_RF_TX_GPIO); - txPin = static_cast(OPENSHOCK_RF_TX_GPIO); + OS_LOGW(TAG, "Configured RF TX pin (%hhi) is invalid, using default pin (%hhi)", txPin, CONFIG_OPENSHOCK_RF_TX_GPIO); + txPin = static_cast(CONFIG_OPENSHOCK_RF_TX_GPIO); if (!Config::SetRFConfigTxPin(txPin)) { OS_LOGE(TAG, "Failed to set RF TX pin in config"); return false; diff --git a/src/EStopManager.cpp b/components/device_control/src/EStopManager.cpp similarity index 99% rename from src/EStopManager.cpp rename to components/device_control/src/EStopManager.cpp index 4c1d79d2..35d14c7f 100644 --- a/src/EStopManager.cpp +++ b/components/device_control/src/EStopManager.cpp @@ -6,10 +6,10 @@ const char* const TAG = "EStopManager"; #include "Chipset.h" #include "config/Config.h" -#include "Core.h" #include "events/Events.h" #include "Logging.h" #include "SimpleMutex.h" +#include "Temporal.h" #include "util/TaskUtils.h" #include diff --git a/src/radio/RFTransmitter.cpp b/components/device_control/src/radio/RFTransmitter.cpp similarity index 77% rename from src/radio/RFTransmitter.cpp rename to components/device_control/src/radio/RFTransmitter.cpp index 9f5de283..93d2d995 100644 --- a/src/radio/RFTransmitter.cpp +++ b/components/device_control/src/radio/RFTransmitter.cpp @@ -4,13 +4,18 @@ const char* const TAG = "RFTransmitter"; -#include "Core.h" #include "estop/EStopManager.h" #include "Logging.h" + +#include #include "radio/rmt/Sequence.h" +#include "Temporal.h" #include "util/FnProxy.h" #include "util/TaskUtils.h" +#include +#include + #include #include @@ -20,6 +25,7 @@ const BaseType_t kTaskPriority = 1; const uint32_t kTaskStackSize = 4096; // PROFILED: 1.4KB stack usage const float kTickrateNs = 1000; const int64_t kTerminatorDurationMs = 300; +const int32_t kRmtTimeoutMs = 100; const uint8_t kFlagOverwrite = 1 << 0; const uint8_t kFlagDeleteTask = 1 << 1; const TickType_t kTaskIdleDelay = pdMS_TO_TICKS(5); @@ -37,21 +43,42 @@ struct RFTransmitter::Command { RFTransmitter::RFTransmitter(gpio_num_t gpioPin) : m_txPin(gpioPin) - , m_rmtHandle(nullptr) , m_queueHandle(nullptr) , m_taskHandle(nullptr) + , m_rmtChannel(nullptr) + , m_rmtEncoder(nullptr) { OS_LOGD(TAG, "[pin-%hhi] Creating RFTransmitter", m_txPin); - m_rmtHandle = rmtInit(static_cast(m_txPin), RMT_TX_MODE, RMT_MEM_64); - if (m_rmtHandle == nullptr) { - OS_LOGE(TAG, "[pin-%hhi] Failed to create rmt object", m_txPin); + // 1 MHz resolution => 1 us tick (RF protocol timings are in microseconds). + rmt_tx_channel_config_t channelConfig = { + .gpio_num = gpioPin, + .clk_src = RMT_CLK_SRC_DEFAULT, + .resolution_hz = 1'000'000, + .mem_block_symbols = SOC_RMT_MEM_WORDS_PER_CHANNEL, // exactly 1 memory block + .trans_queue_depth = 4, + }; + esp_err_t err = rmt_new_tx_channel(&channelConfig, &m_rmtChannel); + if (err != ESP_OK) { + OS_LOGE(TAG, "[pin-%hhi] Failed to create RMT TX channel: %s", m_txPin, esp_err_to_name(err)); destroy(); return; } - float realTick = rmtSetTick(m_rmtHandle, kTickrateNs); - OS_LOGD(TAG, "[pin-%hhi] real tick set to: %fns", m_txPin, realTick); + rmt_copy_encoder_config_t encoderConfig = {}; + err = rmt_new_copy_encoder(&encoderConfig, &m_rmtEncoder); + if (err != ESP_OK) { + OS_LOGE(TAG, "[pin-%hhi] Failed to create RMT copy encoder: %s", m_txPin, esp_err_to_name(err)); + destroy(); + return; + } + + err = rmt_enable(m_rmtChannel); + if (err != ESP_OK) { + OS_LOGE(TAG, "[pin-%hhi] Failed to enable RMT channel: %s", m_txPin, esp_err_to_name(err)); + destroy(); + return; + } m_queueHandle = xQueueCreate(kQueueSize, sizeof(Command)); if (m_queueHandle == nullptr) { @@ -142,9 +169,14 @@ void RFTransmitter::destroy() vQueueDelete(m_queueHandle); m_queueHandle = nullptr; } - if (m_rmtHandle != nullptr) { - rmtDeinit(m_rmtHandle); - m_rmtHandle = nullptr; + if (m_rmtChannel != nullptr) { + rmt_disable(m_rmtChannel); + rmt_del_channel(m_rmtChannel); + m_rmtChannel = nullptr; + } + if (m_rmtEncoder != nullptr) { + rmt_del_encoder(m_rmtEncoder); + m_rmtEncoder = nullptr; } } @@ -173,15 +205,19 @@ static bool modifySequence(std::vector& sequences, ShockerModelTy return false; } -static void writeSequences(rmt_obj_t* rmt_handle, std::vector& sequences) +static void writeSequences(rmt_channel_handle_t channel, rmt_encoder_handle_t encoder, std::vector& sequences) { + rmt_transmit_config_t txConfig = {}; + txConfig.flags.eot_level = 0; + // Send queued commands for (auto seq = sequences.begin(); seq != sequences.end();) { int64_t timeToLive = seq->transmitEnd() - OpenShock::millis(); if (timeToLive > 0) { // Send the command - rmtWriteBlocking(rmt_handle, seq->payload(), seq->size()); + rmt_transmit(channel, encoder, seq->payload(), seq->size() * sizeof(rmt_symbol_word_t), &txConfig); + rmt_tx_wait_all_done(channel, kRmtTimeoutMs); } else { // Remove command if it has sent out its termination sequence for long enough if (timeToLive <= -kTerminatorDurationMs) { @@ -190,7 +226,8 @@ static void writeSequences(rmt_obj_t* rmt_handle, std::vector& se } // Send the termination sequence to stop the shocker - rmtWriteBlocking(rmt_handle, seq->terminator(), seq->size()); + rmt_transmit(channel, encoder, seq->terminator(), seq->size() * sizeof(rmt_symbol_word_t), &txConfig); + rmt_tx_wait_all_done(channel, kRmtTimeoutMs); } // Move to the next command @@ -250,7 +287,7 @@ void RFTransmitter::TransmitTask() } } - writeSequences(m_rmtHandle, sequences); + writeSequences(m_rmtChannel, m_rmtEncoder, sequences); } exit: // Locals (sequences) destruct here before task deletion diff --git a/src/visual/VisualStateManager.cpp b/components/device_control/src/visual/VisualStateManager.cpp similarity index 96% rename from src/visual/VisualStateManager.cpp rename to components/device_control/src/visual/VisualStateManager.cpp index 66419274..3c9d634f 100644 --- a/src/visual/VisualStateManager.cpp +++ b/components/device_control/src/visual/VisualStateManager.cpp @@ -4,22 +4,22 @@ const char* const TAG = "VisualStateManager"; +#include "enums/GatewayClientState.h" #include "estop/EStopState.h" #include "events/Events.h" -#include "GatewayClientState.h" +#include "led_drivers/MonoLedDriver.h" +#include "led_drivers/RgbLedDriver.h" #include "Logging.h" -#include "visual/MonoLedDriver.h" -#include "visual/RgbLedDriver.h" + +#include +#include #include #include -#ifndef OPENSHOCK_LED_GPIO -#define OPENSHOCK_LED_GPIO GPIO_NUM_NC -#endif // OPENSHOCK_LED_GPIO -#ifndef OPENSHOCK_LED_WS2812B -#define OPENSHOCK_LED_WS2812B GPIO_NUM_NC -#endif // OPENSHOCK_LED_WS2812B +// Status LED pins are Kconfig options (CONFIG_OPENSHOCK_*, see +// src/Kconfig.projbuild); -1 (== GPIO_NUM_NC) means the board has no such LED. +#include "sdkconfig.h" const uint64_t kCriticalErrorFlag = 1 << 0; const uint64_t kEmergencyStoppedFlag = 1 << 1; @@ -358,8 +358,8 @@ bool VisualStateManager::Init() { bool ledActive = false; - if (OPENSHOCK_LED_GPIO != GPIO_NUM_NC) { - s_monoLedDriver = std::make_unique(static_cast(OPENSHOCK_LED_GPIO)); + if (CONFIG_OPENSHOCK_LED_GPIO != GPIO_NUM_NC) { + s_monoLedDriver = std::make_unique(static_cast(CONFIG_OPENSHOCK_LED_GPIO)); if (!s_monoLedDriver->IsValid()) { OS_LOGE(TAG, "Failed to initialize built-in LED manager"); return false; @@ -367,8 +367,8 @@ bool VisualStateManager::Init() ledActive = true; } - if (OPENSHOCK_LED_WS2812B != GPIO_NUM_NC) { - s_rgbLedDriver = std::make_unique(static_cast(OPENSHOCK_LED_WS2812B)); + if (CONFIG_OPENSHOCK_LED_WS2812B != GPIO_NUM_NC) { + s_rgbLedDriver = std::make_unique(static_cast(CONFIG_OPENSHOCK_LED_WS2812B)); if (!s_rgbLedDriver->IsValid()) { OS_LOGE(TAG, "Failed to initialize RGB LED manager"); return false; diff --git a/components/dns_server/CMakeLists.txt b/components/dns_server/CMakeLists.txt new file mode 100644 index 00000000..2c1ddd69 --- /dev/null +++ b/components/dns_server/CMakeLists.txt @@ -0,0 +1,8 @@ +# Minimal wildcard DNS responder used by the captive portal (all A queries → a fixed +# IPv4 address). Standalone component so it can be reused independently of the portal. +idf_component_register( + SRCS "src/DNSServer.cpp" + INCLUDE_DIRS "include" + REQUIRES common freertos # OpenShock.h + freertos/* in public header + PRIV_REQUIRES common logging lwip # all .cpp-only (common: Convert/StringHelpers) +) diff --git a/components/dns_server/include/dns_server/DNSServer.h b/components/dns_server/include/dns_server/DNSServer.h new file mode 100644 index 00000000..b0609931 --- /dev/null +++ b/components/dns_server/include/dns_server/DNSServer.h @@ -0,0 +1,38 @@ +#pragma once + +#include +#include + +#include "OpenShock.h" + +#include +#include + +namespace OpenShock { + // Minimal captive-portal DNS responder: answers every A query with a fixed IPv4 + // address (and returns an empty answer for other query types) so client operating + // systems resolve their connectivity-probe hosts to the portal and enter captive + // mode. Runs a single UDP socket on its own task; not a general-purpose resolver. + class DNSServer { + DISABLE_COPY(DNSServer); + DISABLE_MOVE(DNSServer); + + public: + DNSServer(); + ~DNSServer(); + + /// Start answering A queries with responseIpv4 (dotted-decimal, e.g. "4.3.2.1"). + bool start(const char* responseIpv4, uint16_t port = 53); + void stop(); + + bool isRunning() const { return m_taskHandle != nullptr; } + + private: + void task(); + + int m_socket; + TaskHandle_t m_taskHandle; + std::atomic m_stop; + uint8_t m_ip[4]; + }; +} // namespace OpenShock diff --git a/components/dns_server/src/DNSServer.cpp b/components/dns_server/src/DNSServer.cpp new file mode 100644 index 00000000..00afafee --- /dev/null +++ b/components/dns_server/src/DNSServer.cpp @@ -0,0 +1,159 @@ +#include +#include + +#include "dns_server/DNSServer.h" + +const char* const TAG = "DNSServer"; + +#include "Logging.h" +#include "util/FnProxy.h" +#include "util/TaskUtils.h" + +#include + +#include + +using namespace OpenShock; + +DNSServer::DNSServer() + : m_socket(-1) + , m_taskHandle(nullptr) + , m_stop(false) + , m_ip {} +{ +} + +DNSServer::~DNSServer() +{ + stop(); +} + +bool DNSServer::start(const char* responseIpv4, uint16_t port) +{ + if (m_taskHandle != nullptr) { + return true; // already running + } + + // Parse the dotted-decimal address into raw bytes for the answer RDATA. + int a, b, c, d; + if (responseIpv4 == nullptr || sscanf(responseIpv4, "%d.%d.%d.%d", &a, &b, &c, &d) != 4) { + OS_LOGE(TAG, "Invalid response IPv4: %s", responseIpv4 != nullptr ? responseIpv4 : "(null)"); + return false; + } + m_ip[0] = static_cast(a); + m_ip[1] = static_cast(b); + m_ip[2] = static_cast(c); + m_ip[3] = static_cast(d); + + m_socket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + if (m_socket < 0) { + OS_LOGE(TAG, "Failed to create DNS socket"); + return false; + } + + // Short recv timeout so the task can observe stop requests promptly. + struct timeval tv = {}; + tv.tv_sec = 1; + setsockopt(m_socket, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); + + struct sockaddr_in addr = {}; + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl(INADDR_ANY); + addr.sin_port = htons(port); + + if (bind(m_socket, reinterpret_cast(&addr), sizeof(addr)) < 0) { + OS_LOGE(TAG, "Failed to bind DNS socket to port %u", port); + close(m_socket); + m_socket = -1; + return false; + } + + m_stop.store(false, std::memory_order_relaxed); + if (TaskUtils::TaskCreateExpensive(Util::FnProxy<&DNSServer::task>, "DNSServer", 3072, this, 1, &m_taskHandle) != pdPASS) { + OS_LOGE(TAG, "Failed to create DNS task"); + close(m_socket); + m_socket = -1; + return false; + } + + return true; +} + +void DNSServer::stop() +{ + if (m_taskHandle == nullptr) { + if (m_socket >= 0) { + close(m_socket); + m_socket = -1; + } + return; + } + + m_stop.store(true, std::memory_order_relaxed); + if (m_socket >= 0) { + shutdown(m_socket, SHUT_RDWR); // unblock recvfrom + close(m_socket); + m_socket = -1; + } + TaskUtils::StopTask(m_taskHandle, TAG, "DNSServer task"); + m_taskHandle = nullptr; +} + +void DNSServer::task() +{ + uint8_t query[512]; + uint8_t resp[512]; + + while (!m_stop.load(std::memory_order_relaxed)) { + struct sockaddr_in from = {}; + socklen_t fromLen = sizeof(from); + + int n = recvfrom(m_socket, query, sizeof(query), 0, reinterpret_cast(&from), &fromLen); + if (n < 12) { + continue; // timeout (-1) or runt packet + } + size_t len = static_cast(n); + + // Find the end of the question: QNAME (labels terminated by 0) + QTYPE + QCLASS. + // Use size_t throughout so the bounds arithmetic can't provoke -Wstrict-overflow. + size_t pos = 12; + while (pos < len && query[pos] != 0) { + pos += static_cast(query[pos]) + 1; + } + if (pos >= len || len - pos < 5) { + continue; // malformed + } + int qtype = (query[pos + 1] << 8) | query[pos + 2]; + size_t questionEnd = pos + 5; // null(1) + qtype(2) + qclass(2) + + memcpy(resp, query, questionEnd); + resp[2] = 0x81; // QR=1, recursion desired copied + resp[3] = 0x80; // RA=1, RCODE=0 + resp[6] = 0x00; // ANCOUNT hi + resp[7] = (qtype == 1) ? 0x01 : 0x00; + resp[8] = 0x00; // NSCOUNT + resp[9] = 0x00; + resp[10] = 0x00; // ARCOUNT + resp[11] = 0x00; + + size_t respLen = questionEnd; + if (qtype == 1) { // only answer A queries + // clang-format off + const uint8_t answer[] = { + 0xC0, 0x0C, // name pointer → offset 12 (the question) + 0x00, 0x01, // TYPE A + 0x00, 0x01, // CLASS IN + 0x00, 0x00, 0x00, 0x00, // TTL 0 + 0x00, 0x04, // RDLENGTH 4 + m_ip[0], m_ip[1], m_ip[2], m_ip[3], // RDATA — the fixed response IP + }; + // clang-format on + memcpy(resp + respLen, answer, sizeof(answer)); + respLen += sizeof(answer); + } + + sendto(m_socket, resp, respLen, 0, reinterpret_cast(&from), fromLen); + } + + vTaskDelete(nullptr); +} diff --git a/components/events/CMakeLists.txt b/components/events/CMakeLists.txt new file mode 100644 index 00000000..e310c05b --- /dev/null +++ b/components/events/CMakeLists.txt @@ -0,0 +1,14 @@ +# OpenShock application event bus: the OPENSHOCK_EVENTS esp_event base, the event +# id enum, and the POD payload structs posted with each event. Kept as its own +# component so any layer can depend on the event contract without pulling in the +# emitters/consumers. +# +# Public surface: +# include/events/Events.h — OPENSHOCK_EVENTS base, event ids, payload structs, Init() + +idf_component_register( + SRCS "src/Events.cpp" + INCLUDE_DIRS "include" + REQUIRES esp_event # OPENSHOCK_EVENTS base + default event loop (public header) + PRIV_REQUIRES logging # Logging.h +) diff --git a/components/events/include/events/Events.h b/components/events/include/events/Events.h new file mode 100644 index 00000000..7f1a75d8 --- /dev/null +++ b/components/events/include/events/Events.h @@ -0,0 +1,31 @@ +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +ESP_EVENT_DECLARE_BASE(OPENSHOCK_EVENTS); + +enum { + OPENSHOCK_EVENT_ESTOP_STATE_CHANGED, // Event for when the EStop activation state changes + OPENSHOCK_EVENT_GATEWAY_CLIENT_STATE_CHANGED, // Event for when the gateway connection state changes + OPENSHOCK_EVENT_WIFI_STATE_CHANGED, // Event for when the WiFi station connectivity state changes +}; + +// Coarse WiFi station connectivity, posted as the OPENSHOCK_EVENT_WIFI_STATE_CHANGED payload. +typedef enum { + OPENSHOCK_WIFI_STATE_DISCONNECTED = 0, // Not associated to an AP + OPENSHOCK_WIFI_STATE_CONNECTING, // Association / auth in progress + OPENSHOCK_WIFI_STATE_CONNECTED, // Associated (L2), no IP yet + OPENSHOCK_WIFI_STATE_GOT_IP, // Associated and has an IP address (L3, usable) +} OpenShockWiFiState; + +#ifdef __cplusplus +} +#endif + +namespace OpenShock::Events { + bool Init(); +} diff --git a/src/events/Events.cpp b/components/events/src/Events.cpp similarity index 100% rename from src/events/Events.cpp rename to components/events/src/Events.cpp diff --git a/components/flatbuffers/CMakeLists.txt b/components/flatbuffers/CMakeLists.txt new file mode 100644 index 00000000..53761d3d --- /dev/null +++ b/components/flatbuffers/CMakeLists.txt @@ -0,0 +1,4 @@ +# Header-only FlatBuffers runtime consumed by the generated _fbs headers +# (#include "flatbuffers/flatbuffers.h"). Wraps the google/flatbuffers submodule, +# pinned to the same commit the PlatformIO lib_deps entry used. +idf_component_register(INCLUDE_DIRS flatbuffers/include) diff --git a/components/flatbuffers/flatbuffers b/components/flatbuffers/flatbuffers new file mode 160000 index 00000000..81edeb17 --- /dev/null +++ b/components/flatbuffers/flatbuffers @@ -0,0 +1 @@ +Subproject commit 81edeb17d9118143f2c81caf27edfb0df401279e diff --git a/components/fs/CMakeLists.txt b/components/fs/CMakeLists.txt new file mode 100644 index 00000000..70cfa31f --- /dev/null +++ b/components/fs/CMakeLists.txt @@ -0,0 +1,15 @@ +# littlefs-over-esp_partition filesystem layer (no VFS), built on the vendored littlefs +# core. Provides the shared LfsPartition mount kernel plus two façades: +# - StaticFs: read-only, streams pre-built image assets (e.g. the captive portal UI) +# - ConfigFs: read/write blob store for configuration (atomic writes, wear leveling) +# and FsCheck: non-destructive helpers to validate a partition holds a mountable image. +# +# littlefs is a PRIVATE dependency: lfs.h is confined to LfsPartition.cpp so it never +# leaks into consumers of this component's headers. +idf_component_register( + SRCS "src/LfsPartition.cpp" + "src/FsCheck.cpp" + INCLUDE_DIRS "include" + REQUIRES common esp_partition + PRIV_REQUIRES littlefs logging +) diff --git a/components/fs/include/fs/ConfigFs.h b/components/fs/include/fs/ConfigFs.h new file mode 100644 index 00000000..c00b2971 --- /dev/null +++ b/components/fs/include/fs/ConfigFs.h @@ -0,0 +1,39 @@ +#pragma once + +#include "fs/LfsPartition.h" + +#include "OpenShock.h" + +#include + +#include +#include +#include + +namespace OpenShock { + // Read/write view of a littlefs partition tailored for a configuration store: whole + // small blobs, atomic replace (temp file + rename), wear leveling enabled, formats a + // fresh partition on first mount. + // + // Deals only in raw bytes — it has no knowledge of the config serialization format + // (flatbuffers). Serialization and any cross-task locking belong to the caller. + class ConfigFs { + DISABLE_COPY(ConfigFs); + DISABLE_MOVE(ConfigFs); + + public: + ConfigFs() = default; + + bool mount(const esp_partition_t* partition) { return m_fs.mount(partition, true, /*formatOnFail=*/true); } + void unmount() { m_fs.unmount(); } + bool isMounted() const { return m_fs.isMounted(); } + + bool exists(const char* path) { return m_fs.exists(path); } + bool read(const char* path, std::vector& out) { return m_fs.readAll(path, out); } + bool write(const char* path, std::span data) { return m_fs.writeAll(path, data); } + bool remove(const char* path) { return m_fs.remove(path); } + + private: + LfsPartition m_fs; + }; +} // namespace OpenShock diff --git a/components/fs/include/fs/FsCheck.h b/components/fs/include/fs/FsCheck.h new file mode 100644 index 00000000..3ba27801 --- /dev/null +++ b/components/fs/include/fs/FsCheck.h @@ -0,0 +1,18 @@ +#pragma once + +#include + +namespace OpenShock { + // Verify that `partition` holds a valid, mountable littlefs image by mounting it and + // immediately unmounting. Returns false for a null partition or any mount failure. + // + // Non-destructive: `writable` is passed through to the mount (a read-only mount can + // never write), and a corrupt image is never reformatted (formatOnFail=false), so + // this stays a pure check regardless of `writable`. Use it to validate a freshly + // flashed filesystem image before committing to it. + bool TestFilesystem(const esp_partition_t* partition, bool writable = false); + + // Convenience overload: find a data partition by label (any subtype) and test it. + // Returns false if no such partition exists or it fails to mount. + bool TestFilesystem(const char* partitionLabel, bool writable = false); +} // namespace OpenShock diff --git a/components/fs/include/fs/LfsPartition.h b/components/fs/include/fs/LfsPartition.h new file mode 100644 index 00000000..38ea36a3 --- /dev/null +++ b/components/fs/include/fs/LfsPartition.h @@ -0,0 +1,59 @@ +#pragma once + +#include "OpenShock.h" + +#include + +#include +#include +#include +#include + +namespace OpenShock { + namespace detail { + struct LfsState; // defined in LfsPartition.cpp — keeps lfs.h out of public headers + } + + // Mounts a littlefs filesystem directly over a raw esp_partition using the vendored + // littlefs core — no VFS registration. This is the shared kernel behind StaticFs + // (read-only) and ConfigFs (read/write); most callers should use one of those + // façades rather than this class directly. + // + // Not thread-safe: callers that share a partition across tasks must serialize access + // themselves. + class LfsPartition { + DISABLE_COPY(LfsPartition); + DISABLE_MOVE(LfsPartition); + + public: + LfsPartition(); + ~LfsPartition(); + + // Mount `partition`. When `writable` is false, prog/erase are refused and wear + // leveling is disabled (correct for a pre-built, read-only image). When `writable` + // is true and the mount fails, the partition is formatted first if `formatOnFail`. + bool mount(const esp_partition_t* partition, bool writable, bool formatOnFail = false); + void unmount(); + bool isMounted() const { return m_mounted; } + + bool exists(const char* path); + + // Stream a file to `sink` in chunks. Returns false if the file is missing/unreadable + // or if `sink` returns false (which aborts the stream). + bool readStream(const char* path, const std::function)>& sink); + + // Read a whole file into `out`. Returns false (and clears `out`) on any error. + bool readAll(const char* path, std::vector& out); + + // Atomically replace `path` with `data` (write to a temp file, then rename). + // Writable mounts only. + bool writeAll(const char* path, std::span data); + + // Delete `path`. Writable mounts only. + bool remove(const char* path); + + private: + detail::LfsState* m_state; + bool m_mounted; + }; +} // namespace OpenShock diff --git a/components/fs/include/fs/StaticFs.h b/components/fs/include/fs/StaticFs.h new file mode 100644 index 00000000..3ce92aa0 --- /dev/null +++ b/components/fs/include/fs/StaticFs.h @@ -0,0 +1,34 @@ +#pragma once + +#include "fs/LfsPartition.h" + +#include "OpenShock.h" + +#include + +#include +#include +#include + +namespace OpenShock { + // Read-only view of a littlefs partition — for pre-built images (e.g. the captive + // portal's gzipped web assets) that are flashed once and only read at runtime. + // Streams files out to a caller-provided sink; never writes. + class StaticFs { + DISABLE_COPY(StaticFs); + DISABLE_MOVE(StaticFs); + + public: + StaticFs() = default; + + bool mount(const esp_partition_t* partition) { return m_fs.mount(partition, false); } + void unmount() { m_fs.unmount(); } + bool isMounted() const { return m_fs.isMounted(); } + + bool exists(const char* path) { return m_fs.exists(path); } + bool readFile(const char* path, const std::function)>& sink) { return m_fs.readStream(path, sink); } + + private: + LfsPartition m_fs; + }; +} // namespace OpenShock diff --git a/components/fs/src/FsCheck.cpp b/components/fs/src/FsCheck.cpp new file mode 100644 index 00000000..59265c58 --- /dev/null +++ b/components/fs/src/FsCheck.cpp @@ -0,0 +1,26 @@ +#include "fs/FsCheck.h" + +#include "fs/LfsPartition.h" + +using namespace OpenShock; + +bool OpenShock::TestFilesystem(const esp_partition_t* partition, bool writable) +{ + if (partition == nullptr) { + return false; + } + + LfsPartition fs; + if (!fs.mount(partition, writable, /*formatOnFail=*/false)) { + return false; + } + + fs.unmount(); + return true; +} + +bool OpenShock::TestFilesystem(const char* partitionLabel, bool writable) +{ + const esp_partition_t* partition = esp_partition_find_first(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_ANY, partitionLabel); + return TestFilesystem(partition, writable); +} diff --git a/components/fs/src/LfsPartition.cpp b/components/fs/src/LfsPartition.cpp new file mode 100644 index 00000000..eef2215f --- /dev/null +++ b/components/fs/src/LfsPartition.cpp @@ -0,0 +1,252 @@ +#include "fs/LfsPartition.h" + +const char* const TAG = "LfsPartition"; + +#include "Logging.h" + +#include "lfs.h" + +#include +#include + +using namespace OpenShock; + +// Mount parameters must be compatible with how the image is built (see the +// CONFIG_LITTLEFS_* defaults / mklittlefs): 4 KB erase blocks, 128-byte read/prog. +// littlefs only fixes block_size + block_count on disk (validated against the +// superblock at mount); the rest are runtime buffer sizes. +static constexpr lfs_size_t LFS_BLOCK_SIZE = 4096; +static constexpr lfs_size_t LFS_READ_SIZE = 128; +static constexpr lfs_size_t LFS_PROG_SIZE = 128; +static constexpr lfs_size_t LFS_CACHE_SIZE = 512; +static constexpr lfs_size_t LFS_LOOKAHEAD_SIZE = 128; +static constexpr int32_t LFS_BLOCK_CYCLES_RW = 512; // wear leveling for read/write mounts + +// All littlefs state lives here so lfs.h stays out of the public headers. The block +// device callbacks recover it via lfs_config::context. +struct OpenShock::detail::LfsState { + lfs_t lfs; + struct lfs_config cfg; + const esp_partition_t* partition; + bool writable; +}; + +using OpenShock::detail::LfsState; + +static int bdRead(const struct lfs_config* c, lfs_block_t block, lfs_off_t off, void* buffer, lfs_size_t size) +{ + auto* st = static_cast(c->context); + size_t addr = static_cast(block) * c->block_size + off; + return esp_partition_read(st->partition, addr, buffer, size) == ESP_OK ? 0 : LFS_ERR_IO; +} + +static int bdProg(const struct lfs_config* c, lfs_block_t block, lfs_off_t off, const void* buffer, lfs_size_t size) +{ + auto* st = static_cast(c->context); + if (!st->writable) { + return LFS_ERR_IO; + } + size_t addr = static_cast(block) * c->block_size + off; + return esp_partition_write(st->partition, addr, buffer, size) == ESP_OK ? 0 : LFS_ERR_IO; +} + +static int bdErase(const struct lfs_config* c, lfs_block_t block) +{ + auto* st = static_cast(c->context); + if (!st->writable) { + return LFS_ERR_IO; + } + size_t addr = static_cast(block) * c->block_size; + return esp_partition_erase_range(st->partition, addr, c->block_size) == ESP_OK ? 0 : LFS_ERR_IO; +} + +static int bdSync(const struct lfs_config*) +{ + return 0; // esp_partition writes are synchronous +} + +LfsPartition::LfsPartition() + : m_state(new LfsState {}) + , m_mounted(false) +{ +} + +LfsPartition::~LfsPartition() +{ + unmount(); + delete m_state; +} + +bool LfsPartition::mount(const esp_partition_t* partition, bool writable, bool formatOnFail) +{ + if (m_mounted) { + return true; + } + if (partition == nullptr) { + return false; + } + + m_state->partition = partition; + m_state->writable = writable; + + struct lfs_config& cfg = m_state->cfg; + memset(&cfg, 0, sizeof(cfg)); + cfg.context = m_state; + cfg.read = &bdRead; + cfg.prog = &bdProg; + cfg.erase = &bdErase; + cfg.sync = &bdSync; + cfg.read_size = LFS_READ_SIZE; + cfg.prog_size = LFS_PROG_SIZE; + cfg.block_size = LFS_BLOCK_SIZE; + cfg.block_count = partition->size / LFS_BLOCK_SIZE; + cfg.block_cycles = writable ? LFS_BLOCK_CYCLES_RW : -1; // -1 disables wear leveling + cfg.cache_size = LFS_CACHE_SIZE; + cfg.lookahead_size = LFS_LOOKAHEAD_SIZE; + + int err = lfs_mount(&m_state->lfs, &cfg); + if (err < 0) { + if (writable && formatOnFail) { + OS_LOGW(TAG, "Mount failed (lfs error %d), formatting partition", err); + if (lfs_format(&m_state->lfs, &cfg) < 0 || lfs_mount(&m_state->lfs, &cfg) < 0) { + OS_LOGE(TAG, "Failed to format and mount partition"); + m_state->partition = nullptr; + return false; + } + } else { + OS_LOGE(TAG, "Failed to mount partition (lfs error %d)", err); + m_state->partition = nullptr; + return false; + } + } + + m_mounted = true; + return true; +} + +void LfsPartition::unmount() +{ + if (m_mounted) { + lfs_unmount(&m_state->lfs); + m_mounted = false; + } + m_state->partition = nullptr; +} + +bool LfsPartition::exists(const char* path) +{ + if (!m_mounted) { + return false; + } + struct lfs_info info; + return lfs_stat(&m_state->lfs, path, &info) >= 0; +} + +bool LfsPartition::readStream(const char* path, const std::function)>& sink) +{ + if (!m_mounted) { + return false; + } + + lfs_file_t file; + if (lfs_file_open(&m_state->lfs, &file, path, LFS_O_RDONLY) < 0) { + return false; + } + + uint8_t buf[1024]; + bool ok = true; + for (;;) { + lfs_ssize_t r = lfs_file_read(&m_state->lfs, &file, buf, sizeof(buf)); + if (r < 0) { + ok = false; + break; + } + if (r == 0) { + break; // EOF + } + if (!sink(std::span(buf, static_cast(r)))) { + ok = false; + break; + } + } + + lfs_file_close(&m_state->lfs, &file); + return ok; +} + +bool LfsPartition::readAll(const char* path, std::vector& out) +{ + out.clear(); + if (!m_mounted) { + return false; + } + + lfs_file_t file; + if (lfs_file_open(&m_state->lfs, &file, path, LFS_O_RDONLY) < 0) { + return false; + } + + bool ok = true; + lfs_soff_t size = lfs_file_size(&m_state->lfs, &file); + if (size < 0) { + ok = false; + } else if (size > 0) { + out.resize(static_cast(size)); + lfs_ssize_t r = lfs_file_read(&m_state->lfs, &file, out.data(), static_cast(size)); + if (r < 0 || static_cast(r) != static_cast(size)) { + ok = false; + } + } + + lfs_file_close(&m_state->lfs, &file); + if (!ok) { + out.clear(); + } + return ok; +} + +bool LfsPartition::writeAll(const char* path, std::span data) +{ + if (!m_mounted || !m_state->writable) { + return false; + } + + // Write to a temp file then rename, so a power loss can't leave a half-written file. + std::string tmp = std::string(path) + ".tmp"; + + lfs_file_t file; + if (lfs_file_open(&m_state->lfs, &file, tmp.c_str(), LFS_O_WRONLY | LFS_O_CREAT | LFS_O_TRUNC) < 0) { + return false; + } + + bool ok = true; + if (!data.empty()) { + lfs_ssize_t w = lfs_file_write(&m_state->lfs, &file, data.data(), data.size()); + if (w < 0 || static_cast(w) != data.size()) { + ok = false; + } + } + if (lfs_file_close(&m_state->lfs, &file) < 0) { + ok = false; + } + + if (!ok) { + lfs_remove(&m_state->lfs, tmp.c_str()); + return false; + } + + if (lfs_rename(&m_state->lfs, tmp.c_str(), path) < 0) { + lfs_remove(&m_state->lfs, tmp.c_str()); + return false; + } + + return true; +} + +bool LfsPartition::remove(const char* path) +{ + if (!m_mounted || !m_state->writable) { + return false; + } + return lfs_remove(&m_state->lfs, path) >= 0; +} diff --git a/components/http/CMakeLists.txt b/components/http/CMakeLists.txt new file mode 100644 index 00000000..831dd644 --- /dev/null +++ b/components/http/CMakeLists.txt @@ -0,0 +1,19 @@ +# OpenShock HTTP client manager + content type helpers. +# +# Public surface: +# include/http/HTTPRequestManager.h — rate-limited HTTP/HTTPS client +# include/http/ContentTypes.h — MIME type constants +# +# JsonAPI.{h,cpp} intentionally stays in main because it depends on +# serialization/ + config/ (flatbuffers-heavy) which haven't been extracted. + +idf_component_register( + SRCS "src/HTTPRequestManager.cpp" + INCLUDE_DIRS "include" + REQUIRES osjson # json/Json.h in public header + PRIV_REQUIRES common # Common.h, RateLimiter, StringHelpers, SimpleMutex (internal) + temporal # Temporal.h (internal) + logging # Logging.h (internal) + esp_http_client # esp_http_client.h (internal) + mbedtls # esp_crt_bundle.h (internal) +) diff --git a/include/http/ContentTypes.h b/components/http/include/http/ContentTypes.h similarity index 100% rename from include/http/ContentTypes.h rename to components/http/include/http/ContentTypes.h diff --git a/components/http/include/http/HTTPRequestManager.h b/components/http/include/http/HTTPRequestManager.h new file mode 100644 index 00000000..7dca10d5 --- /dev/null +++ b/components/http/include/http/HTTPRequestManager.h @@ -0,0 +1,154 @@ +#pragma once + +#include "json/Json.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +// Forward declarations so this public header doesn't pull in . +// These match the real typedefs exactly, so including the real header elsewhere +// in the same translation unit is fine. +typedef int esp_err_t; +typedef struct esp_http_client* esp_http_client_handle_t; +typedef struct esp_http_client_event esp_http_client_event_t; + +namespace OpenShock::HTTP { + enum class RequestResult : uint8_t { + InternalError, // Internal error + InvalidURL, // Invalid URL + RequestFailed, // Failed to start request + TimedOut, // Request timed out + RateLimited, // Rate limited (can be both local and global) + CodeRejected, // Request completed, but response code was not OK + ParseFailed, // Request completed, but JSON parsing failed + Cancelled, // Request was cancelled + Success, // Request completed successfully + }; + + template + struct [[nodiscard]] Response { + RequestResult result; + int code; + T data; + + Response(RequestResult r, int c, T d) + : result(r) + , code(c) + , data(std::move(d)) + { + } + + inline const char* ResultToString() const + { + switch (result) { + case RequestResult::InternalError: + return "Internal error"; + case RequestResult::InvalidURL: + return "Requested url was invalid"; + case RequestResult::RequestFailed: + return "Request failed"; + case RequestResult::TimedOut: + return "Request timed out"; + case RequestResult::RateLimited: + return "Client was ratelimited"; + case RequestResult::CodeRejected: + return "Unexpected response code"; + case RequestResult::ParseFailed: + return "Parsing the response failed"; + case RequestResult::Cancelled: + return "Request was cancelled"; + case RequestResult::Success: + return "Success"; + default: + return "Unknown reason"; + } + } + }; + + template + using JsonParser = std::function; + using GotContentLengthCallback = std::function; + using DownloadCallback = std::function; + + // A reusable HTTP client. Sequential requests on the same instance reuse the + // underlying TCP/TLS connection (HTTP keep-alive) when they target the same + // host, and transparently reconnect when the host changes or the connection + // drops. The caller owns the lifetime: construct one where several requests + // are issued back-to-back (e.g. an OTA update) so they share a connection. + // + // Not thread-safe. Use a separate Client per task. + class Client { + public: + Client() noexcept; + ~Client(); + + Client(const Client&) = delete; + Client& operator=(const Client&) = delete; + Client(Client&&) = delete; + Client& operator=(Client&&) = delete; + + Response + Download(std::string_view url, const std::map& headers, GotContentLengthCallback contentLengthCallback, DownloadCallback downloadCallback, std::span acceptedCodes, uint32_t timeoutMs = 10'000); + Response GetString(std::string_view url, const std::map& headers, std::span acceptedCodes, uint32_t timeoutMs = 10'000); + + template + Response GetJSON(std::string_view url, const std::map& headers, JsonParser jsonParser, std::span acceptedCodes, uint32_t timeoutMs = 10'000) + { + auto response = GetString(url, headers, acceptedCodes, timeoutMs); + if (response.result != RequestResult::Success) { + return {response.result, response.code, {}}; + } + + // The parsed views point into response.data, which outlives this call; the + // parser copies out everything it needs before we return. + JSON::JsonDocument doc; + if (!doc.parse(response.data)) { + return {RequestResult::ParseFailed, response.code, {}}; + } + + T data; + if (!jsonParser(response.code, doc.root(), data)) { + return {RequestResult::ParseFailed, response.code, {}}; + } + + return {response.result, response.code, std::move(data)}; + } + + private: + static esp_err_t eventHandler(esp_http_client_event_t* evt); + void drop(); + + esp_http_client_handle_t m_handle; + std::vector m_headerKeys; // headers set on the last request, cleared before reuse + std::string m_retryAfter; // "Retry-After" response header, captured by eventHandler + bool m_connectionClose; // server sent "Connection: close" + }; + + // One-shot helpers: perform a single request on a temporary client. Use a + // Client directly when issuing several requests to the same host. + inline Response + Download(std::string_view url, const std::map& headers, GotContentLengthCallback contentLengthCallback, DownloadCallback downloadCallback, std::span acceptedCodes, uint32_t timeoutMs = 10'000) + { + Client client; + return client.Download(url, headers, std::move(contentLengthCallback), std::move(downloadCallback), acceptedCodes, timeoutMs); + } + + inline Response GetString(std::string_view url, const std::map& headers, std::span acceptedCodes, uint32_t timeoutMs = 10'000) + { + Client client; + return client.GetString(url, headers, acceptedCodes, timeoutMs); + } + + template + Response GetJSON(std::string_view url, const std::map& headers, JsonParser jsonParser, std::span acceptedCodes, uint32_t timeoutMs = 10'000) + { + Client client; + return client.GetJSON(url, headers, std::move(jsonParser), acceptedCodes, timeoutMs); + } +} // namespace OpenShock::HTTP diff --git a/components/http/src/HTTPRequestManager.cpp b/components/http/src/HTTPRequestManager.cpp new file mode 100644 index 00000000..e84670a9 --- /dev/null +++ b/components/http/src/HTTPRequestManager.cpp @@ -0,0 +1,359 @@ +#include + +#include "http/HTTPRequestManager.h" + +const char* const TAG = "HTTPRequestManager"; + +#include "Logging.h" + +#include +#include "OpenShock.h" +#include "RateLimiter.h" +#include "SimpleMutex.h" +#include "Temporal.h" +#include "StringHelpers.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +using namespace std::string_view_literals; + +const std::size_t HTTP_BUFFER_SIZE = 4096LLU; +const int HTTP_DOWNLOAD_SIZE_LIMIT = 200 * 1024 * 1024; // 200 MB + +static OpenShock::SimpleMutex s_rateLimitsMutex = {}; +static std::unordered_map> s_rateLimits = {}; + +using namespace OpenShock; + +static std::string_view getDomainFromURL(std::string_view url) +{ + if (url.empty()) { + return {}; + } + + // Remove the protocol eg. "https://api.example.com:443/path" -> "api.example.com:443/path" + auto seperator = url.find("://"); + if (seperator != std::string_view::npos) { + url = url.substr(seperator + 3); + } + + // Remove the path eg. "api.example.com:443/path" -> "api.example.com:443" + seperator = url.find('/'); + if (seperator != std::string_view::npos) { + url = url.substr(0, seperator); + } + + // Remove the port eg. "api.example.com:443" -> "api.example.com" + seperator = url.rfind(':'); + if (seperator != std::string_view::npos) { + url = url.substr(0, seperator); + } + + // Remove all subdomains eg. "api.example.com" -> "example.com" + seperator = url.rfind('.'); + if (seperator == std::string_view::npos) { + return url; // E.g. "localhost" + } + seperator = url.rfind('.', seperator - 1); + if (seperator != std::string_view::npos) { + url = url.substr(seperator + 1); + } + + return url; +} + +static std::shared_ptr createRateLimiterForDomain(std::string_view domain) +{ + auto rateLimit = std::make_shared(); + + // Add default limits + rateLimit->addLimit(1000, 5); // 5 per second + rateLimit->addLimit(10 * 1000, 10); // 10 per 10 seconds + + // per-domain limits + if (domain == CONFIG_OPENSHOCK_API_DOMAIN) { + rateLimit->addLimit(60 * 1000, 12); // 12 per minute + rateLimit->addLimit(60 * 60 * 1000, 120); // 120 per hour + } + + return rateLimit; +} + +static std::shared_ptr createRateLimiterForURL(std::string_view url) +{ + auto domain = std::string(getDomainFromURL(url)); + if (domain.empty()) { + return nullptr; + } + + OpenShock::ScopedLock lock__(&s_rateLimitsMutex); + + auto it = s_rateLimits.find(domain); + if (it == s_rateLimits.end()) { + it = s_rateLimits.emplace(domain, createRateLimiterForDomain(domain)).first; + } + + return it->second; +} + +HTTP::Client::Client() noexcept + : m_handle(nullptr) + , m_headerKeys() + , m_retryAfter() + , m_connectionClose(false) +{ +} + +HTTP::Client::~Client() +{ + drop(); +} + +// esp_http_client only exposes response headers through the event stream, so we +// capture the ones we care about here. user_data points at the owning Client. +esp_err_t HTTP::Client::eventHandler(esp_http_client_event_t* evt) +{ + if (evt->event_id != HTTP_EVENT_ON_HEADER) { + return ESP_OK; + } + + auto* self = static_cast(evt->user_data); + if (self == nullptr) { + return ESP_OK; + } + + if (OpenShock::StringIEquals(evt->header_key, "Retry-After")) { + self->m_retryAfter.assign(evt->header_value); + } else if (OpenShock::StringIEquals(evt->header_key, "Connection")) { + self->m_connectionClose = OpenShock::StringIEquals(evt->header_value, "close"); + } + + return ESP_OK; +} + +void HTTP::Client::drop() +{ + if (m_handle != nullptr) { + esp_http_client_close(m_handle); + esp_http_client_cleanup(m_handle); + m_handle = nullptr; + } + m_headerKeys.clear(); +} + +HTTP::Response + HTTP::Client::Download(std::string_view url, const std::map& headers, HTTP::GotContentLengthCallback contentLengthCallback, HTTP::DownloadCallback downloadCallback, std::span acceptedCodes, uint32_t timeoutMs) +{ + std::shared_ptr rateLimiter = createRateLimiterForURL(url); + if (rateLimiter == nullptr) { + return {RequestResult::InvalidURL, 0, 0}; + } + + if (!rateLimiter->tryRequest()) { + return {RequestResult::RateLimited, 0, 0}; + } + + std::string urlStr(url); + m_retryAfter.clear(); + m_connectionClose = false; + + int64_t begin = OpenShock::millis(); + + // Open the connection, reusing the kept-alive handle when possible. If a + // reused (kept-alive) socket has been closed by the server, drop it and + // reconnect fresh once. HTTPS servers are verified against the compiled-in + // CA bundle via config.crt_bundle_attach below. + esp_err_t err = ESP_FAIL; + for (int attempt = 0; attempt < 2; ++attempt) { + bool reused = m_handle != nullptr; + + if (m_handle == nullptr) { + esp_http_client_config_t config = {}; + config.url = urlStr.c_str(); + config.user_agent = OpenShock::Constants::FW_USERAGENT; + config.method = HTTP_METHOD_GET; + config.timeout_ms = static_cast(timeoutMs); + config.keep_alive_enable = true; + config.event_handler = &Client::eventHandler; + config.user_data = this; + config.crt_bundle_attach = esp_crt_bundle_attach; // verify HTTPS servers against the compiled-in CA bundle + + m_handle = esp_http_client_init(&config); + if (m_handle == nullptr) { + OS_LOGE(TAG, "Failed to initialize HTTP client"); + return {RequestResult::RequestFailed, 0, 0}; + } + } else { + esp_http_client_set_url(m_handle, urlStr.c_str()); + esp_http_client_set_method(m_handle, HTTP_METHOD_GET); + esp_http_client_set_timeout_ms(m_handle, static_cast(timeoutMs)); + } + + // Clear headers left over from a previous request on this reused handle. + for (const auto& key : m_headerKeys) { + esp_http_client_delete_header(m_handle, key.c_str()); + } + m_headerKeys.clear(); + + for (const auto& header : headers) { + esp_http_client_set_header(m_handle, header.first.c_str(), header.second.c_str()); + m_headerKeys.push_back(header.first); + } + + err = esp_http_client_open(m_handle, 0); + if (err == ESP_OK) { + break; + } + + drop(); + if (!reused) { + OS_LOGE(TAG, "Failed to open HTTP connection: %s", esp_err_to_name(err)); + return {RequestResult::RequestFailed, 0, 0}; + } + OS_LOGD(TAG, "Reused connection failed to open, reconnecting"); + } + if (err != ESP_OK) { + return {RequestResult::RequestFailed, 0, 0}; + } + + // Tears down the connection when it can't be safely reused. + auto fail = [&](HTTP::RequestResult result, int code, std::size_t written) -> HTTP::Response { + drop(); + return {result, code, written}; + }; + + int64_t contentLength = esp_http_client_fetch_headers(m_handle); + if (contentLength < 0) { + OS_LOGE(TAG, "Failed to fetch response headers"); + return fail(RequestResult::RequestFailed, 0, 0); + } + + int responseCode = esp_http_client_get_status_code(m_handle); + + if (begin + timeoutMs < OpenShock::millis()) { + OS_LOGW(TAG, "Request timed out"); + return fail(RequestResult::TimedOut, responseCode, 0); + } + + if (responseCode == 429) { // Too Many Requests + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After + long retryAfter = 0; + if (!m_retryAfter.empty() && std::all_of(m_retryAfter.begin(), m_retryAfter.end(), [](char c) { return std::isdigit(static_cast(c)) != 0; })) { + retryAfter = strtol(m_retryAfter.c_str(), nullptr, 10); + } + + // If header missing/unparseable, default to 15 seconds + if (retryAfter <= 0) { + retryAfter = 15; + } + + rateLimiter->blockFor(retryAfter * 1000); + + return fail(RequestResult::RateLimited, responseCode, 0); // body not drained + } + + if (responseCode == 418) { + OS_LOGW(TAG, "The server refused to brew coffee because it is, permanently, a teapot."); + } + + if (std::find(acceptedCodes.begin(), acceptedCodes.end(), responseCode) == acceptedCodes.end()) { + OS_LOGD(TAG, "Received unexpected response code %d", responseCode); + return fail(RequestResult::CodeRejected, responseCode, 0); // body not drained + } + + bool chunked = esp_http_client_is_chunked_response(m_handle); + + if (contentLength > HTTP_DOWNLOAD_SIZE_LIMIT) { + OS_LOGE(TAG, "Content-Length too large"); + return fail(RequestResult::RequestFailed, responseCode, 0); + } + + if (contentLength > 0) { + if (!contentLengthCallback(static_cast(contentLength))) { + OS_LOGW(TAG, "Request cancelled by callback"); + return fail(RequestResult::Cancelled, responseCode, 0); + } + } else if (!chunked) { + // No Content-Length and not chunked => empty body. Nothing to drain. + if (m_connectionClose) { + drop(); + } + return {RequestResult::Success, responseCode, 0}; + } + + uint8_t* buffer = static_cast(malloc(HTTP_BUFFER_SIZE)); + if (buffer == nullptr) { + OS_LOGE(TAG, "Failed to allocate HTTP buffer"); + return fail(RequestResult::InternalError, responseCode, 0); + } + + std::size_t totalWritten = 0; + HTTP::RequestResult result = HTTP::RequestResult::Success; + + // esp_http_client_read transparently decodes chunked transfer-encoding. + int read; + while ((read = esp_http_client_read(m_handle, reinterpret_cast(buffer), static_cast(HTTP_BUFFER_SIZE))) > 0) { + if (begin + timeoutMs < OpenShock::millis()) { + OS_LOGW(TAG, "Request timed out"); + result = HTTP::RequestResult::TimedOut; + break; + } + + if (!downloadCallback(totalWritten, buffer, static_cast(read))) { + OS_LOGW(TAG, "Request cancelled by callback"); + result = HTTP::RequestResult::Cancelled; + break; + } + + totalWritten += static_cast(read); + } + + free(buffer); + + if (result == HTTP::RequestResult::Success && (read < 0 || !esp_http_client_is_complete_data_received(m_handle))) { + OS_LOGW(TAG, "Response body was not fully received"); + result = HTTP::RequestResult::RequestFailed; + } + + if (result != HTTP::RequestResult::Success) { + return fail(result, responseCode, totalWritten); // connection state uncertain + } + + // Success: keep the connection alive for the next request unless the server closed it. + if (m_connectionClose) { + drop(); + } + + return {result, responseCode, totalWritten}; +} + +HTTP::Response HTTP::Client::GetString(std::string_view url, const std::map& headers, std::span acceptedCodes, uint32_t timeoutMs) +{ + std::string result; + + auto allocator = [&result](std::size_t contentLength) { + result.reserve(contentLength); + return true; + }; + auto writer = [&result](std::size_t offset, const uint8_t* data, std::size_t len) { + result.append(reinterpret_cast(data), len); + return true; + }; + + auto response = Download(url, headers, allocator, writer, acceptedCodes, timeoutMs); + if (response.result != RequestResult::Success) { + return {response.result, response.code, {}}; + } + + return {response.result, response.code, result}; +} diff --git a/components/hwutil/CMakeLists.txt b/components/hwutil/CMakeLists.txt new file mode 100644 index 00000000..2013dcf2 --- /dev/null +++ b/components/hwutil/CMakeLists.txt @@ -0,0 +1,17 @@ +# Hardware-dependent utilities split out of `util` so that `util` stays +# pure std-C++ and host-testable. These pull in firmware (esp_partition, HTTP +# OTA) types that do not exist on the linux host target. +# +# Public surface: +# include/hwutil/PartitionUtils.h — hash / flash an esp_partition from a URL + +idf_component_register( + SRCS "src/PartitionUtils.cpp" + INCLUDE_DIRS "include" + REQUIRES esp_partition # esp_partition_t in public header + PRIV_REQUIRES common # Convert, StringHelpers, HexUtils + crypto # Hashing + temporal # Temporal.h + http # HTTPRequestManager (URL flashing) + logging # Logging.h +) diff --git a/components/hwutil/include/hwutil/PartitionUtils.h b/components/hwutil/include/hwutil/PartitionUtils.h new file mode 100644 index 00000000..0d6c1a9b --- /dev/null +++ b/components/hwutil/include/hwutil/PartitionUtils.h @@ -0,0 +1,16 @@ +#pragma once + +#include + +#include +#include +#include + +namespace OpenShock { + namespace HTTP { + class Client; + } + + bool TryGetPartitionHash(const esp_partition_t* partition, char (&hash)[65]); + bool FlashPartitionFromUrl(HTTP::Client& client, const esp_partition_t* partition, std::string_view remoteUrl, const uint8_t (&remoteHash)[32], std::function progressCallback = nullptr); +} // namespace OpenShock diff --git a/src/util/ParitionUtils.cpp b/components/hwutil/src/PartitionUtils.cpp similarity index 89% rename from src/util/ParitionUtils.cpp rename to components/hwutil/src/PartitionUtils.cpp index eede3572..821d67bd 100644 --- a/src/util/ParitionUtils.cpp +++ b/components/hwutil/src/PartitionUtils.cpp @@ -1,11 +1,13 @@ -#include "util/PartitionUtils.h" +#include "hwutil/PartitionUtils.h" const char* const TAG = "PartitionUtils"; -#include "Core.h" #include "Hashing.h" #include "http/HTTPRequestManager.h" #include "Logging.h" + +#include +#include "Temporal.h" #include "util/HexUtils.h" bool OpenShock::TryGetPartitionHash(const esp_partition_t* partition, char (&hash)[65]) @@ -23,7 +25,7 @@ bool OpenShock::TryGetPartitionHash(const esp_partition_t* partition, char (&has return true; } -bool OpenShock::FlashPartitionFromUrl(const esp_partition_t* partition, std::string_view remoteUrl, const uint8_t (&remoteHash)[32], std::function progressCallback) +bool OpenShock::FlashPartitionFromUrl(HTTP::Client& client, const esp_partition_t* partition, std::string_view remoteUrl, const uint8_t (&remoteHash)[32], std::function progressCallback) { OpenShock::SHA256 sha256; if (!sha256.begin()) { @@ -77,7 +79,7 @@ bool OpenShock::FlashPartitionFromUrl(const esp_partition_t* partition, std::str }; // Start streaming binary to app partition. - auto appBinaryResponse = OpenShock::HTTP::Download( + auto appBinaryResponse = client.Download( remoteUrl, { {"Accept", "application/octet-stream"} diff --git a/components/led_drivers/CMakeLists.txt b/components/led_drivers/CMakeLists.txt new file mode 100644 index 00000000..eed9a417 --- /dev/null +++ b/components/led_drivers/CMakeLists.txt @@ -0,0 +1,21 @@ +# OpenShock LED drivers — both mono (PWM via LEDC) and RGB (WS2812B via the +# ESP-IDF RMT driver, esp_driver_rmt). +# +# Public surface: +# led_drivers/MonoLedDriver.h +# led_drivers/RgbLedDriver.h +# +# Note: this is driver-level only. The VisualStateManager that picks patterns +# based on estop/gateway/wifi state is a separate concern and lives in main. + +idf_component_register( + SRCS "src/MonoLedDriver.cpp" + "src/RgbLedDriver.cpp" + INCLUDE_DIRS "include" + REQUIRES common # Common.h, SimpleMutex in public headers + freertos # TaskHandle_t in public headers + esp_driver_rmt # driver/rmt_tx.h, rmt_encoder.h in RgbLedDriver.h + PRIV_REQUIRES chipset # IsValidOutputPin + logging # OS_LOG* + esp_driver_ledc # driver/ledc.h (mono PWM) # FnProxy/TaskUtils via common +) diff --git a/include/visual/MonoLedDriver.h b/components/led_drivers/include/led_drivers/MonoLedDriver.h similarity index 97% rename from include/visual/MonoLedDriver.h rename to components/led_drivers/include/led_drivers/MonoLedDriver.h index 6c9acbd8..af28cef8 100644 --- a/include/visual/MonoLedDriver.h +++ b/components/led_drivers/include/led_drivers/MonoLedDriver.h @@ -1,6 +1,6 @@ #pragma once -#include "Common.h" +#include "OpenShock.h" #include "SimpleMutex.h" #include diff --git a/include/visual/RgbLedDriver.h b/components/led_drivers/include/led_drivers/RgbLedDriver.h similarity index 87% rename from include/visual/RgbLedDriver.h rename to components/led_drivers/include/led_drivers/RgbLedDriver.h index 131f6690..033cda49 100644 --- a/include/visual/RgbLedDriver.h +++ b/components/led_drivers/include/led_drivers/RgbLedDriver.h @@ -1,13 +1,14 @@ #pragma once -#include "Common.h" +#include "OpenShock.h" #include "SimpleMutex.h" #include #include -#include +#include +#include #include #include @@ -49,9 +50,10 @@ namespace OpenShock { gpio_num_t m_gpioPin; uint8_t m_brightness; // 0-255 std::vector m_pattern; - rmt_obj_t* m_rmtHandle; TaskHandle_t m_taskHandle; SimpleMutex m_taskMutex; std::atomic m_stopRequested {false}; + rmt_channel_handle_t m_rmtChannel; + rmt_encoder_handle_t m_rmtEncoder; }; } // namespace OpenShock diff --git a/src/visual/MonoLedDriver.cpp b/components/led_drivers/src/MonoLedDriver.cpp similarity index 89% rename from src/visual/MonoLedDriver.cpp rename to components/led_drivers/src/MonoLedDriver.cpp index d5e6f6d1..77e8119a 100644 --- a/src/visual/MonoLedDriver.cpp +++ b/components/led_drivers/src/MonoLedDriver.cpp @@ -1,6 +1,6 @@ #include -#include "visual/MonoLedDriver.h" +#include "led_drivers/MonoLedDriver.h" const char* const TAG = "MonoLedDriver"; @@ -46,18 +46,22 @@ MonoLedDriver::MonoLedDriver(gpio_num_t gpioPin) .timer_num = OS_LEDC_TIMER, .freq_hz = OS_LEDC_FREQUENCY, .clk_cfg = LEDC_AUTO_CLK, + .deconfigure = false, }; ledc_timer_config(&ledc_config); // TODO: Error handling ledc_channel_config_t ledc_channel = { - .gpio_num = gpioPin, - .speed_mode = OS_LEDC_SPEED, - .channel = OS_LEDC_CHANNEL, - .intr_type = LEDC_INTR_DISABLE, - .timer_sel = OS_LEDC_TIMER, - .duty = 0, - .hpoint = 0, + .gpio_num = gpioPin, + .speed_mode = OS_LEDC_SPEED, + .channel = OS_LEDC_CHANNEL, + .intr_type = LEDC_INTR_DISABLE, + .timer_sel = OS_LEDC_TIMER, + .duty = 0, + .hpoint = 0, + .sleep_mode = LEDC_SLEEP_MODE_NO_ALIVE_NO_PD, // default: no output during light-sleep + .flags = {}, + .deconfigure = false, }; ledc_channel_config(&ledc_channel); // TODO: Error handling diff --git a/src/visual/RgbLedDriver.cpp b/components/led_drivers/src/RgbLedDriver.cpp similarity index 59% rename from src/visual/RgbLedDriver.cpp rename to components/led_drivers/src/RgbLedDriver.cpp index f2f3ea79..c042e358 100644 --- a/src/visual/RgbLedDriver.cpp +++ b/components/led_drivers/src/RgbLedDriver.cpp @@ -1,6 +1,6 @@ #include -#include "visual/RgbLedDriver.h" +#include "led_drivers/RgbLedDriver.h" const char* const TAG = "RGBLedDriver"; @@ -9,8 +9,16 @@ const char* const TAG = "RGBLedDriver"; #include "util/FnProxy.h" #include "util/TaskUtils.h" +#include +#include + #include +// R/G channel swap comes from Kconfig (CONFIG_OPENSHOCK_LED_SWAP_RG_CHANNELS, see +// src/Kconfig.projbuild). Kconfig defines the symbol as 1 only when the bool is +// enabled and leaves it undefined otherwise, so the #if below reads it directly. +#include "sdkconfig.h" + using namespace OpenShock; // Currently this assumes a single WS2812B LED @@ -21,9 +29,10 @@ RgbLedDriver::RgbLedDriver(gpio_num_t gpioPin) : m_gpioPin(GPIO_NUM_NC) , m_brightness(255) , m_pattern() - , m_rmtHandle(nullptr) , m_taskHandle(nullptr) , m_taskMutex() + , m_rmtChannel(nullptr) + , m_rmtEncoder(nullptr) { if (gpioPin == GPIO_NUM_NC) { OS_LOGE(TAG, "Pin is not set"); @@ -35,14 +44,42 @@ RgbLedDriver::RgbLedDriver(gpio_num_t gpioPin) return; } - m_rmtHandle = rmtInit(gpioPin, RMT_TX_MODE, RMT_MEM_64); - if (m_rmtHandle == NULL) { - OS_LOGE(TAG, "Failed to initialize RMT for pin %hhi", gpioPin); + // 10 MHz resolution => 100 ns tick (WS2812B timing). + rmt_tx_channel_config_t channelConfig = { + .gpio_num = gpioPin, + .clk_src = RMT_CLK_SRC_DEFAULT, + .resolution_hz = 10'000'000, + .mem_block_symbols = SOC_RMT_MEM_WORDS_PER_CHANNEL, // exactly 1 memory block + .trans_queue_depth = 4, + .intr_priority = 0, // 0 => driver picks a low/medium priority + .flags = {}, + }; + esp_err_t err = rmt_new_tx_channel(&channelConfig, &m_rmtChannel); + if (err != ESP_OK) { + OS_LOGE(TAG, "Failed to create RMT TX channel for pin %hhi: %s", gpioPin, esp_err_to_name(err)); + m_rmtChannel = nullptr; return; } - float realTick = rmtSetTick(m_rmtHandle, 100.F); - OS_LOGD(TAG, "RMT tick is %f ns for pin %hhi", realTick, gpioPin); + rmt_copy_encoder_config_t encoderConfig = {}; + err = rmt_new_copy_encoder(&encoderConfig, &m_rmtEncoder); + if (err != ESP_OK) { + OS_LOGE(TAG, "Failed to create RMT copy encoder for pin %hhi: %s", gpioPin, esp_err_to_name(err)); + rmt_del_channel(m_rmtChannel); + m_rmtChannel = nullptr; + m_rmtEncoder = nullptr; + return; + } + + err = rmt_enable(m_rmtChannel); + if (err != ESP_OK) { + OS_LOGE(TAG, "Failed to enable RMT channel for pin %hhi: %s", gpioPin, esp_err_to_name(err)); + rmt_del_encoder(m_rmtEncoder); + rmt_del_channel(m_rmtChannel); + m_rmtChannel = nullptr; + m_rmtEncoder = nullptr; + return; + } m_gpioPin = gpioPin; } @@ -51,7 +88,15 @@ RgbLedDriver::~RgbLedDriver() { ClearPattern(); - rmtDeinit(m_rmtHandle); + if (m_rmtChannel != nullptr) { + rmt_disable(m_rmtChannel); + rmt_del_channel(m_rmtChannel); + m_rmtChannel = nullptr; + } + if (m_rmtEncoder != nullptr) { + rmt_del_encoder(m_rmtEncoder); + m_rmtEncoder = nullptr; + } } void RgbLedDriver::SetPattern(const RGBState* pattern, std::size_t patternLength) @@ -105,7 +150,7 @@ void RgbLedDriver::ClearPatternInternal() void RgbLedDriver::RunPattern() { - std::array led_data; // 24 bits per LED (8 bits per color * 3 colors) + std::array led_data; // 24 bits per LED (8 bits per color * 3 colors) while (!m_stopRequested.load(std::memory_order_relaxed)) { for (const auto& state : m_pattern) { @@ -118,7 +163,7 @@ void RgbLedDriver::RunPattern() uint8_t r = static_cast(static_cast(state.red) * m_brightness / 255); uint8_t g = static_cast(static_cast(state.green) * m_brightness / 255); uint8_t b = static_cast(static_cast(state.blue) * m_brightness / 255); -#if OPENSHOCK_LED_SWAP_RG_CHANNELS +#if CONFIG_OPENSHOCK_LED_SWAP_RG_CHANNELS std::swap(r, g); #endif @@ -139,8 +184,11 @@ void RgbLedDriver::RunPattern() } } - // Send the data - rmtWriteBlocking(m_rmtHandle, led_data.data(), led_data.size()); + // Send the data (blocking, wait up to 10ms per frame) + rmt_transmit_config_t txConfig = {}; + txConfig.flags.eot_level = 0; + rmt_transmit(m_rmtChannel, m_rmtEncoder, led_data.data(), led_data.size() * sizeof(rmt_symbol_word_t), &txConfig); + rmt_tx_wait_all_done(m_rmtChannel, 10); // Chunked delay so cooperative shutdown can interrupt long waits uint32_t remaining = state.duration; diff --git a/components/littlefs/CMakeLists.txt b/components/littlefs/CMakeLists.txt new file mode 100644 index 00000000..7f71ec2e --- /dev/null +++ b/components/littlefs/CMakeLists.txt @@ -0,0 +1,17 @@ +# Vendored littlefs core (upstream ARM littlefs, BSD-3-Clause). This is the plain +# filesystem library only — no VFS layer. OpenShock mounts partitions against it +# directly via an esp_partition-backed block device (see the captive portal's +# StaticFs helper). Sources copied from github.com/littlefs-project/littlefs. +idf_component_register( + SRCS "src/lfs.c" + "src/lfs_util.c" + INCLUDE_DIRS "include" +) + +# Quiet the library: it otherwise emits printf-based trace/debug/warn/error output. +target_compile_definitions(${COMPONENT_LIB} PUBLIC + LFS_NO_DEBUG + LFS_NO_WARN + LFS_NO_ERROR + LFS_NO_ASSERT +) diff --git a/components/littlefs/LICENSE.md b/components/littlefs/LICENSE.md new file mode 100644 index 00000000..a4c01b28 --- /dev/null +++ b/components/littlefs/LICENSE.md @@ -0,0 +1,25 @@ +Copyright (c) 2022, The littlefs authors. +Copyright (c) 2017, Arm Limited. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +- Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +- Redistributions in binary form must reproduce the above copyright notice, this + list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. +- Neither the name of ARM nor the names of its contributors may be used to + endorse or promote products derived from this software without specific prior + written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/components/littlefs/include/lfs.h b/components/littlefs/include/lfs.h new file mode 100644 index 00000000..8fbd5b47 --- /dev/null +++ b/components/littlefs/include/lfs.h @@ -0,0 +1,802 @@ +/* + * The little filesystem + * + * Copyright (c) 2022, The littlefs authors. + * Copyright (c) 2017, Arm Limited. All rights reserved. + * SPDX-License-Identifier: BSD-3-Clause + */ +#ifndef LFS_H +#define LFS_H + +#include "lfs_util.h" + +#ifdef __cplusplus +extern "C" +{ +#endif + + +/// Version info /// + +// Software library version +// Major (top-nibble), incremented on backwards incompatible changes +// Minor (bottom-nibble), incremented on feature additions +#define LFS_VERSION 0x0002000b +#define LFS_VERSION_MAJOR (0xffff & (LFS_VERSION >> 16)) +#define LFS_VERSION_MINOR (0xffff & (LFS_VERSION >> 0)) + +// Version of On-disk data structures +// Major (top-nibble), incremented on backwards incompatible changes +// Minor (bottom-nibble), incremented on feature additions +#define LFS_DISK_VERSION 0x00020001 +#define LFS_DISK_VERSION_MAJOR (0xffff & (LFS_DISK_VERSION >> 16)) +#define LFS_DISK_VERSION_MINOR (0xffff & (LFS_DISK_VERSION >> 0)) + + +/// Definitions /// + +// Type definitions +typedef uint32_t lfs_size_t; +typedef uint32_t lfs_off_t; + +typedef int32_t lfs_ssize_t; +typedef int32_t lfs_soff_t; + +typedef uint32_t lfs_block_t; + +// Maximum name size in bytes, may be redefined to reduce the size of the +// info struct. Limited to <= 1022. Stored in superblock and must be +// respected by other littlefs drivers. +#ifndef LFS_NAME_MAX +#define LFS_NAME_MAX 255 +#endif + +// Maximum size of a file in bytes, may be redefined to limit to support other +// drivers. Limited on disk to <= 2147483647. Stored in superblock and must be +// respected by other littlefs drivers. +#ifndef LFS_FILE_MAX +#define LFS_FILE_MAX 2147483647 +#endif + +// Maximum size of custom attributes in bytes, may be redefined, but there is +// no real benefit to using a smaller LFS_ATTR_MAX. Limited to <= 1022. Stored +// in superblock and must be respected by other littlefs drivers. +#ifndef LFS_ATTR_MAX +#define LFS_ATTR_MAX 1022 +#endif + +// Possible error codes, these are negative to allow +// valid positive return values +enum lfs_error { + LFS_ERR_OK = 0, // No error + LFS_ERR_IO = -5, // Error during device operation + LFS_ERR_CORRUPT = -84, // Corrupted + LFS_ERR_NOENT = -2, // No directory entry + LFS_ERR_EXIST = -17, // Entry already exists + LFS_ERR_NOTDIR = -20, // Entry is not a dir + LFS_ERR_ISDIR = -21, // Entry is a dir + LFS_ERR_NOTEMPTY = -39, // Dir is not empty + LFS_ERR_BADF = -9, // Bad file number + LFS_ERR_FBIG = -27, // File too large + LFS_ERR_INVAL = -22, // Invalid parameter + LFS_ERR_NOSPC = -28, // No space left on device + LFS_ERR_NOMEM = -12, // No more memory available + LFS_ERR_NOATTR = -61, // No data/attr available + LFS_ERR_NAMETOOLONG = -36, // File name too long +}; + +// File types +enum lfs_type { + // file types + LFS_TYPE_REG = 0x001, + LFS_TYPE_DIR = 0x002, + + // internally used types + LFS_TYPE_SPLICE = 0x400, + LFS_TYPE_NAME = 0x000, + LFS_TYPE_STRUCT = 0x200, + LFS_TYPE_USERATTR = 0x300, + LFS_TYPE_FROM = 0x100, + LFS_TYPE_TAIL = 0x600, + LFS_TYPE_GLOBALS = 0x700, + LFS_TYPE_CRC = 0x500, + + // internally used type specializations + LFS_TYPE_CREATE = 0x401, + LFS_TYPE_DELETE = 0x4ff, + LFS_TYPE_SUPERBLOCK = 0x0ff, + LFS_TYPE_DIRSTRUCT = 0x200, + LFS_TYPE_CTZSTRUCT = 0x202, + LFS_TYPE_INLINESTRUCT = 0x201, + LFS_TYPE_SOFTTAIL = 0x600, + LFS_TYPE_HARDTAIL = 0x601, + LFS_TYPE_MOVESTATE = 0x7ff, + LFS_TYPE_CCRC = 0x500, + LFS_TYPE_FCRC = 0x5ff, + + // internal chip sources + LFS_FROM_NOOP = 0x000, + LFS_FROM_MOVE = 0x101, + LFS_FROM_USERATTRS = 0x102, +}; + +// File open flags +enum lfs_open_flags { + // open flags + LFS_O_RDONLY = 1, // Open a file as read only +#ifndef LFS_READONLY + LFS_O_WRONLY = 2, // Open a file as write only + LFS_O_RDWR = 3, // Open a file as read and write + LFS_O_CREAT = 0x0100, // Create a file if it does not exist + LFS_O_EXCL = 0x0200, // Fail if a file already exists + LFS_O_TRUNC = 0x0400, // Truncate the existing file to zero size + LFS_O_APPEND = 0x0800, // Move to end of file on every write +#endif + + // internally used flags +#ifndef LFS_READONLY + LFS_F_DIRTY = 0x00010000, // File does not match storage due to write + LFS_F_DUSTY = 0x00020000, // File does not match storage due to desync + LFS_F_WRITING = 0x00040000, // File has been written since last flush +#endif + LFS_F_READING = 0x00080000, // File has been read since last flush +#ifndef LFS_READONLY + LFS_F_ERRED = 0x00100000, // An error occurred during write +#endif + LFS_F_INLINE = 0x01000000, // Currently inlined in directory entry +}; + +// File seek flags +enum lfs_whence_flags { + LFS_SEEK_SET = 0, // Seek relative to an absolute position + LFS_SEEK_CUR = 1, // Seek relative to the current file position + LFS_SEEK_END = 2, // Seek relative to the end of the file +}; + + +// Configuration provided during initialization of the littlefs +struct lfs_config { + // Opaque user provided context that can be used to pass + // information to the block device operations + void *context; + + // Read a region in a block. Negative error codes are propagated + // to the user. + int (*read)(const struct lfs_config *c, lfs_block_t block, + lfs_off_t off, void *buffer, lfs_size_t size); + + // Program a region in a block. The block must have previously + // been erased. Negative error codes are propagated to the user. + // May return LFS_ERR_CORRUPT if the block should be considered bad. + int (*prog)(const struct lfs_config *c, lfs_block_t block, + lfs_off_t off, const void *buffer, lfs_size_t size); + + // Erase a block. A block must be erased before being programmed. + // The state of an erased block is undefined. Negative error codes + // are propagated to the user. + // May return LFS_ERR_CORRUPT if the block should be considered bad. + int (*erase)(const struct lfs_config *c, lfs_block_t block); + + // Sync the state of the underlying block device. Negative error codes + // are propagated to the user. + int (*sync)(const struct lfs_config *c); + +#ifdef LFS_THREADSAFE + // Lock the underlying block device. Negative error codes + // are propagated to the user. + int (*lock)(const struct lfs_config *c); + + // Unlock the underlying block device. Negative error codes + // are propagated to the user. + int (*unlock)(const struct lfs_config *c); +#endif + + // Minimum size of a block read in bytes. All read operations will be a + // multiple of this value. + lfs_size_t read_size; + + // Minimum size of a block program in bytes. All program operations will be + // a multiple of this value. + lfs_size_t prog_size; + + // Size of an erasable block in bytes. This does not impact ram consumption + // and may be larger than the physical erase size. However, non-inlined + // files take up at minimum one block. Must be a multiple of the read and + // program sizes. + lfs_size_t block_size; + + // Number of erasable blocks on the device. Defaults to block_count stored + // on disk when zero. + lfs_size_t block_count; + + // Number of erase cycles before littlefs evicts metadata logs and moves + // the metadata to another block. Suggested values are in the + // range 100-1000, with large values having better performance at the cost + // of less consistent wear distribution. + // + // Set to -1 to disable block-level wear-leveling. + int32_t block_cycles; + + // Size of block caches in bytes. Each cache buffers a portion of a block in + // RAM. The littlefs needs a read cache, a program cache, and one additional + // cache per file. Larger caches can improve performance by storing more + // data and reducing the number of disk accesses. Must be a multiple of the + // read and program sizes, and a factor of the block size. + lfs_size_t cache_size; + + // Size of the lookahead buffer in bytes. A larger lookahead buffer + // increases the number of blocks found during an allocation pass. The + // lookahead buffer is stored as a compact bitmap, so each byte of RAM + // can track 8 blocks. + lfs_size_t lookahead_size; + + // Threshold for metadata compaction during lfs_fs_gc in bytes. Metadata + // pairs that exceed this threshold will be compacted during lfs_fs_gc. + // Defaults to ~88% block_size when zero, though the default may change + // in the future. + // + // Note this only affects lfs_fs_gc. Normal compactions still only occur + // when full. + // + // Set to -1 to disable metadata compaction during lfs_fs_gc. + lfs_size_t compact_thresh; + + // Optional statically allocated read buffer. Must be cache_size. + // By default lfs_malloc is used to allocate this buffer. + void *read_buffer; + + // Optional statically allocated program buffer. Must be cache_size. + // By default lfs_malloc is used to allocate this buffer. + void *prog_buffer; + + // Optional statically allocated lookahead buffer. Must be lookahead_size. + // By default lfs_malloc is used to allocate this buffer. + void *lookahead_buffer; + + // Optional upper limit on length of file names in bytes. No downside for + // larger names except the size of the info struct which is controlled by + // the LFS_NAME_MAX define. Defaults to LFS_NAME_MAX or name_max stored on + // disk when zero. + lfs_size_t name_max; + + // Optional upper limit on files in bytes. No downside for larger files + // but must be <= LFS_FILE_MAX. Defaults to LFS_FILE_MAX or file_max stored + // on disk when zero. + lfs_size_t file_max; + + // Optional upper limit on custom attributes in bytes. No downside for + // larger attributes size but must be <= LFS_ATTR_MAX. Defaults to + // LFS_ATTR_MAX or attr_max stored on disk when zero. + lfs_size_t attr_max; + + // Optional upper limit on total space given to metadata pairs in bytes. On + // devices with large blocks (e.g. 128kB) setting this to a low size (2-8kB) + // can help bound the metadata compaction time. Must be <= block_size. + // Defaults to block_size when zero. + lfs_size_t metadata_max; + + // Optional upper limit on inlined files in bytes. Inlined files live in + // metadata and decrease storage requirements, but may be limited to + // improve metadata-related performance. Must be <= cache_size, <= + // attr_max, and <= block_size/8. Defaults to the largest possible + // inline_max when zero. + // + // Set to -1 to disable inlined files. + lfs_size_t inline_max; + +#ifdef LFS_MULTIVERSION + // On-disk version to use when writing in the form of 16-bit major version + // + 16-bit minor version. This limiting metadata to what is supported by + // older minor versions. Note that some features will be lost. Defaults to + // to the most recent minor version when zero. + uint32_t disk_version; +#endif +}; + +// File info structure +struct lfs_info { + // Type of the file, either LFS_TYPE_REG or LFS_TYPE_DIR + uint8_t type; + + // Size of the file, only valid for REG files. Limited to 32-bits. + lfs_size_t size; + + // Name of the file stored as a null-terminated string. Limited to + // LFS_NAME_MAX+1, which can be changed by redefining LFS_NAME_MAX to + // reduce RAM. LFS_NAME_MAX is stored in superblock and must be + // respected by other littlefs drivers. + char name[LFS_NAME_MAX+1]; +}; + +// Filesystem info structure +struct lfs_fsinfo { + // On-disk version. + uint32_t disk_version; + + // Size of a logical block in bytes. + lfs_size_t block_size; + + // Number of logical blocks in filesystem. + lfs_size_t block_count; + + // Upper limit on the length of file names in bytes. + lfs_size_t name_max; + + // Upper limit on the size of files in bytes. + lfs_size_t file_max; + + // Upper limit on the size of custom attributes in bytes. + lfs_size_t attr_max; +}; + +// Custom attribute structure, used to describe custom attributes +// committed atomically during file writes. +struct lfs_attr { + // 8-bit type of attribute, provided by user and used to + // identify the attribute + uint8_t type; + + // Pointer to buffer containing the attribute + void *buffer; + + // Size of attribute in bytes, limited to LFS_ATTR_MAX + lfs_size_t size; +}; + +// Optional configuration provided during lfs_file_opencfg +struct lfs_file_config { + // Optional statically allocated file buffer. Must be cache_size. + // By default lfs_malloc is used to allocate this buffer. + void *buffer; + + // Optional list of custom attributes related to the file. If the file + // is opened with read access, these attributes will be read from disk + // during the open call. If the file is opened with write access, the + // attributes will be written to disk every file sync or close. This + // write occurs atomically with update to the file's contents. + // + // Custom attributes are uniquely identified by an 8-bit type and limited + // to LFS_ATTR_MAX bytes. When read, if the stored attribute is smaller + // than the buffer, it will be padded with zeros. If the stored attribute + // is larger, then it will be silently truncated. If the attribute is not + // found, it will be created implicitly. + struct lfs_attr *attrs; + + // Number of custom attributes in the list + lfs_size_t attr_count; +}; + + +/// internal littlefs data structures /// +typedef struct lfs_cache { + lfs_block_t block; + lfs_off_t off; + lfs_size_t size; + uint8_t *buffer; +} lfs_cache_t; + +typedef struct lfs_mdir { + lfs_block_t pair[2]; + uint32_t rev; + lfs_off_t off; + uint32_t etag; + uint16_t count; + bool erased; + bool split; + lfs_block_t tail[2]; +} lfs_mdir_t; + +// littlefs directory type +typedef struct lfs_dir { + struct lfs_dir *next; + uint16_t id; + uint8_t type; + lfs_mdir_t m; + + lfs_off_t pos; + lfs_block_t head[2]; +} lfs_dir_t; + +// littlefs file type +typedef struct lfs_file { + struct lfs_file *next; + uint16_t id; + uint8_t type; + lfs_mdir_t m; + + struct lfs_ctz { + lfs_block_t head; + lfs_size_t size; + } ctz; + + uint32_t flags; + lfs_off_t pos; + lfs_block_t block; + lfs_off_t off; + lfs_cache_t cache; + + const struct lfs_file_config *cfg; +} lfs_file_t; + +typedef struct lfs_superblock { + uint32_t version; + lfs_size_t block_size; + lfs_size_t block_count; + lfs_size_t name_max; + lfs_size_t file_max; + lfs_size_t attr_max; +} lfs_superblock_t; + +typedef struct lfs_gstate { + uint32_t tag; + lfs_block_t pair[2]; +} lfs_gstate_t; + +// The littlefs filesystem type +typedef struct lfs { + lfs_cache_t rcache; + lfs_cache_t pcache; + + lfs_block_t root[2]; + struct lfs_mlist { + struct lfs_mlist *next; + uint16_t id; + uint8_t type; + lfs_mdir_t m; + } *mlist; + uint32_t seed; + + lfs_gstate_t gstate; + lfs_gstate_t gdisk; + lfs_gstate_t gdelta; + + struct lfs_lookahead { + lfs_block_t start; + lfs_block_t size; + lfs_block_t next; + lfs_block_t ckpoint; + uint8_t *buffer; + } lookahead; + + const struct lfs_config *cfg; + lfs_size_t block_count; + lfs_size_t name_max; + lfs_size_t file_max; + lfs_size_t attr_max; + lfs_size_t inline_max; + +#ifdef LFS_MIGRATE + struct lfs1 *lfs1; +#endif +} lfs_t; + + +/// Filesystem functions /// + +#ifndef LFS_READONLY +// Format a block device with the littlefs +// +// Requires a littlefs object and config struct. This clobbers the littlefs +// object, and does not leave the filesystem mounted. The config struct must +// be zeroed for defaults and backwards compatibility. +// +// Returns a negative error code on failure. +int lfs_format(lfs_t *lfs, const struct lfs_config *config); +#endif + +// Mounts a littlefs +// +// Requires a littlefs object and config struct. Multiple filesystems +// may be mounted simultaneously with multiple littlefs objects. Both +// lfs and config must be allocated while mounted. The config struct must +// be zeroed for defaults and backwards compatibility. +// +// Returns a negative error code on failure. +int lfs_mount(lfs_t *lfs, const struct lfs_config *config); + +// Unmounts a littlefs +// +// Does nothing besides releasing any allocated resources. +// Returns a negative error code on failure. +int lfs_unmount(lfs_t *lfs); + +/// General operations /// + +#ifndef LFS_READONLY +// Removes a file or directory +// +// If removing a directory, the directory must be empty. +// Returns a negative error code on failure. +int lfs_remove(lfs_t *lfs, const char *path); +#endif + +#ifndef LFS_READONLY +// Rename or move a file or directory +// +// If the destination exists, it must match the source in type. +// If the destination is a directory, the directory must be empty. +// +// Returns a negative error code on failure. +int lfs_rename(lfs_t *lfs, const char *oldpath, const char *newpath); +#endif + +// Find info about a file or directory +// +// Fills out the info structure, based on the specified file or directory. +// Returns a negative error code on failure. +int lfs_stat(lfs_t *lfs, const char *path, struct lfs_info *info); + +// Get a custom attribute +// +// Custom attributes are uniquely identified by an 8-bit type and limited +// to LFS_ATTR_MAX bytes. When read, if the stored attribute is smaller than +// the buffer, it will be padded with zeros. If the stored attribute is larger, +// then it will be silently truncated. If no attribute is found, the error +// LFS_ERR_NOATTR is returned and the buffer is filled with zeros. +// +// Returns the size of the attribute, or a negative error code on failure. +// Note, the returned size is the size of the attribute on disk, irrespective +// of the size of the buffer. This can be used to dynamically allocate a buffer +// or check for existence. +lfs_ssize_t lfs_getattr(lfs_t *lfs, const char *path, + uint8_t type, void *buffer, lfs_size_t size); + +#ifndef LFS_READONLY +// Set custom attributes +// +// Custom attributes are uniquely identified by an 8-bit type and limited +// to LFS_ATTR_MAX bytes. If an attribute is not found, it will be +// implicitly created. +// +// Returns a negative error code on failure. +int lfs_setattr(lfs_t *lfs, const char *path, + uint8_t type, const void *buffer, lfs_size_t size); +#endif + +#ifndef LFS_READONLY +// Removes a custom attribute +// +// If an attribute is not found, nothing happens. +// +// Returns a negative error code on failure. +int lfs_removeattr(lfs_t *lfs, const char *path, uint8_t type); +#endif + + +/// File operations /// + +#ifndef LFS_NO_MALLOC +// Open a file +// +// The mode that the file is opened in is determined by the flags, which +// are values from the enum lfs_open_flags that are bitwise-ored together. +// +// Returns a negative error code on failure. +int lfs_file_open(lfs_t *lfs, lfs_file_t *file, + const char *path, int flags); + +// if LFS_NO_MALLOC is defined, lfs_file_open() will fail with LFS_ERR_NOMEM +// thus use lfs_file_opencfg() with config.buffer set. +#endif + +// Open a file with extra configuration +// +// The mode that the file is opened in is determined by the flags, which +// are values from the enum lfs_open_flags that are bitwise-ored together. +// +// The config struct provides additional config options per file as described +// above. The config struct must remain allocated while the file is open, and +// the config struct must be zeroed for defaults and backwards compatibility. +// +// Returns a negative error code on failure. +int lfs_file_opencfg(lfs_t *lfs, lfs_file_t *file, + const char *path, int flags, + const struct lfs_file_config *config); + +// Close a file +// +// Any pending writes are written out to storage as though +// sync had been called and releases any allocated resources. +// +// Returns a negative error code on failure. +int lfs_file_close(lfs_t *lfs, lfs_file_t *file); + +// Synchronize a file on storage +// +// Any pending writes are written out to storage. +// Returns a negative error code on failure. +int lfs_file_sync(lfs_t *lfs, lfs_file_t *file); + +// Read data from file +// +// Takes a buffer and size indicating where to store the read data. +// Returns the number of bytes read, or a negative error code on failure. +lfs_ssize_t lfs_file_read(lfs_t *lfs, lfs_file_t *file, + void *buffer, lfs_size_t size); + +#ifndef LFS_READONLY +// Write data to file +// +// Takes a buffer and size indicating the data to write. The file will not +// actually be updated on the storage until either sync or close is called. +// +// Returns the number of bytes written, or a negative error code on failure. +lfs_ssize_t lfs_file_write(lfs_t *lfs, lfs_file_t *file, + const void *buffer, lfs_size_t size); +#endif + +// Change the position of the file +// +// The change in position is determined by the offset and whence flag. +// Returns the new position of the file, or a negative error code on failure. +lfs_soff_t lfs_file_seek(lfs_t *lfs, lfs_file_t *file, + lfs_soff_t off, int whence); + +#ifndef LFS_READONLY +// Truncates the size of the file to the specified size +// +// Returns a negative error code on failure. +int lfs_file_truncate(lfs_t *lfs, lfs_file_t *file, lfs_off_t size); +#endif + +// Return the position of the file +// +// Equivalent to lfs_file_seek(lfs, file, 0, LFS_SEEK_CUR) +// Returns the position of the file, or a negative error code on failure. +lfs_soff_t lfs_file_tell(lfs_t *lfs, lfs_file_t *file); + +// Change the position of the file to the beginning of the file +// +// Equivalent to lfs_file_seek(lfs, file, 0, LFS_SEEK_SET) +// Returns a negative error code on failure. +int lfs_file_rewind(lfs_t *lfs, lfs_file_t *file); + +// Return the size of the file +// +// Similar to lfs_file_seek(lfs, file, 0, LFS_SEEK_END) +// Returns the size of the file, or a negative error code on failure. +lfs_soff_t lfs_file_size(lfs_t *lfs, lfs_file_t *file); + + +/// Directory operations /// + +#ifndef LFS_READONLY +// Create a directory +// +// Returns a negative error code on failure. +int lfs_mkdir(lfs_t *lfs, const char *path); +#endif + +// Open a directory +// +// Once open a directory can be used with read to iterate over files. +// Returns a negative error code on failure. +int lfs_dir_open(lfs_t *lfs, lfs_dir_t *dir, const char *path); + +// Close a directory +// +// Releases any allocated resources. +// Returns a negative error code on failure. +int lfs_dir_close(lfs_t *lfs, lfs_dir_t *dir); + +// Read an entry in the directory +// +// Fills out the info structure, based on the specified file or directory. +// Returns a positive value on success, 0 at the end of directory, +// or a negative error code on failure. +int lfs_dir_read(lfs_t *lfs, lfs_dir_t *dir, struct lfs_info *info); + +// Change the position of the directory +// +// The new off must be a value previous returned from tell and specifies +// an absolute offset in the directory seek. +// +// Returns a negative error code on failure. +int lfs_dir_seek(lfs_t *lfs, lfs_dir_t *dir, lfs_off_t off); + +// Return the position of the directory +// +// The returned offset is only meant to be consumed by seek and may not make +// sense, but does indicate the current position in the directory iteration. +// +// Returns the position of the directory, or a negative error code on failure. +lfs_soff_t lfs_dir_tell(lfs_t *lfs, lfs_dir_t *dir); + +// Change the position of the directory to the beginning of the directory +// +// Returns a negative error code on failure. +int lfs_dir_rewind(lfs_t *lfs, lfs_dir_t *dir); + + +/// Filesystem-level filesystem operations + +// Find on-disk info about the filesystem +// +// Fills out the fsinfo structure based on the filesystem found on-disk. +// Returns a negative error code on failure. +int lfs_fs_stat(lfs_t *lfs, struct lfs_fsinfo *fsinfo); + +// Finds the current size of the filesystem +// +// Note: Result is best effort. If files share COW structures, the returned +// size may be larger than the filesystem actually is. +// +// Returns the number of allocated blocks, or a negative error code on failure. +lfs_ssize_t lfs_fs_size(lfs_t *lfs); + +// Traverse through all blocks in use by the filesystem +// +// The provided callback will be called with each block address that is +// currently in use by the filesystem. This can be used to determine which +// blocks are in use or how much of the storage is available. +// +// Returns a negative error code on failure. +int lfs_fs_traverse(lfs_t *lfs, int (*cb)(void*, lfs_block_t), void *data); + +#ifndef LFS_READONLY +// Attempt to make the filesystem consistent and ready for writing +// +// Calling this function is not required, consistency will be implicitly +// enforced on the first operation that writes to the filesystem, but this +// function allows the work to be performed earlier and without other +// filesystem changes. +// +// Returns a negative error code on failure. +int lfs_fs_mkconsistent(lfs_t *lfs); +#endif + +#ifndef LFS_READONLY +// Attempt any janitorial work +// +// This currently: +// 1. Calls mkconsistent if not already consistent +// 2. Compacts metadata > compact_thresh +// 3. Populates the block allocator +// +// Though additional janitorial work may be added in the future. +// +// Calling this function is not required, but may allow the offloading of +// expensive janitorial work to a less time-critical code path. +// +// Returns a negative error code on failure. Accomplishing nothing is not +// an error. +int lfs_fs_gc(lfs_t *lfs); +#endif + +#ifndef LFS_READONLY +// Grows the filesystem to a new size, updating the superblock with the new +// block count. +// +// If LFS_SHRINKNONRELOCATING is defined, this function will also accept +// block_counts smaller than the current configuration, after checking +// that none of the blocks that are being removed are in use. +// Note that littlefs's pseudorandom block allocation means that +// this is very unlikely to work in the general case. +// +// Returns a negative error code on failure. +int lfs_fs_grow(lfs_t *lfs, lfs_size_t block_count); +#endif + +#ifndef LFS_READONLY +#ifdef LFS_MIGRATE +// Attempts to migrate a previous version of littlefs +// +// Behaves similarly to the lfs_format function. Attempts to mount +// the previous version of littlefs and update the filesystem so it can be +// mounted with the current version of littlefs. +// +// Requires a littlefs object and config struct. This clobbers the littlefs +// object, and does not leave the filesystem mounted. The config struct must +// be zeroed for defaults and backwards compatibility. +// +// Returns a negative error code on failure. +int lfs_migrate(lfs_t *lfs, const struct lfs_config *cfg); +#endif +#endif + + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif diff --git a/components/littlefs/include/lfs_util.h b/components/littlefs/include/lfs_util.h new file mode 100644 index 00000000..c1999fa9 --- /dev/null +++ b/components/littlefs/include/lfs_util.h @@ -0,0 +1,273 @@ +/* + * lfs utility functions + * + * Copyright (c) 2022, The littlefs authors. + * Copyright (c) 2017, Arm Limited. All rights reserved. + * SPDX-License-Identifier: BSD-3-Clause + */ +#ifndef LFS_UTIL_H +#define LFS_UTIL_H + +#define LFS_STRINGIZE(x) LFS_STRINGIZE2(x) +#define LFS_STRINGIZE2(x) #x + +// Users can override lfs_util.h with their own configuration by defining +// LFS_CONFIG as a header file to include (-DLFS_CONFIG=lfs_config.h). +// +// If LFS_CONFIG is used, none of the default utils will be emitted and must be +// provided by the config file. To start, I would suggest copying lfs_util.h +// and modifying as needed. +#ifdef LFS_CONFIG +#include LFS_STRINGIZE(LFS_CONFIG) +#else + +// Alternatively, users can provide a header file which defines +// macros and other things consumed by littlefs. +// +// For example, provide my_defines.h, which contains +// something like: +// +// #include +// extern void *my_malloc(size_t sz); +// #define LFS_MALLOC(sz) my_malloc(sz) +// +// And build littlefs with the header by defining LFS_DEFINES. +// (-DLFS_DEFINES=my_defines.h) + +#ifdef LFS_DEFINES +#include LFS_STRINGIZE(LFS_DEFINES) +#endif + +// System includes +#include +#include +#include +#include + +#ifndef LFS_NO_MALLOC +#include +#endif +#ifndef LFS_NO_ASSERT +#include +#endif +#if !defined(LFS_NO_DEBUG) || \ + !defined(LFS_NO_WARN) || \ + !defined(LFS_NO_ERROR) || \ + defined(LFS_YES_TRACE) +#include +#endif + +#ifdef __cplusplus +extern "C" +{ +#endif + + +// Macros, may be replaced by system specific wrappers. Arguments to these +// macros must not have side-effects as the macros can be removed for a smaller +// code footprint + +// Logging functions +#ifndef LFS_TRACE +#ifdef LFS_YES_TRACE +#define LFS_TRACE_(fmt, ...) \ + printf("%s:%d:trace: " fmt "%s\n", __FILE__, __LINE__, __VA_ARGS__) +#define LFS_TRACE(...) LFS_TRACE_(__VA_ARGS__, "") +#else +#define LFS_TRACE(...) +#endif +#endif + +#ifndef LFS_DEBUG +#ifndef LFS_NO_DEBUG +#define LFS_DEBUG_(fmt, ...) \ + printf("%s:%d:debug: " fmt "%s\n", __FILE__, __LINE__, __VA_ARGS__) +#define LFS_DEBUG(...) LFS_DEBUG_(__VA_ARGS__, "") +#else +#define LFS_DEBUG(...) +#endif +#endif + +#ifndef LFS_WARN +#ifndef LFS_NO_WARN +#define LFS_WARN_(fmt, ...) \ + printf("%s:%d:warn: " fmt "%s\n", __FILE__, __LINE__, __VA_ARGS__) +#define LFS_WARN(...) LFS_WARN_(__VA_ARGS__, "") +#else +#define LFS_WARN(...) +#endif +#endif + +#ifndef LFS_ERROR +#ifndef LFS_NO_ERROR +#define LFS_ERROR_(fmt, ...) \ + printf("%s:%d:error: " fmt "%s\n", __FILE__, __LINE__, __VA_ARGS__) +#define LFS_ERROR(...) LFS_ERROR_(__VA_ARGS__, "") +#else +#define LFS_ERROR(...) +#endif +#endif + +// Runtime assertions +#ifndef LFS_ASSERT +#ifndef LFS_NO_ASSERT +#define LFS_ASSERT(test) assert(test) +#else +#define LFS_ASSERT(test) +#endif +#endif + + +// Builtin functions, these may be replaced by more efficient +// toolchain-specific implementations. LFS_NO_INTRINSICS falls back to a more +// expensive basic C implementation for debugging purposes + +// Min/max functions for unsigned 32-bit numbers +static inline uint32_t lfs_max(uint32_t a, uint32_t b) { + return (a > b) ? a : b; +} + +static inline uint32_t lfs_min(uint32_t a, uint32_t b) { + return (a < b) ? a : b; +} + +// Align to nearest multiple of a size +static inline uint32_t lfs_aligndown(uint32_t a, uint32_t alignment) { + return a - (a % alignment); +} + +static inline uint32_t lfs_alignup(uint32_t a, uint32_t alignment) { + return lfs_aligndown(a + alignment-1, alignment); +} + +// Find the smallest power of 2 greater than or equal to a +static inline uint32_t lfs_npw2(uint32_t a) { +#if !defined(LFS_NO_INTRINSICS) && (defined(__GNUC__) || defined(__CC_ARM)) + return 32 - __builtin_clz(a-1); +#else + uint32_t r = 0; + uint32_t s; + a -= 1; + s = (a > 0xffff) << 4; a >>= s; r |= s; + s = (a > 0xff ) << 3; a >>= s; r |= s; + s = (a > 0xf ) << 2; a >>= s; r |= s; + s = (a > 0x3 ) << 1; a >>= s; r |= s; + return (r | (a >> 1)) + 1; +#endif +} + +// Count the number of trailing binary zeros in a +// lfs_ctz(0) may be undefined +static inline uint32_t lfs_ctz(uint32_t a) { +#if !defined(LFS_NO_INTRINSICS) && defined(__GNUC__) + return __builtin_ctz(a); +#else + return lfs_npw2((a & -a) + 1) - 1; +#endif +} + +// Count the number of binary ones in a +static inline uint32_t lfs_popc(uint32_t a) { +#if !defined(LFS_NO_INTRINSICS) && (defined(__GNUC__) || defined(__CC_ARM)) + return __builtin_popcount(a); +#else + a = a - ((a >> 1) & 0x55555555); + a = (a & 0x33333333) + ((a >> 2) & 0x33333333); + return (((a + (a >> 4)) & 0xf0f0f0f) * 0x1010101) >> 24; +#endif +} + +// Find the sequence comparison of a and b, this is the distance +// between a and b ignoring overflow +static inline int lfs_scmp(uint32_t a, uint32_t b) { + return (int)(unsigned)(a - b); +} + +// Convert between 32-bit little-endian and native order +static inline uint32_t lfs_fromle32(uint32_t a) { +#if (defined( BYTE_ORDER ) && defined( ORDER_LITTLE_ENDIAN ) && BYTE_ORDER == ORDER_LITTLE_ENDIAN ) || \ + (defined(__BYTE_ORDER ) && defined(__ORDER_LITTLE_ENDIAN ) && __BYTE_ORDER == __ORDER_LITTLE_ENDIAN ) || \ + (defined(__BYTE_ORDER__) && defined(__ORDER_LITTLE_ENDIAN__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__) + return a; +#elif !defined(LFS_NO_INTRINSICS) && ( \ + (defined( BYTE_ORDER ) && defined( ORDER_BIG_ENDIAN ) && BYTE_ORDER == ORDER_BIG_ENDIAN ) || \ + (defined(__BYTE_ORDER ) && defined(__ORDER_BIG_ENDIAN ) && __BYTE_ORDER == __ORDER_BIG_ENDIAN ) || \ + (defined(__BYTE_ORDER__) && defined(__ORDER_BIG_ENDIAN__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__)) + return __builtin_bswap32(a); +#else + return ((uint32_t)((uint8_t*)&a)[0] << 0) | + ((uint32_t)((uint8_t*)&a)[1] << 8) | + ((uint32_t)((uint8_t*)&a)[2] << 16) | + ((uint32_t)((uint8_t*)&a)[3] << 24); +#endif +} + +static inline uint32_t lfs_tole32(uint32_t a) { + return lfs_fromle32(a); +} + +// Convert between 32-bit big-endian and native order +static inline uint32_t lfs_frombe32(uint32_t a) { +#if !defined(LFS_NO_INTRINSICS) && ( \ + (defined( BYTE_ORDER ) && defined( ORDER_LITTLE_ENDIAN ) && BYTE_ORDER == ORDER_LITTLE_ENDIAN ) || \ + (defined(__BYTE_ORDER ) && defined(__ORDER_LITTLE_ENDIAN ) && __BYTE_ORDER == __ORDER_LITTLE_ENDIAN ) || \ + (defined(__BYTE_ORDER__) && defined(__ORDER_LITTLE_ENDIAN__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__)) + return __builtin_bswap32(a); +#elif (defined( BYTE_ORDER ) && defined( ORDER_BIG_ENDIAN ) && BYTE_ORDER == ORDER_BIG_ENDIAN ) || \ + (defined(__BYTE_ORDER ) && defined(__ORDER_BIG_ENDIAN ) && __BYTE_ORDER == __ORDER_BIG_ENDIAN ) || \ + (defined(__BYTE_ORDER__) && defined(__ORDER_BIG_ENDIAN__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__) + return a; +#else + return ((uint32_t)((uint8_t*)&a)[0] << 24) | + ((uint32_t)((uint8_t*)&a)[1] << 16) | + ((uint32_t)((uint8_t*)&a)[2] << 8) | + ((uint32_t)((uint8_t*)&a)[3] << 0); +#endif +} + +static inline uint32_t lfs_tobe32(uint32_t a) { + return lfs_frombe32(a); +} + +// Calculate CRC-32 with polynomial = 0x04c11db7 +#ifdef LFS_CRC +static inline uint32_t lfs_crc(uint32_t crc, const void *buffer, size_t size) { + return LFS_CRC(crc, buffer, size); +} +#else +uint32_t lfs_crc(uint32_t crc, const void *buffer, size_t size); +#endif + +// Allocate memory, only used if buffers are not provided to littlefs +// +// littlefs current has no alignment requirements, as it only allocates +// byte-level buffers. +static inline void *lfs_malloc(size_t size) { +#if defined(LFS_MALLOC) + return LFS_MALLOC(size); +#elif !defined(LFS_NO_MALLOC) + return malloc(size); +#else + (void)size; + return NULL; +#endif +} + +// Deallocate memory, only used if buffers are not provided to littlefs +static inline void lfs_free(void *p) { +#if defined(LFS_FREE) + LFS_FREE(p); +#elif !defined(LFS_NO_MALLOC) + free(p); +#else + (void)p; +#endif +} + + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif +#endif diff --git a/components/littlefs/src/lfs.c b/components/littlefs/src/lfs.c new file mode 100644 index 00000000..c83c7e0b --- /dev/null +++ b/components/littlefs/src/lfs.c @@ -0,0 +1,6558 @@ +/* + * The little filesystem + * + * Copyright (c) 2022, The littlefs authors. + * Copyright (c) 2017, Arm Limited. All rights reserved. + * SPDX-License-Identifier: BSD-3-Clause + */ +#include "lfs.h" +#include "lfs_util.h" + + +// some constants used throughout the code +#define LFS_BLOCK_NULL ((lfs_block_t)-1) +#define LFS_BLOCK_INLINE ((lfs_block_t)-2) + +enum { + LFS_OK_RELOCATED = 1, + LFS_OK_DROPPED = 2, + LFS_OK_ORPHANED = 3, +}; + +enum { + LFS_CMP_EQ = 0, + LFS_CMP_LT = 1, + LFS_CMP_GT = 2, +}; + + +/// Caching block device operations /// + +static inline void lfs_cache_drop(lfs_t *lfs, lfs_cache_t *rcache) { + // do not zero, cheaper if cache is readonly or only going to be + // written with identical data (during relocates) + (void)lfs; + rcache->block = LFS_BLOCK_NULL; +} + +static inline void lfs_cache_zero(lfs_t *lfs, lfs_cache_t *pcache) { + // zero to avoid information leak + memset(pcache->buffer, 0xff, lfs->cfg->cache_size); + pcache->block = LFS_BLOCK_NULL; +} + +static int lfs_bd_read(lfs_t *lfs, + const lfs_cache_t *pcache, lfs_cache_t *rcache, lfs_size_t hint, + lfs_block_t block, lfs_off_t off, + void *buffer, lfs_size_t size) { + uint8_t *data = buffer; + if (off+size > lfs->cfg->block_size + || (lfs->block_count && block >= lfs->block_count)) { + return LFS_ERR_CORRUPT; + } + + while (size > 0) { + lfs_size_t diff = size; + + if (pcache && block == pcache->block && + off < pcache->off + pcache->size) { + if (off >= pcache->off) { + // is already in pcache? + diff = lfs_min(diff, pcache->size - (off-pcache->off)); + memcpy(data, &pcache->buffer[off-pcache->off], diff); + + data += diff; + off += diff; + size -= diff; + continue; + } + + // pcache takes priority + diff = lfs_min(diff, pcache->off-off); + } + + if (block == rcache->block && + off < rcache->off + rcache->size) { + if (off >= rcache->off) { + // is already in rcache? + diff = lfs_min(diff, rcache->size - (off-rcache->off)); + memcpy(data, &rcache->buffer[off-rcache->off], diff); + + data += diff; + off += diff; + size -= diff; + continue; + } + + // rcache takes priority + diff = lfs_min(diff, rcache->off-off); + } + + if (size >= hint && off % lfs->cfg->read_size == 0 && + size >= lfs->cfg->read_size) { + // bypass cache? + diff = lfs_aligndown(diff, lfs->cfg->read_size); + int err = lfs->cfg->read(lfs->cfg, block, off, data, diff); + LFS_ASSERT(err <= 0); + if (err) { + return err; + } + + data += diff; + off += diff; + size -= diff; + continue; + } + + // load to cache, first condition can no longer fail + LFS_ASSERT(!lfs->block_count || block < lfs->block_count); + rcache->block = block; + rcache->off = lfs_aligndown(off, lfs->cfg->read_size); + rcache->size = lfs_min( + lfs_min( + lfs_alignup(off+hint, lfs->cfg->read_size), + lfs->cfg->block_size) + - rcache->off, + lfs->cfg->cache_size); + int err = lfs->cfg->read(lfs->cfg, rcache->block, + rcache->off, rcache->buffer, rcache->size); + LFS_ASSERT(err <= 0); + if (err) { + return err; + } + } + + return 0; +} + +static int lfs_bd_cmp(lfs_t *lfs, + const lfs_cache_t *pcache, lfs_cache_t *rcache, lfs_size_t hint, + lfs_block_t block, lfs_off_t off, + const void *buffer, lfs_size_t size) { + const uint8_t *data = buffer; + lfs_size_t diff = 0; + + for (lfs_off_t i = 0; i < size; i += diff) { + uint8_t dat[8]; + + diff = lfs_min(size-i, sizeof(dat)); + int err = lfs_bd_read(lfs, + pcache, rcache, hint-i, + block, off+i, &dat, diff); + if (err) { + return err; + } + + int res = memcmp(dat, data + i, diff); + if (res) { + return res < 0 ? LFS_CMP_LT : LFS_CMP_GT; + } + } + + return LFS_CMP_EQ; +} + +static int lfs_bd_crc(lfs_t *lfs, + const lfs_cache_t *pcache, lfs_cache_t *rcache, lfs_size_t hint, + lfs_block_t block, lfs_off_t off, lfs_size_t size, uint32_t *crc) { + lfs_size_t diff = 0; + + for (lfs_off_t i = 0; i < size; i += diff) { + uint8_t dat[8]; + diff = lfs_min(size-i, sizeof(dat)); + int err = lfs_bd_read(lfs, + pcache, rcache, hint-i, + block, off+i, &dat, diff); + if (err) { + return err; + } + + *crc = lfs_crc(*crc, &dat, diff); + } + + return 0; +} + +#ifndef LFS_READONLY +static int lfs_bd_flush(lfs_t *lfs, + lfs_cache_t *pcache, lfs_cache_t *rcache, bool validate) { + if (pcache->block != LFS_BLOCK_NULL && pcache->block != LFS_BLOCK_INLINE) { + LFS_ASSERT(pcache->block < lfs->block_count); + lfs_size_t diff = lfs_alignup(pcache->size, lfs->cfg->prog_size); + int err = lfs->cfg->prog(lfs->cfg, pcache->block, + pcache->off, pcache->buffer, diff); + LFS_ASSERT(err <= 0); + if (err) { + return err; + } + + if (validate) { + // check data on disk + lfs_cache_drop(lfs, rcache); + int res = lfs_bd_cmp(lfs, + NULL, rcache, diff, + pcache->block, pcache->off, pcache->buffer, diff); + if (res < 0) { + return res; + } + + if (res != LFS_CMP_EQ) { + return LFS_ERR_CORRUPT; + } + } + + lfs_cache_zero(lfs, pcache); + } + + return 0; +} +#endif + +#ifndef LFS_READONLY +static int lfs_bd_sync(lfs_t *lfs, + lfs_cache_t *pcache, lfs_cache_t *rcache, bool validate) { + lfs_cache_drop(lfs, rcache); + + int err = lfs_bd_flush(lfs, pcache, rcache, validate); + if (err) { + return err; + } + + err = lfs->cfg->sync(lfs->cfg); + LFS_ASSERT(err <= 0); + return err; +} +#endif + +#ifndef LFS_READONLY +static int lfs_bd_prog(lfs_t *lfs, + lfs_cache_t *pcache, lfs_cache_t *rcache, bool validate, + lfs_block_t block, lfs_off_t off, + const void *buffer, lfs_size_t size) { + const uint8_t *data = buffer; + LFS_ASSERT(block == LFS_BLOCK_INLINE || block < lfs->block_count); + LFS_ASSERT(off + size <= lfs->cfg->block_size); + + while (size > 0) { + if (block == pcache->block && + off >= pcache->off && + off < pcache->off + lfs->cfg->cache_size) { + // already fits in pcache? + lfs_size_t diff = lfs_min(size, + lfs->cfg->cache_size - (off-pcache->off)); + memcpy(&pcache->buffer[off-pcache->off], data, diff); + + data += diff; + off += diff; + size -= diff; + + pcache->size = lfs_max(pcache->size, off - pcache->off); + if (pcache->size == lfs->cfg->cache_size) { + // eagerly flush out pcache if we fill up + int err = lfs_bd_flush(lfs, pcache, rcache, validate); + if (err) { + return err; + } + } + + continue; + } + + // pcache must have been flushed, either by programming an + // entire block or manually flushing the pcache + LFS_ASSERT(pcache->block == LFS_BLOCK_NULL); + + // prepare pcache, first condition can no longer fail + pcache->block = block; + pcache->off = lfs_aligndown(off, lfs->cfg->prog_size); + pcache->size = 0; + } + + return 0; +} +#endif + +#ifndef LFS_READONLY +static int lfs_bd_erase(lfs_t *lfs, lfs_block_t block) { + LFS_ASSERT(block < lfs->block_count); + int err = lfs->cfg->erase(lfs->cfg, block); + LFS_ASSERT(err <= 0); + return err; +} +#endif + + +/// Small type-level utilities /// + +// some operations on paths +static inline lfs_size_t lfs_path_namelen(const char *path) { + return (lfs_size_t)strcspn(path, "/"); +} + +static inline bool lfs_path_islast(const char *path) { + lfs_size_t namelen = lfs_path_namelen(path); + return path[namelen + strspn(path + namelen, "/")] == '\0'; +} + +static inline bool lfs_path_isdir(const char *path) { + return path[lfs_path_namelen(path)] != '\0'; +} + +// operations on block pairs +static inline void lfs_pair_swap(lfs_block_t pair[2]) { + lfs_block_t t = pair[0]; + pair[0] = pair[1]; + pair[1] = t; +} + +static inline bool lfs_pair_isnull(const lfs_block_t pair[2]) { + return pair[0] == LFS_BLOCK_NULL || pair[1] == LFS_BLOCK_NULL; +} + +static inline int lfs_pair_cmp( + const lfs_block_t paira[2], + const lfs_block_t pairb[2]) { + return !(paira[0] == pairb[0] || paira[1] == pairb[1] || + paira[0] == pairb[1] || paira[1] == pairb[0]); +} + +static inline bool lfs_pair_issync( + const lfs_block_t paira[2], + const lfs_block_t pairb[2]) { + return (paira[0] == pairb[0] && paira[1] == pairb[1]) || + (paira[0] == pairb[1] && paira[1] == pairb[0]); +} + +static inline void lfs_pair_fromle32(lfs_block_t pair[2]) { + pair[0] = lfs_fromle32(pair[0]); + pair[1] = lfs_fromle32(pair[1]); +} + +#ifndef LFS_READONLY +static inline void lfs_pair_tole32(lfs_block_t pair[2]) { + pair[0] = lfs_tole32(pair[0]); + pair[1] = lfs_tole32(pair[1]); +} +#endif + +// operations on 32-bit entry tags +typedef uint32_t lfs_tag_t; +typedef int32_t lfs_stag_t; + +#define LFS_MKTAG(type, id, size) \ + (((lfs_tag_t)(type) << 20) | ((lfs_tag_t)(id) << 10) | (lfs_tag_t)(size)) + +#define LFS_MKTAG_IF(cond, type, id, size) \ + ((cond) ? LFS_MKTAG(type, id, size) : LFS_MKTAG(LFS_FROM_NOOP, 0, 0)) + +#define LFS_MKTAG_IF_ELSE(cond, type1, id1, size1, type2, id2, size2) \ + ((cond) ? LFS_MKTAG(type1, id1, size1) : LFS_MKTAG(type2, id2, size2)) + +static inline bool lfs_tag_isvalid(lfs_tag_t tag) { + return !(tag & 0x80000000); +} + +static inline bool lfs_tag_isdelete(lfs_tag_t tag) { + return ((int32_t)(tag << 22) >> 22) == -1; +} + +static inline uint16_t lfs_tag_type1(lfs_tag_t tag) { + return (tag & 0x70000000) >> 20; +} + +static inline uint16_t lfs_tag_type2(lfs_tag_t tag) { + return (tag & 0x78000000) >> 20; +} + +static inline uint16_t lfs_tag_type3(lfs_tag_t tag) { + return (tag & 0x7ff00000) >> 20; +} + +static inline uint8_t lfs_tag_chunk(lfs_tag_t tag) { + return (tag & 0x0ff00000) >> 20; +} + +static inline int8_t lfs_tag_splice(lfs_tag_t tag) { + return (int8_t)lfs_tag_chunk(tag); +} + +static inline uint16_t lfs_tag_id(lfs_tag_t tag) { + return (tag & 0x000ffc00) >> 10; +} + +static inline lfs_size_t lfs_tag_size(lfs_tag_t tag) { + return tag & 0x000003ff; +} + +static inline lfs_size_t lfs_tag_dsize(lfs_tag_t tag) { + return sizeof(tag) + lfs_tag_size(tag + lfs_tag_isdelete(tag)); +} + +// operations on attributes in attribute lists +struct lfs_mattr { + lfs_tag_t tag; + const void *buffer; +}; + +struct lfs_diskoff { + lfs_block_t block; + lfs_off_t off; +}; + +#define LFS_MKATTRS(...) \ + (struct lfs_mattr[]){__VA_ARGS__}, \ + sizeof((struct lfs_mattr[]){__VA_ARGS__}) / sizeof(struct lfs_mattr) + +// operations on global state +static inline void lfs_gstate_xor(lfs_gstate_t *a, const lfs_gstate_t *b) { + a->tag ^= b->tag; + a->pair[0] ^= b->pair[0]; + a->pair[1] ^= b->pair[1]; +} + +static inline bool lfs_gstate_iszero(const lfs_gstate_t *a) { + return a->tag == 0 + && a->pair[0] == 0 + && a->pair[1] == 0; +} + +#ifndef LFS_READONLY +static inline bool lfs_gstate_hasorphans(const lfs_gstate_t *a) { + return lfs_tag_size(a->tag); +} + +static inline uint8_t lfs_gstate_getorphans(const lfs_gstate_t *a) { + return lfs_tag_size(a->tag) & 0x1ff; +} + +static inline bool lfs_gstate_hasmove(const lfs_gstate_t *a) { + return lfs_tag_type1(a->tag); +} +#endif + +static inline bool lfs_gstate_needssuperblock(const lfs_gstate_t *a) { + return lfs_tag_size(a->tag) >> 9; +} + +static inline bool lfs_gstate_hasmovehere(const lfs_gstate_t *a, + const lfs_block_t *pair) { + return lfs_tag_type1(a->tag) && lfs_pair_cmp(a->pair, pair) == 0; +} + +static inline void lfs_gstate_fromle32(lfs_gstate_t *a) { + a->tag = lfs_fromle32(a->tag); + a->pair[0] = lfs_fromle32(a->pair[0]); + a->pair[1] = lfs_fromle32(a->pair[1]); +} + +#ifndef LFS_READONLY +static inline void lfs_gstate_tole32(lfs_gstate_t *a) { + a->tag = lfs_tole32(a->tag); + a->pair[0] = lfs_tole32(a->pair[0]); + a->pair[1] = lfs_tole32(a->pair[1]); +} +#endif + +// operations on forward-CRCs used to track erased state +struct lfs_fcrc { + lfs_size_t size; + uint32_t crc; +}; + +static void lfs_fcrc_fromle32(struct lfs_fcrc *fcrc) { + fcrc->size = lfs_fromle32(fcrc->size); + fcrc->crc = lfs_fromle32(fcrc->crc); +} + +#ifndef LFS_READONLY +static void lfs_fcrc_tole32(struct lfs_fcrc *fcrc) { + fcrc->size = lfs_tole32(fcrc->size); + fcrc->crc = lfs_tole32(fcrc->crc); +} +#endif + +// other endianness operations +static void lfs_ctz_fromle32(struct lfs_ctz *ctz) { + ctz->head = lfs_fromle32(ctz->head); + ctz->size = lfs_fromle32(ctz->size); +} + +#ifndef LFS_READONLY +static void lfs_ctz_tole32(struct lfs_ctz *ctz) { + ctz->head = lfs_tole32(ctz->head); + ctz->size = lfs_tole32(ctz->size); +} +#endif + +static inline void lfs_superblock_fromle32(lfs_superblock_t *superblock) { + superblock->version = lfs_fromle32(superblock->version); + superblock->block_size = lfs_fromle32(superblock->block_size); + superblock->block_count = lfs_fromle32(superblock->block_count); + superblock->name_max = lfs_fromle32(superblock->name_max); + superblock->file_max = lfs_fromle32(superblock->file_max); + superblock->attr_max = lfs_fromle32(superblock->attr_max); +} + +#ifndef LFS_READONLY +static inline void lfs_superblock_tole32(lfs_superblock_t *superblock) { + superblock->version = lfs_tole32(superblock->version); + superblock->block_size = lfs_tole32(superblock->block_size); + superblock->block_count = lfs_tole32(superblock->block_count); + superblock->name_max = lfs_tole32(superblock->name_max); + superblock->file_max = lfs_tole32(superblock->file_max); + superblock->attr_max = lfs_tole32(superblock->attr_max); +} +#endif + +#ifndef LFS_NO_ASSERT +static bool lfs_mlist_isopen(struct lfs_mlist *head, + struct lfs_mlist *node) { + for (struct lfs_mlist **p = &head; *p; p = &(*p)->next) { + if (*p == (struct lfs_mlist*)node) { + return true; + } + } + + return false; +} +#endif + +static void lfs_mlist_remove(lfs_t *lfs, struct lfs_mlist *mlist) { + for (struct lfs_mlist **p = &lfs->mlist; *p; p = &(*p)->next) { + if (*p == mlist) { + *p = (*p)->next; + break; + } + } +} + +static void lfs_mlist_append(lfs_t *lfs, struct lfs_mlist *mlist) { + mlist->next = lfs->mlist; + lfs->mlist = mlist; +} + +// some other filesystem operations +static uint32_t lfs_fs_disk_version(lfs_t *lfs) { + (void)lfs; +#ifdef LFS_MULTIVERSION + if (lfs->cfg->disk_version) { + return lfs->cfg->disk_version; + } else +#endif + { + return LFS_DISK_VERSION; + } +} + +static uint16_t lfs_fs_disk_version_major(lfs_t *lfs) { + return 0xffff & (lfs_fs_disk_version(lfs) >> 16); + +} + +static uint16_t lfs_fs_disk_version_minor(lfs_t *lfs) { + return 0xffff & (lfs_fs_disk_version(lfs) >> 0); +} + + +/// Internal operations predeclared here /// +#ifndef LFS_READONLY +static int lfs_dir_commit(lfs_t *lfs, lfs_mdir_t *dir, + const struct lfs_mattr *attrs, int attrcount); +static int lfs_dir_compact(lfs_t *lfs, + lfs_mdir_t *dir, const struct lfs_mattr *attrs, int attrcount, + lfs_mdir_t *source, uint16_t begin, uint16_t end); +static lfs_ssize_t lfs_file_flushedwrite(lfs_t *lfs, lfs_file_t *file, + const void *buffer, lfs_size_t size); +static lfs_ssize_t lfs_file_write_(lfs_t *lfs, lfs_file_t *file, + const void *buffer, lfs_size_t size); +static int lfs_file_sync_(lfs_t *lfs, lfs_file_t *file); +static int lfs_file_outline(lfs_t *lfs, lfs_file_t *file); +static int lfs_file_flush(lfs_t *lfs, lfs_file_t *file); + +static int lfs_fs_deorphan(lfs_t *lfs, bool powerloss); +static int lfs_fs_preporphans(lfs_t *lfs, int8_t orphans); +static void lfs_fs_prepmove(lfs_t *lfs, + uint16_t id, const lfs_block_t pair[2]); +static int lfs_fs_pred(lfs_t *lfs, const lfs_block_t dir[2], + lfs_mdir_t *pdir); +static lfs_stag_t lfs_fs_parent(lfs_t *lfs, const lfs_block_t dir[2], + lfs_mdir_t *parent); +static int lfs_fs_forceconsistency(lfs_t *lfs); +#endif + +static void lfs_fs_prepsuperblock(lfs_t *lfs, bool needssuperblock); + +#ifdef LFS_MIGRATE +static int lfs1_traverse(lfs_t *lfs, + int (*cb)(void*, lfs_block_t), void *data); +#endif + +static int lfs_dir_rewind_(lfs_t *lfs, lfs_dir_t *dir); + +static lfs_ssize_t lfs_file_flushedread(lfs_t *lfs, lfs_file_t *file, + void *buffer, lfs_size_t size); +static lfs_ssize_t lfs_file_read_(lfs_t *lfs, lfs_file_t *file, + void *buffer, lfs_size_t size); +static int lfs_file_close_(lfs_t *lfs, lfs_file_t *file); +static lfs_soff_t lfs_file_size_(lfs_t *lfs, lfs_file_t *file); + +static lfs_ssize_t lfs_fs_size_(lfs_t *lfs); +static int lfs_fs_traverse_(lfs_t *lfs, + int (*cb)(void *data, lfs_block_t block), void *data, + bool includeorphans); + +static int lfs_deinit(lfs_t *lfs); +static int lfs_unmount_(lfs_t *lfs); + + +/// Block allocator /// + +// allocations should call this when all allocated blocks are committed to +// the filesystem +// +// after a checkpoint, the block allocator may realloc any untracked blocks +static void lfs_alloc_ckpoint(lfs_t *lfs) { + lfs->lookahead.ckpoint = lfs->block_count; +} + +// drop the lookahead buffer, this is done during mounting and failed +// traversals in order to avoid invalid lookahead state +static void lfs_alloc_drop(lfs_t *lfs) { + lfs->lookahead.size = 0; + lfs->lookahead.next = 0; + lfs_alloc_ckpoint(lfs); +} + +#ifndef LFS_READONLY +static int lfs_alloc_lookahead(void *p, lfs_block_t block) { + lfs_t *lfs = (lfs_t*)p; + lfs_block_t off = ((block - lfs->lookahead.start) + + lfs->block_count) % lfs->block_count; + + if (off < lfs->lookahead.size) { + lfs->lookahead.buffer[off / 8] |= 1U << (off % 8); + } + + return 0; +} +#endif + +#ifndef LFS_READONLY +static int lfs_alloc_scan(lfs_t *lfs) { + // move lookahead buffer to the first unused block + // + // note we limit the lookahead buffer to at most the amount of blocks + // checkpointed, this prevents the math in lfs_alloc from underflowing + lfs->lookahead.start = (lfs->lookahead.start + lfs->lookahead.next) + % lfs->block_count; + lfs->lookahead.next = 0; + lfs->lookahead.size = lfs_min( + 8*lfs->cfg->lookahead_size, + lfs->lookahead.ckpoint); + + // find mask of free blocks from tree + memset(lfs->lookahead.buffer, 0, lfs->cfg->lookahead_size); + int err = lfs_fs_traverse_(lfs, lfs_alloc_lookahead, lfs, true); + if (err) { + lfs_alloc_drop(lfs); + return err; + } + + return 0; +} +#endif + +#ifndef LFS_READONLY +static int lfs_alloc(lfs_t *lfs, lfs_block_t *block) { + while (true) { + // scan our lookahead buffer for free blocks + while (lfs->lookahead.next < lfs->lookahead.size) { + if (!(lfs->lookahead.buffer[lfs->lookahead.next / 8] + & (1U << (lfs->lookahead.next % 8)))) { + // found a free block + *block = (lfs->lookahead.start + lfs->lookahead.next) + % lfs->block_count; + + // eagerly find next free block to maximize how many blocks + // lfs_alloc_ckpoint makes available for scanning + while (true) { + lfs->lookahead.next += 1; + lfs->lookahead.ckpoint -= 1; + + if (lfs->lookahead.next >= lfs->lookahead.size + || !(lfs->lookahead.buffer[lfs->lookahead.next / 8] + & (1U << (lfs->lookahead.next % 8)))) { + return 0; + } + } + } + + lfs->lookahead.next += 1; + lfs->lookahead.ckpoint -= 1; + } + + // In order to keep our block allocator from spinning forever when our + // filesystem is full, we mark points where there are no in-flight + // allocations with a checkpoint before starting a set of allocations. + // + // If we've looked at all blocks since the last checkpoint, we report + // the filesystem as out of storage. + // + if (lfs->lookahead.ckpoint <= 0) { + LFS_ERROR("No more free space 0x%"PRIx32, + (lfs->lookahead.start + lfs->lookahead.next) + % lfs->block_count); + return LFS_ERR_NOSPC; + } + + // No blocks in our lookahead buffer, we need to scan the filesystem for + // unused blocks in the next lookahead window. + int err = lfs_alloc_scan(lfs); + if(err) { + return err; + } + } +} +#endif + +/// Metadata pair and directory operations /// +static lfs_stag_t lfs_dir_getslice(lfs_t *lfs, const lfs_mdir_t *dir, + lfs_tag_t gmask, lfs_tag_t gtag, + lfs_off_t goff, void *gbuffer, lfs_size_t gsize) { + lfs_off_t off = dir->off; + lfs_tag_t ntag = dir->etag; + lfs_stag_t gdiff = 0; + + // synthetic moves + if (lfs_gstate_hasmovehere(&lfs->gdisk, dir->pair) && + lfs_tag_id(gmask) != 0) { + if (lfs_tag_id(lfs->gdisk.tag) == lfs_tag_id(gtag)) { + return LFS_ERR_NOENT; + } else if (lfs_tag_id(lfs->gdisk.tag) < lfs_tag_id(gtag)) { + gdiff -= LFS_MKTAG(0, 1, 0); + } + } + + // iterate over dir block backwards (for faster lookups) + while (off >= sizeof(lfs_tag_t) + lfs_tag_dsize(ntag)) { + off -= lfs_tag_dsize(ntag); + lfs_tag_t tag = ntag; + int err = lfs_bd_read(lfs, + NULL, &lfs->rcache, sizeof(ntag), + dir->pair[0], off, &ntag, sizeof(ntag)); + LFS_ASSERT(err <= 0); + if (err) { + return err; + } + + ntag = (lfs_frombe32(ntag) ^ tag) & 0x7fffffff; + + if (lfs_tag_id(gmask) != 0 && + lfs_tag_type1(tag) == LFS_TYPE_SPLICE && + lfs_tag_id(tag) <= lfs_tag_id(gtag - gdiff)) { + if (tag == (LFS_MKTAG(LFS_TYPE_CREATE, 0, 0) | + (LFS_MKTAG(0, 0x3ff, 0) & (gtag - gdiff)))) { + // found where we were created + return LFS_ERR_NOENT; + } + + // move around splices + gdiff += LFS_MKTAG(0, lfs_tag_splice(tag), 0); + } + + if ((gmask & tag) == (gmask & (gtag - gdiff))) { + if (lfs_tag_isdelete(tag)) { + return LFS_ERR_NOENT; + } + + lfs_size_t diff = lfs_min(lfs_tag_size(tag), gsize); + err = lfs_bd_read(lfs, + NULL, &lfs->rcache, diff, + dir->pair[0], off+sizeof(tag)+goff, gbuffer, diff); + LFS_ASSERT(err <= 0); + if (err) { + return err; + } + + memset((uint8_t*)gbuffer + diff, 0, gsize - diff); + + return tag + gdiff; + } + } + + return LFS_ERR_NOENT; +} + +static lfs_stag_t lfs_dir_get(lfs_t *lfs, const lfs_mdir_t *dir, + lfs_tag_t gmask, lfs_tag_t gtag, void *buffer) { + return lfs_dir_getslice(lfs, dir, + gmask, gtag, + 0, buffer, lfs_tag_size(gtag)); +} + +static int lfs_dir_getread(lfs_t *lfs, const lfs_mdir_t *dir, + const lfs_cache_t *pcache, lfs_cache_t *rcache, lfs_size_t hint, + lfs_tag_t gmask, lfs_tag_t gtag, + lfs_off_t off, void *buffer, lfs_size_t size) { + uint8_t *data = buffer; + if (off+size > lfs->cfg->block_size) { + return LFS_ERR_CORRUPT; + } + + while (size > 0) { + lfs_size_t diff = size; + + if (pcache && pcache->block == LFS_BLOCK_INLINE && + off < pcache->off + pcache->size) { + if (off >= pcache->off) { + // is already in pcache? + diff = lfs_min(diff, pcache->size - (off-pcache->off)); + memcpy(data, &pcache->buffer[off-pcache->off], diff); + + data += diff; + off += diff; + size -= diff; + continue; + } + + // pcache takes priority + diff = lfs_min(diff, pcache->off-off); + } + + if (rcache->block == LFS_BLOCK_INLINE && + off < rcache->off + rcache->size) { + if (off >= rcache->off) { + // is already in rcache? + diff = lfs_min(diff, rcache->size - (off-rcache->off)); + memcpy(data, &rcache->buffer[off-rcache->off], diff); + + data += diff; + off += diff; + size -= diff; + continue; + } + } + + // load to cache, first condition can no longer fail + rcache->block = LFS_BLOCK_INLINE; + rcache->off = lfs_aligndown(off, lfs->cfg->read_size); + rcache->size = lfs_min(lfs_alignup(off+hint, lfs->cfg->read_size), + lfs->cfg->cache_size); + int err = lfs_dir_getslice(lfs, dir, gmask, gtag, + rcache->off, rcache->buffer, rcache->size); + if (err < 0) { + return err; + } + } + + return 0; +} + +#ifndef LFS_READONLY +static int lfs_dir_traverse_filter(void *p, + lfs_tag_t tag, const void *buffer) { + lfs_tag_t *filtertag = p; + (void)buffer; + + // which mask depends on unique bit in tag structure + uint32_t mask = (tag & LFS_MKTAG(0x100, 0, 0)) + ? LFS_MKTAG(0x7ff, 0x3ff, 0) + : LFS_MKTAG(0x700, 0x3ff, 0); + + // check for redundancy + if ((mask & tag) == (mask & *filtertag) || + lfs_tag_isdelete(*filtertag) || + (LFS_MKTAG(0x7ff, 0x3ff, 0) & tag) == ( + LFS_MKTAG(LFS_TYPE_DELETE, 0, 0) | + (LFS_MKTAG(0, 0x3ff, 0) & *filtertag))) { + *filtertag = LFS_MKTAG(LFS_FROM_NOOP, 0, 0); + return true; + } + + // check if we need to adjust for created/deleted tags + if (lfs_tag_type1(tag) == LFS_TYPE_SPLICE && + lfs_tag_id(tag) <= lfs_tag_id(*filtertag)) { + *filtertag += LFS_MKTAG(0, lfs_tag_splice(tag), 0); + } + + return false; +} +#endif + +#ifndef LFS_READONLY +// maximum recursive depth of lfs_dir_traverse, the deepest call: +// +// traverse with commit +// '-> traverse with move +// '-> traverse with filter +// +#define LFS_DIR_TRAVERSE_DEPTH 3 + +struct lfs_dir_traverse { + const lfs_mdir_t *dir; + lfs_off_t off; + lfs_tag_t ptag; + const struct lfs_mattr *attrs; + int attrcount; + + lfs_tag_t tmask; + lfs_tag_t ttag; + uint16_t begin; + uint16_t end; + int16_t diff; + + int (*cb)(void *data, lfs_tag_t tag, const void *buffer); + void *data; + + lfs_tag_t tag; + const void *buffer; + struct lfs_diskoff disk; +}; + +static int lfs_dir_traverse(lfs_t *lfs, + const lfs_mdir_t *dir, lfs_off_t off, lfs_tag_t ptag, + const struct lfs_mattr *attrs, int attrcount, + lfs_tag_t tmask, lfs_tag_t ttag, + uint16_t begin, uint16_t end, int16_t diff, + int (*cb)(void *data, lfs_tag_t tag, const void *buffer), void *data) { + // This function in inherently recursive, but bounded. To allow tool-based + // analysis without unnecessary code-cost we use an explicit stack + struct lfs_dir_traverse stack[LFS_DIR_TRAVERSE_DEPTH-1]; + unsigned sp = 0; + int res; + + // iterate over directory and attrs + lfs_tag_t tag; + const void *buffer; + struct lfs_diskoff disk = {0}; + while (true) { + { + if (off+lfs_tag_dsize(ptag) < dir->off) { + off += lfs_tag_dsize(ptag); + int err = lfs_bd_read(lfs, + NULL, &lfs->rcache, sizeof(tag), + dir->pair[0], off, &tag, sizeof(tag)); + if (err) { + return err; + } + + tag = (lfs_frombe32(tag) ^ ptag) | 0x80000000; + disk.block = dir->pair[0]; + disk.off = off+sizeof(lfs_tag_t); + buffer = &disk; + ptag = tag; + } else if (attrcount > 0) { + tag = attrs[0].tag; + buffer = attrs[0].buffer; + attrs += 1; + attrcount -= 1; + } else { + // finished traversal, pop from stack? + res = 0; + break; + } + + // do we need to filter? + lfs_tag_t mask = LFS_MKTAG(0x7ff, 0, 0); + if ((mask & tmask & tag) != (mask & tmask & ttag)) { + continue; + } + + if (lfs_tag_id(tmask) != 0) { + LFS_ASSERT(sp < LFS_DIR_TRAVERSE_DEPTH); + // recurse, scan for duplicates, and update tag based on + // creates/deletes + stack[sp] = (struct lfs_dir_traverse){ + .dir = dir, + .off = off, + .ptag = ptag, + .attrs = attrs, + .attrcount = attrcount, + .tmask = tmask, + .ttag = ttag, + .begin = begin, + .end = end, + .diff = diff, + .cb = cb, + .data = data, + .tag = tag, + .buffer = buffer, + .disk = disk, + }; + sp += 1; + + tmask = 0; + ttag = 0; + begin = 0; + end = 0; + diff = 0; + cb = lfs_dir_traverse_filter; + data = &stack[sp-1].tag; + continue; + } + } + +popped: + // in filter range? + if (lfs_tag_id(tmask) != 0 && + !(lfs_tag_id(tag) >= begin && lfs_tag_id(tag) < end)) { + continue; + } + + // handle special cases for mcu-side operations + if (lfs_tag_type3(tag) == LFS_FROM_NOOP) { + // do nothing + } else if (lfs_tag_type3(tag) == LFS_FROM_MOVE) { + // Without this condition, lfs_dir_traverse can exhibit an + // extremely expensive O(n^3) of nested loops when renaming. + // This happens because lfs_dir_traverse tries to filter tags by + // the tags in the source directory, triggering a second + // lfs_dir_traverse with its own filter operation. + // + // traverse with commit + // '-> traverse with filter + // '-> traverse with move + // '-> traverse with filter + // + // However we don't actually care about filtering the second set of + // tags, since duplicate tags have no effect when filtering. + // + // This check skips this unnecessary recursive filtering explicitly, + // reducing this runtime from O(n^3) to O(n^2). + if (cb == lfs_dir_traverse_filter) { + continue; + } + + // recurse into move + stack[sp] = (struct lfs_dir_traverse){ + .dir = dir, + .off = off, + .ptag = ptag, + .attrs = attrs, + .attrcount = attrcount, + .tmask = tmask, + .ttag = ttag, + .begin = begin, + .end = end, + .diff = diff, + .cb = cb, + .data = data, + .tag = LFS_MKTAG(LFS_FROM_NOOP, 0, 0), + }; + sp += 1; + + uint16_t fromid = lfs_tag_size(tag); + uint16_t toid = lfs_tag_id(tag); + dir = buffer; + off = 0; + ptag = 0xffffffff; + attrs = NULL; + attrcount = 0; + tmask = LFS_MKTAG(0x600, 0x3ff, 0); + ttag = LFS_MKTAG(LFS_TYPE_STRUCT, 0, 0); + begin = fromid; + end = fromid+1; + diff = toid-fromid+diff; + } else if (lfs_tag_type3(tag) == LFS_FROM_USERATTRS) { + for (unsigned i = 0; i < lfs_tag_size(tag); i++) { + const struct lfs_attr *a = buffer; + res = cb(data, LFS_MKTAG(LFS_TYPE_USERATTR + a[i].type, + lfs_tag_id(tag) + diff, a[i].size), a[i].buffer); + if (res < 0) { + return res; + } + + if (res) { + break; + } + } + } else { + res = cb(data, tag + LFS_MKTAG(0, diff, 0), buffer); + if (res < 0) { + return res; + } + + if (res) { + break; + } + } + } + + if (sp > 0) { + // pop from the stack and return, fortunately all pops share + // a destination + dir = stack[sp-1].dir; + off = stack[sp-1].off; + ptag = stack[sp-1].ptag; + attrs = stack[sp-1].attrs; + attrcount = stack[sp-1].attrcount; + tmask = stack[sp-1].tmask; + ttag = stack[sp-1].ttag; + begin = stack[sp-1].begin; + end = stack[sp-1].end; + diff = stack[sp-1].diff; + cb = stack[sp-1].cb; + data = stack[sp-1].data; + tag = stack[sp-1].tag; + buffer = stack[sp-1].buffer; + disk = stack[sp-1].disk; + sp -= 1; + goto popped; + } else { + return res; + } +} +#endif + +static lfs_stag_t lfs_dir_fetchmatch(lfs_t *lfs, + lfs_mdir_t *dir, const lfs_block_t pair[2], + lfs_tag_t fmask, lfs_tag_t ftag, uint16_t *id, + int (*cb)(void *data, lfs_tag_t tag, const void *buffer), void *data) { + // we can find tag very efficiently during a fetch, since we're already + // scanning the entire directory + lfs_stag_t besttag = -1; + + // if either block address is invalid we return LFS_ERR_CORRUPT here, + // otherwise later writes to the pair could fail + if (lfs->block_count + && (pair[0] >= lfs->block_count || pair[1] >= lfs->block_count)) { + return LFS_ERR_CORRUPT; + } + + // find the block with the most recent revision + uint32_t revs[2] = {0, 0}; + int r = 0; + for (int i = 0; i < 2; i++) { + int err = lfs_bd_read(lfs, + NULL, &lfs->rcache, sizeof(revs[i]), + pair[i], 0, &revs[i], sizeof(revs[i])); + revs[i] = lfs_fromle32(revs[i]); + if (err && err != LFS_ERR_CORRUPT) { + return err; + } + + if (err != LFS_ERR_CORRUPT && + lfs_scmp(revs[i], revs[(i+1)%2]) > 0) { + r = i; + } + } + + dir->pair[0] = pair[(r+0)%2]; + dir->pair[1] = pair[(r+1)%2]; + dir->rev = revs[(r+0)%2]; + dir->off = 0; // nonzero = found some commits + + // now scan tags to fetch the actual dir and find possible match + for (int i = 0; i < 2; i++) { + lfs_off_t off = 0; + lfs_tag_t ptag = 0xffffffff; + + uint16_t tempcount = 0; + lfs_block_t temptail[2] = {LFS_BLOCK_NULL, LFS_BLOCK_NULL}; + bool tempsplit = false; + lfs_stag_t tempbesttag = besttag; + + // assume not erased until proven otherwise + bool maybeerased = false; + bool hasfcrc = false; + struct lfs_fcrc fcrc; + + dir->rev = lfs_tole32(dir->rev); + uint32_t crc = lfs_crc(0xffffffff, &dir->rev, sizeof(dir->rev)); + dir->rev = lfs_fromle32(dir->rev); + + while (true) { + // extract next tag + lfs_tag_t tag; + off += lfs_tag_dsize(ptag); + int err = lfs_bd_read(lfs, + NULL, &lfs->rcache, lfs->cfg->block_size, + dir->pair[0], off, &tag, sizeof(tag)); + if (err) { + if (err == LFS_ERR_CORRUPT) { + // can't continue? + break; + } + return err; + } + + crc = lfs_crc(crc, &tag, sizeof(tag)); + tag = lfs_frombe32(tag) ^ ptag; + + // next commit not yet programmed? + if (!lfs_tag_isvalid(tag)) { + // we only might be erased if the last tag was a crc + maybeerased = (lfs_tag_type2(ptag) == LFS_TYPE_CCRC); + break; + // out of range? + } else if (off + lfs_tag_dsize(tag) > lfs->cfg->block_size) { + break; + } + + ptag = tag; + + if (lfs_tag_type2(tag) == LFS_TYPE_CCRC) { + // check the crc attr + uint32_t dcrc; + err = lfs_bd_read(lfs, + NULL, &lfs->rcache, lfs->cfg->block_size, + dir->pair[0], off+sizeof(tag), &dcrc, sizeof(dcrc)); + if (err) { + if (err == LFS_ERR_CORRUPT) { + break; + } + return err; + } + dcrc = lfs_fromle32(dcrc); + + if (crc != dcrc) { + break; + } + + // reset the next bit if we need to + ptag ^= (lfs_tag_t)(lfs_tag_chunk(tag) & 1U) << 31; + + // toss our crc into the filesystem seed for + // pseudorandom numbers, note we use another crc here + // as a collection function because it is sufficiently + // random and convenient + lfs->seed = lfs_crc(lfs->seed, &crc, sizeof(crc)); + + // update with what's found so far + besttag = tempbesttag; + dir->off = off + lfs_tag_dsize(tag); + dir->etag = ptag; + dir->count = tempcount; + dir->tail[0] = temptail[0]; + dir->tail[1] = temptail[1]; + dir->split = tempsplit; + + // reset crc, hasfcrc + crc = 0xffffffff; + continue; + } + + // crc the entry first, hopefully leaving it in the cache + err = lfs_bd_crc(lfs, + NULL, &lfs->rcache, lfs->cfg->block_size, + dir->pair[0], off+sizeof(tag), + lfs_tag_dsize(tag)-sizeof(tag), &crc); + if (err) { + if (err == LFS_ERR_CORRUPT) { + break; + } + return err; + } + + // directory modification tags? + if (lfs_tag_type1(tag) == LFS_TYPE_NAME) { + // increase count of files if necessary + if (lfs_tag_id(tag) >= tempcount) { + tempcount = lfs_tag_id(tag) + 1; + } + } else if (lfs_tag_type1(tag) == LFS_TYPE_SPLICE) { + tempcount += lfs_tag_splice(tag); + + if (tag == (LFS_MKTAG(LFS_TYPE_DELETE, 0, 0) | + (LFS_MKTAG(0, 0x3ff, 0) & tempbesttag))) { + tempbesttag |= 0x80000000; + } else if (tempbesttag != -1 && + lfs_tag_id(tag) <= lfs_tag_id(tempbesttag)) { + tempbesttag += LFS_MKTAG(0, lfs_tag_splice(tag), 0); + } + } else if (lfs_tag_type1(tag) == LFS_TYPE_TAIL) { + tempsplit = (lfs_tag_chunk(tag) & 1); + + err = lfs_bd_read(lfs, + NULL, &lfs->rcache, lfs->cfg->block_size, + dir->pair[0], off+sizeof(tag), &temptail, 8); + if (err) { + if (err == LFS_ERR_CORRUPT) { + break; + } + return err; + } + lfs_pair_fromle32(temptail); + } else if (lfs_tag_type3(tag) == LFS_TYPE_FCRC) { + err = lfs_bd_read(lfs, + NULL, &lfs->rcache, lfs->cfg->block_size, + dir->pair[0], off+sizeof(tag), + &fcrc, sizeof(fcrc)); + if (err) { + if (err == LFS_ERR_CORRUPT) { + break; + } + return err; + } + + lfs_fcrc_fromle32(&fcrc); + hasfcrc = true; + } + + // found a match for our fetcher? + if ((fmask & tag) == (fmask & ftag)) { + LFS_ASSERT(cb != NULL); + int res = cb(data, tag, &(struct lfs_diskoff){ + dir->pair[0], off+sizeof(tag)}); + if (res < 0) { + if (res == LFS_ERR_CORRUPT) { + break; + } + return res; + } + + if (res == LFS_CMP_EQ) { + // found a match + tempbesttag = tag; + } else if ((LFS_MKTAG(0x7ff, 0x3ff, 0) & tag) == + (LFS_MKTAG(0x7ff, 0x3ff, 0) & tempbesttag)) { + // found an identical tag, but contents didn't match + // this must mean that our besttag has been overwritten + tempbesttag = -1; + } else if (res == LFS_CMP_GT && + lfs_tag_id(tag) <= lfs_tag_id(tempbesttag)) { + // found a greater match, keep track to keep things sorted + tempbesttag = tag | 0x80000000; + } + } + } + + // found no valid commits? + if (dir->off == 0) { + // try the other block? + lfs_pair_swap(dir->pair); + dir->rev = revs[(r+1)%2]; + continue; + } + + // did we end on a valid commit? we may have an erased block + dir->erased = false; + if (maybeerased && dir->off % lfs->cfg->prog_size == 0) { + #ifdef LFS_MULTIVERSION + // note versions < lfs2.1 did not have fcrc tags, if + // we're < lfs2.1 treat missing fcrc as erased data + // + // we don't strictly need to do this, but otherwise writing + // to lfs2.0 disks becomes very inefficient + if (lfs_fs_disk_version(lfs) < 0x00020001) { + dir->erased = true; + + } else + #endif + if (hasfcrc) { + // check for an fcrc matching the next prog's erased state, if + // this failed most likely a previous prog was interrupted, we + // need a new erase + uint32_t fcrc_ = 0xffffffff; + int err = lfs_bd_crc(lfs, + NULL, &lfs->rcache, lfs->cfg->block_size, + dir->pair[0], dir->off, fcrc.size, &fcrc_); + if (err && err != LFS_ERR_CORRUPT) { + return err; + } + + // found beginning of erased part? + dir->erased = (fcrc_ == fcrc.crc); + } + } + + // synthetic move + if (lfs_gstate_hasmovehere(&lfs->gdisk, dir->pair)) { + if (lfs_tag_id(lfs->gdisk.tag) == lfs_tag_id(besttag)) { + besttag |= 0x80000000; + } else if (besttag != -1 && + lfs_tag_id(lfs->gdisk.tag) < lfs_tag_id(besttag)) { + besttag -= LFS_MKTAG(0, 1, 0); + } + } + + // found tag? or found best id? + if (id) { + *id = lfs_min(lfs_tag_id(besttag), dir->count); + } + + if (lfs_tag_isvalid(besttag)) { + return besttag; + } else if (lfs_tag_id(besttag) < dir->count) { + return LFS_ERR_NOENT; + } else { + return 0; + } + } + + LFS_ERROR("Corrupted dir pair at {0x%"PRIx32", 0x%"PRIx32"}", + dir->pair[0], dir->pair[1]); + return LFS_ERR_CORRUPT; +} + +static int lfs_dir_fetch(lfs_t *lfs, + lfs_mdir_t *dir, const lfs_block_t pair[2]) { + // note, mask=-1, tag=-1 can never match a tag since this + // pattern has the invalid bit set + return (int)lfs_dir_fetchmatch(lfs, dir, pair, + (lfs_tag_t)-1, (lfs_tag_t)-1, NULL, NULL, NULL); +} + +static int lfs_dir_getgstate(lfs_t *lfs, const lfs_mdir_t *dir, + lfs_gstate_t *gstate) { + lfs_gstate_t temp; + lfs_stag_t res = lfs_dir_get(lfs, dir, LFS_MKTAG(0x7ff, 0, 0), + LFS_MKTAG(LFS_TYPE_MOVESTATE, 0, sizeof(temp)), &temp); + if (res < 0 && res != LFS_ERR_NOENT) { + return res; + } + + if (res != LFS_ERR_NOENT) { + // xor together to find resulting gstate + lfs_gstate_fromle32(&temp); + lfs_gstate_xor(gstate, &temp); + } + + return 0; +} + +static int lfs_dir_getinfo(lfs_t *lfs, lfs_mdir_t *dir, + uint16_t id, struct lfs_info *info) { + if (id == 0x3ff) { + // special case for root + strcpy(info->name, "/"); + info->type = LFS_TYPE_DIR; + return 0; + } + + lfs_stag_t tag = lfs_dir_get(lfs, dir, LFS_MKTAG(0x780, 0x3ff, 0), + LFS_MKTAG(LFS_TYPE_NAME, id, lfs->name_max+1), info->name); + if (tag < 0) { + return (int)tag; + } + + info->type = lfs_tag_type3(tag); + + struct lfs_ctz ctz; + tag = lfs_dir_get(lfs, dir, LFS_MKTAG(0x700, 0x3ff, 0), + LFS_MKTAG(LFS_TYPE_STRUCT, id, sizeof(ctz)), &ctz); + if (tag < 0) { + return (int)tag; + } + lfs_ctz_fromle32(&ctz); + + if (lfs_tag_type3(tag) == LFS_TYPE_CTZSTRUCT) { + info->size = ctz.size; + } else if (lfs_tag_type3(tag) == LFS_TYPE_INLINESTRUCT) { + info->size = lfs_tag_size(tag); + } + + return 0; +} + +struct lfs_dir_find_match { + lfs_t *lfs; + const void *name; + lfs_size_t size; +}; + +static int lfs_dir_find_match(void *data, + lfs_tag_t tag, const void *buffer) { + struct lfs_dir_find_match *name = data; + lfs_t *lfs = name->lfs; + const struct lfs_diskoff *disk = buffer; + + // compare with disk + lfs_size_t diff = lfs_min(name->size, lfs_tag_size(tag)); + int res = lfs_bd_cmp(lfs, + NULL, &lfs->rcache, diff, + disk->block, disk->off, name->name, diff); + if (res != LFS_CMP_EQ) { + return res; + } + + // only equal if our size is still the same + if (name->size != lfs_tag_size(tag)) { + return (name->size < lfs_tag_size(tag)) ? LFS_CMP_LT : LFS_CMP_GT; + } + + // found a match! + return LFS_CMP_EQ; +} + +// lfs_dir_find tries to set path and id even if file is not found +// +// returns: +// - 0 if file is found +// - LFS_ERR_NOENT if file or parent is not found +// - LFS_ERR_NOTDIR if parent is not a dir +static lfs_stag_t lfs_dir_find(lfs_t *lfs, lfs_mdir_t *dir, + const char **path, uint16_t *id) { + // we reduce path to a single name if we can find it + const char *name = *path; + + // default to root dir + lfs_stag_t tag = LFS_MKTAG(LFS_TYPE_DIR, 0x3ff, 0); + dir->tail[0] = lfs->root[0]; + dir->tail[1] = lfs->root[1]; + + // empty paths are not allowed + if (*name == '\0') { + return LFS_ERR_INVAL; + } + + while (true) { +nextname: + // skip slashes if we're a directory + if (lfs_tag_type3(tag) == LFS_TYPE_DIR) { + name += strspn(name, "/"); + } + lfs_size_t namelen = (lfs_size_t)strcspn(name, "/"); + + // skip '.' + if (namelen == 1 && memcmp(name, ".", 1) == 0) { + name += namelen; + goto nextname; + } + + // error on unmatched '..', trying to go above root? + if (namelen == 2 && memcmp(name, "..", 2) == 0) { + return LFS_ERR_INVAL; + } + + // skip if matched by '..' in name + const char *suffix = name + namelen; + lfs_size_t sufflen; + int depth = 1; + while (true) { + suffix += strspn(suffix, "/"); + sufflen = (lfs_size_t)strcspn(suffix, "/"); + if (sufflen == 0) { + break; + } + + if (sufflen == 1 && memcmp(suffix, ".", 1) == 0) { + // noop + } else if (sufflen == 2 && memcmp(suffix, "..", 2) == 0) { + depth -= 1; + if (depth == 0) { + name = suffix + sufflen; + goto nextname; + } + } else { + depth += 1; + } + + suffix += sufflen; + } + + // found path + if (*name == '\0') { + return tag; + } + + // update what we've found so far + *path = name; + + // only continue if we're a directory + if (lfs_tag_type3(tag) != LFS_TYPE_DIR) { + return LFS_ERR_NOTDIR; + } + + // grab the entry data + if (lfs_tag_id(tag) != 0x3ff) { + lfs_stag_t res = lfs_dir_get(lfs, dir, LFS_MKTAG(0x700, 0x3ff, 0), + LFS_MKTAG(LFS_TYPE_STRUCT, lfs_tag_id(tag), 8), dir->tail); + if (res < 0) { + return res; + } + lfs_pair_fromle32(dir->tail); + } + + // find entry matching name + while (true) { + tag = lfs_dir_fetchmatch(lfs, dir, dir->tail, + LFS_MKTAG(0x780, 0, 0), + LFS_MKTAG(LFS_TYPE_NAME, 0, namelen), + id, + lfs_dir_find_match, &(struct lfs_dir_find_match){ + lfs, name, namelen}); + if (tag < 0) { + return tag; + } + + if (tag) { + break; + } + + if (!dir->split) { + return LFS_ERR_NOENT; + } + } + + // to next name + name += namelen; + } +} + +// commit logic +struct lfs_commit { + lfs_block_t block; + lfs_off_t off; + lfs_tag_t ptag; + uint32_t crc; + + lfs_off_t begin; + lfs_off_t end; +}; + +#ifndef LFS_READONLY +static int lfs_dir_commitprog(lfs_t *lfs, struct lfs_commit *commit, + const void *buffer, lfs_size_t size) { + int err = lfs_bd_prog(lfs, + &lfs->pcache, &lfs->rcache, false, + commit->block, commit->off , + (const uint8_t*)buffer, size); + if (err) { + return err; + } + + commit->crc = lfs_crc(commit->crc, buffer, size); + commit->off += size; + return 0; +} +#endif + +#ifndef LFS_READONLY +static int lfs_dir_commitattr(lfs_t *lfs, struct lfs_commit *commit, + lfs_tag_t tag, const void *buffer) { + // check if we fit + lfs_size_t dsize = lfs_tag_dsize(tag); + if (commit->off + dsize > commit->end) { + return LFS_ERR_NOSPC; + } + + // write out tag + lfs_tag_t ntag = lfs_tobe32((tag & 0x7fffffff) ^ commit->ptag); + int err = lfs_dir_commitprog(lfs, commit, &ntag, sizeof(ntag)); + if (err) { + return err; + } + + if (!(tag & 0x80000000)) { + // from memory + err = lfs_dir_commitprog(lfs, commit, buffer, dsize-sizeof(tag)); + if (err) { + return err; + } + } else { + // from disk + const struct lfs_diskoff *disk = buffer; + for (lfs_off_t i = 0; i < dsize-sizeof(tag); i++) { + // rely on caching to make this efficient + uint8_t dat; + err = lfs_bd_read(lfs, + NULL, &lfs->rcache, dsize-sizeof(tag)-i, + disk->block, disk->off+i, &dat, 1); + if (err) { + return err; + } + + err = lfs_dir_commitprog(lfs, commit, &dat, 1); + if (err) { + return err; + } + } + } + + commit->ptag = tag & 0x7fffffff; + return 0; +} +#endif + +#ifndef LFS_READONLY + +static int lfs_dir_commitcrc(lfs_t *lfs, struct lfs_commit *commit) { + // align to program units + // + // this gets a bit complex as we have two types of crcs: + // - 5-word crc with fcrc to check following prog (middle of block) + // - 2-word crc with no following prog (end of block) + const lfs_off_t end = lfs_alignup( + lfs_min(commit->off + 5*sizeof(uint32_t), lfs->cfg->block_size), + lfs->cfg->prog_size); + + lfs_off_t off1 = 0; + uint32_t crc1 = 0; + + // create crc tags to fill up remainder of commit, note that + // padding is not crced, which lets fetches skip padding but + // makes committing a bit more complicated + while (commit->off < end) { + lfs_off_t noff = ( + lfs_min(end - (commit->off+sizeof(lfs_tag_t)), 0x3fe) + + (commit->off+sizeof(lfs_tag_t))); + // too large for crc tag? need padding commits + if (noff < end) { + noff = lfs_min(noff, end - 5*sizeof(uint32_t)); + } + + // space for fcrc? + uint8_t eperturb = (uint8_t)-1; + if (noff >= end && noff <= lfs->cfg->block_size - lfs->cfg->prog_size) { + // first read the leading byte, this always contains a bit + // we can perturb to avoid writes that don't change the fcrc + int err = lfs_bd_read(lfs, + NULL, &lfs->rcache, lfs->cfg->prog_size, + commit->block, noff, &eperturb, 1); + if (err && err != LFS_ERR_CORRUPT) { + return err; + } + + #ifdef LFS_MULTIVERSION + // unfortunately fcrcs break mdir fetching < lfs2.1, so only write + // these if we're a >= lfs2.1 filesystem + if (lfs_fs_disk_version(lfs) <= 0x00020000) { + // don't write fcrc + } else + #endif + { + // find the expected fcrc, don't bother avoiding a reread + // of the eperturb, it should still be in our cache + struct lfs_fcrc fcrc = { + .size = lfs->cfg->prog_size, + .crc = 0xffffffff + }; + err = lfs_bd_crc(lfs, + NULL, &lfs->rcache, lfs->cfg->prog_size, + commit->block, noff, fcrc.size, &fcrc.crc); + if (err && err != LFS_ERR_CORRUPT) { + return err; + } + + lfs_fcrc_tole32(&fcrc); + err = lfs_dir_commitattr(lfs, commit, + LFS_MKTAG(LFS_TYPE_FCRC, 0x3ff, sizeof(struct lfs_fcrc)), + &fcrc); + if (err) { + return err; + } + } + } + + // build commit crc + struct { + lfs_tag_t tag; + uint32_t crc; + } ccrc; + lfs_tag_t ntag = LFS_MKTAG( + LFS_TYPE_CCRC + (((uint8_t)~eperturb) >> 7), 0x3ff, + noff - (commit->off+sizeof(lfs_tag_t))); + ccrc.tag = lfs_tobe32(ntag ^ commit->ptag); + commit->crc = lfs_crc(commit->crc, &ccrc.tag, sizeof(lfs_tag_t)); + ccrc.crc = lfs_tole32(commit->crc); + + int err = lfs_bd_prog(lfs, + &lfs->pcache, &lfs->rcache, false, + commit->block, commit->off, &ccrc, sizeof(ccrc)); + if (err) { + return err; + } + + // keep track of non-padding checksum to verify + if (off1 == 0) { + off1 = commit->off + sizeof(lfs_tag_t); + crc1 = commit->crc; + } + + commit->off = noff; + // perturb valid bit? + commit->ptag = ntag ^ ((lfs_tag_t)(0x80 & ~eperturb) << 24); + // reset crc for next commit + commit->crc = 0xffffffff; + + // manually flush here since we don't prog the padding, this confuses + // the caching layer + if (noff >= end || noff >= lfs->pcache.off + lfs->cfg->cache_size) { + // flush buffers + int err = lfs_bd_sync(lfs, &lfs->pcache, &lfs->rcache, false); + if (err) { + return err; + } + } + } + + // successful commit, check checksums to make sure + // + // note that we don't need to check padding commits, worst + // case if they are corrupted we would have had to compact anyways + lfs_off_t off = commit->begin; + uint32_t crc = 0xffffffff; + int err = lfs_bd_crc(lfs, + NULL, &lfs->rcache, off1+sizeof(uint32_t), + commit->block, off, off1-off, &crc); + if (err) { + return err; + } + + // check non-padding commits against known crc + if (crc != crc1) { + return LFS_ERR_CORRUPT; + } + + // make sure to check crc in case we happen to pick + // up an unrelated crc (frozen block?) + err = lfs_bd_crc(lfs, + NULL, &lfs->rcache, sizeof(uint32_t), + commit->block, off1, sizeof(uint32_t), &crc); + if (err) { + return err; + } + + if (crc != 0) { + return LFS_ERR_CORRUPT; + } + + return 0; +} +#endif + +#ifndef LFS_READONLY +static int lfs_dir_alloc(lfs_t *lfs, lfs_mdir_t *dir) { + // allocate pair of dir blocks (backwards, so we write block 1 first) + for (int i = 0; i < 2; i++) { + int err = lfs_alloc(lfs, &dir->pair[(i+1)%2]); + if (err) { + return err; + } + } + + // zero for reproducibility in case initial block is unreadable + dir->rev = 0; + + // rather than clobbering one of the blocks we just pretend + // the revision may be valid + int err = lfs_bd_read(lfs, + NULL, &lfs->rcache, sizeof(dir->rev), + dir->pair[0], 0, &dir->rev, sizeof(dir->rev)); + dir->rev = lfs_fromle32(dir->rev); + if (err && err != LFS_ERR_CORRUPT) { + return err; + } + + // to make sure we don't immediately evict, align the new revision count + // to our block_cycles modulus, see lfs_dir_compact for why our modulus + // is tweaked this way + if (lfs->cfg->block_cycles > 0) { + dir->rev = lfs_alignup(dir->rev, ((lfs->cfg->block_cycles+1)|1)); + } + + // set defaults + dir->off = sizeof(dir->rev); + dir->etag = 0xffffffff; + dir->count = 0; + dir->tail[0] = LFS_BLOCK_NULL; + dir->tail[1] = LFS_BLOCK_NULL; + dir->erased = false; + dir->split = false; + + // don't write out yet, let caller take care of that + return 0; +} +#endif + +#ifndef LFS_READONLY +static int lfs_dir_drop(lfs_t *lfs, lfs_mdir_t *dir, lfs_mdir_t *tail) { + // steal state + int err = lfs_dir_getgstate(lfs, tail, &lfs->gdelta); + if (err) { + return err; + } + + // steal tail + lfs_pair_tole32(tail->tail); + err = lfs_dir_commit(lfs, dir, LFS_MKATTRS( + {LFS_MKTAG(LFS_TYPE_TAIL + tail->split, 0x3ff, 8), tail->tail})); + lfs_pair_fromle32(tail->tail); + if (err) { + return err; + } + + return 0; +} +#endif + +#ifndef LFS_READONLY +static int lfs_dir_split(lfs_t *lfs, + lfs_mdir_t *dir, const struct lfs_mattr *attrs, int attrcount, + lfs_mdir_t *source, uint16_t split, uint16_t end) { + // create tail metadata pair + lfs_mdir_t tail; + int err = lfs_dir_alloc(lfs, &tail); + if (err) { + return err; + } + + tail.split = dir->split; + tail.tail[0] = dir->tail[0]; + tail.tail[1] = dir->tail[1]; + + // note we don't care about LFS_OK_RELOCATED + int res = lfs_dir_compact(lfs, &tail, attrs, attrcount, source, split, end); + if (res < 0) { + return res; + } + + dir->tail[0] = tail.pair[0]; + dir->tail[1] = tail.pair[1]; + dir->split = true; + + // update root if needed + if (lfs_pair_cmp(dir->pair, lfs->root) == 0 && split == 0) { + lfs->root[0] = tail.pair[0]; + lfs->root[1] = tail.pair[1]; + } + + return 0; +} +#endif + +#ifndef LFS_READONLY +static int lfs_dir_commit_size(void *p, lfs_tag_t tag, const void *buffer) { + lfs_size_t *size = p; + (void)buffer; + + *size += lfs_tag_dsize(tag); + return 0; +} +#endif + +#ifndef LFS_READONLY +struct lfs_dir_commit_commit { + lfs_t *lfs; + struct lfs_commit *commit; +}; +#endif + +#ifndef LFS_READONLY +static int lfs_dir_commit_commit(void *p, lfs_tag_t tag, const void *buffer) { + struct lfs_dir_commit_commit *commit = p; + return lfs_dir_commitattr(commit->lfs, commit->commit, tag, buffer); +} +#endif + +#ifndef LFS_READONLY +static bool lfs_dir_needsrelocation(lfs_t *lfs, lfs_mdir_t *dir) { + // If our revision count == n * block_cycles, we should force a relocation, + // this is how littlefs wear-levels at the metadata-pair level. Note that we + // actually use (block_cycles+1)|1, this is to avoid two corner cases: + // 1. block_cycles = 1, which would prevent relocations from terminating + // 2. block_cycles = 2n, which, due to aliasing, would only ever relocate + // one metadata block in the pair, effectively making this useless + return (lfs->cfg->block_cycles > 0 + && ((dir->rev + 1) % ((lfs->cfg->block_cycles+1)|1) == 0)); +} +#endif + +#ifndef LFS_READONLY +static int lfs_dir_compact(lfs_t *lfs, + lfs_mdir_t *dir, const struct lfs_mattr *attrs, int attrcount, + lfs_mdir_t *source, uint16_t begin, uint16_t end) { + // save some state in case block is bad + bool relocated = false; + bool tired = lfs_dir_needsrelocation(lfs, dir); + + // increment revision count + dir->rev += 1; + + // do not proactively relocate blocks during migrations, this + // can cause a number of failure states such: clobbering the + // v1 superblock if we relocate root, and invalidating directory + // pointers if we relocate the head of a directory. On top of + // this, relocations increase the overall complexity of + // lfs_migration, which is already a delicate operation. +#ifdef LFS_MIGRATE + if (lfs->lfs1) { + tired = false; + } +#endif + + if (tired && lfs_pair_cmp(dir->pair, (const lfs_block_t[2]){0, 1}) != 0) { + // we're writing too much, time to relocate + goto relocate; + } + + // begin loop to commit compaction to blocks until a compact sticks + while (true) { + { + // setup commit state + struct lfs_commit commit = { + .block = dir->pair[1], + .off = 0, + .ptag = 0xffffffff, + .crc = 0xffffffff, + + .begin = 0, + .end = (lfs->cfg->metadata_max ? + lfs->cfg->metadata_max : lfs->cfg->block_size) - 8, + }; + + // erase block to write to + int err = lfs_bd_erase(lfs, dir->pair[1]); + if (err) { + if (err == LFS_ERR_CORRUPT) { + goto relocate; + } + return err; + } + + // write out header + dir->rev = lfs_tole32(dir->rev); + err = lfs_dir_commitprog(lfs, &commit, + &dir->rev, sizeof(dir->rev)); + dir->rev = lfs_fromle32(dir->rev); + if (err) { + if (err == LFS_ERR_CORRUPT) { + goto relocate; + } + return err; + } + + // traverse the directory, this time writing out all unique tags + err = lfs_dir_traverse(lfs, + source, 0, 0xffffffff, attrs, attrcount, + LFS_MKTAG(0x400, 0x3ff, 0), + LFS_MKTAG(LFS_TYPE_NAME, 0, 0), + begin, end, -begin, + lfs_dir_commit_commit, &(struct lfs_dir_commit_commit){ + lfs, &commit}); + if (err) { + if (err == LFS_ERR_CORRUPT) { + goto relocate; + } + return err; + } + + // commit tail, which may be new after last size check + if (!lfs_pair_isnull(dir->tail)) { + lfs_pair_tole32(dir->tail); + err = lfs_dir_commitattr(lfs, &commit, + LFS_MKTAG(LFS_TYPE_TAIL + dir->split, 0x3ff, 8), + dir->tail); + lfs_pair_fromle32(dir->tail); + if (err) { + if (err == LFS_ERR_CORRUPT) { + goto relocate; + } + return err; + } + } + + // bring over gstate? + lfs_gstate_t delta = {0}; + if (!relocated) { + lfs_gstate_xor(&delta, &lfs->gdisk); + lfs_gstate_xor(&delta, &lfs->gstate); + } + lfs_gstate_xor(&delta, &lfs->gdelta); + delta.tag &= ~LFS_MKTAG(0, 0, 0x3ff); + + err = lfs_dir_getgstate(lfs, dir, &delta); + if (err) { + return err; + } + + if (!lfs_gstate_iszero(&delta)) { + lfs_gstate_tole32(&delta); + err = lfs_dir_commitattr(lfs, &commit, + LFS_MKTAG(LFS_TYPE_MOVESTATE, 0x3ff, + sizeof(delta)), &delta); + if (err) { + if (err == LFS_ERR_CORRUPT) { + goto relocate; + } + return err; + } + } + + // complete commit with crc + err = lfs_dir_commitcrc(lfs, &commit); + if (err) { + if (err == LFS_ERR_CORRUPT) { + goto relocate; + } + return err; + } + + // successful compaction, swap dir pair to indicate most recent + LFS_ASSERT(commit.off % lfs->cfg->prog_size == 0); + lfs_pair_swap(dir->pair); + dir->count = end - begin; + dir->off = commit.off; + dir->etag = commit.ptag; + // update gstate + lfs->gdelta = (lfs_gstate_t){0}; + if (!relocated) { + lfs->gdisk = lfs->gstate; + } + } + break; + +relocate: + // commit was corrupted, drop caches and prepare to relocate block + relocated = true; + lfs_cache_drop(lfs, &lfs->pcache); + if (!tired) { + LFS_DEBUG("Bad block at 0x%"PRIx32, dir->pair[1]); + } + + // can't relocate superblock, filesystem is now frozen + if (lfs_pair_cmp(dir->pair, (const lfs_block_t[2]){0, 1}) == 0) { + LFS_WARN("Superblock 0x%"PRIx32" has become unwritable", + dir->pair[1]); + return LFS_ERR_NOSPC; + } + + // relocate half of pair + int err = lfs_alloc(lfs, &dir->pair[1]); + if (err && (err != LFS_ERR_NOSPC || !tired)) { + return err; + } + + tired = false; + continue; + } + + return relocated ? LFS_OK_RELOCATED : 0; +} +#endif + +#ifndef LFS_READONLY +static int lfs_dir_splittingcompact(lfs_t *lfs, lfs_mdir_t *dir, + const struct lfs_mattr *attrs, int attrcount, + lfs_mdir_t *source, uint16_t begin, uint16_t end) { + while (true) { + // find size of first split, we do this by halving the split until + // the metadata is guaranteed to fit + // + // Note that this isn't a true binary search, we never increase the + // split size. This may result in poorly distributed metadata but isn't + // worth the extra code size or performance hit to fix. + lfs_size_t split = begin; + while (end - split > 1) { + lfs_size_t size = 0; + int err = lfs_dir_traverse(lfs, + source, 0, 0xffffffff, attrs, attrcount, + LFS_MKTAG(0x400, 0x3ff, 0), + LFS_MKTAG(LFS_TYPE_NAME, 0, 0), + split, end, -split, + lfs_dir_commit_size, &size); + if (err) { + return err; + } + + // space is complicated, we need room for: + // + // - tail: 4+2*4 = 12 bytes + // - gstate: 4+3*4 = 16 bytes + // - move delete: 4 = 4 bytes + // - crc: 4+4 = 8 bytes + // total = 40 bytes + // + // And we cap at half a block to avoid degenerate cases with + // nearly-full metadata blocks. + // + lfs_size_t metadata_max = (lfs->cfg->metadata_max) + ? lfs->cfg->metadata_max + : lfs->cfg->block_size; + if (end - split < 0xff + && size <= lfs_min( + metadata_max - 40, + lfs_alignup( + metadata_max/2, + lfs->cfg->prog_size))) { + break; + } + + split = split + ((end - split) / 2); + } + + if (split == begin) { + // no split needed + break; + } + + // split into two metadata pairs and continue + int err = lfs_dir_split(lfs, dir, attrs, attrcount, + source, split, end); + if (err && err != LFS_ERR_NOSPC) { + return err; + } + + if (err) { + // we can't allocate a new block, try to compact with degraded + // performance + LFS_WARN("Unable to split {0x%"PRIx32", 0x%"PRIx32"}", + dir->pair[0], dir->pair[1]); + break; + } else { + end = split; + } + } + + if (lfs_dir_needsrelocation(lfs, dir) + && lfs_pair_cmp(dir->pair, (const lfs_block_t[2]){0, 1}) == 0) { + // oh no! we're writing too much to the superblock, + // should we expand? + lfs_ssize_t size = lfs_fs_size_(lfs); + if (size < 0) { + return size; + } + + // littlefs cannot reclaim expanded superblocks, so expand cautiously + // + // if our filesystem is more than ~88% full, don't expand, this is + // somewhat arbitrary + if (lfs->block_count - size > lfs->block_count/8) { + LFS_DEBUG("Expanding superblock at rev %"PRIu32, dir->rev); + int err = lfs_dir_split(lfs, dir, attrs, attrcount, + source, begin, end); + if (err && err != LFS_ERR_NOSPC) { + return err; + } + + if (err) { + // welp, we tried, if we ran out of space there's not much + // we can do, we'll error later if we've become frozen + LFS_WARN("Unable to expand superblock"); + } else { + // duplicate the superblock entry into the new superblock + end = 1; + } + } + } + + return lfs_dir_compact(lfs, dir, attrs, attrcount, source, begin, end); +} +#endif + +#ifndef LFS_READONLY +static int lfs_dir_relocatingcommit(lfs_t *lfs, lfs_mdir_t *dir, + const lfs_block_t pair[2], + const struct lfs_mattr *attrs, int attrcount, + lfs_mdir_t *pdir) { + int state = 0; + + // calculate changes to the directory + bool hasdelete = false; + for (int i = 0; i < attrcount; i++) { + if (lfs_tag_type3(attrs[i].tag) == LFS_TYPE_CREATE) { + dir->count += 1; + } else if (lfs_tag_type3(attrs[i].tag) == LFS_TYPE_DELETE) { + LFS_ASSERT(dir->count > 0); + dir->count -= 1; + hasdelete = true; + } else if (lfs_tag_type1(attrs[i].tag) == LFS_TYPE_TAIL) { + dir->tail[0] = ((lfs_block_t*)attrs[i].buffer)[0]; + dir->tail[1] = ((lfs_block_t*)attrs[i].buffer)[1]; + dir->split = (lfs_tag_chunk(attrs[i].tag) & 1); + lfs_pair_fromle32(dir->tail); + } + } + + // should we actually drop the directory block? + if (hasdelete && dir->count == 0) { + LFS_ASSERT(pdir); + int err = lfs_fs_pred(lfs, dir->pair, pdir); + if (err && err != LFS_ERR_NOENT) { + return err; + } + + if (err != LFS_ERR_NOENT && pdir->split) { + state = LFS_OK_DROPPED; + goto fixmlist; + } + } + + if (dir->erased && dir->count < 0xff) { + // try to commit + struct lfs_commit commit = { + .block = dir->pair[0], + .off = dir->off, + .ptag = dir->etag, + .crc = 0xffffffff, + + .begin = dir->off, + .end = (lfs->cfg->metadata_max ? + lfs->cfg->metadata_max : lfs->cfg->block_size) - 8, + }; + + // traverse attrs that need to be written out + lfs_pair_tole32(dir->tail); + int err = lfs_dir_traverse(lfs, + dir, dir->off, dir->etag, attrs, attrcount, + 0, 0, 0, 0, 0, + lfs_dir_commit_commit, &(struct lfs_dir_commit_commit){ + lfs, &commit}); + lfs_pair_fromle32(dir->tail); + if (err) { + if (err == LFS_ERR_NOSPC || err == LFS_ERR_CORRUPT) { + goto compact; + } + return err; + } + + // commit any global diffs if we have any + lfs_gstate_t delta = {0}; + lfs_gstate_xor(&delta, &lfs->gstate); + lfs_gstate_xor(&delta, &lfs->gdisk); + lfs_gstate_xor(&delta, &lfs->gdelta); + delta.tag &= ~LFS_MKTAG(0, 0, 0x3ff); + if (!lfs_gstate_iszero(&delta)) { + err = lfs_dir_getgstate(lfs, dir, &delta); + if (err) { + return err; + } + + lfs_gstate_tole32(&delta); + err = lfs_dir_commitattr(lfs, &commit, + LFS_MKTAG(LFS_TYPE_MOVESTATE, 0x3ff, + sizeof(delta)), &delta); + if (err) { + if (err == LFS_ERR_NOSPC || err == LFS_ERR_CORRUPT) { + goto compact; + } + return err; + } + } + + // finalize commit with the crc + err = lfs_dir_commitcrc(lfs, &commit); + if (err) { + if (err == LFS_ERR_NOSPC || err == LFS_ERR_CORRUPT) { + goto compact; + } + return err; + } + + // successful commit, update dir + LFS_ASSERT(commit.off % lfs->cfg->prog_size == 0); + dir->off = commit.off; + dir->etag = commit.ptag; + // and update gstate + lfs->gdisk = lfs->gstate; + lfs->gdelta = (lfs_gstate_t){0}; + + goto fixmlist; + } + +compact: + // fall back to compaction + lfs_cache_drop(lfs, &lfs->pcache); + + state = lfs_dir_splittingcompact(lfs, dir, attrs, attrcount, + dir, 0, dir->count); + if (state < 0) { + return state; + } + + goto fixmlist; + +fixmlist:; + // this complicated bit of logic is for fixing up any active + // metadata-pairs that we may have affected + // + // note we have to make two passes since the mdir passed to + // lfs_dir_commit could also be in this list, and even then + // we need to copy the pair so they don't get clobbered if we refetch + // our mdir. + lfs_block_t oldpair[2] = {pair[0], pair[1]}; + for (struct lfs_mlist *d = lfs->mlist; d; d = d->next) { + if (lfs_pair_cmp(d->m.pair, oldpair) == 0) { + d->m = *dir; + if (d->m.pair != pair) { + for (int i = 0; i < attrcount; i++) { + if (lfs_tag_type3(attrs[i].tag) == LFS_TYPE_DELETE && + d->id == lfs_tag_id(attrs[i].tag) && + d->type != LFS_TYPE_DIR) { + d->m.pair[0] = LFS_BLOCK_NULL; + d->m.pair[1] = LFS_BLOCK_NULL; + } else if (lfs_tag_type3(attrs[i].tag) == LFS_TYPE_DELETE && + d->id > lfs_tag_id(attrs[i].tag)) { + d->id -= 1; + if (d->type == LFS_TYPE_DIR) { + ((lfs_dir_t*)d)->pos -= 1; + } + } else if (lfs_tag_type3(attrs[i].tag) == LFS_TYPE_CREATE && + d->id >= lfs_tag_id(attrs[i].tag)) { + d->id += 1; + if (d->type == LFS_TYPE_DIR) { + ((lfs_dir_t*)d)->pos += 1; + } + } + } + } + + while (d->id >= d->m.count && d->m.split) { + // we split and id is on tail now + if (lfs_pair_cmp(d->m.tail, lfs->root) != 0) { + d->id -= d->m.count; + } + int err = lfs_dir_fetch(lfs, &d->m, d->m.tail); + if (err) { + return err; + } + } + } + } + + return state; +} +#endif + +#ifndef LFS_READONLY +static int lfs_dir_orphaningcommit(lfs_t *lfs, lfs_mdir_t *dir, + const struct lfs_mattr *attrs, int attrcount) { + // check for any inline files that aren't RAM backed and + // forcefully evict them, needed for filesystem consistency + for (lfs_file_t *f = (lfs_file_t*)lfs->mlist; f; f = f->next) { + if (dir != &f->m && lfs_pair_cmp(f->m.pair, dir->pair) == 0 && + f->type == LFS_TYPE_REG && (f->flags & LFS_F_INLINE) && + f->ctz.size > lfs->cfg->cache_size) { + int err = lfs_file_outline(lfs, f); + if (err) { + return err; + } + + err = lfs_file_flush(lfs, f); + if (err) { + return err; + } + } + } + + lfs_block_t lpair[2] = {dir->pair[0], dir->pair[1]}; + lfs_mdir_t ldir = *dir; + lfs_mdir_t pdir; + int state = lfs_dir_relocatingcommit(lfs, &ldir, dir->pair, + attrs, attrcount, &pdir); + if (state < 0) { + return state; + } + + // update if we're not in mlist, note we may have already been + // updated if we are in mlist + if (lfs_pair_cmp(dir->pair, lpair) == 0) { + *dir = ldir; + } + + // commit was successful, but may require other changes in the + // filesystem, these would normally be tail recursive, but we have + // flattened them here avoid unbounded stack usage + + // need to drop? + if (state == LFS_OK_DROPPED) { + // steal state + int err = lfs_dir_getgstate(lfs, dir, &lfs->gdelta); + if (err) { + return err; + } + + // steal tail, note that this can't create a recursive drop + lpair[0] = pdir.pair[0]; + lpair[1] = pdir.pair[1]; + lfs_pair_tole32(dir->tail); + state = lfs_dir_relocatingcommit(lfs, &pdir, lpair, LFS_MKATTRS( + {LFS_MKTAG(LFS_TYPE_TAIL + dir->split, 0x3ff, 8), + dir->tail}), + NULL); + lfs_pair_fromle32(dir->tail); + if (state < 0) { + return state; + } + + ldir = pdir; + } + + // need to relocate? + bool orphans = false; + while (state == LFS_OK_RELOCATED) { + LFS_DEBUG("Relocating {0x%"PRIx32", 0x%"PRIx32"} " + "-> {0x%"PRIx32", 0x%"PRIx32"}", + lpair[0], lpair[1], ldir.pair[0], ldir.pair[1]); + state = 0; + + // update internal root + if (lfs_pair_cmp(lpair, lfs->root) == 0) { + lfs->root[0] = ldir.pair[0]; + lfs->root[1] = ldir.pair[1]; + } + + // update internally tracked dirs + for (struct lfs_mlist *d = lfs->mlist; d; d = d->next) { + if (lfs_pair_cmp(lpair, d->m.pair) == 0) { + d->m.pair[0] = ldir.pair[0]; + d->m.pair[1] = ldir.pair[1]; + } + + if (d->type == LFS_TYPE_DIR && + lfs_pair_cmp(lpair, ((lfs_dir_t*)d)->head) == 0) { + ((lfs_dir_t*)d)->head[0] = ldir.pair[0]; + ((lfs_dir_t*)d)->head[1] = ldir.pair[1]; + } + } + + // find parent + lfs_stag_t tag = lfs_fs_parent(lfs, lpair, &pdir); + if (tag < 0 && tag != LFS_ERR_NOENT) { + return tag; + } + + bool hasparent = (tag != LFS_ERR_NOENT); + if (tag != LFS_ERR_NOENT) { + // note that if we have a parent, we must have a pred, so this will + // always create an orphan + int err = lfs_fs_preporphans(lfs, +1); + if (err) { + return err; + } + + // fix pending move in this pair? this looks like an optimization but + // is in fact _required_ since relocating may outdate the move. + uint16_t moveid = 0x3ff; + if (lfs_gstate_hasmovehere(&lfs->gstate, pdir.pair)) { + moveid = lfs_tag_id(lfs->gstate.tag); + LFS_DEBUG("Fixing move while relocating " + "{0x%"PRIx32", 0x%"PRIx32"} 0x%"PRIx16"\n", + pdir.pair[0], pdir.pair[1], moveid); + lfs_fs_prepmove(lfs, 0x3ff, NULL); + if (moveid < lfs_tag_id(tag)) { + tag -= LFS_MKTAG(0, 1, 0); + } + } + + lfs_block_t ppair[2] = {pdir.pair[0], pdir.pair[1]}; + lfs_pair_tole32(ldir.pair); + state = lfs_dir_relocatingcommit(lfs, &pdir, ppair, LFS_MKATTRS( + {LFS_MKTAG_IF(moveid != 0x3ff, + LFS_TYPE_DELETE, moveid, 0), NULL}, + {tag, ldir.pair}), + NULL); + lfs_pair_fromle32(ldir.pair); + if (state < 0) { + return state; + } + + if (state == LFS_OK_RELOCATED) { + lpair[0] = ppair[0]; + lpair[1] = ppair[1]; + ldir = pdir; + orphans = true; + continue; + } + } + + // find pred + int err = lfs_fs_pred(lfs, lpair, &pdir); + if (err && err != LFS_ERR_NOENT) { + return err; + } + LFS_ASSERT(!(hasparent && err == LFS_ERR_NOENT)); + + // if we can't find dir, it must be new + if (err != LFS_ERR_NOENT) { + if (lfs_gstate_hasorphans(&lfs->gstate)) { + // next step, clean up orphans + err = lfs_fs_preporphans(lfs, -(int8_t)hasparent); + if (err) { + return err; + } + } + + // fix pending move in this pair? this looks like an optimization + // but is in fact _required_ since relocating may outdate the move. + uint16_t moveid = 0x3ff; + if (lfs_gstate_hasmovehere(&lfs->gstate, pdir.pair)) { + moveid = lfs_tag_id(lfs->gstate.tag); + LFS_DEBUG("Fixing move while relocating " + "{0x%"PRIx32", 0x%"PRIx32"} 0x%"PRIx16"\n", + pdir.pair[0], pdir.pair[1], moveid); + lfs_fs_prepmove(lfs, 0x3ff, NULL); + } + + // replace bad pair, either we clean up desync, or no desync occured + lpair[0] = pdir.pair[0]; + lpair[1] = pdir.pair[1]; + lfs_pair_tole32(ldir.pair); + state = lfs_dir_relocatingcommit(lfs, &pdir, lpair, LFS_MKATTRS( + {LFS_MKTAG_IF(moveid != 0x3ff, + LFS_TYPE_DELETE, moveid, 0), NULL}, + {LFS_MKTAG(LFS_TYPE_TAIL + pdir.split, 0x3ff, 8), + ldir.pair}), + NULL); + lfs_pair_fromle32(ldir.pair); + if (state < 0) { + return state; + } + + ldir = pdir; + } + } + + return orphans ? LFS_OK_ORPHANED : 0; +} +#endif + +#ifndef LFS_READONLY +static int lfs_dir_commit(lfs_t *lfs, lfs_mdir_t *dir, + const struct lfs_mattr *attrs, int attrcount) { + int orphans = lfs_dir_orphaningcommit(lfs, dir, attrs, attrcount); + if (orphans < 0) { + return orphans; + } + + if (orphans) { + // make sure we've removed all orphans, this is a noop if there + // are none, but if we had nested blocks failures we may have + // created some + int err = lfs_fs_deorphan(lfs, false); + if (err) { + return err; + } + } + + return 0; +} +#endif + + +/// Top level directory operations /// +#ifndef LFS_READONLY +static int lfs_mkdir_(lfs_t *lfs, const char *path) { + // deorphan if we haven't yet, needed at most once after poweron + int err = lfs_fs_forceconsistency(lfs); + if (err) { + return err; + } + + struct lfs_mlist cwd; + cwd.next = lfs->mlist; + uint16_t id; + err = lfs_dir_find(lfs, &cwd.m, &path, &id); + if (!(err == LFS_ERR_NOENT && lfs_path_islast(path))) { + return (err < 0) ? err : LFS_ERR_EXIST; + } + + // check that name fits + lfs_size_t nlen = lfs_path_namelen(path); + if (nlen > lfs->name_max) { + return LFS_ERR_NAMETOOLONG; + } + + // build up new directory + lfs_alloc_ckpoint(lfs); + lfs_mdir_t dir; + err = lfs_dir_alloc(lfs, &dir); + if (err) { + return err; + } + + // find end of list + lfs_mdir_t pred = cwd.m; + while (pred.split) { + err = lfs_dir_fetch(lfs, &pred, pred.tail); + if (err) { + return err; + } + } + + // setup dir + lfs_pair_tole32(pred.tail); + err = lfs_dir_commit(lfs, &dir, LFS_MKATTRS( + {LFS_MKTAG(LFS_TYPE_SOFTTAIL, 0x3ff, 8), pred.tail})); + lfs_pair_fromle32(pred.tail); + if (err) { + return err; + } + + // current block not end of list? + if (cwd.m.split) { + // update tails, this creates a desync + err = lfs_fs_preporphans(lfs, +1); + if (err) { + return err; + } + + // it's possible our predecessor has to be relocated, and if + // our parent is our predecessor's predecessor, this could have + // caused our parent to go out of date, fortunately we can hook + // ourselves into littlefs to catch this + cwd.type = 0; + cwd.id = 0; + lfs->mlist = &cwd; + + lfs_pair_tole32(dir.pair); + err = lfs_dir_commit(lfs, &pred, LFS_MKATTRS( + {LFS_MKTAG(LFS_TYPE_SOFTTAIL, 0x3ff, 8), dir.pair})); + lfs_pair_fromle32(dir.pair); + if (err) { + lfs->mlist = cwd.next; + return err; + } + + lfs->mlist = cwd.next; + err = lfs_fs_preporphans(lfs, -1); + if (err) { + return err; + } + } + + // now insert into our parent block + lfs_pair_tole32(dir.pair); + err = lfs_dir_commit(lfs, &cwd.m, LFS_MKATTRS( + {LFS_MKTAG(LFS_TYPE_CREATE, id, 0), NULL}, + {LFS_MKTAG(LFS_TYPE_DIR, id, nlen), path}, + {LFS_MKTAG(LFS_TYPE_DIRSTRUCT, id, 8), dir.pair}, + {LFS_MKTAG_IF(!cwd.m.split, + LFS_TYPE_SOFTTAIL, 0x3ff, 8), dir.pair})); + lfs_pair_fromle32(dir.pair); + if (err) { + return err; + } + + return 0; +} +#endif + +static int lfs_dir_open_(lfs_t *lfs, lfs_dir_t *dir, const char *path) { + lfs_stag_t tag = lfs_dir_find(lfs, &dir->m, &path, NULL); + if (tag < 0) { + return tag; + } + + if (lfs_tag_type3(tag) != LFS_TYPE_DIR) { + return LFS_ERR_NOTDIR; + } + + lfs_block_t pair[2]; + if (lfs_tag_id(tag) == 0x3ff) { + // handle root dir separately + pair[0] = lfs->root[0]; + pair[1] = lfs->root[1]; + } else { + // get dir pair from parent + lfs_stag_t res = lfs_dir_get(lfs, &dir->m, LFS_MKTAG(0x700, 0x3ff, 0), + LFS_MKTAG(LFS_TYPE_STRUCT, lfs_tag_id(tag), 8), pair); + if (res < 0) { + return res; + } + lfs_pair_fromle32(pair); + } + + // fetch first pair + int err = lfs_dir_fetch(lfs, &dir->m, pair); + if (err) { + return err; + } + + // setup entry + dir->head[0] = dir->m.pair[0]; + dir->head[1] = dir->m.pair[1]; + dir->id = 0; + dir->pos = 0; + + // add to list of mdirs + dir->type = LFS_TYPE_DIR; + lfs_mlist_append(lfs, (struct lfs_mlist *)dir); + + return 0; +} + +static int lfs_dir_close_(lfs_t *lfs, lfs_dir_t *dir) { + // remove from list of mdirs + lfs_mlist_remove(lfs, (struct lfs_mlist *)dir); + + return 0; +} + +static int lfs_dir_read_(lfs_t *lfs, lfs_dir_t *dir, struct lfs_info *info) { + memset(info, 0, sizeof(*info)); + + // special offset for '.' and '..' + if (dir->pos == 0) { + info->type = LFS_TYPE_DIR; + strcpy(info->name, "."); + dir->pos += 1; + return true; + } else if (dir->pos == 1) { + info->type = LFS_TYPE_DIR; + strcpy(info->name, ".."); + dir->pos += 1; + return true; + } + + while (true) { + if (dir->id == dir->m.count) { + if (!dir->m.split) { + return false; + } + + int err = lfs_dir_fetch(lfs, &dir->m, dir->m.tail); + if (err) { + return err; + } + + dir->id = 0; + } + + int err = lfs_dir_getinfo(lfs, &dir->m, dir->id, info); + if (err && err != LFS_ERR_NOENT) { + return err; + } + + dir->id += 1; + if (err != LFS_ERR_NOENT) { + break; + } + } + + dir->pos += 1; + return true; +} + +static int lfs_dir_seek_(lfs_t *lfs, lfs_dir_t *dir, lfs_off_t off) { + // simply walk from head dir + int err = lfs_dir_rewind_(lfs, dir); + if (err) { + return err; + } + + // first two for ./.. + dir->pos = lfs_min(2, off); + off -= dir->pos; + + // skip superblock entry + dir->id = (off > 0 && lfs_pair_cmp(dir->head, lfs->root) == 0); + + while (off > 0) { + if (dir->id == dir->m.count) { + if (!dir->m.split) { + return LFS_ERR_INVAL; + } + + err = lfs_dir_fetch(lfs, &dir->m, dir->m.tail); + if (err) { + return err; + } + + dir->id = 0; + } + + int diff = lfs_min(dir->m.count - dir->id, off); + dir->id += diff; + dir->pos += diff; + off -= diff; + } + + return 0; +} + +static lfs_soff_t lfs_dir_tell_(lfs_t *lfs, lfs_dir_t *dir) { + (void)lfs; + return dir->pos; +} + +static int lfs_dir_rewind_(lfs_t *lfs, lfs_dir_t *dir) { + // reload the head dir + int err = lfs_dir_fetch(lfs, &dir->m, dir->head); + if (err) { + return err; + } + + dir->id = 0; + dir->pos = 0; + return 0; +} + + +/// File index list operations /// +static int lfs_ctz_index(lfs_t *lfs, lfs_off_t *off) { + lfs_off_t size = *off; + lfs_off_t b = lfs->cfg->block_size - 2*4; + lfs_off_t i = size / b; + if (i == 0) { + return 0; + } + + i = (size - 4*(lfs_popc(i-1)+2)) / b; + *off = size - b*i - 4*lfs_popc(i); + return i; +} + +static int lfs_ctz_find(lfs_t *lfs, + const lfs_cache_t *pcache, lfs_cache_t *rcache, + lfs_block_t head, lfs_size_t size, + lfs_size_t pos, lfs_block_t *block, lfs_off_t *off) { + if (size == 0) { + *block = LFS_BLOCK_NULL; + *off = 0; + return 0; + } + + lfs_off_t current = lfs_ctz_index(lfs, &(lfs_off_t){size-1}); + lfs_off_t target = lfs_ctz_index(lfs, &pos); + + while (current > target) { + lfs_size_t skip = lfs_min( + lfs_npw2(current-target+1) - 1, + lfs_ctz(current)); + + int err = lfs_bd_read(lfs, + pcache, rcache, sizeof(head), + head, 4*skip, &head, sizeof(head)); + head = lfs_fromle32(head); + if (err) { + return err; + } + + current -= 1 << skip; + } + + *block = head; + *off = pos; + return 0; +} + +#ifndef LFS_READONLY +static int lfs_ctz_extend(lfs_t *lfs, + lfs_cache_t *pcache, lfs_cache_t *rcache, + lfs_block_t head, lfs_size_t size, + lfs_block_t *block, lfs_off_t *off) { + while (true) { + // go ahead and grab a block + lfs_block_t nblock; + int err = lfs_alloc(lfs, &nblock); + if (err) { + return err; + } + + { + err = lfs_bd_erase(lfs, nblock); + if (err) { + if (err == LFS_ERR_CORRUPT) { + goto relocate; + } + return err; + } + + if (size == 0) { + *block = nblock; + *off = 0; + return 0; + } + + lfs_size_t noff = size - 1; + lfs_off_t index = lfs_ctz_index(lfs, &noff); + noff = noff + 1; + + // just copy out the last block if it is incomplete + if (noff != lfs->cfg->block_size) { + for (lfs_off_t i = 0; i < noff; i++) { + uint8_t data; + err = lfs_bd_read(lfs, + NULL, rcache, noff-i, + head, i, &data, 1); + if (err) { + return err; + } + + err = lfs_bd_prog(lfs, + pcache, rcache, true, + nblock, i, &data, 1); + if (err) { + if (err == LFS_ERR_CORRUPT) { + goto relocate; + } + return err; + } + } + + *block = nblock; + *off = noff; + return 0; + } + + // append block + index += 1; + lfs_size_t skips = lfs_ctz(index) + 1; + lfs_block_t nhead = head; + for (lfs_off_t i = 0; i < skips; i++) { + nhead = lfs_tole32(nhead); + err = lfs_bd_prog(lfs, pcache, rcache, true, + nblock, 4*i, &nhead, 4); + nhead = lfs_fromle32(nhead); + if (err) { + if (err == LFS_ERR_CORRUPT) { + goto relocate; + } + return err; + } + + if (i != skips-1) { + err = lfs_bd_read(lfs, + NULL, rcache, sizeof(nhead), + nhead, 4*i, &nhead, sizeof(nhead)); + nhead = lfs_fromle32(nhead); + if (err) { + return err; + } + } + } + + *block = nblock; + *off = 4*skips; + return 0; + } + +relocate: + LFS_DEBUG("Bad block at 0x%"PRIx32, nblock); + + // just clear cache and try a new block + lfs_cache_drop(lfs, pcache); + } +} +#endif + +static int lfs_ctz_traverse(lfs_t *lfs, + const lfs_cache_t *pcache, lfs_cache_t *rcache, + lfs_block_t head, lfs_size_t size, + int (*cb)(void*, lfs_block_t), void *data) { + if (size == 0) { + return 0; + } + + lfs_off_t index = lfs_ctz_index(lfs, &(lfs_off_t){size-1}); + + while (true) { + int err = cb(data, head); + if (err) { + return err; + } + + if (index == 0) { + return 0; + } + + lfs_block_t heads[2]; + int count = 2 - (index & 1); + err = lfs_bd_read(lfs, + pcache, rcache, count*sizeof(head), + head, 0, &heads, count*sizeof(head)); + heads[0] = lfs_fromle32(heads[0]); + heads[1] = lfs_fromle32(heads[1]); + if (err) { + return err; + } + + for (int i = 0; i < count-1; i++) { + err = cb(data, heads[i]); + if (err) { + return err; + } + } + + head = heads[count-1]; + index -= count; + } +} + + +/// Top level file operations /// +static int lfs_file_opencfg_(lfs_t *lfs, lfs_file_t *file, + const char *path, int flags, + const struct lfs_file_config *cfg) { +#ifndef LFS_READONLY + // deorphan if we haven't yet, needed at most once after poweron + if ((flags & LFS_O_WRONLY) == LFS_O_WRONLY) { + int err = lfs_fs_forceconsistency(lfs); + if (err) { + return err; + } + } +#else + LFS_ASSERT((flags & LFS_O_RDONLY) == LFS_O_RDONLY); +#endif + + // setup simple file details + int err; + file->cfg = cfg; + file->flags = flags; + file->pos = 0; + file->off = 0; + file->cache.buffer = NULL; + + // allocate entry for file if it doesn't exist + lfs_stag_t tag = lfs_dir_find(lfs, &file->m, &path, &file->id); + if (tag < 0 && !(tag == LFS_ERR_NOENT && lfs_path_islast(path))) { + err = tag; + goto cleanup; + } + + // get id, add to list of mdirs to catch update changes + file->type = LFS_TYPE_REG; + lfs_mlist_append(lfs, (struct lfs_mlist *)file); + +#ifdef LFS_READONLY + if (tag == LFS_ERR_NOENT) { + err = LFS_ERR_NOENT; + goto cleanup; +#else + if (tag == LFS_ERR_NOENT) { + if (!(flags & LFS_O_CREAT)) { + err = LFS_ERR_NOENT; + goto cleanup; + } + + // don't allow trailing slashes + if (lfs_path_isdir(path)) { + err = LFS_ERR_NOTDIR; + goto cleanup; + } + + // check that name fits + lfs_size_t nlen = lfs_path_namelen(path); + if (nlen > lfs->name_max) { + err = LFS_ERR_NAMETOOLONG; + goto cleanup; + } + + // get next slot and create entry to remember name + err = lfs_dir_commit(lfs, &file->m, LFS_MKATTRS( + {LFS_MKTAG(LFS_TYPE_CREATE, file->id, 0), NULL}, + {LFS_MKTAG(LFS_TYPE_REG, file->id, nlen), path}, + {LFS_MKTAG(LFS_TYPE_INLINESTRUCT, file->id, 0), NULL})); + + // it may happen that the file name doesn't fit in the metadata blocks, e.g., a 256 byte file name will + // not fit in a 128 byte block. + err = (err == LFS_ERR_NOSPC) ? LFS_ERR_NAMETOOLONG : err; + if (err) { + goto cleanup; + } + + tag = LFS_MKTAG(LFS_TYPE_INLINESTRUCT, 0, 0); + } else if (flags & LFS_O_EXCL) { + err = LFS_ERR_EXIST; + goto cleanup; +#endif + } else if (lfs_tag_type3(tag) != LFS_TYPE_REG) { + err = LFS_ERR_ISDIR; + goto cleanup; +#ifndef LFS_READONLY + } else if (flags & LFS_O_TRUNC) { + // truncate if requested + tag = LFS_MKTAG(LFS_TYPE_INLINESTRUCT, file->id, 0); + file->flags |= LFS_F_DIRTY; +#endif + } else { + // try to load what's on disk, if it's inlined we'll fix it later + tag = lfs_dir_get(lfs, &file->m, LFS_MKTAG(0x700, 0x3ff, 0), + LFS_MKTAG(LFS_TYPE_STRUCT, file->id, 8), &file->ctz); + if (tag < 0) { + err = tag; + goto cleanup; + } + lfs_ctz_fromle32(&file->ctz); + } + + // fetch attrs + for (unsigned i = 0; i < file->cfg->attr_count; i++) { + // if opened for read / read-write operations + if ((file->flags & LFS_O_RDONLY) == LFS_O_RDONLY) { + lfs_stag_t res = lfs_dir_get(lfs, &file->m, + LFS_MKTAG(0x7ff, 0x3ff, 0), + LFS_MKTAG(LFS_TYPE_USERATTR + file->cfg->attrs[i].type, + file->id, file->cfg->attrs[i].size), + file->cfg->attrs[i].buffer); + if (res < 0 && res != LFS_ERR_NOENT) { + err = res; + goto cleanup; + } + } + +#ifndef LFS_READONLY + // if opened for write / read-write operations + if ((file->flags & LFS_O_WRONLY) == LFS_O_WRONLY) { + if (file->cfg->attrs[i].size > lfs->attr_max) { + err = LFS_ERR_NOSPC; + goto cleanup; + } + + file->flags |= LFS_F_DIRTY; + } +#endif + } + + // allocate buffer if needed + if (file->cfg->buffer) { + file->cache.buffer = file->cfg->buffer; + } else { + file->cache.buffer = lfs_malloc(lfs->cfg->cache_size); + if (!file->cache.buffer) { + err = LFS_ERR_NOMEM; + goto cleanup; + } + } + + // zero to avoid information leak + lfs_cache_zero(lfs, &file->cache); + + if (lfs_tag_type3(tag) == LFS_TYPE_INLINESTRUCT) { + // load inline files + file->ctz.head = LFS_BLOCK_INLINE; + file->ctz.size = lfs_tag_size(tag); + file->flags |= LFS_F_INLINE; + file->cache.block = file->ctz.head; + file->cache.off = 0; + file->cache.size = lfs->cfg->cache_size; + + // don't always read (may be new/trunc file) + if (file->ctz.size > 0) { + lfs_stag_t res = lfs_dir_get(lfs, &file->m, + LFS_MKTAG(0x700, 0x3ff, 0), + LFS_MKTAG(LFS_TYPE_STRUCT, file->id, + lfs_min(file->cache.size, 0x3fe)), + file->cache.buffer); + if (res < 0) { + err = res; + goto cleanup; + } + } + } + + return 0; + +cleanup: + // clean up lingering resources +#ifndef LFS_READONLY + file->flags |= LFS_F_ERRED; +#endif + lfs_file_close_(lfs, file); + return err; +} + +#ifndef LFS_NO_MALLOC +static int lfs_file_open_(lfs_t *lfs, lfs_file_t *file, + const char *path, int flags) { + static const struct lfs_file_config defaults = {0}; + int err = lfs_file_opencfg_(lfs, file, path, flags, &defaults); + return err; +} +#endif + +static int lfs_file_close_(lfs_t *lfs, lfs_file_t *file) { + int err = 0; +#ifndef LFS_READONLY + // it's not safe to do anything if our file errored + if (!(file->flags & LFS_F_ERRED)) { + err = lfs_file_sync_(lfs, file); + } +#endif + + // remove from list of mdirs + lfs_mlist_remove(lfs, (struct lfs_mlist*)file); + + // clean up memory + if (!file->cfg->buffer) { + lfs_free(file->cache.buffer); + } + + return err; +} + + +#ifndef LFS_READONLY +static int lfs_file_relocate(lfs_t *lfs, lfs_file_t *file) { + while (true) { + // just relocate what exists into new block + lfs_block_t nblock; + int err = lfs_alloc(lfs, &nblock); + if (err) { + return err; + } + + err = lfs_bd_erase(lfs, nblock); + if (err) { + if (err == LFS_ERR_CORRUPT) { + goto relocate; + } + return err; + } + + // either read from dirty cache or disk + for (lfs_off_t i = 0; i < file->off; i++) { + uint8_t data; + if (file->flags & LFS_F_INLINE) { + err = lfs_dir_getread(lfs, &file->m, + // note we evict inline files before they can be dirty + NULL, &file->cache, file->off-i, + LFS_MKTAG(0xfff, 0x1ff, 0), + LFS_MKTAG(LFS_TYPE_INLINESTRUCT, file->id, 0), + i, &data, 1); + if (err) { + return err; + } + } else { + err = lfs_bd_read(lfs, + &file->cache, &lfs->rcache, file->off-i, + file->block, i, &data, 1); + if (err) { + return err; + } + } + + err = lfs_bd_prog(lfs, + &lfs->pcache, &lfs->rcache, true, + nblock, i, &data, 1); + if (err) { + if (err == LFS_ERR_CORRUPT) { + goto relocate; + } + return err; + } + } + + // copy over new state of file + memcpy(file->cache.buffer, lfs->pcache.buffer, lfs->cfg->cache_size); + file->cache.block = lfs->pcache.block; + file->cache.off = lfs->pcache.off; + file->cache.size = lfs->pcache.size; + lfs_cache_zero(lfs, &lfs->pcache); + + file->block = nblock; + file->flags |= LFS_F_WRITING; + return 0; + +relocate: + LFS_DEBUG("Bad block at 0x%"PRIx32, nblock); + + // just clear cache and try a new block + lfs_cache_drop(lfs, &lfs->pcache); + } +} +#endif + +#ifndef LFS_READONLY +static int lfs_file_outline(lfs_t *lfs, lfs_file_t *file) { + file->off = file->pos; + lfs_alloc_ckpoint(lfs); + int err = lfs_file_relocate(lfs, file); + if (err) { + return err; + } + + file->flags &= ~LFS_F_INLINE; + return 0; +} +#endif + +static int lfs_file_flush(lfs_t *lfs, lfs_file_t *file) { + if (file->flags & LFS_F_READING) { + if (!(file->flags & LFS_F_INLINE)) { + lfs_cache_drop(lfs, &file->cache); + } + file->flags &= ~LFS_F_READING; + } + +#ifndef LFS_READONLY + if (file->flags & LFS_F_WRITING) { + lfs_off_t pos = file->pos; + + if (!(file->flags & LFS_F_INLINE)) { + // copy over anything after current branch + lfs_file_t orig = { + .ctz.head = file->ctz.head, + .ctz.size = file->ctz.size, + .flags = LFS_O_RDONLY, + .pos = file->pos, + .cache = lfs->rcache, + }; + lfs_cache_drop(lfs, &lfs->rcache); + + while (file->pos < file->ctz.size) { + // copy over a byte at a time, leave it up to caching + // to make this efficient + uint8_t data; + lfs_ssize_t res = lfs_file_flushedread(lfs, &orig, &data, 1); + if (res < 0) { + return res; + } + + res = lfs_file_flushedwrite(lfs, file, &data, 1); + if (res < 0) { + return res; + } + + // keep our reference to the rcache in sync + if (lfs->rcache.block != LFS_BLOCK_NULL) { + lfs_cache_drop(lfs, &orig.cache); + lfs_cache_drop(lfs, &lfs->rcache); + } + } + + // write out what we have + while (true) { + int err = lfs_bd_flush(lfs, &file->cache, &lfs->rcache, true); + if (err) { + if (err == LFS_ERR_CORRUPT) { + goto relocate; + } + return err; + } + + break; + +relocate: + LFS_DEBUG("Bad block at 0x%"PRIx32, file->block); + err = lfs_file_relocate(lfs, file); + if (err) { + return err; + } + } + } else { + file->pos = lfs_max(file->pos, file->ctz.size); + } + + // actual file updates + file->ctz.head = file->block; + file->ctz.size = file->pos; + file->flags &= ~LFS_F_WRITING; + file->flags |= LFS_F_DIRTY; + + file->pos = pos; + } +#endif + + return 0; +} + +#ifndef LFS_READONLY +static int lfs_file_sync_(lfs_t *lfs, lfs_file_t *file) { + int err = lfs_file_flush(lfs, file); + if (err) { + file->flags |= LFS_F_ERRED; + return err; + } + + if ((file->flags & LFS_F_DIRTY) && + !lfs_pair_isnull(file->m.pair)) { + // before we commit metadata, we need sync the disk to make sure + // data writes don't complete after metadata writes + if (!(file->flags & LFS_F_INLINE)) { + err = lfs_bd_sync(lfs, &lfs->pcache, &lfs->rcache, false); + if (err) { + return err; + } + } + + // update dir entry + uint16_t type; + const void *buffer; + lfs_size_t size; + struct lfs_ctz ctz; + if (file->flags & LFS_F_INLINE) { + // inline the whole file + type = LFS_TYPE_INLINESTRUCT; + buffer = file->cache.buffer; + size = file->ctz.size; + } else { + // update the ctz reference + type = LFS_TYPE_CTZSTRUCT; + // copy ctz so alloc will work during a relocate + ctz = file->ctz; + lfs_ctz_tole32(&ctz); + buffer = &ctz; + size = sizeof(ctz); + } + + // commit file data and attributes + err = lfs_dir_commit(lfs, &file->m, LFS_MKATTRS( + {LFS_MKTAG(type, file->id, size), buffer}, + {LFS_MKTAG(LFS_FROM_USERATTRS, file->id, + file->cfg->attr_count), file->cfg->attrs})); + if (err) { + file->flags |= LFS_F_ERRED; + return err; + } + + file->flags &= ~LFS_F_DIRTY; + } + + // mark any other file handles as dirty + desync + for (lfs_file_t *f = (lfs_file_t*)lfs->mlist; f; f = f->next) { + if (file != f + && f->type == LFS_TYPE_REG + && lfs_pair_cmp(f->m.pair, file->m.pair) == 0 + && f->id == file->id) { + f->flags |= LFS_F_DUSTY; + } + } + + file->flags &= ~LFS_F_ERRED & ~LFS_F_DUSTY; + return 0; +} +#endif + +static lfs_ssize_t lfs_file_flushedread(lfs_t *lfs, lfs_file_t *file, + void *buffer, lfs_size_t size) { + uint8_t *data = buffer; + lfs_size_t nsize = size; + + if (file->pos >= file->ctz.size) { + // eof if past end + return 0; + } + + size = lfs_min(size, file->ctz.size - file->pos); + nsize = size; + + while (nsize > 0) { + // check if we need a new block + if (!(file->flags & LFS_F_READING) || + file->off == lfs->cfg->block_size) { + if (!(file->flags & LFS_F_INLINE)) { + int err = lfs_ctz_find(lfs, NULL, &file->cache, + file->ctz.head, file->ctz.size, + file->pos, &file->block, &file->off); + if (err) { + return err; + } + } else { + file->block = LFS_BLOCK_INLINE; + file->off = file->pos; + } + + file->flags |= LFS_F_READING; + } + + // read as much as we can in current block + lfs_size_t diff = lfs_min(nsize, lfs->cfg->block_size - file->off); + if (file->flags & LFS_F_INLINE) { + int err = lfs_dir_getread(lfs, &file->m, + NULL, &file->cache, lfs->cfg->block_size, + LFS_MKTAG(0xfff, 0x1ff, 0), + LFS_MKTAG(LFS_TYPE_INLINESTRUCT, file->id, 0), + file->off, data, diff); + if (err) { + return err; + } + } else { + int err = lfs_bd_read(lfs, + NULL, &file->cache, lfs->cfg->block_size, + file->block, file->off, data, diff); + if (err) { + return err; + } + } + + file->pos += diff; + file->off += diff; + data += diff; + nsize -= diff; + } + + return size; +} + +static lfs_ssize_t lfs_file_read_(lfs_t *lfs, lfs_file_t *file, + void *buffer, lfs_size_t size) { + LFS_ASSERT((file->flags & LFS_O_RDONLY) == LFS_O_RDONLY); + +#ifndef LFS_READONLY + if (file->flags & LFS_F_WRITING) { + // flush out any writes + int err = lfs_file_flush(lfs, file); + if (err) { + return err; + } + } +#endif + + return lfs_file_flushedread(lfs, file, buffer, size); +} + + +#ifndef LFS_READONLY +static lfs_ssize_t lfs_file_flushedwrite(lfs_t *lfs, lfs_file_t *file, + const void *buffer, lfs_size_t size) { + const uint8_t *data = buffer; + lfs_size_t nsize = size; + + if ((file->flags & LFS_F_INLINE) && + lfs_max(file->pos+nsize, file->ctz.size) > lfs->inline_max) { + // inline file doesn't fit anymore + int err = lfs_file_outline(lfs, file); + if (err) { + file->flags |= LFS_F_ERRED; + return err; + } + } + + while (nsize > 0) { + // check if we need a new block + if (!(file->flags & LFS_F_WRITING) || + file->off == lfs->cfg->block_size) { + if (!(file->flags & LFS_F_INLINE)) { + if (!(file->flags & LFS_F_WRITING) && file->pos > 0) { + // find out which block we're extending from + int err = lfs_ctz_find(lfs, NULL, &file->cache, + file->ctz.head, file->ctz.size, + file->pos-1, &file->block, &(lfs_off_t){0}); + if (err) { + file->flags |= LFS_F_ERRED; + return err; + } + + // mark cache as dirty since we may have read data into it + lfs_cache_zero(lfs, &file->cache); + } + + // extend file with new blocks + lfs_alloc_ckpoint(lfs); + int err = lfs_ctz_extend(lfs, &file->cache, &lfs->rcache, + file->block, file->pos, + &file->block, &file->off); + if (err) { + file->flags |= LFS_F_ERRED; + return err; + } + } else { + file->block = LFS_BLOCK_INLINE; + file->off = file->pos; + } + + file->flags |= LFS_F_WRITING; + } + + // program as much as we can in current block + lfs_size_t diff = lfs_min(nsize, lfs->cfg->block_size - file->off); + while (true) { + int err = lfs_bd_prog(lfs, &file->cache, &lfs->rcache, true, + file->block, file->off, data, diff); + if (err) { + if (err == LFS_ERR_CORRUPT) { + goto relocate; + } + file->flags |= LFS_F_ERRED; + return err; + } + + break; +relocate: + err = lfs_file_relocate(lfs, file); + if (err) { + file->flags |= LFS_F_ERRED; + return err; + } + } + + file->pos += diff; + file->off += diff; + data += diff; + nsize -= diff; + + lfs_alloc_ckpoint(lfs); + } + + return size; +} + +static lfs_ssize_t lfs_file_write_(lfs_t *lfs, lfs_file_t *file, + const void *buffer, lfs_size_t size) { + LFS_ASSERT((file->flags & LFS_O_WRONLY) == LFS_O_WRONLY); + + if (file->flags & LFS_F_READING) { + // drop any reads + int err = lfs_file_flush(lfs, file); + if (err) { + return err; + } + } + + if ((file->flags & LFS_O_APPEND) && file->pos < file->ctz.size) { + file->pos = file->ctz.size; + } + + if (file->pos + size > lfs->file_max) { + // Larger than file limit? + return LFS_ERR_FBIG; + } + + if (!(file->flags & LFS_F_WRITING) && file->pos > file->ctz.size) { + // fill with zeros + lfs_off_t pos = file->pos; + file->pos = file->ctz.size; + + while (file->pos < pos) { + lfs_ssize_t res = lfs_file_flushedwrite(lfs, file, &(uint8_t){0}, 1); + if (res < 0) { + return res; + } + } + } + + lfs_ssize_t nsize = lfs_file_flushedwrite(lfs, file, buffer, size); + if (nsize < 0) { + return nsize; + } + + file->flags &= ~LFS_F_ERRED & ~LFS_F_DUSTY; + return nsize; +} +#endif + +static lfs_soff_t lfs_file_seek_(lfs_t *lfs, lfs_file_t *file, + lfs_soff_t off, int whence) { + // find new pos + // + // fortunately for us, littlefs is limited to 31-bit file sizes, so we + // don't have to worry too much about integer overflow + lfs_off_t npos = file->pos; + if (whence == LFS_SEEK_SET) { + npos = off; + } else if (whence == LFS_SEEK_CUR) { + npos = file->pos + (lfs_off_t)off; + } else if (whence == LFS_SEEK_END) { + npos = (lfs_off_t)lfs_file_size_(lfs, file) + (lfs_off_t)off; + } + + if (npos > lfs->file_max) { + // file position out of range + return LFS_ERR_INVAL; + } + + if (file->pos == npos) { + // noop - position has not changed + return npos; + } + + // if we're only reading and our new offset is still in the file's cache + // we can avoid flushing and needing to reread the data + if ((file->flags & LFS_F_READING) + && file->off != lfs->cfg->block_size) { + int oindex = lfs_ctz_index(lfs, &(lfs_off_t){file->pos}); + lfs_off_t noff = npos; + int nindex = lfs_ctz_index(lfs, &noff); + if (oindex == nindex + && noff >= file->cache.off + && noff < file->cache.off + file->cache.size) { + file->pos = npos; + file->off = noff; + return npos; + } + } + + // write out everything beforehand, may be noop if rdonly + int err = lfs_file_flush(lfs, file); + if (err) { + return err; + } + + // update pos + file->pos = npos; + return npos; +} + +#ifndef LFS_READONLY +static int lfs_file_truncate_(lfs_t *lfs, lfs_file_t *file, lfs_off_t size) { + LFS_ASSERT((file->flags & LFS_O_WRONLY) == LFS_O_WRONLY); + + if (size > LFS_FILE_MAX) { + return LFS_ERR_INVAL; + } + + lfs_off_t pos = file->pos; + lfs_off_t oldsize = lfs_file_size_(lfs, file); + if (size < oldsize) { + // revert to inline file? + if (size <= lfs->inline_max) { + // flush+seek to head + lfs_soff_t res = lfs_file_seek_(lfs, file, 0, LFS_SEEK_SET); + if (res < 0) { + return (int)res; + } + + // read our data into rcache temporarily + lfs_cache_drop(lfs, &lfs->rcache); + res = lfs_file_flushedread(lfs, file, + lfs->rcache.buffer, size); + if (res < 0) { + return (int)res; + } + + file->ctz.head = LFS_BLOCK_INLINE; + file->ctz.size = size; + file->flags |= LFS_F_DIRTY | LFS_F_READING | LFS_F_INLINE; + file->cache.block = file->ctz.head; + file->cache.off = 0; + file->cache.size = lfs->cfg->cache_size; + memcpy(file->cache.buffer, lfs->rcache.buffer, size); + + } else { + // need to flush since directly changing metadata + int err = lfs_file_flush(lfs, file); + if (err) { + return err; + } + + // lookup new head in ctz skip list + err = lfs_ctz_find(lfs, NULL, &file->cache, + file->ctz.head, file->ctz.size, + size-1, &file->block, &(lfs_off_t){0}); + if (err) { + return err; + } + + // need to set pos/block/off consistently so seeking back to + // the old position does not get confused + file->pos = size; + file->ctz.head = file->block; + file->ctz.size = size; + file->flags |= LFS_F_DIRTY | LFS_F_READING; + } + } else if (size > oldsize) { + // flush+seek if not already at end + lfs_soff_t res = lfs_file_seek_(lfs, file, 0, LFS_SEEK_END); + if (res < 0) { + return (int)res; + } + + // fill with zeros + while (file->pos < size) { + res = lfs_file_write_(lfs, file, &(uint8_t){0}, 1); + if (res < 0) { + return (int)res; + } + } + } + + // restore pos + lfs_soff_t res = lfs_file_seek_(lfs, file, pos, LFS_SEEK_SET); + if (res < 0) { + return (int)res; + } + + return 0; +} +#endif + +static lfs_soff_t lfs_file_tell_(lfs_t *lfs, lfs_file_t *file) { + (void)lfs; + return file->pos; +} + +static int lfs_file_rewind_(lfs_t *lfs, lfs_file_t *file) { + lfs_soff_t res = lfs_file_seek_(lfs, file, 0, LFS_SEEK_SET); + if (res < 0) { + return (int)res; + } + + return 0; +} + +static lfs_soff_t lfs_file_size_(lfs_t *lfs, lfs_file_t *file) { + (void)lfs; + +#ifndef LFS_READONLY + if (file->flags & LFS_F_WRITING) { + return lfs_max(file->pos, file->ctz.size); + } +#endif + + return file->ctz.size; +} + + +/// General fs operations /// +static int lfs_stat_(lfs_t *lfs, const char *path, struct lfs_info *info) { + lfs_mdir_t cwd; + lfs_stag_t tag = lfs_dir_find(lfs, &cwd, &path, NULL); + if (tag < 0) { + return (int)tag; + } + + // only allow trailing slashes on dirs + if (strchr(path, '/') != NULL + && lfs_tag_type3(tag) != LFS_TYPE_DIR) { + return LFS_ERR_NOTDIR; + } + + return lfs_dir_getinfo(lfs, &cwd, lfs_tag_id(tag), info); +} + +#ifndef LFS_READONLY +static int lfs_remove_(lfs_t *lfs, const char *path) { + // deorphan if we haven't yet, needed at most once after poweron + int err = lfs_fs_forceconsistency(lfs); + if (err) { + return err; + } + + lfs_mdir_t cwd; + lfs_stag_t tag = lfs_dir_find(lfs, &cwd, &path, NULL); + if (tag < 0 || lfs_tag_id(tag) == 0x3ff) { + return (tag < 0) ? (int)tag : LFS_ERR_INVAL; + } + + struct lfs_mlist dir; + dir.next = lfs->mlist; + if (lfs_tag_type3(tag) == LFS_TYPE_DIR) { + // must be empty before removal + lfs_block_t pair[2]; + lfs_stag_t res = lfs_dir_get(lfs, &cwd, LFS_MKTAG(0x700, 0x3ff, 0), + LFS_MKTAG(LFS_TYPE_STRUCT, lfs_tag_id(tag), 8), pair); + if (res < 0) { + return (int)res; + } + lfs_pair_fromle32(pair); + + err = lfs_dir_fetch(lfs, &dir.m, pair); + if (err) { + return err; + } + + if (dir.m.count > 0 || dir.m.split) { + return LFS_ERR_NOTEMPTY; + } + + // mark fs as orphaned + err = lfs_fs_preporphans(lfs, +1); + if (err) { + return err; + } + + // I know it's crazy but yes, dir can be changed by our parent's + // commit (if predecessor is child) + dir.type = 0; + dir.id = 0; + lfs->mlist = &dir; + } + + // delete the entry + err = lfs_dir_commit(lfs, &cwd, LFS_MKATTRS( + {LFS_MKTAG(LFS_TYPE_DELETE, lfs_tag_id(tag), 0), NULL})); + if (err) { + lfs->mlist = dir.next; + return err; + } + + lfs->mlist = dir.next; + if (lfs_gstate_hasorphans(&lfs->gstate)) { + LFS_ASSERT(lfs_tag_type3(tag) == LFS_TYPE_DIR); + + // fix orphan + err = lfs_fs_preporphans(lfs, -1); + if (err) { + return err; + } + + err = lfs_fs_pred(lfs, dir.m.pair, &cwd); + if (err) { + return err; + } + + err = lfs_dir_drop(lfs, &cwd, &dir.m); + if (err) { + return err; + } + } + + return 0; +} +#endif + +#ifndef LFS_READONLY +static int lfs_rename_(lfs_t *lfs, const char *oldpath, const char *newpath) { + // deorphan if we haven't yet, needed at most once after poweron + int err = lfs_fs_forceconsistency(lfs); + if (err) { + return err; + } + + // find old entry + lfs_mdir_t oldcwd; + lfs_stag_t oldtag = lfs_dir_find(lfs, &oldcwd, &oldpath, NULL); + if (oldtag < 0 || lfs_tag_id(oldtag) == 0x3ff) { + return (oldtag < 0) ? (int)oldtag : LFS_ERR_INVAL; + } + + // find new entry + lfs_mdir_t newcwd; + uint16_t newid; + lfs_stag_t prevtag = lfs_dir_find(lfs, &newcwd, &newpath, &newid); + if ((prevtag < 0 || lfs_tag_id(prevtag) == 0x3ff) && + !(prevtag == LFS_ERR_NOENT && lfs_path_islast(newpath))) { + return (prevtag < 0) ? (int)prevtag : LFS_ERR_INVAL; + } + + // if we're in the same pair there's a few special cases... + bool samepair = (lfs_pair_cmp(oldcwd.pair, newcwd.pair) == 0); + uint16_t newoldid = lfs_tag_id(oldtag); + + struct lfs_mlist prevdir; + prevdir.next = lfs->mlist; + if (prevtag == LFS_ERR_NOENT) { + // if we're a file, don't allow trailing slashes + if (lfs_path_isdir(newpath) + && lfs_tag_type3(oldtag) != LFS_TYPE_DIR) { + return LFS_ERR_NOTDIR; + } + + // check that name fits + lfs_size_t nlen = lfs_path_namelen(newpath); + if (nlen > lfs->name_max) { + return LFS_ERR_NAMETOOLONG; + } + + // there is a small chance we are being renamed in the same + // directory/ to an id less than our old id, the global update + // to handle this is a bit messy + if (samepair && newid <= newoldid) { + newoldid += 1; + } + } else if (lfs_tag_type3(prevtag) != lfs_tag_type3(oldtag)) { + return (lfs_tag_type3(prevtag) == LFS_TYPE_DIR) + ? LFS_ERR_ISDIR + : LFS_ERR_NOTDIR; + } else if (samepair && newid == newoldid) { + // we're renaming to ourselves?? + return 0; + } else if (lfs_tag_type3(prevtag) == LFS_TYPE_DIR) { + // must be empty before removal + lfs_block_t prevpair[2]; + lfs_stag_t res = lfs_dir_get(lfs, &newcwd, LFS_MKTAG(0x700, 0x3ff, 0), + LFS_MKTAG(LFS_TYPE_STRUCT, newid, 8), prevpair); + if (res < 0) { + return (int)res; + } + lfs_pair_fromle32(prevpair); + + // must be empty before removal + err = lfs_dir_fetch(lfs, &prevdir.m, prevpair); + if (err) { + return err; + } + + if (prevdir.m.count > 0 || prevdir.m.split) { + return LFS_ERR_NOTEMPTY; + } + + // mark fs as orphaned + err = lfs_fs_preporphans(lfs, +1); + if (err) { + return err; + } + + // I know it's crazy but yes, dir can be changed by our parent's + // commit (if predecessor is child) + prevdir.type = 0; + prevdir.id = 0; + lfs->mlist = &prevdir; + } + + if (!samepair) { + lfs_fs_prepmove(lfs, newoldid, oldcwd.pair); + } + + // move over all attributes + err = lfs_dir_commit(lfs, &newcwd, LFS_MKATTRS( + {LFS_MKTAG_IF(prevtag != LFS_ERR_NOENT, + LFS_TYPE_DELETE, newid, 0), NULL}, + {LFS_MKTAG(LFS_TYPE_CREATE, newid, 0), NULL}, + {LFS_MKTAG(lfs_tag_type3(oldtag), + newid, lfs_path_namelen(newpath)), newpath}, + {LFS_MKTAG(LFS_FROM_MOVE, newid, lfs_tag_id(oldtag)), &oldcwd}, + {LFS_MKTAG_IF(samepair, + LFS_TYPE_DELETE, newoldid, 0), NULL})); + if (err) { + lfs->mlist = prevdir.next; + return err; + } + + // let commit clean up after move (if we're different! otherwise move + // logic already fixed it for us) + if (!samepair && lfs_gstate_hasmove(&lfs->gstate)) { + // prep gstate and delete move id + lfs_fs_prepmove(lfs, 0x3ff, NULL); + err = lfs_dir_commit(lfs, &oldcwd, LFS_MKATTRS( + {LFS_MKTAG(LFS_TYPE_DELETE, lfs_tag_id(oldtag), 0), NULL})); + if (err) { + lfs->mlist = prevdir.next; + return err; + } + } + + lfs->mlist = prevdir.next; + if (lfs_gstate_hasorphans(&lfs->gstate)) { + LFS_ASSERT(prevtag != LFS_ERR_NOENT + && lfs_tag_type3(prevtag) == LFS_TYPE_DIR); + + // fix orphan + err = lfs_fs_preporphans(lfs, -1); + if (err) { + return err; + } + + err = lfs_fs_pred(lfs, prevdir.m.pair, &newcwd); + if (err) { + return err; + } + + err = lfs_dir_drop(lfs, &newcwd, &prevdir.m); + if (err) { + return err; + } + } + + return 0; +} +#endif + +static lfs_ssize_t lfs_getattr_(lfs_t *lfs, const char *path, + uint8_t type, void *buffer, lfs_size_t size) { + lfs_mdir_t cwd; + lfs_stag_t tag = lfs_dir_find(lfs, &cwd, &path, NULL); + if (tag < 0) { + return tag; + } + + uint16_t id = lfs_tag_id(tag); + if (id == 0x3ff) { + // special case for root + id = 0; + int err = lfs_dir_fetch(lfs, &cwd, lfs->root); + if (err) { + return err; + } + } + + tag = lfs_dir_get(lfs, &cwd, LFS_MKTAG(0x7ff, 0x3ff, 0), + LFS_MKTAG(LFS_TYPE_USERATTR + type, + id, lfs_min(size, lfs->attr_max)), + buffer); + if (tag < 0) { + if (tag == LFS_ERR_NOENT) { + return LFS_ERR_NOATTR; + } + + return tag; + } + + return lfs_tag_size(tag); +} + +#ifndef LFS_READONLY +static int lfs_commitattr(lfs_t *lfs, const char *path, + uint8_t type, const void *buffer, lfs_size_t size) { + lfs_mdir_t cwd; + lfs_stag_t tag = lfs_dir_find(lfs, &cwd, &path, NULL); + if (tag < 0) { + return tag; + } + + uint16_t id = lfs_tag_id(tag); + if (id == 0x3ff) { + // special case for root + id = 0; + int err = lfs_dir_fetch(lfs, &cwd, lfs->root); + if (err) { + return err; + } + } + + return lfs_dir_commit(lfs, &cwd, LFS_MKATTRS( + {LFS_MKTAG(LFS_TYPE_USERATTR + type, id, size), buffer})); +} +#endif + +#ifndef LFS_READONLY +static int lfs_setattr_(lfs_t *lfs, const char *path, + uint8_t type, const void *buffer, lfs_size_t size) { + if (size > lfs->attr_max) { + return LFS_ERR_NOSPC; + } + + return lfs_commitattr(lfs, path, type, buffer, size); +} +#endif + +#ifndef LFS_READONLY +static int lfs_removeattr_(lfs_t *lfs, const char *path, uint8_t type) { + return lfs_commitattr(lfs, path, type, NULL, 0x3ff); +} +#endif + + +/// Filesystem operations /// + +// compile time checks, see lfs.h for why these limits exist +#if LFS_NAME_MAX > 1022 +#error "Invalid LFS_NAME_MAX, must be <= 1022" +#endif + +#if LFS_FILE_MAX > 2147483647 +#error "Invalid LFS_FILE_MAX, must be <= 2147483647" +#endif + +#if LFS_ATTR_MAX > 1022 +#error "Invalid LFS_ATTR_MAX, must be <= 1022" +#endif + +// common filesystem initialization +static int lfs_init(lfs_t *lfs, const struct lfs_config *cfg) { + lfs->cfg = cfg; + lfs->block_count = cfg->block_count; // May be 0 + int err = 0; + +#ifdef LFS_MULTIVERSION + // this driver only supports minor version < current minor version + LFS_ASSERT(!lfs->cfg->disk_version || ( + (0xffff & (lfs->cfg->disk_version >> 16)) + == LFS_DISK_VERSION_MAJOR + && (0xffff & (lfs->cfg->disk_version >> 0)) + <= LFS_DISK_VERSION_MINOR)); +#endif + + // check that bool is a truthy-preserving type + // + // note the most common reason for this failure is a before-c99 compiler, + // which littlefs currently does not support + LFS_ASSERT((bool)0x80000000); + + // check that the required io functions are provided + LFS_ASSERT(lfs->cfg->read != NULL); +#ifndef LFS_READONLY + LFS_ASSERT(lfs->cfg->prog != NULL); + LFS_ASSERT(lfs->cfg->erase != NULL); + LFS_ASSERT(lfs->cfg->sync != NULL); +#endif + + // validate that the lfs-cfg sizes were initiated properly before + // performing any arithmetic logics with them + LFS_ASSERT(lfs->cfg->read_size != 0); + LFS_ASSERT(lfs->cfg->prog_size != 0); + LFS_ASSERT(lfs->cfg->cache_size != 0); + + // check that block size is a multiple of cache size is a multiple + // of prog and read sizes + LFS_ASSERT(lfs->cfg->cache_size % lfs->cfg->read_size == 0); + LFS_ASSERT(lfs->cfg->cache_size % lfs->cfg->prog_size == 0); + LFS_ASSERT(lfs->cfg->block_size % lfs->cfg->cache_size == 0); + + // check that the block size is large enough to fit all ctz pointers + LFS_ASSERT(lfs->cfg->block_size >= 128); + // this is the exact calculation for all ctz pointers, if this fails + // and the simpler assert above does not, math must be broken + LFS_ASSERT(4*lfs_npw2(0xffffffff / (lfs->cfg->block_size-2*4)) + <= lfs->cfg->block_size); + + // block_cycles = 0 is no longer supported. + // + // block_cycles is the number of erase cycles before littlefs evicts + // metadata logs as a part of wear leveling. Suggested values are in the + // range of 100-1000, or set block_cycles to -1 to disable block-level + // wear-leveling. + LFS_ASSERT(lfs->cfg->block_cycles != 0); + + // check that compact_thresh makes sense + // + // metadata can't be compacted below block_size/2, and metadata can't + // exceed a block_size + LFS_ASSERT(lfs->cfg->compact_thresh == 0 + || lfs->cfg->compact_thresh >= lfs->cfg->block_size/2); + LFS_ASSERT(lfs->cfg->compact_thresh == (lfs_size_t)-1 + || lfs->cfg->compact_thresh <= lfs->cfg->block_size); + + // check that metadata_max is a multiple of read_size and prog_size, + // and a factor of the block_size + LFS_ASSERT(!lfs->cfg->metadata_max + || lfs->cfg->metadata_max % lfs->cfg->read_size == 0); + LFS_ASSERT(!lfs->cfg->metadata_max + || lfs->cfg->metadata_max % lfs->cfg->prog_size == 0); + LFS_ASSERT(!lfs->cfg->metadata_max + || lfs->cfg->block_size % lfs->cfg->metadata_max == 0); + + // setup read cache + if (lfs->cfg->read_buffer) { + lfs->rcache.buffer = lfs->cfg->read_buffer; + } else { + lfs->rcache.buffer = lfs_malloc(lfs->cfg->cache_size); + if (!lfs->rcache.buffer) { + err = LFS_ERR_NOMEM; + goto cleanup; + } + } + + // setup program cache + if (lfs->cfg->prog_buffer) { + lfs->pcache.buffer = lfs->cfg->prog_buffer; + } else { + lfs->pcache.buffer = lfs_malloc(lfs->cfg->cache_size); + if (!lfs->pcache.buffer) { + err = LFS_ERR_NOMEM; + goto cleanup; + } + } + + // zero to avoid information leaks + lfs_cache_zero(lfs, &lfs->rcache); + lfs_cache_zero(lfs, &lfs->pcache); + + // setup lookahead buffer, note mount finishes initializing this after + // we establish a decent pseudo-random seed + LFS_ASSERT(lfs->cfg->lookahead_size > 0); + if (lfs->cfg->lookahead_buffer) { + lfs->lookahead.buffer = lfs->cfg->lookahead_buffer; + } else { + lfs->lookahead.buffer = lfs_malloc(lfs->cfg->lookahead_size); + if (!lfs->lookahead.buffer) { + err = LFS_ERR_NOMEM; + goto cleanup; + } + } + + // check that the size limits are sane + LFS_ASSERT(lfs->cfg->name_max <= LFS_NAME_MAX); + lfs->name_max = lfs->cfg->name_max; + if (!lfs->name_max) { + lfs->name_max = LFS_NAME_MAX; + } + + LFS_ASSERT(lfs->cfg->file_max <= LFS_FILE_MAX); + lfs->file_max = lfs->cfg->file_max; + if (!lfs->file_max) { + lfs->file_max = LFS_FILE_MAX; + } + + LFS_ASSERT(lfs->cfg->attr_max <= LFS_ATTR_MAX); + lfs->attr_max = lfs->cfg->attr_max; + if (!lfs->attr_max) { + lfs->attr_max = LFS_ATTR_MAX; + } + + LFS_ASSERT(lfs->cfg->metadata_max <= lfs->cfg->block_size); + + LFS_ASSERT(lfs->cfg->inline_max == (lfs_size_t)-1 + || lfs->cfg->inline_max <= lfs->cfg->cache_size); + LFS_ASSERT(lfs->cfg->inline_max == (lfs_size_t)-1 + || lfs->cfg->inline_max <= lfs->attr_max); + LFS_ASSERT(lfs->cfg->inline_max == (lfs_size_t)-1 + || lfs->cfg->inline_max <= ((lfs->cfg->metadata_max) + ? lfs->cfg->metadata_max + : lfs->cfg->block_size)/8); + lfs->inline_max = lfs->cfg->inline_max; + if (lfs->inline_max == (lfs_size_t)-1) { + lfs->inline_max = 0; + } else if (lfs->inline_max == 0) { + lfs->inline_max = lfs_min( + lfs->cfg->cache_size, + lfs_min( + lfs->attr_max, + ((lfs->cfg->metadata_max) + ? lfs->cfg->metadata_max + : lfs->cfg->block_size)/8)); + } + + // setup default state + lfs->root[0] = LFS_BLOCK_NULL; + lfs->root[1] = LFS_BLOCK_NULL; + lfs->mlist = NULL; + lfs->seed = 0; + lfs->gdisk = (lfs_gstate_t){0}; + lfs->gstate = (lfs_gstate_t){0}; + lfs->gdelta = (lfs_gstate_t){0}; +#ifdef LFS_MIGRATE + lfs->lfs1 = NULL; +#endif + + return 0; + +cleanup: + lfs_deinit(lfs); + return err; +} + +static int lfs_deinit(lfs_t *lfs) { + // free allocated memory + if (!lfs->cfg->read_buffer) { + lfs_free(lfs->rcache.buffer); + } + + if (!lfs->cfg->prog_buffer) { + lfs_free(lfs->pcache.buffer); + } + + if (!lfs->cfg->lookahead_buffer) { + lfs_free(lfs->lookahead.buffer); + } + + return 0; +} + + + +#ifndef LFS_READONLY +static int lfs_format_(lfs_t *lfs, const struct lfs_config *cfg) { + int err = 0; + { + err = lfs_init(lfs, cfg); + if (err) { + return err; + } + + LFS_ASSERT(cfg->block_count != 0); + + // create free lookahead + memset(lfs->lookahead.buffer, 0, lfs->cfg->lookahead_size); + lfs->lookahead.start = 0; + lfs->lookahead.size = lfs_min(8*lfs->cfg->lookahead_size, + lfs->block_count); + lfs->lookahead.next = 0; + lfs_alloc_ckpoint(lfs); + + // create root dir + lfs_mdir_t root; + err = lfs_dir_alloc(lfs, &root); + if (err) { + goto cleanup; + } + + // write one superblock + lfs_superblock_t superblock = { + .version = lfs_fs_disk_version(lfs), + .block_size = lfs->cfg->block_size, + .block_count = lfs->block_count, + .name_max = lfs->name_max, + .file_max = lfs->file_max, + .attr_max = lfs->attr_max, + }; + + lfs_superblock_tole32(&superblock); + err = lfs_dir_commit(lfs, &root, LFS_MKATTRS( + {LFS_MKTAG(LFS_TYPE_CREATE, 0, 0), NULL}, + {LFS_MKTAG(LFS_TYPE_SUPERBLOCK, 0, 8), "littlefs"}, + {LFS_MKTAG(LFS_TYPE_INLINESTRUCT, 0, sizeof(superblock)), + &superblock})); + if (err) { + goto cleanup; + } + + // force compaction to prevent accidentally mounting any + // older version of littlefs that may live on disk + root.erased = false; + err = lfs_dir_commit(lfs, &root, NULL, 0); + if (err) { + goto cleanup; + } + + // sanity check that fetch works + err = lfs_dir_fetch(lfs, &root, (const lfs_block_t[2]){0, 1}); + if (err) { + goto cleanup; + } + } + +cleanup: + lfs_deinit(lfs); + return err; + +} +#endif + +struct lfs_tortoise_t { + lfs_block_t pair[2]; + lfs_size_t i; + lfs_size_t period; +}; + +static int lfs_tortoise_detectcycles( + const lfs_mdir_t *dir, struct lfs_tortoise_t *tortoise) { + // detect cycles with Brent's algorithm + if (lfs_pair_issync(dir->tail, tortoise->pair)) { + LFS_WARN("Cycle detected in tail list"); + return LFS_ERR_CORRUPT; + } + if (tortoise->i == tortoise->period) { + tortoise->pair[0] = dir->tail[0]; + tortoise->pair[1] = dir->tail[1]; + tortoise->i = 0; + tortoise->period *= 2; + } + tortoise->i += 1; + + return LFS_ERR_OK; +} + +static int lfs_mount_(lfs_t *lfs, const struct lfs_config *cfg) { + int err = lfs_init(lfs, cfg); + if (err) { + return err; + } + + // scan directory blocks for superblock and any global updates + lfs_mdir_t dir = {.tail = {0, 1}}; + struct lfs_tortoise_t tortoise = { + .pair = {LFS_BLOCK_NULL, LFS_BLOCK_NULL}, + .i = 1, + .period = 1, + }; + while (!lfs_pair_isnull(dir.tail)) { + err = lfs_tortoise_detectcycles(&dir, &tortoise); + if (err < 0) { + goto cleanup; + } + + // fetch next block in tail list + lfs_stag_t tag = lfs_dir_fetchmatch(lfs, &dir, dir.tail, + LFS_MKTAG(0x7ff, 0x3ff, 0), + LFS_MKTAG(LFS_TYPE_SUPERBLOCK, 0, 8), + NULL, + lfs_dir_find_match, &(struct lfs_dir_find_match){ + lfs, "littlefs", 8}); + if (tag < 0) { + err = tag; + goto cleanup; + } + + // has superblock? + if (tag && !lfs_tag_isdelete(tag)) { + // update root + lfs->root[0] = dir.pair[0]; + lfs->root[1] = dir.pair[1]; + + // grab superblock + lfs_superblock_t superblock; + tag = lfs_dir_get(lfs, &dir, LFS_MKTAG(0x7ff, 0x3ff, 0), + LFS_MKTAG(LFS_TYPE_INLINESTRUCT, 0, sizeof(superblock)), + &superblock); + if (tag < 0) { + err = tag; + goto cleanup; + } + lfs_superblock_fromle32(&superblock); + + // check version + uint16_t major_version = (0xffff & (superblock.version >> 16)); + uint16_t minor_version = (0xffff & (superblock.version >> 0)); + if (major_version != lfs_fs_disk_version_major(lfs) + || minor_version > lfs_fs_disk_version_minor(lfs)) { + LFS_ERROR("Invalid version " + "v%"PRIu16".%"PRIu16" != v%"PRIu16".%"PRIu16, + major_version, + minor_version, + lfs_fs_disk_version_major(lfs), + lfs_fs_disk_version_minor(lfs)); + err = LFS_ERR_INVAL; + goto cleanup; + } + + // found older minor version? set an in-device only bit in the + // gstate so we know we need to rewrite the superblock before + // the first write + bool needssuperblock = false; + if (minor_version < lfs_fs_disk_version_minor(lfs)) { + LFS_DEBUG("Found older minor version " + "v%"PRIu16".%"PRIu16" < v%"PRIu16".%"PRIu16, + major_version, + minor_version, + lfs_fs_disk_version_major(lfs), + lfs_fs_disk_version_minor(lfs)); + needssuperblock = true; + } + // note this bit is reserved on disk, so fetching more gstate + // will not interfere here + lfs_fs_prepsuperblock(lfs, needssuperblock); + + // check superblock configuration + if (superblock.name_max) { + if (superblock.name_max > lfs->name_max) { + LFS_ERROR("Unsupported name_max (%"PRIu32" > %"PRIu32")", + superblock.name_max, lfs->name_max); + err = LFS_ERR_INVAL; + goto cleanup; + } + + lfs->name_max = superblock.name_max; + } + + if (superblock.file_max) { + if (superblock.file_max > lfs->file_max) { + LFS_ERROR("Unsupported file_max (%"PRIu32" > %"PRIu32")", + superblock.file_max, lfs->file_max); + err = LFS_ERR_INVAL; + goto cleanup; + } + + lfs->file_max = superblock.file_max; + } + + if (superblock.attr_max) { + if (superblock.attr_max > lfs->attr_max) { + LFS_ERROR("Unsupported attr_max (%"PRIu32" > %"PRIu32")", + superblock.attr_max, lfs->attr_max); + err = LFS_ERR_INVAL; + goto cleanup; + } + + lfs->attr_max = superblock.attr_max; + + // we also need to update inline_max in case attr_max changed + lfs->inline_max = lfs_min(lfs->inline_max, lfs->attr_max); + } + + // this is where we get the block_count from disk if block_count=0 + if (lfs->cfg->block_count + && superblock.block_count != lfs->cfg->block_count) { + LFS_ERROR("Invalid block count (%"PRIu32" != %"PRIu32")", + superblock.block_count, lfs->cfg->block_count); + err = LFS_ERR_INVAL; + goto cleanup; + } + + lfs->block_count = superblock.block_count; + + if (superblock.block_size != lfs->cfg->block_size) { + LFS_ERROR("Invalid block size (%"PRIu32" != %"PRIu32")", + superblock.block_size, lfs->cfg->block_size); + err = LFS_ERR_INVAL; + goto cleanup; + } + } + + // has gstate? + err = lfs_dir_getgstate(lfs, &dir, &lfs->gstate); + if (err) { + goto cleanup; + } + } + + // update littlefs with gstate + if (!lfs_gstate_iszero(&lfs->gstate)) { + LFS_DEBUG("Found pending gstate 0x%08"PRIx32"%08"PRIx32"%08"PRIx32, + lfs->gstate.tag, + lfs->gstate.pair[0], + lfs->gstate.pair[1]); + } + lfs->gstate.tag += !lfs_tag_isvalid(lfs->gstate.tag); + lfs->gdisk = lfs->gstate; + + // setup free lookahead, to distribute allocations uniformly across + // boots, we start the allocator at a random location + lfs->lookahead.start = lfs->seed % lfs->block_count; + lfs_alloc_drop(lfs); + + return 0; + +cleanup: + lfs_unmount_(lfs); + return err; +} + +static int lfs_unmount_(lfs_t *lfs) { + return lfs_deinit(lfs); +} + + +/// Filesystem filesystem operations /// +static int lfs_fs_stat_(lfs_t *lfs, struct lfs_fsinfo *fsinfo) { + // if the superblock is up-to-date, we must be on the most recent + // minor version of littlefs + if (!lfs_gstate_needssuperblock(&lfs->gstate)) { + fsinfo->disk_version = lfs_fs_disk_version(lfs); + + // otherwise we need to read the minor version on disk + } else { + // fetch the superblock + lfs_mdir_t dir; + int err = lfs_dir_fetch(lfs, &dir, lfs->root); + if (err) { + return err; + } + + lfs_superblock_t superblock; + lfs_stag_t tag = lfs_dir_get(lfs, &dir, LFS_MKTAG(0x7ff, 0x3ff, 0), + LFS_MKTAG(LFS_TYPE_INLINESTRUCT, 0, sizeof(superblock)), + &superblock); + if (tag < 0) { + return tag; + } + lfs_superblock_fromle32(&superblock); + + // read the on-disk version + fsinfo->disk_version = superblock.version; + } + + // filesystem geometry + fsinfo->block_size = lfs->cfg->block_size; + fsinfo->block_count = lfs->block_count; + + // other on-disk configuration, we cache all of these for internal use + fsinfo->name_max = lfs->name_max; + fsinfo->file_max = lfs->file_max; + fsinfo->attr_max = lfs->attr_max; + + return 0; +} + +int lfs_fs_traverse_(lfs_t *lfs, + int (*cb)(void *data, lfs_block_t block), void *data, + bool includeorphans) { + // iterate over metadata pairs + lfs_mdir_t dir = {.tail = {0, 1}}; + +#ifdef LFS_MIGRATE + // also consider v1 blocks during migration + if (lfs->lfs1) { + int err = lfs1_traverse(lfs, cb, data); + if (err) { + return err; + } + + dir.tail[0] = lfs->root[0]; + dir.tail[1] = lfs->root[1]; + } +#endif + + struct lfs_tortoise_t tortoise = { + .pair = {LFS_BLOCK_NULL, LFS_BLOCK_NULL}, + .i = 1, + .period = 1, + }; + int err = LFS_ERR_OK; + while (!lfs_pair_isnull(dir.tail)) { + err = lfs_tortoise_detectcycles(&dir, &tortoise); + if (err < 0) { + return LFS_ERR_CORRUPT; + } + + for (int i = 0; i < 2; i++) { + int err = cb(data, dir.tail[i]); + if (err) { + return err; + } + } + + // iterate through ids in directory + int err = lfs_dir_fetch(lfs, &dir, dir.tail); + if (err) { + return err; + } + + for (uint16_t id = 0; id < dir.count; id++) { + struct lfs_ctz ctz; + lfs_stag_t tag = lfs_dir_get(lfs, &dir, LFS_MKTAG(0x700, 0x3ff, 0), + LFS_MKTAG(LFS_TYPE_STRUCT, id, sizeof(ctz)), &ctz); + if (tag < 0) { + if (tag == LFS_ERR_NOENT) { + continue; + } + return tag; + } + lfs_ctz_fromle32(&ctz); + + if (lfs_tag_type3(tag) == LFS_TYPE_CTZSTRUCT) { + err = lfs_ctz_traverse(lfs, NULL, &lfs->rcache, + ctz.head, ctz.size, cb, data); + if (err) { + return err; + } + } else if (includeorphans && + lfs_tag_type3(tag) == LFS_TYPE_DIRSTRUCT) { + for (int i = 0; i < 2; i++) { + err = cb(data, (&ctz.head)[i]); + if (err) { + return err; + } + } + } + } + } + +#ifndef LFS_READONLY + // iterate over any open files + for (lfs_file_t *f = (lfs_file_t*)lfs->mlist; f; f = f->next) { + if (f->type != LFS_TYPE_REG) { + continue; + } + + if (((f->flags & LFS_F_DIRTY) || (f->flags & LFS_F_DUSTY)) + && !(f->flags & LFS_F_INLINE)) { + int err = lfs_ctz_traverse(lfs, &f->cache, &lfs->rcache, + f->ctz.head, f->ctz.size, cb, data); + if (err) { + return err; + } + } + + if ((f->flags & LFS_F_WRITING) && !(f->flags & LFS_F_INLINE)) { + int err = lfs_ctz_traverse(lfs, &f->cache, &lfs->rcache, + f->block, f->pos, cb, data); + if (err) { + return err; + } + } + } +#endif + + return 0; +} + +#ifndef LFS_READONLY +static int lfs_fs_pred(lfs_t *lfs, + const lfs_block_t pair[2], lfs_mdir_t *pdir) { + // iterate over all directory directory entries + pdir->tail[0] = 0; + pdir->tail[1] = 1; + struct lfs_tortoise_t tortoise = { + .pair = {LFS_BLOCK_NULL, LFS_BLOCK_NULL}, + .i = 1, + .period = 1, + }; + int err = LFS_ERR_OK; + while (!lfs_pair_isnull(pdir->tail)) { + err = lfs_tortoise_detectcycles(pdir, &tortoise); + if (err < 0) { + return LFS_ERR_CORRUPT; + } + + if (lfs_pair_cmp(pdir->tail, pair) == 0) { + return 0; + } + + int err = lfs_dir_fetch(lfs, pdir, pdir->tail); + if (err) { + return err; + } + } + + return LFS_ERR_NOENT; +} +#endif + +#ifndef LFS_READONLY +struct lfs_fs_parent_match { + lfs_t *lfs; + const lfs_block_t pair[2]; +}; +#endif + +#ifndef LFS_READONLY +static int lfs_fs_parent_match(void *data, + lfs_tag_t tag, const void *buffer) { + struct lfs_fs_parent_match *find = data; + lfs_t *lfs = find->lfs; + const struct lfs_diskoff *disk = buffer; + (void)tag; + + lfs_block_t child[2]; + int err = lfs_bd_read(lfs, + &lfs->pcache, &lfs->rcache, lfs->cfg->block_size, + disk->block, disk->off, &child, sizeof(child)); + if (err) { + return err; + } + + lfs_pair_fromle32(child); + return (lfs_pair_cmp(child, find->pair) == 0) ? LFS_CMP_EQ : LFS_CMP_LT; +} +#endif + +#ifndef LFS_READONLY +static lfs_stag_t lfs_fs_parent(lfs_t *lfs, const lfs_block_t pair[2], + lfs_mdir_t *parent) { + // use fetchmatch with callback to find pairs + parent->tail[0] = 0; + parent->tail[1] = 1; + struct lfs_tortoise_t tortoise = { + .pair = {LFS_BLOCK_NULL, LFS_BLOCK_NULL}, + .i = 1, + .period = 1, + }; + int err = LFS_ERR_OK; + while (!lfs_pair_isnull(parent->tail)) { + err = lfs_tortoise_detectcycles(parent, &tortoise); + if (err < 0) { + return err; + } + + lfs_stag_t tag = lfs_dir_fetchmatch(lfs, parent, parent->tail, + LFS_MKTAG(0x7ff, 0, 0x3ff), + LFS_MKTAG(LFS_TYPE_DIRSTRUCT, 0, 8), + NULL, + lfs_fs_parent_match, &(struct lfs_fs_parent_match){ + lfs, {pair[0], pair[1]}}); + if (tag && tag != LFS_ERR_NOENT) { + return tag; + } + } + + return LFS_ERR_NOENT; +} +#endif + +static void lfs_fs_prepsuperblock(lfs_t *lfs, bool needssuperblock) { + lfs->gstate.tag = (lfs->gstate.tag & ~LFS_MKTAG(0, 0, 0x200)) + | (uint32_t)needssuperblock << 9; +} + +#ifndef LFS_READONLY +static int lfs_fs_preporphans(lfs_t *lfs, int8_t orphans) { + LFS_ASSERT(lfs_tag_size(lfs->gstate.tag) > 0x000 || orphans >= 0); + LFS_ASSERT(lfs_tag_size(lfs->gstate.tag) < 0x1ff || orphans <= 0); + lfs->gstate.tag += orphans; + lfs->gstate.tag = ((lfs->gstate.tag & ~LFS_MKTAG(0x800, 0, 0)) | + ((uint32_t)lfs_gstate_hasorphans(&lfs->gstate) << 31)); + + return 0; +} +#endif + +#ifndef LFS_READONLY +static void lfs_fs_prepmove(lfs_t *lfs, + uint16_t id, const lfs_block_t pair[2]) { + lfs->gstate.tag = ((lfs->gstate.tag & ~LFS_MKTAG(0x7ff, 0x3ff, 0)) | + ((id != 0x3ff) ? LFS_MKTAG(LFS_TYPE_DELETE, id, 0) : 0)); + lfs->gstate.pair[0] = (id != 0x3ff) ? pair[0] : 0; + lfs->gstate.pair[1] = (id != 0x3ff) ? pair[1] : 0; +} +#endif + +#ifndef LFS_READONLY +static int lfs_fs_desuperblock(lfs_t *lfs) { + if (!lfs_gstate_needssuperblock(&lfs->gstate)) { + return 0; + } + + LFS_DEBUG("Rewriting superblock {0x%"PRIx32", 0x%"PRIx32"}", + lfs->root[0], + lfs->root[1]); + + lfs_mdir_t root; + int err = lfs_dir_fetch(lfs, &root, lfs->root); + if (err) { + return err; + } + + // write a new superblock + lfs_superblock_t superblock = { + .version = lfs_fs_disk_version(lfs), + .block_size = lfs->cfg->block_size, + .block_count = lfs->block_count, + .name_max = lfs->name_max, + .file_max = lfs->file_max, + .attr_max = lfs->attr_max, + }; + + lfs_superblock_tole32(&superblock); + err = lfs_dir_commit(lfs, &root, LFS_MKATTRS( + {LFS_MKTAG(LFS_TYPE_INLINESTRUCT, 0, sizeof(superblock)), + &superblock})); + if (err) { + return err; + } + + lfs_fs_prepsuperblock(lfs, false); + return 0; +} +#endif + +#ifndef LFS_READONLY +static int lfs_fs_demove(lfs_t *lfs) { + if (!lfs_gstate_hasmove(&lfs->gdisk)) { + return 0; + } + + // Fix bad moves + LFS_DEBUG("Fixing move {0x%"PRIx32", 0x%"PRIx32"} 0x%"PRIx16, + lfs->gdisk.pair[0], + lfs->gdisk.pair[1], + lfs_tag_id(lfs->gdisk.tag)); + + // no other gstate is supported at this time, so if we found something else + // something most likely went wrong in gstate calculation + LFS_ASSERT(lfs_tag_type3(lfs->gdisk.tag) == LFS_TYPE_DELETE); + + // fetch and delete the moved entry + lfs_mdir_t movedir; + int err = lfs_dir_fetch(lfs, &movedir, lfs->gdisk.pair); + if (err) { + return err; + } + + // prep gstate and delete move id + uint16_t moveid = lfs_tag_id(lfs->gdisk.tag); + lfs_fs_prepmove(lfs, 0x3ff, NULL); + err = lfs_dir_commit(lfs, &movedir, LFS_MKATTRS( + {LFS_MKTAG(LFS_TYPE_DELETE, moveid, 0), NULL})); + if (err) { + return err; + } + + return 0; +} +#endif + +#ifndef LFS_READONLY +static int lfs_fs_deorphan(lfs_t *lfs, bool powerloss) { + if (!lfs_gstate_hasorphans(&lfs->gstate)) { + return 0; + } + + // Check for orphans in two separate passes: + // - 1 for half-orphans (relocations) + // - 2 for full-orphans (removes/renames) + // + // Two separate passes are needed as half-orphans can contain outdated + // references to full-orphans, effectively hiding them from the deorphan + // search. + // + int pass = 0; + while (pass < 2) { + // Fix any orphans + lfs_mdir_t pdir = {.split = true, .tail = {0, 1}}; + lfs_mdir_t dir; + bool moreorphans = false; + + // iterate over all directory directory entries + while (!lfs_pair_isnull(pdir.tail)) { + int err = lfs_dir_fetch(lfs, &dir, pdir.tail); + if (err) { + return err; + } + + // check head blocks for orphans + if (!pdir.split) { + // check if we have a parent + lfs_mdir_t parent; + lfs_stag_t tag = lfs_fs_parent(lfs, pdir.tail, &parent); + if (tag < 0 && tag != LFS_ERR_NOENT) { + return tag; + } + + if (pass == 0 && tag != LFS_ERR_NOENT) { + lfs_block_t pair[2]; + lfs_stag_t state = lfs_dir_get(lfs, &parent, + LFS_MKTAG(0x7ff, 0x3ff, 0), tag, pair); + if (state < 0) { + return state; + } + lfs_pair_fromle32(pair); + + if (!lfs_pair_issync(pair, pdir.tail)) { + // we have desynced + LFS_DEBUG("Fixing half-orphan " + "{0x%"PRIx32", 0x%"PRIx32"} " + "-> {0x%"PRIx32", 0x%"PRIx32"}", + pdir.tail[0], pdir.tail[1], pair[0], pair[1]); + + // fix pending move in this pair? this looks like an + // optimization but is in fact _required_ since + // relocating may outdate the move. + uint16_t moveid = 0x3ff; + if (lfs_gstate_hasmovehere(&lfs->gstate, pdir.pair)) { + moveid = lfs_tag_id(lfs->gstate.tag); + LFS_DEBUG("Fixing move while fixing orphans " + "{0x%"PRIx32", 0x%"PRIx32"} 0x%"PRIx16"\n", + pdir.pair[0], pdir.pair[1], moveid); + lfs_fs_prepmove(lfs, 0x3ff, NULL); + } + + lfs_pair_tole32(pair); + state = lfs_dir_orphaningcommit(lfs, &pdir, LFS_MKATTRS( + {LFS_MKTAG_IF(moveid != 0x3ff, + LFS_TYPE_DELETE, moveid, 0), NULL}, + {LFS_MKTAG(LFS_TYPE_SOFTTAIL, 0x3ff, 8), + pair})); + lfs_pair_fromle32(pair); + if (state < 0) { + return state; + } + + // did our commit create more orphans? + if (state == LFS_OK_ORPHANED) { + moreorphans = true; + } + + // refetch tail + continue; + } + } + + // note we only check for full orphans if we may have had a + // power-loss, otherwise orphans are created intentionally + // during operations such as lfs_mkdir + if (pass == 1 && tag == LFS_ERR_NOENT && powerloss) { + // we are an orphan + LFS_DEBUG("Fixing orphan {0x%"PRIx32", 0x%"PRIx32"}", + pdir.tail[0], pdir.tail[1]); + + // steal state + err = lfs_dir_getgstate(lfs, &dir, &lfs->gdelta); + if (err) { + return err; + } + + // steal tail + lfs_pair_tole32(dir.tail); + int state = lfs_dir_orphaningcommit(lfs, &pdir, LFS_MKATTRS( + {LFS_MKTAG(LFS_TYPE_TAIL + dir.split, 0x3ff, 8), + dir.tail})); + lfs_pair_fromle32(dir.tail); + if (state < 0) { + return state; + } + + // did our commit create more orphans? + if (state == LFS_OK_ORPHANED) { + moreorphans = true; + } + + // refetch tail + continue; + } + } + + pdir = dir; + } + + pass = moreorphans ? 0 : pass+1; + } + + // mark orphans as fixed + return lfs_fs_preporphans(lfs, -lfs_gstate_getorphans(&lfs->gstate)); +} +#endif + +#ifndef LFS_READONLY +static int lfs_fs_forceconsistency(lfs_t *lfs) { + int err = lfs_fs_desuperblock(lfs); + if (err) { + return err; + } + + err = lfs_fs_demove(lfs); + if (err) { + return err; + } + + err = lfs_fs_deorphan(lfs, true); + if (err) { + return err; + } + + return 0; +} +#endif + +#ifndef LFS_READONLY +static int lfs_fs_mkconsistent_(lfs_t *lfs) { + // lfs_fs_forceconsistency does most of the work here + int err = lfs_fs_forceconsistency(lfs); + if (err) { + return err; + } + + // do we have any pending gstate? + lfs_gstate_t delta = {0}; + lfs_gstate_xor(&delta, &lfs->gdisk); + lfs_gstate_xor(&delta, &lfs->gstate); + if (!lfs_gstate_iszero(&delta)) { + // lfs_dir_commit will implicitly write out any pending gstate + lfs_mdir_t root; + err = lfs_dir_fetch(lfs, &root, lfs->root); + if (err) { + return err; + } + + err = lfs_dir_commit(lfs, &root, NULL, 0); + if (err) { + return err; + } + } + + return 0; +} +#endif + +static int lfs_fs_size_count(void *p, lfs_block_t block) { + (void)block; + lfs_size_t *size = p; + *size += 1; + return 0; +} + +static lfs_ssize_t lfs_fs_size_(lfs_t *lfs) { + lfs_size_t size = 0; + int err = lfs_fs_traverse_(lfs, lfs_fs_size_count, &size, false); + if (err) { + return err; + } + + return size; +} + +// explicit garbage collection +#ifndef LFS_READONLY +static int lfs_fs_gc_(lfs_t *lfs) { + // force consistency, even if we're not necessarily going to write, + // because this function is supposed to take care of janitorial work + // isn't it? + int err = lfs_fs_forceconsistency(lfs); + if (err) { + return err; + } + + // try to compact metadata pairs, note we can't really accomplish + // anything if compact_thresh doesn't at least leave a prog_size + // available + if (lfs->cfg->compact_thresh + < lfs->cfg->block_size - lfs->cfg->prog_size) { + // iterate over all mdirs + lfs_mdir_t mdir = {.tail = {0, 1}}; + while (!lfs_pair_isnull(mdir.tail)) { + err = lfs_dir_fetch(lfs, &mdir, mdir.tail); + if (err) { + return err; + } + + // not erased? exceeds our compaction threshold? + if (!mdir.erased || ((lfs->cfg->compact_thresh == 0) + ? mdir.off > lfs->cfg->block_size - lfs->cfg->block_size/8 + : mdir.off > lfs->cfg->compact_thresh)) { + // the easiest way to trigger a compaction is to mark + // the mdir as unerased and add an empty commit + mdir.erased = false; + err = lfs_dir_commit(lfs, &mdir, NULL, 0); + if (err) { + return err; + } + } + } + } + + // try to populate the lookahead buffer, unless it's already full + if (lfs->lookahead.size < lfs_min( + 8 * lfs->cfg->lookahead_size, + lfs->block_count)) { + err = lfs_alloc_scan(lfs); + if (err) { + return err; + } + } + + return 0; +} +#endif + +#ifndef LFS_READONLY +#ifdef LFS_SHRINKNONRELOCATING +static int lfs_shrink_checkblock(void *data, lfs_block_t block) { + lfs_size_t threshold = *((lfs_size_t*)data); + if (block >= threshold) { + return LFS_ERR_NOTEMPTY; + } + return 0; +} +#endif + +static int lfs_fs_grow_(lfs_t *lfs, lfs_size_t block_count) { + int err; + + if (block_count == lfs->block_count) { + return 0; + } + + +#ifndef LFS_SHRINKNONRELOCATING + // shrinking is not supported + LFS_ASSERT(block_count >= lfs->block_count); +#endif +#ifdef LFS_SHRINKNONRELOCATING + if (block_count < lfs->block_count) { + err = lfs_fs_traverse_(lfs, lfs_shrink_checkblock, &block_count, true); + if (err) { + return err; + } + } +#endif + + lfs->block_count = block_count; + + // fetch the root + lfs_mdir_t root; + err = lfs_dir_fetch(lfs, &root, lfs->root); + if (err) { + return err; + } + + // update the superblock + lfs_superblock_t superblock; + lfs_stag_t tag = lfs_dir_get(lfs, &root, LFS_MKTAG(0x7ff, 0x3ff, 0), + LFS_MKTAG(LFS_TYPE_INLINESTRUCT, 0, sizeof(superblock)), + &superblock); + if (tag < 0) { + return tag; + } + lfs_superblock_fromle32(&superblock); + + superblock.block_count = lfs->block_count; + + lfs_superblock_tole32(&superblock); + err = lfs_dir_commit(lfs, &root, LFS_MKATTRS( + {tag, &superblock})); + if (err) { + return err; + } + return 0; +} +#endif + +#ifdef LFS_MIGRATE +////// Migration from littelfs v1 below this ////// + +/// Version info /// + +// Software library version +// Major (top-nibble), incremented on backwards incompatible changes +// Minor (bottom-nibble), incremented on feature additions +#define LFS1_VERSION 0x00010007 +#define LFS1_VERSION_MAJOR (0xffff & (LFS1_VERSION >> 16)) +#define LFS1_VERSION_MINOR (0xffff & (LFS1_VERSION >> 0)) + +// Version of On-disk data structures +// Major (top-nibble), incremented on backwards incompatible changes +// Minor (bottom-nibble), incremented on feature additions +#define LFS1_DISK_VERSION 0x00010001 +#define LFS1_DISK_VERSION_MAJOR (0xffff & (LFS1_DISK_VERSION >> 16)) +#define LFS1_DISK_VERSION_MINOR (0xffff & (LFS1_DISK_VERSION >> 0)) + + +/// v1 Definitions /// + +// File types +enum lfs1_type { + LFS1_TYPE_REG = 0x11, + LFS1_TYPE_DIR = 0x22, + LFS1_TYPE_SUPERBLOCK = 0x2e, +}; + +typedef struct lfs1 { + lfs_block_t root[2]; +} lfs1_t; + +typedef struct lfs1_entry { + lfs_off_t off; + + struct lfs1_disk_entry { + uint8_t type; + uint8_t elen; + uint8_t alen; + uint8_t nlen; + union { + struct { + lfs_block_t head; + lfs_size_t size; + } file; + lfs_block_t dir[2]; + } u; + } d; +} lfs1_entry_t; + +typedef struct lfs1_dir { + struct lfs1_dir *next; + lfs_block_t pair[2]; + lfs_off_t off; + + lfs_block_t head[2]; + lfs_off_t pos; + + struct lfs1_disk_dir { + uint32_t rev; + lfs_size_t size; + lfs_block_t tail[2]; + } d; +} lfs1_dir_t; + +typedef struct lfs1_superblock { + lfs_off_t off; + + struct lfs1_disk_superblock { + uint8_t type; + uint8_t elen; + uint8_t alen; + uint8_t nlen; + lfs_block_t root[2]; + uint32_t block_size; + uint32_t block_count; + uint32_t version; + char magic[8]; + } d; +} lfs1_superblock_t; + + +/// Low-level wrappers v1->v2 /// +static void lfs1_crc(uint32_t *crc, const void *buffer, size_t size) { + *crc = lfs_crc(*crc, buffer, size); +} + +static int lfs1_bd_read(lfs_t *lfs, lfs_block_t block, + lfs_off_t off, void *buffer, lfs_size_t size) { + // if we ever do more than writes to alternating pairs, + // this may need to consider pcache + return lfs_bd_read(lfs, &lfs->pcache, &lfs->rcache, size, + block, off, buffer, size); +} + +static int lfs1_bd_crc(lfs_t *lfs, lfs_block_t block, + lfs_off_t off, lfs_size_t size, uint32_t *crc) { + for (lfs_off_t i = 0; i < size; i++) { + uint8_t c; + int err = lfs1_bd_read(lfs, block, off+i, &c, 1); + if (err) { + return err; + } + + lfs1_crc(crc, &c, 1); + } + + return 0; +} + + +/// Endian swapping functions /// +static void lfs1_dir_fromle32(struct lfs1_disk_dir *d) { + d->rev = lfs_fromle32(d->rev); + d->size = lfs_fromle32(d->size); + d->tail[0] = lfs_fromle32(d->tail[0]); + d->tail[1] = lfs_fromle32(d->tail[1]); +} + +static void lfs1_dir_tole32(struct lfs1_disk_dir *d) { + d->rev = lfs_tole32(d->rev); + d->size = lfs_tole32(d->size); + d->tail[0] = lfs_tole32(d->tail[0]); + d->tail[1] = lfs_tole32(d->tail[1]); +} + +static void lfs1_entry_fromle32(struct lfs1_disk_entry *d) { + d->u.dir[0] = lfs_fromle32(d->u.dir[0]); + d->u.dir[1] = lfs_fromle32(d->u.dir[1]); +} + +static void lfs1_entry_tole32(struct lfs1_disk_entry *d) { + d->u.dir[0] = lfs_tole32(d->u.dir[0]); + d->u.dir[1] = lfs_tole32(d->u.dir[1]); +} + +static void lfs1_superblock_fromle32(struct lfs1_disk_superblock *d) { + d->root[0] = lfs_fromle32(d->root[0]); + d->root[1] = lfs_fromle32(d->root[1]); + d->block_size = lfs_fromle32(d->block_size); + d->block_count = lfs_fromle32(d->block_count); + d->version = lfs_fromle32(d->version); +} + + +///// Metadata pair and directory operations /// +static inline lfs_size_t lfs1_entry_size(const lfs1_entry_t *entry) { + return 4 + entry->d.elen + entry->d.alen + entry->d.nlen; +} + +static int lfs1_dir_fetch(lfs_t *lfs, + lfs1_dir_t *dir, const lfs_block_t pair[2]) { + // copy out pair, otherwise may be aliasing dir + const lfs_block_t tpair[2] = {pair[0], pair[1]}; + bool valid = false; + + // check both blocks for the most recent revision + for (int i = 0; i < 2; i++) { + struct lfs1_disk_dir test; + int err = lfs1_bd_read(lfs, tpair[i], 0, &test, sizeof(test)); + lfs1_dir_fromle32(&test); + if (err) { + if (err == LFS_ERR_CORRUPT) { + continue; + } + return err; + } + + if (valid && lfs_scmp(test.rev, dir->d.rev) < 0) { + continue; + } + + if ((0x7fffffff & test.size) < sizeof(test)+4 || + (0x7fffffff & test.size) > lfs->cfg->block_size) { + continue; + } + + uint32_t crc = 0xffffffff; + lfs1_dir_tole32(&test); + lfs1_crc(&crc, &test, sizeof(test)); + lfs1_dir_fromle32(&test); + err = lfs1_bd_crc(lfs, tpair[i], sizeof(test), + (0x7fffffff & test.size) - sizeof(test), &crc); + if (err) { + if (err == LFS_ERR_CORRUPT) { + continue; + } + return err; + } + + if (crc != 0) { + continue; + } + + valid = true; + + // setup dir in case it's valid + dir->pair[0] = tpair[(i+0) % 2]; + dir->pair[1] = tpair[(i+1) % 2]; + dir->off = sizeof(dir->d); + dir->d = test; + } + + if (!valid) { + LFS_ERROR("Corrupted dir pair at {0x%"PRIx32", 0x%"PRIx32"}", + tpair[0], tpair[1]); + return LFS_ERR_CORRUPT; + } + + return 0; +} + +static int lfs1_dir_next(lfs_t *lfs, lfs1_dir_t *dir, lfs1_entry_t *entry) { + while (dir->off + sizeof(entry->d) > (0x7fffffff & dir->d.size)-4) { + if (!(0x80000000 & dir->d.size)) { + entry->off = dir->off; + return LFS_ERR_NOENT; + } + + int err = lfs1_dir_fetch(lfs, dir, dir->d.tail); + if (err) { + return err; + } + + dir->off = sizeof(dir->d); + dir->pos += sizeof(dir->d) + 4; + } + + int err = lfs1_bd_read(lfs, dir->pair[0], dir->off, + &entry->d, sizeof(entry->d)); + lfs1_entry_fromle32(&entry->d); + if (err) { + return err; + } + + entry->off = dir->off; + dir->off += lfs1_entry_size(entry); + dir->pos += lfs1_entry_size(entry); + return 0; +} + +/// littlefs v1 specific operations /// +int lfs1_traverse(lfs_t *lfs, int (*cb)(void*, lfs_block_t), void *data) { + if (lfs_pair_isnull(lfs->lfs1->root)) { + return 0; + } + + // iterate over metadata pairs + lfs1_dir_t dir; + lfs1_entry_t entry; + lfs_block_t cwd[2] = {0, 1}; + + while (true) { + for (int i = 0; i < 2; i++) { + int err = cb(data, cwd[i]); + if (err) { + return err; + } + } + + int err = lfs1_dir_fetch(lfs, &dir, cwd); + if (err) { + return err; + } + + // iterate over contents + while (dir.off + sizeof(entry.d) <= (0x7fffffff & dir.d.size)-4) { + err = lfs1_bd_read(lfs, dir.pair[0], dir.off, + &entry.d, sizeof(entry.d)); + lfs1_entry_fromle32(&entry.d); + if (err) { + return err; + } + + dir.off += lfs1_entry_size(&entry); + if ((0x70 & entry.d.type) == (0x70 & LFS1_TYPE_REG)) { + err = lfs_ctz_traverse(lfs, NULL, &lfs->rcache, + entry.d.u.file.head, entry.d.u.file.size, cb, data); + if (err) { + return err; + } + } + } + + // we also need to check if we contain a threaded v2 directory + lfs_mdir_t dir2 = {.split=true, .tail={cwd[0], cwd[1]}}; + while (dir2.split) { + err = lfs_dir_fetch(lfs, &dir2, dir2.tail); + if (err) { + break; + } + + for (int i = 0; i < 2; i++) { + err = cb(data, dir2.pair[i]); + if (err) { + return err; + } + } + } + + cwd[0] = dir.d.tail[0]; + cwd[1] = dir.d.tail[1]; + + if (lfs_pair_isnull(cwd)) { + break; + } + } + + return 0; +} + +static int lfs1_moved(lfs_t *lfs, const void *e) { + if (lfs_pair_isnull(lfs->lfs1->root)) { + return 0; + } + + // skip superblock + lfs1_dir_t cwd; + int err = lfs1_dir_fetch(lfs, &cwd, (const lfs_block_t[2]){0, 1}); + if (err) { + return err; + } + + // iterate over all directory directory entries + lfs1_entry_t entry; + while (!lfs_pair_isnull(cwd.d.tail)) { + err = lfs1_dir_fetch(lfs, &cwd, cwd.d.tail); + if (err) { + return err; + } + + while (true) { + err = lfs1_dir_next(lfs, &cwd, &entry); + if (err && err != LFS_ERR_NOENT) { + return err; + } + + if (err == LFS_ERR_NOENT) { + break; + } + + if (!(0x80 & entry.d.type) && + memcmp(&entry.d.u, e, sizeof(entry.d.u)) == 0) { + return true; + } + } + } + + return false; +} + +/// Filesystem operations /// +static int lfs1_mount(lfs_t *lfs, struct lfs1 *lfs1, + const struct lfs_config *cfg) { + int err = 0; + { + err = lfs_init(lfs, cfg); + if (err) { + return err; + } + + lfs->lfs1 = lfs1; + lfs->lfs1->root[0] = LFS_BLOCK_NULL; + lfs->lfs1->root[1] = LFS_BLOCK_NULL; + + // setup free lookahead + lfs->lookahead.start = 0; + lfs->lookahead.size = 0; + lfs->lookahead.next = 0; + lfs_alloc_ckpoint(lfs); + + // load superblock + lfs1_dir_t dir; + lfs1_superblock_t superblock; + err = lfs1_dir_fetch(lfs, &dir, (const lfs_block_t[2]){0, 1}); + if (err && err != LFS_ERR_CORRUPT) { + goto cleanup; + } + + if (!err) { + err = lfs1_bd_read(lfs, dir.pair[0], sizeof(dir.d), + &superblock.d, sizeof(superblock.d)); + lfs1_superblock_fromle32(&superblock.d); + if (err) { + goto cleanup; + } + + lfs->lfs1->root[0] = superblock.d.root[0]; + lfs->lfs1->root[1] = superblock.d.root[1]; + } + + if (err || memcmp(superblock.d.magic, "littlefs", 8) != 0) { + LFS_ERROR("Invalid superblock at {0x%"PRIx32", 0x%"PRIx32"}", + 0, 1); + err = LFS_ERR_CORRUPT; + goto cleanup; + } + + uint16_t major_version = (0xffff & (superblock.d.version >> 16)); + uint16_t minor_version = (0xffff & (superblock.d.version >> 0)); + if ((major_version != LFS1_DISK_VERSION_MAJOR || + minor_version > LFS1_DISK_VERSION_MINOR)) { + LFS_ERROR("Invalid version v%d.%d", major_version, minor_version); + err = LFS_ERR_INVAL; + goto cleanup; + } + + return 0; + } + +cleanup: + lfs_deinit(lfs); + return err; +} + +static int lfs1_unmount(lfs_t *lfs) { + return lfs_deinit(lfs); +} + +/// v1 migration /// +static int lfs_migrate_(lfs_t *lfs, const struct lfs_config *cfg) { + struct lfs1 lfs1; + + // Indeterminate filesystem size not allowed for migration. + LFS_ASSERT(cfg->block_count != 0); + + int err = lfs1_mount(lfs, &lfs1, cfg); + if (err) { + return err; + } + + { + // iterate through each directory, copying over entries + // into new directory + lfs1_dir_t dir1; + lfs_mdir_t dir2; + dir1.d.tail[0] = lfs->lfs1->root[0]; + dir1.d.tail[1] = lfs->lfs1->root[1]; + while (!lfs_pair_isnull(dir1.d.tail)) { + // iterate old dir + err = lfs1_dir_fetch(lfs, &dir1, dir1.d.tail); + if (err) { + goto cleanup; + } + + // create new dir and bind as temporary pretend root + err = lfs_dir_alloc(lfs, &dir2); + if (err) { + goto cleanup; + } + + dir2.rev = dir1.d.rev; + dir1.head[0] = dir1.pair[0]; + dir1.head[1] = dir1.pair[1]; + lfs->root[0] = dir2.pair[0]; + lfs->root[1] = dir2.pair[1]; + + err = lfs_dir_commit(lfs, &dir2, NULL, 0); + if (err) { + goto cleanup; + } + + while (true) { + lfs1_entry_t entry1; + err = lfs1_dir_next(lfs, &dir1, &entry1); + if (err && err != LFS_ERR_NOENT) { + goto cleanup; + } + + if (err == LFS_ERR_NOENT) { + break; + } + + // check that entry has not been moved + if (entry1.d.type & 0x80) { + int moved = lfs1_moved(lfs, &entry1.d.u); + if (moved < 0) { + err = moved; + goto cleanup; + } + + if (moved) { + continue; + } + + entry1.d.type &= ~0x80; + } + + // also fetch name + char name[LFS_NAME_MAX+1]; + memset(name, 0, sizeof(name)); + err = lfs1_bd_read(lfs, dir1.pair[0], + entry1.off + 4+entry1.d.elen+entry1.d.alen, + name, entry1.d.nlen); + if (err) { + goto cleanup; + } + + bool isdir = (entry1.d.type == LFS1_TYPE_DIR); + + // create entry in new dir + err = lfs_dir_fetch(lfs, &dir2, lfs->root); + if (err) { + goto cleanup; + } + + uint16_t id; + err = lfs_dir_find(lfs, &dir2, &(const char*){name}, &id); + if (!(err == LFS_ERR_NOENT && id != 0x3ff)) { + err = (err < 0) ? err : LFS_ERR_EXIST; + goto cleanup; + } + + lfs1_entry_tole32(&entry1.d); + err = lfs_dir_commit(lfs, &dir2, LFS_MKATTRS( + {LFS_MKTAG(LFS_TYPE_CREATE, id, 0), NULL}, + {LFS_MKTAG_IF_ELSE(isdir, + LFS_TYPE_DIR, id, entry1.d.nlen, + LFS_TYPE_REG, id, entry1.d.nlen), + name}, + {LFS_MKTAG_IF_ELSE(isdir, + LFS_TYPE_DIRSTRUCT, id, sizeof(entry1.d.u), + LFS_TYPE_CTZSTRUCT, id, sizeof(entry1.d.u)), + &entry1.d.u})); + lfs1_entry_fromle32(&entry1.d); + if (err) { + goto cleanup; + } + } + + if (!lfs_pair_isnull(dir1.d.tail)) { + // find last block and update tail to thread into fs + err = lfs_dir_fetch(lfs, &dir2, lfs->root); + if (err) { + goto cleanup; + } + + while (dir2.split) { + err = lfs_dir_fetch(lfs, &dir2, dir2.tail); + if (err) { + goto cleanup; + } + } + + lfs_pair_tole32(dir2.pair); + err = lfs_dir_commit(lfs, &dir2, LFS_MKATTRS( + {LFS_MKTAG(LFS_TYPE_SOFTTAIL, 0x3ff, 8), dir1.d.tail})); + lfs_pair_fromle32(dir2.pair); + if (err) { + goto cleanup; + } + } + + // Copy over first block to thread into fs. Unfortunately + // if this fails there is not much we can do. + LFS_DEBUG("Migrating {0x%"PRIx32", 0x%"PRIx32"} " + "-> {0x%"PRIx32", 0x%"PRIx32"}", + lfs->root[0], lfs->root[1], dir1.head[0], dir1.head[1]); + + err = lfs_bd_erase(lfs, dir1.head[1]); + if (err) { + goto cleanup; + } + + err = lfs_dir_fetch(lfs, &dir2, lfs->root); + if (err) { + goto cleanup; + } + + for (lfs_off_t i = 0; i < dir2.off; i++) { + uint8_t dat; + err = lfs_bd_read(lfs, + NULL, &lfs->rcache, dir2.off, + dir2.pair[0], i, &dat, 1); + if (err) { + goto cleanup; + } + + err = lfs_bd_prog(lfs, + &lfs->pcache, &lfs->rcache, true, + dir1.head[1], i, &dat, 1); + if (err) { + goto cleanup; + } + } + + err = lfs_bd_flush(lfs, &lfs->pcache, &lfs->rcache, true); + if (err) { + goto cleanup; + } + } + + // Create new superblock. This marks a successful migration! + err = lfs1_dir_fetch(lfs, &dir1, (const lfs_block_t[2]){0, 1}); + if (err) { + goto cleanup; + } + + dir2.pair[0] = dir1.pair[0]; + dir2.pair[1] = dir1.pair[1]; + dir2.rev = dir1.d.rev; + dir2.off = sizeof(dir2.rev); + dir2.etag = 0xffffffff; + dir2.count = 0; + dir2.tail[0] = lfs->lfs1->root[0]; + dir2.tail[1] = lfs->lfs1->root[1]; + dir2.erased = false; + dir2.split = true; + + lfs_superblock_t superblock = { + .version = LFS_DISK_VERSION, + .block_size = lfs->cfg->block_size, + .block_count = lfs->cfg->block_count, + .name_max = lfs->name_max, + .file_max = lfs->file_max, + .attr_max = lfs->attr_max, + }; + + lfs_superblock_tole32(&superblock); + err = lfs_dir_commit(lfs, &dir2, LFS_MKATTRS( + {LFS_MKTAG(LFS_TYPE_CREATE, 0, 0), NULL}, + {LFS_MKTAG(LFS_TYPE_SUPERBLOCK, 0, 8), "littlefs"}, + {LFS_MKTAG(LFS_TYPE_INLINESTRUCT, 0, sizeof(superblock)), + &superblock})); + if (err) { + goto cleanup; + } + + // sanity check that fetch works + err = lfs_dir_fetch(lfs, &dir2, (const lfs_block_t[2]){0, 1}); + if (err) { + goto cleanup; + } + + // force compaction to prevent accidentally mounting v1 + dir2.erased = false; + err = lfs_dir_commit(lfs, &dir2, NULL, 0); + if (err) { + goto cleanup; + } + } + +cleanup: + lfs1_unmount(lfs); + return err; +} + +#endif + + +/// Public API wrappers /// + +// Here we can add tracing/thread safety easily + +// Thread-safe wrappers if enabled +#ifdef LFS_THREADSAFE +#define LFS_LOCK(cfg) cfg->lock(cfg) +#define LFS_UNLOCK(cfg) cfg->unlock(cfg) +#else +#define LFS_LOCK(cfg) ((void)cfg, 0) +#define LFS_UNLOCK(cfg) ((void)cfg) +#endif + +// Public API +#ifndef LFS_READONLY +int lfs_format(lfs_t *lfs, const struct lfs_config *cfg) { + int err = LFS_LOCK(cfg); + if (err) { + return err; + } + LFS_TRACE("lfs_format(%p, %p {.context=%p, " + ".read=%p, .prog=%p, .erase=%p, .sync=%p, " + ".read_size=%"PRIu32", .prog_size=%"PRIu32", " + ".block_size=%"PRIu32", .block_count=%"PRIu32", " + ".block_cycles=%"PRId32", .cache_size=%"PRIu32", " + ".lookahead_size=%"PRIu32", .read_buffer=%p, " + ".prog_buffer=%p, .lookahead_buffer=%p, " + ".name_max=%"PRIu32", .file_max=%"PRIu32", " + ".attr_max=%"PRIu32"})", + (void*)lfs, (void*)cfg, cfg->context, + (void*)(uintptr_t)cfg->read, (void*)(uintptr_t)cfg->prog, + (void*)(uintptr_t)cfg->erase, (void*)(uintptr_t)cfg->sync, + cfg->read_size, cfg->prog_size, cfg->block_size, cfg->block_count, + cfg->block_cycles, cfg->cache_size, cfg->lookahead_size, + cfg->read_buffer, cfg->prog_buffer, cfg->lookahead_buffer, + cfg->name_max, cfg->file_max, cfg->attr_max); + + err = lfs_format_(lfs, cfg); + + LFS_TRACE("lfs_format -> %d", err); + LFS_UNLOCK(cfg); + return err; +} +#endif + +int lfs_mount(lfs_t *lfs, const struct lfs_config *cfg) { + int err = LFS_LOCK(cfg); + if (err) { + return err; + } + LFS_TRACE("lfs_mount(%p, %p {.context=%p, " + ".read=%p, .prog=%p, .erase=%p, .sync=%p, " + ".read_size=%"PRIu32", .prog_size=%"PRIu32", " + ".block_size=%"PRIu32", .block_count=%"PRIu32", " + ".block_cycles=%"PRId32", .cache_size=%"PRIu32", " + ".lookahead_size=%"PRIu32", .read_buffer=%p, " + ".prog_buffer=%p, .lookahead_buffer=%p, " + ".name_max=%"PRIu32", .file_max=%"PRIu32", " + ".attr_max=%"PRIu32"})", + (void*)lfs, (void*)cfg, cfg->context, + (void*)(uintptr_t)cfg->read, (void*)(uintptr_t)cfg->prog, + (void*)(uintptr_t)cfg->erase, (void*)(uintptr_t)cfg->sync, + cfg->read_size, cfg->prog_size, cfg->block_size, cfg->block_count, + cfg->block_cycles, cfg->cache_size, cfg->lookahead_size, + cfg->read_buffer, cfg->prog_buffer, cfg->lookahead_buffer, + cfg->name_max, cfg->file_max, cfg->attr_max); + + err = lfs_mount_(lfs, cfg); + + LFS_TRACE("lfs_mount -> %d", err); + LFS_UNLOCK(cfg); + return err; +} + +int lfs_unmount(lfs_t *lfs) { + int err = LFS_LOCK(lfs->cfg); + if (err) { + return err; + } + LFS_TRACE("lfs_unmount(%p)", (void*)lfs); + + err = lfs_unmount_(lfs); + + LFS_TRACE("lfs_unmount -> %d", err); + LFS_UNLOCK(lfs->cfg); + return err; +} + +#ifndef LFS_READONLY +int lfs_remove(lfs_t *lfs, const char *path) { + int err = LFS_LOCK(lfs->cfg); + if (err) { + return err; + } + LFS_TRACE("lfs_remove(%p, \"%s\")", (void*)lfs, path); + + err = lfs_remove_(lfs, path); + + LFS_TRACE("lfs_remove -> %d", err); + LFS_UNLOCK(lfs->cfg); + return err; +} +#endif + +#ifndef LFS_READONLY +int lfs_rename(lfs_t *lfs, const char *oldpath, const char *newpath) { + int err = LFS_LOCK(lfs->cfg); + if (err) { + return err; + } + LFS_TRACE("lfs_rename(%p, \"%s\", \"%s\")", (void*)lfs, oldpath, newpath); + + err = lfs_rename_(lfs, oldpath, newpath); + + LFS_TRACE("lfs_rename -> %d", err); + LFS_UNLOCK(lfs->cfg); + return err; +} +#endif + +int lfs_stat(lfs_t *lfs, const char *path, struct lfs_info *info) { + int err = LFS_LOCK(lfs->cfg); + if (err) { + return err; + } + LFS_TRACE("lfs_stat(%p, \"%s\", %p)", (void*)lfs, path, (void*)info); + + err = lfs_stat_(lfs, path, info); + + LFS_TRACE("lfs_stat -> %d", err); + LFS_UNLOCK(lfs->cfg); + return err; +} + +lfs_ssize_t lfs_getattr(lfs_t *lfs, const char *path, + uint8_t type, void *buffer, lfs_size_t size) { + int err = LFS_LOCK(lfs->cfg); + if (err) { + return err; + } + LFS_TRACE("lfs_getattr(%p, \"%s\", %"PRIu8", %p, %"PRIu32")", + (void*)lfs, path, type, buffer, size); + + lfs_ssize_t res = lfs_getattr_(lfs, path, type, buffer, size); + + LFS_TRACE("lfs_getattr -> %"PRId32, res); + LFS_UNLOCK(lfs->cfg); + return res; +} + +#ifndef LFS_READONLY +int lfs_setattr(lfs_t *lfs, const char *path, + uint8_t type, const void *buffer, lfs_size_t size) { + int err = LFS_LOCK(lfs->cfg); + if (err) { + return err; + } + LFS_TRACE("lfs_setattr(%p, \"%s\", %"PRIu8", %p, %"PRIu32")", + (void*)lfs, path, type, buffer, size); + + err = lfs_setattr_(lfs, path, type, buffer, size); + + LFS_TRACE("lfs_setattr -> %d", err); + LFS_UNLOCK(lfs->cfg); + return err; +} +#endif + +#ifndef LFS_READONLY +int lfs_removeattr(lfs_t *lfs, const char *path, uint8_t type) { + int err = LFS_LOCK(lfs->cfg); + if (err) { + return err; + } + LFS_TRACE("lfs_removeattr(%p, \"%s\", %"PRIu8")", (void*)lfs, path, type); + + err = lfs_removeattr_(lfs, path, type); + + LFS_TRACE("lfs_removeattr -> %d", err); + LFS_UNLOCK(lfs->cfg); + return err; +} +#endif + +#ifndef LFS_NO_MALLOC +int lfs_file_open(lfs_t *lfs, lfs_file_t *file, const char *path, int flags) { + int err = LFS_LOCK(lfs->cfg); + if (err) { + return err; + } + LFS_TRACE("lfs_file_open(%p, %p, \"%s\", %x)", + (void*)lfs, (void*)file, path, (unsigned)flags); + LFS_ASSERT(!lfs_mlist_isopen(lfs->mlist, (struct lfs_mlist*)file)); + + err = lfs_file_open_(lfs, file, path, flags); + + LFS_TRACE("lfs_file_open -> %d", err); + LFS_UNLOCK(lfs->cfg); + return err; +} +#endif + +int lfs_file_opencfg(lfs_t *lfs, lfs_file_t *file, + const char *path, int flags, + const struct lfs_file_config *cfg) { + int err = LFS_LOCK(lfs->cfg); + if (err) { + return err; + } + LFS_TRACE("lfs_file_opencfg(%p, %p, \"%s\", %x, %p {" + ".buffer=%p, .attrs=%p, .attr_count=%"PRIu32"})", + (void*)lfs, (void*)file, path, (unsigned)flags, + (void*)cfg, cfg->buffer, (void*)cfg->attrs, cfg->attr_count); + LFS_ASSERT(!lfs_mlist_isopen(lfs->mlist, (struct lfs_mlist*)file)); + + err = lfs_file_opencfg_(lfs, file, path, flags, cfg); + + LFS_TRACE("lfs_file_opencfg -> %d", err); + LFS_UNLOCK(lfs->cfg); + return err; +} + +int lfs_file_close(lfs_t *lfs, lfs_file_t *file) { + int err = LFS_LOCK(lfs->cfg); + if (err) { + return err; + } + LFS_TRACE("lfs_file_close(%p, %p)", (void*)lfs, (void*)file); + LFS_ASSERT(lfs_mlist_isopen(lfs->mlist, (struct lfs_mlist*)file)); + + err = lfs_file_close_(lfs, file); + + LFS_TRACE("lfs_file_close -> %d", err); + LFS_UNLOCK(lfs->cfg); + return err; +} + +#ifndef LFS_READONLY +int lfs_file_sync(lfs_t *lfs, lfs_file_t *file) { + int err = LFS_LOCK(lfs->cfg); + if (err) { + return err; + } + LFS_TRACE("lfs_file_sync(%p, %p)", (void*)lfs, (void*)file); + LFS_ASSERT(lfs_mlist_isopen(lfs->mlist, (struct lfs_mlist*)file)); + + err = lfs_file_sync_(lfs, file); + + LFS_TRACE("lfs_file_sync -> %d", err); + LFS_UNLOCK(lfs->cfg); + return err; +} +#endif + +lfs_ssize_t lfs_file_read(lfs_t *lfs, lfs_file_t *file, + void *buffer, lfs_size_t size) { + int err = LFS_LOCK(lfs->cfg); + if (err) { + return err; + } + LFS_TRACE("lfs_file_read(%p, %p, %p, %"PRIu32")", + (void*)lfs, (void*)file, buffer, size); + LFS_ASSERT(lfs_mlist_isopen(lfs->mlist, (struct lfs_mlist*)file)); + + lfs_ssize_t res = lfs_file_read_(lfs, file, buffer, size); + + LFS_TRACE("lfs_file_read -> %"PRId32, res); + LFS_UNLOCK(lfs->cfg); + return res; +} + +#ifndef LFS_READONLY +lfs_ssize_t lfs_file_write(lfs_t *lfs, lfs_file_t *file, + const void *buffer, lfs_size_t size) { + int err = LFS_LOCK(lfs->cfg); + if (err) { + return err; + } + LFS_TRACE("lfs_file_write(%p, %p, %p, %"PRIu32")", + (void*)lfs, (void*)file, buffer, size); + LFS_ASSERT(lfs_mlist_isopen(lfs->mlist, (struct lfs_mlist*)file)); + + lfs_ssize_t res = lfs_file_write_(lfs, file, buffer, size); + + LFS_TRACE("lfs_file_write -> %"PRId32, res); + LFS_UNLOCK(lfs->cfg); + return res; +} +#endif + +lfs_soff_t lfs_file_seek(lfs_t *lfs, lfs_file_t *file, + lfs_soff_t off, int whence) { + int err = LFS_LOCK(lfs->cfg); + if (err) { + return err; + } + LFS_TRACE("lfs_file_seek(%p, %p, %"PRId32", %d)", + (void*)lfs, (void*)file, off, whence); + LFS_ASSERT(lfs_mlist_isopen(lfs->mlist, (struct lfs_mlist*)file)); + + lfs_soff_t res = lfs_file_seek_(lfs, file, off, whence); + + LFS_TRACE("lfs_file_seek -> %"PRId32, res); + LFS_UNLOCK(lfs->cfg); + return res; +} + +#ifndef LFS_READONLY +int lfs_file_truncate(lfs_t *lfs, lfs_file_t *file, lfs_off_t size) { + int err = LFS_LOCK(lfs->cfg); + if (err) { + return err; + } + LFS_TRACE("lfs_file_truncate(%p, %p, %"PRIu32")", + (void*)lfs, (void*)file, size); + LFS_ASSERT(lfs_mlist_isopen(lfs->mlist, (struct lfs_mlist*)file)); + + err = lfs_file_truncate_(lfs, file, size); + + LFS_TRACE("lfs_file_truncate -> %d", err); + LFS_UNLOCK(lfs->cfg); + return err; +} +#endif + +lfs_soff_t lfs_file_tell(lfs_t *lfs, lfs_file_t *file) { + int err = LFS_LOCK(lfs->cfg); + if (err) { + return err; + } + LFS_TRACE("lfs_file_tell(%p, %p)", (void*)lfs, (void*)file); + LFS_ASSERT(lfs_mlist_isopen(lfs->mlist, (struct lfs_mlist*)file)); + + lfs_soff_t res = lfs_file_tell_(lfs, file); + + LFS_TRACE("lfs_file_tell -> %"PRId32, res); + LFS_UNLOCK(lfs->cfg); + return res; +} + +int lfs_file_rewind(lfs_t *lfs, lfs_file_t *file) { + int err = LFS_LOCK(lfs->cfg); + if (err) { + return err; + } + LFS_TRACE("lfs_file_rewind(%p, %p)", (void*)lfs, (void*)file); + + err = lfs_file_rewind_(lfs, file); + + LFS_TRACE("lfs_file_rewind -> %d", err); + LFS_UNLOCK(lfs->cfg); + return err; +} + +lfs_soff_t lfs_file_size(lfs_t *lfs, lfs_file_t *file) { + int err = LFS_LOCK(lfs->cfg); + if (err) { + return err; + } + LFS_TRACE("lfs_file_size(%p, %p)", (void*)lfs, (void*)file); + LFS_ASSERT(lfs_mlist_isopen(lfs->mlist, (struct lfs_mlist*)file)); + + lfs_soff_t res = lfs_file_size_(lfs, file); + + LFS_TRACE("lfs_file_size -> %"PRIu32, res); + LFS_UNLOCK(lfs->cfg); + return res; +} + +#ifndef LFS_READONLY +int lfs_mkdir(lfs_t *lfs, const char *path) { + int err = LFS_LOCK(lfs->cfg); + if (err) { + return err; + } + LFS_TRACE("lfs_mkdir(%p, \"%s\")", (void*)lfs, path); + + err = lfs_mkdir_(lfs, path); + + LFS_TRACE("lfs_mkdir -> %d", err); + LFS_UNLOCK(lfs->cfg); + return err; +} +#endif + +int lfs_dir_open(lfs_t *lfs, lfs_dir_t *dir, const char *path) { + int err = LFS_LOCK(lfs->cfg); + if (err) { + return err; + } + LFS_TRACE("lfs_dir_open(%p, %p, \"%s\")", (void*)lfs, (void*)dir, path); + LFS_ASSERT(!lfs_mlist_isopen(lfs->mlist, (struct lfs_mlist*)dir)); + + err = lfs_dir_open_(lfs, dir, path); + + LFS_TRACE("lfs_dir_open -> %d", err); + LFS_UNLOCK(lfs->cfg); + return err; +} + +int lfs_dir_close(lfs_t *lfs, lfs_dir_t *dir) { + int err = LFS_LOCK(lfs->cfg); + if (err) { + return err; + } + LFS_TRACE("lfs_dir_close(%p, %p)", (void*)lfs, (void*)dir); + + err = lfs_dir_close_(lfs, dir); + + LFS_TRACE("lfs_dir_close -> %d", err); + LFS_UNLOCK(lfs->cfg); + return err; +} + +int lfs_dir_read(lfs_t *lfs, lfs_dir_t *dir, struct lfs_info *info) { + int err = LFS_LOCK(lfs->cfg); + if (err) { + return err; + } + LFS_TRACE("lfs_dir_read(%p, %p, %p)", + (void*)lfs, (void*)dir, (void*)info); + + err = lfs_dir_read_(lfs, dir, info); + + LFS_TRACE("lfs_dir_read -> %d", err); + LFS_UNLOCK(lfs->cfg); + return err; +} + +int lfs_dir_seek(lfs_t *lfs, lfs_dir_t *dir, lfs_off_t off) { + int err = LFS_LOCK(lfs->cfg); + if (err) { + return err; + } + LFS_TRACE("lfs_dir_seek(%p, %p, %"PRIu32")", + (void*)lfs, (void*)dir, off); + + err = lfs_dir_seek_(lfs, dir, off); + + LFS_TRACE("lfs_dir_seek -> %d", err); + LFS_UNLOCK(lfs->cfg); + return err; +} + +lfs_soff_t lfs_dir_tell(lfs_t *lfs, lfs_dir_t *dir) { + int err = LFS_LOCK(lfs->cfg); + if (err) { + return err; + } + LFS_TRACE("lfs_dir_tell(%p, %p)", (void*)lfs, (void*)dir); + + lfs_soff_t res = lfs_dir_tell_(lfs, dir); + + LFS_TRACE("lfs_dir_tell -> %"PRId32, res); + LFS_UNLOCK(lfs->cfg); + return res; +} + +int lfs_dir_rewind(lfs_t *lfs, lfs_dir_t *dir) { + int err = LFS_LOCK(lfs->cfg); + if (err) { + return err; + } + LFS_TRACE("lfs_dir_rewind(%p, %p)", (void*)lfs, (void*)dir); + + err = lfs_dir_rewind_(lfs, dir); + + LFS_TRACE("lfs_dir_rewind -> %d", err); + LFS_UNLOCK(lfs->cfg); + return err; +} + +int lfs_fs_stat(lfs_t *lfs, struct lfs_fsinfo *fsinfo) { + int err = LFS_LOCK(lfs->cfg); + if (err) { + return err; + } + LFS_TRACE("lfs_fs_stat(%p, %p)", (void*)lfs, (void*)fsinfo); + + err = lfs_fs_stat_(lfs, fsinfo); + + LFS_TRACE("lfs_fs_stat -> %d", err); + LFS_UNLOCK(lfs->cfg); + return err; +} + +lfs_ssize_t lfs_fs_size(lfs_t *lfs) { + int err = LFS_LOCK(lfs->cfg); + if (err) { + return err; + } + LFS_TRACE("lfs_fs_size(%p)", (void*)lfs); + + lfs_ssize_t res = lfs_fs_size_(lfs); + + LFS_TRACE("lfs_fs_size -> %"PRId32, res); + LFS_UNLOCK(lfs->cfg); + return res; +} + +int lfs_fs_traverse(lfs_t *lfs, int (*cb)(void *, lfs_block_t), void *data) { + int err = LFS_LOCK(lfs->cfg); + if (err) { + return err; + } + LFS_TRACE("lfs_fs_traverse(%p, %p, %p)", + (void*)lfs, (void*)(uintptr_t)cb, data); + + err = lfs_fs_traverse_(lfs, cb, data, true); + + LFS_TRACE("lfs_fs_traverse -> %d", err); + LFS_UNLOCK(lfs->cfg); + return err; +} + +#ifndef LFS_READONLY +int lfs_fs_mkconsistent(lfs_t *lfs) { + int err = LFS_LOCK(lfs->cfg); + if (err) { + return err; + } + LFS_TRACE("lfs_fs_mkconsistent(%p)", (void*)lfs); + + err = lfs_fs_mkconsistent_(lfs); + + LFS_TRACE("lfs_fs_mkconsistent -> %d", err); + LFS_UNLOCK(lfs->cfg); + return err; +} +#endif + +#ifndef LFS_READONLY +int lfs_fs_gc(lfs_t *lfs) { + int err = LFS_LOCK(lfs->cfg); + if (err) { + return err; + } + LFS_TRACE("lfs_fs_gc(%p)", (void*)lfs); + + err = lfs_fs_gc_(lfs); + + LFS_TRACE("lfs_fs_gc -> %d", err); + LFS_UNLOCK(lfs->cfg); + return err; +} +#endif + +#ifndef LFS_READONLY +int lfs_fs_grow(lfs_t *lfs, lfs_size_t block_count) { + int err = LFS_LOCK(lfs->cfg); + if (err) { + return err; + } + LFS_TRACE("lfs_fs_grow(%p, %"PRIu32")", (void*)lfs, block_count); + + err = lfs_fs_grow_(lfs, block_count); + + LFS_TRACE("lfs_fs_grow -> %d", err); + LFS_UNLOCK(lfs->cfg); + return err; +} +#endif + +#ifdef LFS_MIGRATE +int lfs_migrate(lfs_t *lfs, const struct lfs_config *cfg) { + int err = LFS_LOCK(cfg); + if (err) { + return err; + } + LFS_TRACE("lfs_migrate(%p, %p {.context=%p, " + ".read=%p, .prog=%p, .erase=%p, .sync=%p, " + ".read_size=%"PRIu32", .prog_size=%"PRIu32", " + ".block_size=%"PRIu32", .block_count=%"PRIu32", " + ".block_cycles=%"PRId32", .cache_size=%"PRIu32", " + ".lookahead_size=%"PRIu32", .read_buffer=%p, " + ".prog_buffer=%p, .lookahead_buffer=%p, " + ".name_max=%"PRIu32", .file_max=%"PRIu32", " + ".attr_max=%"PRIu32"})", + (void*)lfs, (void*)cfg, cfg->context, + (void*)(uintptr_t)cfg->read, (void*)(uintptr_t)cfg->prog, + (void*)(uintptr_t)cfg->erase, (void*)(uintptr_t)cfg->sync, + cfg->read_size, cfg->prog_size, cfg->block_size, cfg->block_count, + cfg->block_cycles, cfg->cache_size, cfg->lookahead_size, + cfg->read_buffer, cfg->prog_buffer, cfg->lookahead_buffer, + cfg->name_max, cfg->file_max, cfg->attr_max); + + err = lfs_migrate_(lfs, cfg); + + LFS_TRACE("lfs_migrate -> %d", err); + LFS_UNLOCK(cfg); + return err; +} +#endif + diff --git a/components/littlefs/src/lfs_util.c b/components/littlefs/src/lfs_util.c new file mode 100644 index 00000000..dac72abc --- /dev/null +++ b/components/littlefs/src/lfs_util.c @@ -0,0 +1,37 @@ +/* + * lfs util functions + * + * Copyright (c) 2022, The littlefs authors. + * Copyright (c) 2017, Arm Limited. All rights reserved. + * SPDX-License-Identifier: BSD-3-Clause + */ +#include "lfs_util.h" + +// Only compile if user does not provide custom config +#ifndef LFS_CONFIG + + +// If user provides their own CRC impl we don't need this +#ifndef LFS_CRC +// Software CRC implementation with small lookup table +uint32_t lfs_crc(uint32_t crc, const void *buffer, size_t size) { + static const uint32_t rtable[16] = { + 0x00000000, 0x1db71064, 0x3b6e20c8, 0x26d930ac, + 0x76dc4190, 0x6b6b51f4, 0x4db26158, 0x5005713c, + 0xedb88320, 0xf00f9344, 0xd6d6a3e8, 0xcb61b38c, + 0x9b64c2b0, 0x86d3d2d4, 0xa00ae278, 0xbdbdf21c, + }; + + const uint8_t *data = buffer; + + for (size_t i = 0; i < size; i++) { + crc = (crc >> 4) ^ rtable[(crc ^ (data[i] >> 0)) & 0xf]; + crc = (crc >> 4) ^ rtable[(crc ^ (data[i] >> 4)) & 0xf]; + } + + return crc; +} +#endif + + +#endif diff --git a/components/logging/CMakeLists.txt b/components/logging/CMakeLists.txt new file mode 100644 index 00000000..2752ca50 --- /dev/null +++ b/components/logging/CMakeLists.txt @@ -0,0 +1,16 @@ +# OpenShock logging + panic macros — OS_LOG*, OS_PANIC*. +# +# openshock_log_printf (Logging.cpp) writes OS_LOG* output to the low-level `serial` +# transport (raw bytes to the console port), not C stdio. The OS_PANIC* macros +# (folded in from the former `panic` component) restart the device, so Logging.h +# pulls in esp_ota_ops / esp_system / freertos and is firmware-only. + +idf_component_register( + SRCS "src/Logging.cpp" + INCLUDE_DIRS "include" + REQUIRES temporal # Temporal.h -> OpenShock::millis() (public header) + esp_system # esp_restart (OS_PANIC* in public header) + app_update # esp_ota_mark_app_invalid_rollback_and_reboot (OS_PANIC_OTA) + freertos # vTaskDelay (OS_PANIC / OS_PANIC_OTA) + PRIV_REQUIRES serial # Serial::Write (Logging.cpp only) +) diff --git a/include/Logging.h b/components/logging/include/Logging.h similarity index 67% rename from include/Logging.h rename to components/logging/include/Logging.h index 2f2aa38f..4a74e932 100644 --- a/include/Logging.h +++ b/components/logging/include/Logging.h @@ -1,14 +1,20 @@ #pragma once -#include "Core.h" +#include "Temporal.h" -#include - -#include #include #include -extern "C" int log_printf(const char* fmt, ...); +#include +#include + +// openshock_log_printf forwards to vprintf (the IDF console: UART0 + any secondary +// USB-Serial-JTAG/CDC output). Defined in Logging.cpp so the logging path carries +// no hardware include of its own. The OS_PANIC* macros below, however, pull in +// esp_ota_ops / esp_system / freertos, so this header is firmware-only (host tests +// shadow it with a no-op stub). +// Named distinctly from the framework's own log_printf to avoid a symbol clash. +extern "C" int openshock_log_printf(const char* fmt, ...); template constexpr const char* openshockPathToFileName(const char (&path)[N]) @@ -33,7 +39,7 @@ constexpr const char* openshockPathToFileName(const char (&path)[N]) #define OPENSHOCK_LOG_FORMAT(letter, format) "[%lli][" #letter "][%s:%u] %s(): " format "\r\n", OpenShock::millis(), openshockPathToFileName(__FILE__), __LINE__, __FUNCTION__ #if OPENSHOCK_LOG_LEVEL >= OPENSHOCK_LOG_LEVEL_VERBOSE -#define OS_LOGV(TAG, format, ...) log_printf(OPENSHOCK_LOG_FORMAT(V, "[%s] " format), TAG, ##__VA_ARGS__) +#define OS_LOGV(TAG, format, ...) openshock_log_printf(OPENSHOCK_LOG_FORMAT(V, "[%s] " format), TAG, ##__VA_ARGS__) #else #define OS_LOGV(TAG, format, ...) \ do { \ @@ -41,7 +47,7 @@ constexpr const char* openshockPathToFileName(const char (&path)[N]) #endif #if OPENSHOCK_LOG_LEVEL >= OPENSHOCK_LOG_LEVEL_DEBUG -#define OS_LOGD(TAG, format, ...) log_printf(OPENSHOCK_LOG_FORMAT(D, "[%s] " format), TAG, ##__VA_ARGS__) +#define OS_LOGD(TAG, format, ...) openshock_log_printf(OPENSHOCK_LOG_FORMAT(D, "[%s] " format), TAG, ##__VA_ARGS__) #else #define OS_LOGD(TAG, format, ...) \ do { \ @@ -49,7 +55,7 @@ constexpr const char* openshockPathToFileName(const char (&path)[N]) #endif #if OPENSHOCK_LOG_LEVEL >= OPENSHOCK_LOG_LEVEL_INFO -#define OS_LOGI(TAG, format, ...) log_printf(OPENSHOCK_LOG_FORMAT(I, "[%s] " format), TAG, ##__VA_ARGS__) +#define OS_LOGI(TAG, format, ...) openshock_log_printf(OPENSHOCK_LOG_FORMAT(I, "[%s] " format), TAG, ##__VA_ARGS__) #else #define OS_LOGI(TAG, format, ...) \ do { \ @@ -57,7 +63,7 @@ constexpr const char* openshockPathToFileName(const char (&path)[N]) #endif #if OPENSHOCK_LOG_LEVEL >= OPENSHOCK_LOG_LEVEL_WARN -#define OS_LOGW(TAG, format, ...) log_printf(OPENSHOCK_LOG_FORMAT(W, "[%s] " format), TAG, ##__VA_ARGS__) +#define OS_LOGW(TAG, format, ...) openshock_log_printf(OPENSHOCK_LOG_FORMAT(W, "[%s] " format), TAG, ##__VA_ARGS__) #else #define OS_LOGW(TAG, format, ...) \ do { \ @@ -65,7 +71,7 @@ constexpr const char* openshockPathToFileName(const char (&path)[N]) #endif #if OPENSHOCK_LOG_LEVEL >= OPENSHOCK_LOG_LEVEL_ERROR -#define OS_LOGE(TAG, format, ...) log_printf(OPENSHOCK_LOG_FORMAT(E, "[%s] " format), TAG, ##__VA_ARGS__) +#define OS_LOGE(TAG, format, ...) openshock_log_printf(OPENSHOCK_LOG_FORMAT(E, "[%s] " format), TAG, ##__VA_ARGS__) #else #define OS_LOGE(TAG, format, ...) \ do { \ @@ -73,13 +79,16 @@ constexpr const char* openshockPathToFileName(const char (&path)[N]) #endif #if OPENSHOCK_LOG_LEVEL >= OPENSHOCK_LOG_LEVEL_NONE -#define OS_LOGN(TAG, format, ...) log_printf(OPENSHOCK_LOG_FORMAT(E, "[%s] " format), TAG, ##__VA_ARGS__) +#define OS_LOGN(TAG, format, ...) openshock_log_printf(OPENSHOCK_LOG_FORMAT(E, "[%s] " format), TAG, ##__VA_ARGS__) #else #define OS_LOGN(TAG, format, ...) \ do { \ } while (0) #endif +// Panic/abort macros. These log at error level, then restart the device; they +// pull in esp_ota_ops / esp_system / freertos (included above), so anything that +// includes Logging.h is firmware-only. #define OS_PANIC_PRINT(TAG, format, ...) OS_LOGE(TAG, "PANIC: " format, ##__VA_ARGS__) #define OS_PANIC(TAG, format, ...) \ diff --git a/components/logging/src/Logging.cpp b/components/logging/src/Logging.cpp new file mode 100644 index 00000000..3bc2ded8 --- /dev/null +++ b/components/logging/src/Logging.cpp @@ -0,0 +1,48 @@ +#include "Logging.h" + +#include "serial/Serial.h" + +#include +#include +#include + +// Forwards OpenShock's OS_LOG* output to the low-level serial transport: format +// the line into a buffer, then write the raw bytes to the console port. This +// bypasses C stdio (no vprintf/stdout); before the serial driver is installed, +// Serial::Write falls back to the ROM serial output so early-boot logs survive. +extern "C" int openshock_log_printf(const char* fmt, ...) +{ + char stackBuf[256]; + + va_list args; + va_start(args, fmt); + int len = vsnprintf(stackBuf, sizeof(stackBuf), fmt, args); + va_end(args); + + if (len <= 0) { + return len; + } + + // Fits the stack buffer: write it directly. + if (static_cast(len) < sizeof(stackBuf)) { + OpenShock::Serial::Write(reinterpret_cast(stackBuf), static_cast(len)); + return len; + } + + // Longer than the stack buffer (e.g. a config dump): format again into a heap + // buffer so the line isn't truncated. Fall back to the truncated stack buffer + // if the allocation fails. + char* heapBuf = static_cast(malloc(static_cast(len) + 1)); + if (heapBuf == nullptr) { + OpenShock::Serial::Write(reinterpret_cast(stackBuf), sizeof(stackBuf) - 1); + return static_cast(sizeof(stackBuf) - 1); + } + + va_start(args, fmt); + vsnprintf(heapBuf, static_cast(len) + 1, fmt, args); + va_end(args); + + OpenShock::Serial::Write(reinterpret_cast(heapBuf), static_cast(len)); + free(heapBuf); + return len; +} diff --git a/components/osjson/CMakeLists.txt b/components/osjson/CMakeLists.txt new file mode 100644 index 00000000..ef58553f --- /dev/null +++ b/components/osjson/CMakeLists.txt @@ -0,0 +1,17 @@ +# OpenShock JSON handling. +# +# Zero-copy parsing built on raw jsmn (values are std::string_view into the +# source buffer — no NUL-termination, no strlen) plus streaming generation via +# json_generator. Deliberately not named "json" to avoid shadowing IDF's +# built-in cJSON component. +# +# Public surface: +# json/Json.h — OpenShock::JSON::{JsonView, JsonDocument, StringWriter} + +idf_component_register( + SRCS "src/Json.cpp" + INCLUDE_DIRS "include" + REQUIRES espressif__jsmn # jsmn.h types in public header + espressif__json_generator # json_generator.h in public header + PRIV_REQUIRES common # OpenShock::Convert strict string->int (Json.cpp) +) diff --git a/components/osjson/host_test/CMakeLists.txt b/components/osjson/host_test/CMakeLists.txt new file mode 100644 index 00000000..726dbd0f --- /dev/null +++ b/components/osjson/host_test/CMakeLists.txt @@ -0,0 +1,14 @@ +cmake_minimum_required(VERSION 3.16) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) + +# Only build 'main' and its dependency closure (osjson + jsmn + json_generator). +set(COMPONENTS main) + +# Make the OpenShock components (osjson) discoverable, and use the linux-target +# freertos mock so app_main is provided on the host. +list(APPEND EXTRA_COMPONENT_DIRS + "${CMAKE_CURRENT_LIST_DIR}/../.." # firmware/components (contains osjson) + "$ENV{IDF_PATH}/tools/mocks/freertos/") + +project(osjson_host_test) diff --git a/components/osjson/host_test/main/CMakeLists.txt b/components/osjson/host_test/main/CMakeLists.txt new file mode 100644 index 00000000..7106f440 --- /dev/null +++ b/components/osjson/host_test/main/CMakeLists.txt @@ -0,0 +1,11 @@ +idf_component_register( + SRCS "test_json.cpp" # main() entry + smoke tests + "test_parse.cpp" # JsonDocument::parse + root() + "test_getters.cpp" # tryGetStr/Bool/I64/Double + predicates + "test_navigation.cpp" # operator[], at(), count(), raw(), skip() + "test_generate.cpp" # StringWriter / json_generator output + "test_stress.cpp" # scale, token-cap, deep nesting + INCLUDE_DIRS "." + REQUIRES unity osjson + WHOLE_ARCHIVE # keep the auto-registered TEST_CASEs from being stripped +) diff --git a/components/osjson/host_test/main/test_generate.cpp b/components/osjson/host_test/main/test_generate.cpp new file mode 100644 index 00000000..b566fbb8 --- /dev/null +++ b/components/osjson/host_test/main/test_generate.cpp @@ -0,0 +1,306 @@ +// StringWriter / json_generator output: shape, escaping, flush boundary. +#include "unity.h" + +#include "json/Json.h" + +#include +#include + +using namespace OpenShock; + +TEST_CASE("generate: empty object and empty array", "[osjson][gen]") +{ + { + JSON::StringWriter w; + json_gen_start_object(w.gen()); + json_gen_end_object(w.gen()); + TEST_ASSERT_TRUE(w.finish() == "{}"); + } + { + JSON::StringWriter w; + json_gen_start_array(w.gen()); + json_gen_end_array(w.gen()); + TEST_ASSERT_TRUE(w.finish() == "[]"); + } +} + +TEST_CASE("generate: flat object with mixed scalar types", "[osjson][gen]") +{ + JSON::StringWriter w; + json_gen_str_t* g = w.gen(); + json_gen_start_object(g); + json_gen_obj_set_string(g, "s", "hi"); + json_gen_obj_set_int(g, "n", -7); + json_gen_obj_set_bool(g, "b", true); + json_gen_obj_set_null(g, "z"); + json_gen_end_object(g); + + TEST_ASSERT_TRUE(w.finish() == R"({"s":"hi","n":-7,"b":true,"z":null})"); +} + +TEST_CASE("generate: nested objects and arrays", "[osjson][gen]") +{ + JSON::StringWriter w; + json_gen_str_t* g = w.gen(); + json_gen_start_object(g); + json_gen_push_object(g, "outer"); + json_gen_obj_set_int(g, "x", 1); + json_gen_push_array(g, "list"); + json_gen_arr_set_int(g, 10); + json_gen_arr_set_int(g, 20); + json_gen_pop_array(g); + json_gen_pop_object(g); + json_gen_end_object(g); + + TEST_ASSERT_TRUE(w.finish() == R"({"outer":{"x":1,"list":[10,20]}})"); +} + +TEST_CASE("generate: array of objects", "[osjson][gen]") +{ + JSON::StringWriter w; + json_gen_str_t* g = w.gen(); + json_gen_start_array(g); + for (int i = 1; i <= 3; ++i) { + json_gen_start_object(g); + json_gen_obj_set_int(g, "id", i); + json_gen_end_object(g); + } + json_gen_end_array(g); + + TEST_ASSERT_TRUE(w.finish() == R"([{"id":1},{"id":2},{"id":3}])"); +} + +// The RAW library call json_gen_obj_set_string writes VALUES verbatim between +// quotes - no escaping - so a value with '"', '\' or a control char yields +// INVALID JSON. The two tests below pin that unsafe behaviour so it stays +// visible; production code must use JSON::objSetString / JSON::arrSetString +// instead (see the "escapes ..." tests further down). +TEST_CASE("generate: raw json_gen_obj_set_string does NOT escape quotes/backslashes", "[osjson][gen][raw-unsafe]") +{ + JSON::StringWriter w; + json_gen_str_t* g = w.gen(); + json_gen_start_object(g); + json_gen_obj_set_string(g, "k", R"(a"b\c)"); // value bytes: a " b \ c + json_gen_end_object(g); + + // Verbatim passthrough -> the inner quote breaks the string. Invalid JSON. + TEST_ASSERT_TRUE(w.finish() == R"({"k":"a"b\c"})"); +} + +TEST_CASE("generate: raw json_gen_obj_set_string does NOT escape control chars", "[osjson][gen][raw-unsafe]") +{ + JSON::StringWriter w; + json_gen_str_t* g = w.gen(); + json_gen_start_object(g); + json_gen_obj_set_string(g, "k", "line1\nline2\ttab"); + json_gen_end_object(g); + + // Raw newline/tab bytes end up in the output (not \n / \t escapes). + TEST_ASSERT_TRUE(w.finish() == "{\"k\":\"line1\nline2\ttab\"}"); +} + +// ---- JSON::objSetString / arrSetString: the escaping wrappers ---- + +TEST_CASE("escape: objSetString escapes quotes and backslashes", "[osjson][gen][escape]") +{ + JSON::StringWriter w; + json_gen_str_t* g = w.gen(); + json_gen_start_object(g); + JSON::objSetString(g, "k", R"(a"b\c)"); // value bytes: a " b \ c + json_gen_end_object(g); + + std::string out = w.finish(); + TEST_ASSERT_TRUE(out == R"({"k":"a\"b\\c"})"); + + // and unlike the raw path, the escaped output is now valid JSON + JSON::JsonDocument doc; + TEST_ASSERT_TRUE(doc.parse(out)); + TEST_ASSERT_TRUE(doc.root().isObject()); +} + +TEST_CASE("escape: objSetString escapes the short-form control chars", "[osjson][gen][escape]") +{ + JSON::StringWriter w; + json_gen_str_t* g = w.gen(); + json_gen_start_object(g); + JSON::objSetString(g, "k", "\n\t\r\b\f"); // LF TAB CR BS FF + json_gen_end_object(g); + + TEST_ASSERT_TRUE(w.finish() == R"({"k":"\n\t\r\b\f"})"); +} + +TEST_CASE("escape: objSetString \\u-escapes other control chars", "[osjson][gen][escape]") +{ + std::string value; + value += '\x01'; + value += '\x1f'; + + JSON::StringWriter w; + json_gen_str_t* g = w.gen(); + json_gen_start_object(g); + JSON::objSetString(g, "k", value); + json_gen_end_object(g); + + TEST_ASSERT_TRUE(w.finish() == "{\"k\":\"\\u0001\\u001f\"}"); +} + +TEST_CASE("escape: RFC 8259 boundaries (NUL, solidus, DEL, 0x1F/0x20)", "[osjson][gen][escape]") +{ + // U+0000 must be escaped as 0000 (embedded NUL survives via string_view). + { + std::string v; + v += '\0'; + JSON::StringWriter w; + json_gen_str_t* g = w.gen(); + json_gen_start_object(g); + JSON::objSetString(g, "k", v); + json_gen_end_object(g); + TEST_ASSERT_TRUE(w.finish() == "{\"k\":\"\\u0000\"}"); + } + // '/' (0x2F) is NOT required to be escaped -> passthrough. + { + JSON::StringWriter w; + json_gen_str_t* g = w.gen(); + json_gen_start_object(g); + JSON::objSetString(g, "k", "a/b"); + json_gen_end_object(g); + TEST_ASSERT_TRUE(w.finish() == R"({"k":"a/b"})"); + } + // 0x1F is the last control char (escaped); 0x20 (space) and 0x7F (DEL) are + // both in the RFC `unescaped` set -> passthrough. + { + std::string v; + v += '\x1f'; + v += ' '; + v += '\x7f'; + JSON::StringWriter w; + json_gen_str_t* g = w.gen(); + json_gen_start_object(g); + JSON::objSetString(g, "k", v); + json_gen_end_object(g); + TEST_ASSERT_TRUE(w.finish() == "{\"k\":\"\\u001f \x7f\"}"); + } +} + +TEST_CASE("escape: plain and UTF-8 strings pass through unchanged", "[osjson][gen][escape]") +{ + { + JSON::StringWriter w; + json_gen_str_t* g = w.gen(); + json_gen_start_object(g); + JSON::objSetString(g, "k", "hello world"); + json_gen_end_object(g); + TEST_ASSERT_TRUE(w.finish() == R"({"k":"hello world"})"); + } + { + // UTF-8 multibyte (bytes >= 0x20) must not be touched. + JSON::StringWriter w; + json_gen_str_t* g = w.gen(); + json_gen_start_object(g); + JSON::objSetString(g, "k", "caf\xc3\xa9"); // "café" + json_gen_end_object(g); + TEST_ASSERT_TRUE(w.finish() == "{\"k\":\"caf\xc3\xa9\"}"); + } +} + +TEST_CASE("escape: arrSetString escapes array string elements", "[osjson][gen][escape]") +{ + JSON::StringWriter w; + json_gen_str_t* g = w.gen(); + json_gen_start_array(g); + JSON::arrSetString(g, R"(a"b)"); + JSON::arrSetString(g, "plain"); + json_gen_end_array(g); + + std::string out = w.finish(); + TEST_ASSERT_TRUE(out == R"(["a\"b","plain"])"); + + JSON::JsonDocument doc; + TEST_ASSERT_TRUE(doc.parse(out)); + TEST_ASSERT_EQUAL_INT(2, doc.root().count()); +} + +TEST_CASE("escape: a WiFi password with a quote serializes to valid JSON", "[osjson][gen][escape]") +{ + // The real-world motivation: a password containing a double quote. + JSON::StringWriter w; + json_gen_str_t* g = w.gen(); + json_gen_start_object(g); + JSON::objSetString(g, "ssid", "Home"); + JSON::objSetString(g, "password", R"(p@ss"word\)"); + json_gen_end_object(g); + std::string out = w.finish(); + + JSON::JsonDocument doc; + TEST_ASSERT_TRUE(doc.parse(out)); // would be malformed via the raw call + JSON::JsonView root = doc.root(); + + std::string_view ssid; + TEST_ASSERT_TRUE(root["ssid"].tryGetStr(ssid)); + TEST_ASSERT_TRUE(ssid == "Home"); + // password round-trips as its escaped form (osjson parse does not unescape) + std::string_view pw; + TEST_ASSERT_TRUE(root["password"].tryGetStr(pw)); + TEST_ASSERT_TRUE(pw == R"(p@ss\"word\\)"); +} + +TEST_CASE("generate: value larger than the 256-byte flush buffer", "[osjson][gen]") +{ + // Forces StringWriter::flushCb to run several times; output must be intact. + std::string big(1000, 'x'); + + JSON::StringWriter w; + json_gen_str_t* g = w.gen(); + json_gen_start_object(g); + json_gen_obj_set_string(g, "data", big.c_str()); + json_gen_end_object(g); + std::string out = w.finish(); + + std::string expected = "{\"data\":\"" + big + "\"}"; + TEST_ASSERT_EQUAL_size_t(expected.size(), out.size()); + TEST_ASSERT_TRUE(out == expected); +} + +TEST_CASE("generate -> parse round-trip preserves scalar values", "[osjson][gen]") +{ + std::string out; + { + JSON::StringWriter w; + json_gen_str_t* g = w.gen(); + json_gen_start_object(g); + json_gen_obj_set_string(g, "ssid", "net"); + json_gen_obj_set_int(g, "id", 7); + json_gen_obj_set_bool(g, "on", true); + json_gen_push_array(g, "nums"); + json_gen_arr_set_int(g, 1); + json_gen_arr_set_int(g, 2); + json_gen_arr_set_int(g, 3); + json_gen_pop_array(g); + json_gen_end_object(g); + out = w.finish(); + } + + JSON::JsonDocument doc; + TEST_ASSERT_TRUE(doc.parse(out)); + JSON::JsonView root = doc.root(); + + std::string_view ssid; + TEST_ASSERT_TRUE(root["ssid"].tryGetStr(ssid)); + TEST_ASSERT_TRUE(ssid == "net"); + + int64_t id = 0; + TEST_ASSERT_TRUE(root["id"].tryGetI64(id)); + TEST_ASSERT_EQUAL_INT64(7, id); + + bool on = false; + TEST_ASSERT_TRUE(root["on"].tryGetBool(on)); + TEST_ASSERT_TRUE(on); + + TEST_ASSERT_EQUAL_INT(3, root["nums"].count()); + int64_t sum = 0, v = 0; + for (int i = 0; i < root["nums"].count(); ++i) { + TEST_ASSERT_TRUE(root["nums"].at(i).tryGetI64(v)); + sum += v; + } + TEST_ASSERT_EQUAL_INT64(6, sum); +} diff --git a/components/osjson/host_test/main/test_getters.cpp b/components/osjson/host_test/main/test_getters.cpp new file mode 100644 index 00000000..6b2415aa --- /dev/null +++ b/components/osjson/host_test/main/test_getters.cpp @@ -0,0 +1,240 @@ +// Typed getters (tryGetStr/Bool/I64/Double) and type predicates. +#include "unity.h" + +#include "json/Json.h" + +#include +#include +#include +#include + +using namespace OpenShock; + +// ---- tryGetStr ------------------------------------------------------------- + +TEST_CASE("tryGetStr: strings succeed, non-strings fail", "[osjson][getters]") +{ + static const char json[] = R"({"s":"hi","n":5,"b":true,"z":null,"o":{},"a":[]})"; + JSON::JsonDocument doc; + TEST_ASSERT_TRUE(doc.parse(json)); + JSON::JsonView root = doc.root(); + + std::string_view sv; + TEST_ASSERT_TRUE(root["s"].tryGetStr(sv)); + TEST_ASSERT_TRUE(sv == "hi"); + + TEST_ASSERT_FALSE(root["n"].tryGetStr(sv)); + TEST_ASSERT_FALSE(root["b"].tryGetStr(sv)); + TEST_ASSERT_FALSE(root["z"].tryGetStr(sv)); + TEST_ASSERT_FALSE(root["o"].tryGetStr(sv)); + TEST_ASSERT_FALSE(root["a"].tryGetStr(sv)); + TEST_ASSERT_FALSE(root["missing"].tryGetStr(sv)); +} + +TEST_CASE("tryGetStr: empty string value", "[osjson][getters]") +{ + JSON::JsonDocument doc; + TEST_ASSERT_TRUE(doc.parse(R"({"s":""})")); + std::string_view sv; + TEST_ASSERT_TRUE(doc.root()["s"].tryGetStr(sv)); + TEST_ASSERT_EQUAL_size_t(0, sv.size()); + TEST_ASSERT_TRUE(sv.empty()); +} + +TEST_CASE("tryGetStr: escape sequences are returned RAW (jsmn does not unescape)", "[osjson][getters]") +{ + // Documents current behaviour: the view is the raw bytes between the quotes, + // escapes are NOT processed. Consumers that need real bytes must unescape. + static const char json[] = R"({"s":"a\nb\"c"})"; + JSON::JsonDocument doc; + TEST_ASSERT_TRUE(doc.parse(json)); + std::string_view sv; + TEST_ASSERT_TRUE(doc.root()["s"].tryGetStr(sv)); + TEST_ASSERT_TRUE(sv == R"(a\nb\"c)"); // backslash-n and backslash-quote, literally + TEST_ASSERT_EQUAL_size_t(7, sv.size()); +} + +// ---- tryGetBool ------------------------------------------------------------ + +TEST_CASE("tryGetBool: true/false succeed, everything else fails", "[osjson][getters]") +{ + static const char json[] = R"({"t":true,"f":false,"n":1,"s":"true","z":null})"; + JSON::JsonDocument doc; + TEST_ASSERT_TRUE(doc.parse(json)); + JSON::JsonView root = doc.root(); + + bool b = false; + TEST_ASSERT_TRUE(root["t"].tryGetBool(b)); + TEST_ASSERT_TRUE(b); + TEST_ASSERT_TRUE(root["f"].tryGetBool(b)); + TEST_ASSERT_FALSE(b); + + TEST_ASSERT_FALSE(root["n"].tryGetBool(b)); // number + TEST_ASSERT_FALSE(root["s"].tryGetBool(b)); // the STRING "true" + TEST_ASSERT_FALSE(root["z"].tryGetBool(b)); // null + TEST_ASSERT_FALSE(root["missing"].tryGetBool(b)); +} + +// ---- tryGetI64 ------------------------------------------------------------- + +TEST_CASE("tryGetI64: valid integers including boundaries", "[osjson][getters]") +{ + static const char json[] = R"({"zero":0,"pos":123,"neg":-123,"i32over":2147483648,)" + R"("max":9223372036854775807,"min":-9223372036854775808,"leadzero":007})"; + JSON::JsonDocument doc; + TEST_ASSERT_TRUE(doc.parse(json)); + JSON::JsonView root = doc.root(); + + int64_t v = 0; + TEST_ASSERT_TRUE(root["zero"].tryGetI64(v)); + TEST_ASSERT_EQUAL_INT64(0, v); + TEST_ASSERT_TRUE(root["pos"].tryGetI64(v)); + TEST_ASSERT_EQUAL_INT64(123, v); + TEST_ASSERT_TRUE(root["neg"].tryGetI64(v)); + TEST_ASSERT_EQUAL_INT64(-123, v); + TEST_ASSERT_TRUE(root["i32over"].tryGetI64(v)); + TEST_ASSERT_EQUAL_INT64(2147483648LL, v); + TEST_ASSERT_TRUE(root["max"].tryGetI64(v)); + TEST_ASSERT_EQUAL_INT64(INT64_MAX, v); + TEST_ASSERT_TRUE(root["min"].tryGetI64(v)); + TEST_ASSERT_EQUAL_INT64(INT64_MIN, v); + TEST_ASSERT_FALSE(root["leadzero"].tryGetI64(v)); // "007" -> rejected (Convert disallows leading zeros) +} + +TEST_CASE("tryGetI64: rejects non-integers and partial parses", "[osjson][getters]") +{ + static const char json[] = R"({"flt":1.5,"exp":"1e3","over":9223372036854775808,)" + R"("plus":"+5","hex":"0x1F","word":"abc","s":"42","b":true,"z":null})"; + // NOTE: exp/plus/hex/word/s are quoted so they are valid JSON string tokens; + // tryGetI64 must still reject them because they are not numbers. + JSON::JsonDocument doc; + TEST_ASSERT_TRUE(doc.parse(json)); + JSON::JsonView root = doc.root(); + + int64_t v = 0; + TEST_ASSERT_FALSE(root["flt"].tryGetI64(v)); // 1.5 -> '.' is not a digit + TEST_ASSERT_FALSE(root["over"].tryGetI64(v)); // overflows int64 + TEST_ASSERT_FALSE(root["exp"].tryGetI64(v)); // string, not a number + TEST_ASSERT_FALSE(root["plus"].tryGetI64(v)); + TEST_ASSERT_FALSE(root["hex"].tryGetI64(v)); + TEST_ASSERT_FALSE(root["word"].tryGetI64(v)); + TEST_ASSERT_FALSE(root["s"].tryGetI64(v)); + TEST_ASSERT_FALSE(root["b"].tryGetI64(v)); + TEST_ASSERT_FALSE(root["z"].tryGetI64(v)); + TEST_ASSERT_FALSE(root["missing"].tryGetI64(v)); +} + +// ---- tryGetDouble ---------------------------------------------------------- + +static bool nearly(double a, double b) +{ + return std::fabs(a - b) < 1e-9; +} + +TEST_CASE("tryGetDouble: accepts ints, decimals and exponents", "[osjson][getters]") +{ + static const char json[] = R"({"i":42,"d":3.14,"neg":-2.5,"e":1e3,"eneg":1.5e-2,"zero":0.0})"; + JSON::JsonDocument doc; + TEST_ASSERT_TRUE(doc.parse(json)); + JSON::JsonView root = doc.root(); + + double d = 0; + TEST_ASSERT_TRUE(root["i"].tryGetDouble(d)); + TEST_ASSERT_TRUE(nearly(42.0, d)); + TEST_ASSERT_TRUE(root["d"].tryGetDouble(d)); + TEST_ASSERT_TRUE(nearly(3.14, d)); + TEST_ASSERT_TRUE(root["neg"].tryGetDouble(d)); + TEST_ASSERT_TRUE(nearly(-2.5, d)); + TEST_ASSERT_TRUE(root["e"].tryGetDouble(d)); + TEST_ASSERT_TRUE(nearly(1000.0, d)); + TEST_ASSERT_TRUE(root["eneg"].tryGetDouble(d)); + TEST_ASSERT_TRUE(nearly(0.015, d)); + TEST_ASSERT_TRUE(root["zero"].tryGetDouble(d)); + TEST_ASSERT_TRUE(nearly(0.0, d)); +} + +TEST_CASE("tryGetDouble: rejects non-numbers", "[osjson][getters]") +{ + static const char json[] = R"({"s":"3.14","b":true,"z":null,"o":{},"a":[]})"; + JSON::JsonDocument doc; + TEST_ASSERT_TRUE(doc.parse(json)); + JSON::JsonView root = doc.root(); + + double d = 0; + TEST_ASSERT_FALSE(root["s"].tryGetDouble(d)); + TEST_ASSERT_FALSE(root["b"].tryGetDouble(d)); + TEST_ASSERT_FALSE(root["z"].tryGetDouble(d)); + TEST_ASSERT_FALSE(root["o"].tryGetDouble(d)); + TEST_ASSERT_FALSE(root["a"].tryGetDouble(d)); + TEST_ASSERT_FALSE(root["missing"].tryGetDouble(d)); +} + +TEST_CASE("tryGetDouble: number at/over the 64-byte stack-copy limit", "[osjson][getters]") +{ + // 63-char number fits the internal buf[64]; 64-char number is rejected. + std::string n63(63, '9'); + std::string n64(64, '9'); + std::string json = "{\"a\":" + n63 + ",\"b\":" + n64 + "}"; + + JSON::JsonDocument doc; + TEST_ASSERT_TRUE(doc.parse(json)); + JSON::JsonView root = doc.root(); + + double d = 0; + TEST_ASSERT_TRUE(root["a"].tryGetDouble(d)); // fits + TEST_ASSERT_FALSE(root["b"].tryGetDouble(d)); // too long -> rejected, not truncated +} + +// ---- type predicates ------------------------------------------------------- + +TEST_CASE("predicates: exactly one container/leaf kind per value", "[osjson][getters]") +{ + static const char json[] = R"({"o":{},"a":[],"s":"x","n":5,"t":true,"z":null})"; + JSON::JsonDocument doc; + TEST_ASSERT_TRUE(doc.parse(json)); + JSON::JsonView root = doc.root(); + + TEST_ASSERT_TRUE(root["o"].isObject()); + TEST_ASSERT_TRUE(root["a"].isArray()); + TEST_ASSERT_TRUE(root["s"].isString()); + + // number: primitive + isNumber, but not null/bool-ish + TEST_ASSERT_TRUE(root["n"].isPrimitive()); + TEST_ASSERT_TRUE(root["n"].isNumber()); + TEST_ASSERT_FALSE(root["n"].isNull()); + + // true: primitive but NOT a number + TEST_ASSERT_TRUE(root["t"].isPrimitive()); + TEST_ASSERT_FALSE(root["t"].isNumber()); + TEST_ASSERT_FALSE(root["t"].isNull()); + + // null: primitive, isNull, not a number + TEST_ASSERT_TRUE(root["z"].isPrimitive()); + TEST_ASSERT_TRUE(root["z"].isNull()); + TEST_ASSERT_FALSE(root["z"].isNumber()); +} + +TEST_CASE("predicates: an invalid/default view answers false everywhere", "[osjson][getters]") +{ + JSON::JsonView v; // default-constructed + TEST_ASSERT_FALSE(v.valid()); + TEST_ASSERT_FALSE(v.isObject()); + TEST_ASSERT_FALSE(v.isArray()); + TEST_ASSERT_FALSE(v.isString()); + TEST_ASSERT_FALSE(v.isPrimitive()); + TEST_ASSERT_FALSE(v.isNumber()); + TEST_ASSERT_FALSE(v.isNull()); + TEST_ASSERT_EQUAL_INT(0, v.count()); + TEST_ASSERT_FALSE(v.at(0).valid()); + TEST_ASSERT_FALSE(v["k"].valid()); + TEST_ASSERT_TRUE(v.raw().empty()); + + std::string_view sv; + int64_t i; + double d; + bool b; + TEST_ASSERT_FALSE(v.tryGetStr(sv)); + TEST_ASSERT_FALSE(v.tryGetI64(i)); + TEST_ASSERT_FALSE(v.tryGetDouble(d)); + TEST_ASSERT_FALSE(v.tryGetBool(b)); +} diff --git a/components/osjson/host_test/main/test_json.cpp b/components/osjson/host_test/main/test_json.cpp new file mode 100644 index 00000000..24b1b1c8 --- /dev/null +++ b/components/osjson/host_test/main/test_json.cpp @@ -0,0 +1,134 @@ +// Host (linux target) unit tests for the osjson component. +// +// cd components/osjson/host_test +// idf.py --preview set-target linux +// idf.py build +// ./build/osjson_host_test.elf +// +#include "unity.h" + +#include "json/Json.h" + +#include +#include +#include + +using namespace OpenShock; + +TEST_CASE("parse scalar object", "[osjson]") +{ + static const char json[] = R"({"name":"hello","n":42,"flag":true,"neg":-7})"; + + JSON::JsonDocument doc; + TEST_ASSERT_TRUE(doc.parse(json)); + + JSON::JsonView root = doc.root(); + TEST_ASSERT_TRUE(root.isObject()); + + std::string_view s; + TEST_ASSERT_TRUE(root["name"].tryGetStr(s)); + TEST_ASSERT_EQUAL_size_t(5, s.size()); + TEST_ASSERT_TRUE(s == "hello"); + // Zero-copy: the view must point straight into the source buffer. + TEST_ASSERT_TRUE(s.data() >= json && s.data() < json + sizeof(json)); + + int64_t n = 0; + TEST_ASSERT_TRUE(root["n"].tryGetI64(n)); + TEST_ASSERT_EQUAL_INT64(42, n); + + int64_t neg = 0; + TEST_ASSERT_TRUE(root["neg"].tryGetI64(neg)); + TEST_ASSERT_EQUAL_INT64(-7, neg); + + bool flag = false; + TEST_ASSERT_TRUE(root["flag"].tryGetBool(flag)); + TEST_ASSERT_TRUE(flag); +} + +TEST_CASE("missing key and type mismatch", "[osjson]") +{ + static const char json[] = R"({"name":"x","n":5})"; + + JSON::JsonDocument doc; + TEST_ASSERT_TRUE(doc.parse(json)); + JSON::JsonView root = doc.root(); + + TEST_ASSERT_FALSE(root["missing"].valid()); + + bool b = false; + TEST_ASSERT_FALSE(root["name"].tryGetBool(b)); // string, not bool + int64_t n = 0; + TEST_ASSERT_FALSE(root["name"].tryGetI64(n)); // string, not number + std::string_view s; + TEST_ASSERT_FALSE(root["n"].tryGetStr(s)); // number, not string +} + +TEST_CASE("nested objects and arrays", "[osjson]") +{ + static const char json[] = R"({"obj":{"x":1,"y":2},"arr":[10,20,30]})"; + + JSON::JsonDocument doc; + TEST_ASSERT_TRUE(doc.parse(json)); + JSON::JsonView root = doc.root(); + + int64_t y = 0; + TEST_ASSERT_TRUE(root["obj"]["y"].tryGetI64(y)); + TEST_ASSERT_EQUAL_INT64(2, y); + + JSON::JsonView arr = root["arr"]; + TEST_ASSERT_TRUE(arr.isArray()); + TEST_ASSERT_EQUAL_INT(3, arr.count()); + + int64_t v = 0; + TEST_ASSERT_TRUE(arr.at(1).tryGetI64(v)); + TEST_ASSERT_EQUAL_INT64(20, v); + TEST_ASSERT_FALSE(arr.at(3).valid()); // out of range +} + +TEST_CASE("malformed input is rejected", "[osjson]") +{ + JSON::JsonDocument doc; + TEST_ASSERT_FALSE(doc.parse("{ not valid json")); + TEST_ASSERT_FALSE(doc.parse("")); + TEST_ASSERT_FALSE(doc.root().valid()); +} + +TEST_CASE("generate then parse round-trip", "[osjson]") +{ + std::string out; + { + JSON::StringWriter writer; + json_gen_str_t* gen = writer.gen(); + json_gen_start_object(gen); + json_gen_obj_set_string(gen, "ssid", "net"); + json_gen_obj_set_int(gen, "id", 7); + json_gen_obj_set_bool(gen, "on", true); + json_gen_end_object(gen); + out = writer.finish(); + } + + JSON::JsonDocument doc; + TEST_ASSERT_TRUE(doc.parse(out)); + JSON::JsonView root = doc.root(); + + std::string_view ssid; + TEST_ASSERT_TRUE(root["ssid"].tryGetStr(ssid)); + TEST_ASSERT_TRUE(ssid == "net"); + + int64_t id = 0; + TEST_ASSERT_TRUE(root["id"].tryGetI64(id)); + TEST_ASSERT_EQUAL_INT64(7, id); + + bool on = false; + TEST_ASSERT_TRUE(root["on"].tryGetBool(on)); + TEST_ASSERT_TRUE(on); +} + +// Host-only (linux target) test binary. FreeRTOS is mocked, so there is no ESP +// startup to call app_main - provide a plain main() that drives Unity directly. +extern "C" int main(void) +{ + UNITY_BEGIN(); + unity_run_all_tests(); + return UNITY_END(); +} diff --git a/components/osjson/host_test/main/test_navigation.cpp b/components/osjson/host_test/main/test_navigation.cpp new file mode 100644 index 00000000..ec48b06c --- /dev/null +++ b/components/osjson/host_test/main/test_navigation.cpp @@ -0,0 +1,185 @@ +// Object member lookup, array indexing, count(), raw(), and skip() correctness. +#include "unity.h" + +#include "json/Json.h" + +#include +#include +#include + +using namespace OpenShock; + +TEST_CASE("operator[]: present/missing keys and non-objects", "[osjson][nav]") +{ + static const char json[] = R"({"a":1,"b":2})"; + JSON::JsonDocument doc; + TEST_ASSERT_TRUE(doc.parse(json)); + JSON::JsonView root = doc.root(); + + TEST_ASSERT_TRUE(root["a"].valid()); + TEST_ASSERT_TRUE(root["b"].valid()); + TEST_ASSERT_FALSE(root["c"].valid()); + TEST_ASSERT_FALSE(root[""].valid()); + + // indexing into a non-object yields an invalid view (no crash) + TEST_ASSERT_FALSE(root["a"]["nested"].valid()); +} + +TEST_CASE("operator[]: case-sensitive and prefix-distinct keys", "[osjson][nav]") +{ + static const char json[] = R"({"name":1,"Name":2,"n":3,"nam":4})"; + JSON::JsonDocument doc; + TEST_ASSERT_TRUE(doc.parse(json)); + JSON::JsonView root = doc.root(); + + int64_t v = 0; + TEST_ASSERT_TRUE(root["name"].tryGetI64(v)); + TEST_ASSERT_EQUAL_INT64(1, v); + TEST_ASSERT_TRUE(root["Name"].tryGetI64(v)); // different case -> different key + TEST_ASSERT_EQUAL_INT64(2, v); + TEST_ASSERT_TRUE(root["n"].tryGetI64(v)); // must not match "name"/"nam" + TEST_ASSERT_EQUAL_INT64(3, v); + TEST_ASSERT_TRUE(root["nam"].tryGetI64(v)); + TEST_ASSERT_EQUAL_INT64(4, v); +} + +TEST_CASE("operator[]: empty-string key can be looked up", "[osjson][nav]") +{ + JSON::JsonDocument doc; + TEST_ASSERT_TRUE(doc.parse(R"({"":42})")); + int64_t v = 0; + TEST_ASSERT_TRUE(doc.root()[""].tryGetI64(v)); + TEST_ASSERT_EQUAL_INT64(42, v); +} + +TEST_CASE("operator[]: duplicate keys resolve to the first occurrence", "[osjson][nav]") +{ + JSON::JsonDocument doc; + TEST_ASSERT_TRUE(doc.parse(R"({"k":1,"k":2})")); + int64_t v = 0; + TEST_ASSERT_TRUE(doc.root()["k"].tryGetI64(v)); + TEST_ASSERT_EQUAL_INT64(1, v); +} + +TEST_CASE("operator[]: lookup skips over object- and array-valued members", "[osjson][nav]") +{ + // The value before "target" is a nested structure; skip() must jump the whole + // subtree so the following key is found correctly. + static const char json[] = R"({"big":{"x":[1,2,{"y":3}],"z":{"w":4}},"arr":[9,8,7],"target":123})"; + JSON::JsonDocument doc; + TEST_ASSERT_TRUE(doc.parse(json)); + JSON::JsonView root = doc.root(); + + int64_t v = 0; + TEST_ASSERT_TRUE(root["target"].tryGetI64(v)); + TEST_ASSERT_EQUAL_INT64(123, v); + + // and the nested paths themselves resolve + TEST_ASSERT_TRUE(root["big"]["z"]["w"].tryGetI64(v)); + TEST_ASSERT_EQUAL_INT64(4, v); + TEST_ASSERT_TRUE(root["big"]["x"].at(2)["y"].tryGetI64(v)); + TEST_ASSERT_EQUAL_INT64(3, v); +} + +TEST_CASE("count(): arrays, objects, primitives, invalid", "[osjson][nav]") +{ + JSON::JsonDocument doc; + TEST_ASSERT_TRUE(doc.parse(R"({"a":[1,2,3,4],"o":{"x":1,"y":2},"n":5})")); + JSON::JsonView root = doc.root(); + + TEST_ASSERT_EQUAL_INT(3, root.count()); // object: member count + TEST_ASSERT_EQUAL_INT(4, root["a"].count()); // array length + TEST_ASSERT_EQUAL_INT(2, root["o"].count()); // nested object members + TEST_ASSERT_EQUAL_INT(0, root["n"].count()); // primitive + TEST_ASSERT_EQUAL_INT(0, root["missing"].count()); +} + +TEST_CASE("at(): valid indices, out of range, and non-arrays", "[osjson][nav]") +{ + JSON::JsonDocument doc; + TEST_ASSERT_TRUE(doc.parse(R"([10,20,30])")); + JSON::JsonView arr = doc.root(); + + int64_t v = 0; + TEST_ASSERT_TRUE(arr.at(0).tryGetI64(v)); + TEST_ASSERT_EQUAL_INT64(10, v); + TEST_ASSERT_TRUE(arr.at(2).tryGetI64(v)); + TEST_ASSERT_EQUAL_INT64(30, v); + + TEST_ASSERT_FALSE(arr.at(3).valid()); // past the end + TEST_ASSERT_FALSE(arr.at(-1).valid()); // negative + TEST_ASSERT_FALSE(arr.at(1000).valid()); + + // at() on an object is invalid (only arrays are indexable) + JSON::JsonDocument obj; + TEST_ASSERT_TRUE(obj.parse(R"({"a":1})")); + TEST_ASSERT_FALSE(obj.root().at(0).valid()); +} + +TEST_CASE("arrays: nested and mixed-type elements", "[osjson][nav]") +{ + static const char json[] = R"([[1,2],[3,4],"five",true,null,{"k":6},[]])"; + JSON::JsonDocument doc; + TEST_ASSERT_TRUE(doc.parse(json)); + JSON::JsonView root = doc.root(); + + TEST_ASSERT_EQUAL_INT(7, root.count()); + + int64_t v = 0; + TEST_ASSERT_TRUE(root.at(0).isArray()); + TEST_ASSERT_TRUE(root.at(0).at(1).tryGetI64(v)); + TEST_ASSERT_EQUAL_INT64(2, v); + TEST_ASSERT_TRUE(root.at(1).at(0).tryGetI64(v)); + TEST_ASSERT_EQUAL_INT64(3, v); + + std::string_view sv; + TEST_ASSERT_TRUE(root.at(2).tryGetStr(sv)); + TEST_ASSERT_TRUE(sv == "five"); + + bool b = false; + TEST_ASSERT_TRUE(root.at(3).tryGetBool(b)); + TEST_ASSERT_TRUE(b); + + TEST_ASSERT_TRUE(root.at(4).isNull()); + + TEST_ASSERT_TRUE(root.at(5).isObject()); + TEST_ASSERT_TRUE(root.at(5)["k"].tryGetI64(v)); + TEST_ASSERT_EQUAL_INT64(6, v); + + TEST_ASSERT_TRUE(root.at(6).isArray()); + TEST_ASSERT_EQUAL_INT(0, root.at(6).count()); +} + +TEST_CASE("raw(): zero-copy token text for strings, numbers, containers", "[osjson][nav]") +{ + static const char json[] = R"({"s":"hi","n":-12.5,"a":[1,2]})"; + JSON::JsonDocument doc; + TEST_ASSERT_TRUE(doc.parse(json)); + JSON::JsonView root = doc.root(); + + // string raw() is the content WITHOUT the surrounding quotes + std::string_view s = root["s"].raw(); + TEST_ASSERT_TRUE(s == "hi"); + TEST_ASSERT_TRUE(s.data() >= json && s.data() < json + sizeof(json)); + + // primitive raw() is the literal text + TEST_ASSERT_TRUE(root["n"].raw() == "-12.5"); + + // container raw() spans the whole array text + std::string_view a = root["a"].raw(); + TEST_ASSERT_TRUE(a.size() >= 2); + TEST_ASSERT_EQUAL_INT('[', a.front()); + TEST_ASSERT_EQUAL_INT(']', a.back()); +} + +TEST_CASE("chaining on invalid views never dereferences", "[osjson][nav]") +{ + JSON::JsonDocument doc; + TEST_ASSERT_TRUE(doc.parse(R"({"a":1})")); + JSON::JsonView root = doc.root(); + + // deep chain through missing keys and bad indices stays invalid, no crash + TEST_ASSERT_FALSE(root["nope"]["deeper"].at(3)["evenmore"].valid()); + int64_t v = 0; + TEST_ASSERT_FALSE(root["nope"].at(0)["x"].tryGetI64(v)); +} diff --git a/components/osjson/host_test/main/test_parse.cpp b/components/osjson/host_test/main/test_parse.cpp new file mode 100644 index 00000000..4ee4a3ea --- /dev/null +++ b/components/osjson/host_test/main/test_parse.cpp @@ -0,0 +1,134 @@ +// JsonDocument::parse + root() edge cases. +#include "unity.h" + +#include "json/Json.h" + +#include +#include + +using namespace OpenShock; + +TEST_CASE("parse: empty and whitespace-only inputs are rejected", "[osjson][parse]") +{ + JSON::JsonDocument doc; + TEST_ASSERT_FALSE(doc.parse("")); + TEST_ASSERT_FALSE(doc.ok()); + TEST_ASSERT_FALSE(doc.root().valid()); + + TEST_ASSERT_FALSE(doc.parse(" ")); + TEST_ASSERT_FALSE(doc.parse("\t\n\r ")); +} + +TEST_CASE("parse: root() is invalid before any parse", "[osjson][parse]") +{ + JSON::JsonDocument doc; + TEST_ASSERT_FALSE(doc.ok()); + TEST_ASSERT_FALSE(doc.root().valid()); +} + +TEST_CASE("parse: malformed documents are rejected", "[osjson][parse]") +{ + JSON::JsonDocument doc; + TEST_ASSERT_FALSE(doc.parse("{")); + TEST_ASSERT_FALSE(doc.parse("[")); + TEST_ASSERT_FALSE(doc.parse("[1,2")); + TEST_ASSERT_FALSE(doc.parse("{\"a\":")); + TEST_ASSERT_FALSE(doc.parse("{\"a\"")); // key with no value + TEST_ASSERT_FALSE(doc.parse("\"unterminated")); // unterminated string + TEST_ASSERT_FALSE(doc.parse("{ not valid json")); +} + +TEST_CASE("parse: a failed parse leaves root() invalid", "[osjson][parse]") +{ + JSON::JsonDocument doc; + TEST_ASSERT_TRUE(doc.parse(R"({"a":1})")); + TEST_ASSERT_TRUE(doc.root().valid()); + + TEST_ASSERT_FALSE(doc.parse("{bad")); + TEST_ASSERT_FALSE(doc.ok()); + TEST_ASSERT_FALSE(doc.root().valid()); +} + +TEST_CASE("parse: object / array roots report the right container type", "[osjson][parse]") +{ + JSON::JsonDocument obj; + TEST_ASSERT_TRUE(obj.parse(R"({"a":1})")); + TEST_ASSERT_TRUE(obj.root().isObject()); + TEST_ASSERT_FALSE(obj.root().isArray()); + + JSON::JsonDocument arr; + TEST_ASSERT_TRUE(arr.parse(R"([1,2,3])")); + TEST_ASSERT_TRUE(arr.root().isArray()); + TEST_ASSERT_FALSE(arr.root().isObject()); +} + +TEST_CASE("parse: empty object and empty array", "[osjson][parse]") +{ + JSON::JsonDocument obj; + TEST_ASSERT_TRUE(obj.parse("{}")); + TEST_ASSERT_TRUE(obj.root().isObject()); + TEST_ASSERT_EQUAL_INT(0, obj.root().count()); + TEST_ASSERT_FALSE(obj.root()["anything"].valid()); + + JSON::JsonDocument arr; + TEST_ASSERT_TRUE(arr.parse("[]")); + TEST_ASSERT_TRUE(arr.root().isArray()); + TEST_ASSERT_EQUAL_INT(0, arr.root().count()); + TEST_ASSERT_FALSE(arr.root().at(0).valid()); +} + +TEST_CASE("parse: scalar document at the root", "[osjson][parse]") +{ + JSON::JsonDocument num; + TEST_ASSERT_TRUE(num.parse("42")); + TEST_ASSERT_TRUE(num.root().isNumber()); + int64_t n = 0; + TEST_ASSERT_TRUE(num.root().tryGetI64(n)); + TEST_ASSERT_EQUAL_INT64(42, n); + + JSON::JsonDocument str; + TEST_ASSERT_TRUE(str.parse("\"hello\"")); + TEST_ASSERT_TRUE(str.root().isString()); + std::string_view sv; + TEST_ASSERT_TRUE(str.root().tryGetStr(sv)); + TEST_ASSERT_TRUE(sv == "hello"); + + JSON::JsonDocument boolean; + TEST_ASSERT_TRUE(boolean.parse("true")); + bool b = false; + TEST_ASSERT_TRUE(boolean.root().tryGetBool(b)); + TEST_ASSERT_TRUE(b); + + JSON::JsonDocument null; + TEST_ASSERT_TRUE(null.parse("null")); + TEST_ASSERT_TRUE(null.root().isNull()); +} + +TEST_CASE("parse: re-parsing the same document reuses state cleanly", "[osjson][parse]") +{ + JSON::JsonDocument doc; + + TEST_ASSERT_TRUE(doc.parse(R"({"a":1,"b":2,"c":3})")); + TEST_ASSERT_EQUAL_INT(3, doc.root().count()); + + TEST_ASSERT_TRUE(doc.parse(R"({"x":9})")); + TEST_ASSERT_EQUAL_INT(1, doc.root().count()); + int64_t x = 0; + TEST_ASSERT_TRUE(doc.root()["x"].tryGetI64(x)); + TEST_ASSERT_EQUAL_INT64(9, x); + TEST_ASSERT_FALSE(doc.root()["a"].valid()); // key from the previous parse is gone +} + +TEST_CASE("parse: source buffer must outlive the document (zero-copy contract)", "[osjson][parse]") +{ + std::string src = R"({"name":"value"})"; + + JSON::JsonDocument doc; + TEST_ASSERT_TRUE(doc.parse(src)); + + std::string_view sv; + TEST_ASSERT_TRUE(doc.root()["name"].tryGetStr(sv)); + // The view must point straight into src, not into a copy. + TEST_ASSERT_TRUE(sv.data() >= src.data() && sv.data() < src.data() + src.size()); + TEST_ASSERT_TRUE(sv == "value"); +} diff --git a/components/osjson/host_test/main/test_stress.cpp b/components/osjson/host_test/main/test_stress.cpp new file mode 100644 index 00000000..2097ed27 --- /dev/null +++ b/components/osjson/host_test/main/test_stress.cpp @@ -0,0 +1,154 @@ +// Scale / limits: token-buffer growth, the parse token cap, deep nesting, +// and large generate->parse round-trips. +#include "unity.h" + +#include "json/Json.h" + +#include +#include +#include + +using namespace OpenShock; + +TEST_CASE("stress: large flat array forces the token buffer to grow", "[osjson][stress]") +{ + // 1000 elements -> ~1001 tokens, well past the initial capacity of 32, so the + // grow-and-retry path in parse() runs several times. + const int N = 1000; + std::string json = "["; + for (int i = 0; i < N; ++i) { + if (i) json += ','; + json += std::to_string(i); + } + json += ']'; + + JSON::JsonDocument doc; + TEST_ASSERT_TRUE(doc.parse(json)); + JSON::JsonView root = doc.root(); + TEST_ASSERT_TRUE(root.isArray()); + TEST_ASSERT_EQUAL_INT(N, root.count()); + + int64_t v = 0; + TEST_ASSERT_TRUE(root.at(0).tryGetI64(v)); + TEST_ASSERT_EQUAL_INT64(0, v); + TEST_ASSERT_TRUE(root.at(500).tryGetI64(v)); + TEST_ASSERT_EQUAL_INT64(500, v); + TEST_ASSERT_TRUE(root.at(N - 1).tryGetI64(v)); + TEST_ASSERT_EQUAL_INT64(N - 1, v); + TEST_ASSERT_FALSE(root.at(N).valid()); +} + +TEST_CASE("stress: object with many members, every key resolvable", "[osjson][stress]") +{ + const int N = 300; + std::string json = "{"; + for (int i = 0; i < N; ++i) { + if (i) json += ','; + json += "\"k" + std::to_string(i) + "\":" + std::to_string(i * 2); + } + json += '}'; + + JSON::JsonDocument doc; + TEST_ASSERT_TRUE(doc.parse(json)); + JSON::JsonView root = doc.root(); + TEST_ASSERT_EQUAL_INT(N, root.count()); + + // spot-check first, middle, last, and a miss + int64_t v = 0; + TEST_ASSERT_TRUE(root["k0"].tryGetI64(v)); + TEST_ASSERT_EQUAL_INT64(0, v); + TEST_ASSERT_TRUE(root["k150"].tryGetI64(v)); + TEST_ASSERT_EQUAL_INT64(300, v); + TEST_ASSERT_TRUE(root["k299"].tryGetI64(v)); + TEST_ASSERT_EQUAL_INT64(598, v); + TEST_ASSERT_FALSE(root["k300"].valid()); +} + +TEST_CASE("stress: document needing > 16384 tokens is rejected by the cap", "[osjson][stress]") +{ + // Array of 20000 numbers => 20001 tokens > cap => parse() bails out to false + // rather than growing unboundedly. + const int N = 20000; + std::string json = "["; + for (int i = 0; i < N; ++i) { + if (i) json += ','; + json += '1'; + } + json += ']'; + + JSON::JsonDocument doc; + TEST_ASSERT_FALSE(doc.parse(json)); + TEST_ASSERT_FALSE(doc.root().valid()); +} + +TEST_CASE("stress: just under the cap still parses", "[osjson][stress]") +{ + // 16000 elements -> 16001 tokens, needs capacity 32768? No: capacity doubles + // 32,64,...,16384. 16001 <= 16384 so it fits without tripping the cap. + const int N = 16000; + std::string json = "["; + for (int i = 0; i < N; ++i) { + if (i) json += ','; + json += '7'; + } + json += ']'; + + JSON::JsonDocument doc; + TEST_ASSERT_TRUE(doc.parse(json)); + TEST_ASSERT_EQUAL_INT(N, doc.root().count()); + int64_t v = 0; + TEST_ASSERT_TRUE(doc.root().at(N - 1).tryGetI64(v)); + TEST_ASSERT_EQUAL_INT64(7, v); +} + +TEST_CASE("stress: moderately deep nesting navigates correctly", "[osjson][stress]") +{ + // Build {"a":{"a":{...{"v":depth}...}}} and walk back down to the leaf. + const int depth = 40; + std::string json; + for (int i = 0; i < depth; ++i) json += "{\"a\":"; + json += std::to_string(depth); + for (int i = 0; i < depth; ++i) json += "}"; + + JSON::JsonDocument doc; + TEST_ASSERT_TRUE(doc.parse(json)); + + JSON::JsonView v = doc.root(); + for (int i = 0; i < depth; ++i) { + TEST_ASSERT_TRUE(v.isObject()); + v = v["a"]; + } + int64_t leaf = 0; + TEST_ASSERT_TRUE(v.tryGetI64(leaf)); + TEST_ASSERT_EQUAL_INT64(depth, leaf); +} + +TEST_CASE("stress: large generate -> parse round-trip", "[osjson][stress]") +{ + const int N = 2000; + + std::string out; + { + JSON::StringWriter w; + json_gen_str_t* g = w.gen(); + json_gen_start_array(g); + for (int i = 0; i < N; ++i) { + json_gen_start_object(g); + json_gen_obj_set_int(g, "i", i); + json_gen_end_object(g); + } + json_gen_end_array(g); + out = w.finish(); + } + + JSON::JsonDocument doc; + TEST_ASSERT_TRUE(doc.parse(out)); + JSON::JsonView root = doc.root(); + TEST_ASSERT_EQUAL_INT(N, root.count()); + + int64_t v = 0; + TEST_ASSERT_TRUE(root.at(0)["i"].tryGetI64(v)); + TEST_ASSERT_EQUAL_INT64(0, v); + TEST_ASSERT_TRUE(root.at(N - 1)["i"].tryGetI64(v)); + TEST_ASSERT_EQUAL_INT64(N - 1, v); +} diff --git a/components/osjson/host_test/sdkconfig.defaults b/components/osjson/host_test/sdkconfig.defaults new file mode 100644 index 00000000..c3d7cf2c --- /dev/null +++ b/components/osjson/host_test/sdkconfig.defaults @@ -0,0 +1,4 @@ +CONFIG_IDF_TARGET="linux" +CONFIG_IDF_TARGET_LINUX=y +CONFIG_COMPILER_CXX_EXCEPTIONS=y +CONFIG_UNITY_ENABLE_IDF_TEST_RUNNER=y diff --git a/components/osjson/idf_component.yml b/components/osjson/idf_component.yml new file mode 100644 index 00000000..943f1c5a --- /dev/null +++ b/components/osjson/idf_component.yml @@ -0,0 +1,6 @@ +# OpenShock JSON handling (util/Json.h) is built on these espressif components. +# Declared here explicitly rather than relying on them being pulled in as +# transitive private dependencies of other components. +dependencies: + espressif/jsmn: "^1.1.0" + espressif/json_generator: "^1.1.2" diff --git a/components/osjson/include/json/Json.h b/components/osjson/include/json/Json.h new file mode 100644 index 00000000..6ebc3c58 --- /dev/null +++ b/components/osjson/include/json/Json.h @@ -0,0 +1,124 @@ +#pragma once + +// jsmn is header-only: the parser implementation is compiled exactly once, in +// Json.cpp (which includes *without* JSMN_HEADER before this header). +// Everywhere else we only want the type/enum declarations. +#ifndef JSMN_HEADER +#define JSMN_HEADER +#endif +#include + +#include + +#include +#include +#include +#include + +namespace OpenShock::JSON { + // Zero-copy, read-only view over a single node of a parsed jsmn token tree. + // + // Strings and numbers are exposed as std::string_view directly into the + // source buffer (no NUL-termination, no strlen). The source buffer must + // outlive every JsonView derived from it. + class JsonView { + public: + JsonView() noexcept = default; + JsonView(const char* json, const jsmntok_t* tokens, int count, int index) noexcept + : m_json(json) + , m_tokens(tokens) + , m_count(count) + , m_index(index) + { + } + + [[nodiscard]] bool valid() const noexcept { return m_tokens != nullptr && m_index >= 0 && m_index < m_count; } + [[nodiscard]] bool isObject() const noexcept { return valid() && m_tokens[m_index].type == JSMN_OBJECT; } + [[nodiscard]] bool isArray() const noexcept { return valid() && m_tokens[m_index].type == JSMN_ARRAY; } + [[nodiscard]] bool isString() const noexcept { return valid() && m_tokens[m_index].type == JSMN_STRING; } + [[nodiscard]] bool isPrimitive() const noexcept { return valid() && m_tokens[m_index].type == JSMN_PRIMITIVE; } + [[nodiscard]] bool isNull() const noexcept; + [[nodiscard]] bool isNumber() const noexcept; // primitive that is not true/false/null + + // Raw token text, zero-copy into the source buffer. + [[nodiscard]] std::string_view raw() const noexcept; + + // Typed getters. Return false if this node is not of the requested kind. + [[nodiscard]] bool tryGetStr(std::string_view& out) const noexcept; + [[nodiscard]] bool tryGetBool(bool& out) const noexcept; + [[nodiscard]] bool tryGetU8(uint8_t& out) const noexcept; + [[nodiscard]] bool tryGetU16(uint16_t& out) const noexcept; + [[nodiscard]] bool tryGetI32(int32_t& out) const noexcept; + [[nodiscard]] bool tryGetI64(int64_t& out) const noexcept; + [[nodiscard]] bool tryGetDouble(double& out) const noexcept; + + // Object member lookup. Returns an invalid view if not found or not an object. + [[nodiscard]] JsonView operator[](std::string_view key) const noexcept; + + // Array access (also reports object member count). + [[nodiscard]] int count() const noexcept; + [[nodiscard]] JsonView at(int index) const noexcept; + + private: + [[nodiscard]] int skip(int index) const noexcept; // index just past the subtree rooted at `index` + + const char* m_json = nullptr; + const jsmntok_t* m_tokens = nullptr; + int m_count = 0; + int m_index = -1; + }; + + // Owns the jsmn token array for a parsed document. Does NOT own the JSON text; + // the buffer passed to parse() must outlive the document and its views. + class JsonDocument { + public: + [[nodiscard]] bool parse(std::string_view json); + [[nodiscard]] bool ok() const noexcept { return m_ok; } + [[nodiscard]] JsonView root() const noexcept; + + private: + std::string_view m_json; + std::vector m_tokens; + bool m_ok = false; + }; + + // json_generator wrapper that accumulates output into a std::string. + // Non-copyable/non-movable (the generator holds a pointer to the inline buffer). + class StringWriter { + public: + StringWriter(); + StringWriter(const StringWriter&) = delete; + StringWriter& operator=(const StringWriter&) = delete; + + [[nodiscard]] json_gen_str_t* gen() noexcept { return &m_gen; } + + // Finalizes the JSON and returns the accumulated string. Call once. + [[nodiscard]] std::string finish(); + + private: + static void flushCb(char* buf, void* priv); + + std::string m_out; + char m_buf[256]; + json_gen_str_t m_gen; + }; + + // json_generator writes string VALUES verbatim between quotes - it does NOT + // JSON-escape them, so a value containing '"', '\\' or a control character + // would emit invalid JSON. These wrappers escape the value first, then hand it + // to json_gen_{obj,arr}_set_string (which only adds the surrounding quotes). + // + // Keys/names are NOT escaped here (all our keys are safe literals); pass a + // pre-escaped name if that ever changes. Return value matches the underlying + // json_gen_* call. + int objSetString(json_gen_str_t* gen, const char* name, std::string_view value); + int arrSetString(json_gen_str_t* gen, std::string_view value); + + // Opens/closes an object on the generator. When `name` is non-null the object + // is added as a named member of the enclosing object (push/pop); when it is + // null the object is anonymous (start/end), e.g. the document root or an array + // element. Config ToJSON() implementations wrap their members in these so each + // config owns its own object, mirroring the source firmware's cJSON approach. + void objBegin(json_gen_str_t* gen, const char* name); + void objEnd(json_gen_str_t* gen, const char* name); +} // namespace OpenShock::JSON diff --git a/components/osjson/src/Json.cpp b/components/osjson/src/Json.cpp new file mode 100644 index 00000000..48913e72 --- /dev/null +++ b/components/osjson/src/Json.cpp @@ -0,0 +1,342 @@ +// Compile the jsmn implementation here (and only here): include +// WITHOUT JSMN_HEADER, before any header that pulls it in declaration-only. +#include + +#include "json/Json.h" + +#include "Convert.h" + +#include +#include +#include + +using namespace OpenShock; + +// JSON-escape a string per RFC 8259 section 7. The grammar requires escaping +// exactly the quotation mark (U+0022), reverse solidus (U+005C) and every +// control character U+0000-U+001F; everything else is `unescaped` +// (%x20-21 / %x23-5B / %x5D-10FFFF) and passes through byte-for-byte. +// +// quote (") and backslash -> \" and \\ (two-char escapes) +// 0x08 0x0C 0x0A 0x0D 0x09 -> \b \f \n \r \t (the RFC's short forms) +// other 0x00-0x1F -> \u00XX +// >= 0x20 (incl. 0x7F/DEL and UTF-8 multibyte) -> passthrough +// +// The solidus '/' is NOT escaped (the RFC lists \/ as permitted, not required). +// Non-ASCII is emitted as raw UTF-8, so no \u surrogate pairs are ever produced. +// Output validity therefore assumes valid UTF-8 input - the escaper escapes, it +// does not transcode (same contract cJSON had). +static std::string jsonEscape(std::string_view in) +{ + std::string out; + out.reserve(in.size() + 8); + + for (unsigned char c : in) { + switch (c) { + case '"': + out += "\\\""; + break; + case '\\': + out += "\\\\"; + break; + case '\b': + out += "\\b"; + break; + case '\f': + out += "\\f"; + break; + case '\n': + out += "\\n"; + break; + case '\r': + out += "\\r"; + break; + case '\t': + out += "\\t"; + break; + default: + if (c < 0x20) { + char buf[7]; + std::snprintf(buf, sizeof(buf), "\\u%04x", c); + out += buf; + } else { + out += static_cast(c); + } + break; + } + } + + return out; +} + +int JSON::objSetString(json_gen_str_t* gen, const char* name, std::string_view value) +{ + std::string escaped = jsonEscape(value); + return json_gen_obj_set_string(gen, name, escaped.c_str()); +} + +int JSON::arrSetString(json_gen_str_t* gen, std::string_view value) +{ + std::string escaped = jsonEscape(value); + return json_gen_arr_set_string(gen, escaped.c_str()); +} + +void JSON::objBegin(json_gen_str_t* gen, const char* name) +{ + if (name != nullptr) { + json_gen_push_object(gen, name); + } else { + json_gen_start_object(gen); + } +} + +void JSON::objEnd(json_gen_str_t* gen, const char* name) +{ + if (name != nullptr) { + json_gen_pop_object(gen); + } else { + json_gen_end_object(gen); + } +} + +int JSON::JsonView::skip(int index) const noexcept +{ + const jsmntok_t& tok = m_tokens[index]; + + if (tok.type == JSMN_OBJECT) { + int j = index + 1; + for (int m = 0; m < tok.size; ++m) { + j = skip(j); // key + j = skip(j); // value + } + return j; + } + + if (tok.type == JSMN_ARRAY) { + int j = index + 1; + for (int e = 0; e < tok.size; ++e) { + j = skip(j); + } + return j; + } + + return index + 1; // string / primitive leaf +} + +std::string_view JSON::JsonView::raw() const noexcept +{ + if (!valid()) { + return {}; + } + const jsmntok_t& tok = m_tokens[m_index]; + if (tok.end < tok.start) { + return {}; + } + return std::string_view(m_json + tok.start, static_cast(tok.end - tok.start)); +} + +bool JSON::JsonView::isNull() const noexcept +{ + return isPrimitive() && raw() == "null"; +} + +bool JSON::JsonView::isNumber() const noexcept +{ + if (!isPrimitive()) { + return false; + } + std::string_view s = raw(); + return s != "true" && s != "false" && s != "null"; +} + +bool JSON::JsonView::tryGetStr(std::string_view& out) const noexcept +{ + if (!isString()) { + return false; + } + out = raw(); + return true; +} + +bool JSON::JsonView::tryGetBool(bool& out) const noexcept +{ + if (!isPrimitive()) { + return false; + } + std::string_view s = raw(); + if (s == "true") { + out = true; + return true; + } + if (s == "false") { + out = false; + return true; + } + return false; +} + +bool JSON::JsonView::tryGetU8(uint8_t& out) const noexcept +{ + if (!isNumber()) { + return false; + } + return Convert::ToUint8(raw(), out); +} + +bool JSON::JsonView::tryGetU16(uint16_t& out) const noexcept +{ + if (!isNumber()) { + return false; + } + return Convert::ToUint16(raw(), out); +} + +bool JSON::JsonView::tryGetI32(int32_t& out) const noexcept +{ + if (!isNumber()) { + return false; + } + return Convert::ToInt32(raw(), out); +} + +bool JSON::JsonView::tryGetI64(int64_t& out) const noexcept +{ + if (!isNumber()) { + return false; + } + return Convert::ToInt64(raw(), out); +} + +bool JSON::JsonView::tryGetDouble(double& out) const noexcept +{ + if (!isNumber()) { + return false; + } + std::string_view s = raw(); + + // strtod needs a NUL-terminated string; JSON numbers are short, so a small + // stack copy is fine and avoids touching the (non-terminated) source buffer. + char buf[64]; + if (s.size() >= sizeof(buf)) { + return false; + } + std::memcpy(buf, s.data(), s.size()); + buf[s.size()] = '\0'; + + char* end = nullptr; + double value = std::strtod(buf, &end); + if (end != buf + s.size()) { + return false; + } + out = value; + return true; +} + +JSON::JsonView JSON::JsonView::operator[](std::string_view key) const noexcept +{ + if (!isObject()) { + return {}; + } + + const int members = m_tokens[m_index].size; + int j = m_index + 1; + for (int m = 0; m < members; ++m) { + const int keyIdx = j; + const int valIdx = keyIdx + 1; + + const jsmntok_t& k = m_tokens[keyIdx]; + if (k.type == JSMN_STRING) { + std::string_view keyView(m_json + k.start, static_cast(k.end - k.start)); + if (keyView == key) { + return JsonView(m_json, m_tokens, m_count, valIdx); + } + } + + j = skip(valIdx); // advance past this member's value to the next key + } + + return {}; +} + +int JSON::JsonView::count() const noexcept +{ + if (!isObject() && !isArray()) { + return 0; + } + return m_tokens[m_index].size; +} + +JSON::JsonView JSON::JsonView::at(int index) const noexcept +{ + if (!isArray()) { + return {}; + } + + const int elements = m_tokens[m_index].size; + if (index < 0 || index >= elements) { + return {}; + } + + int j = m_index + 1; + for (int e = 0; e < index; ++e) { + j = skip(j); + } + return JsonView(m_json, m_tokens, m_count, j); +} + +bool JSON::JsonDocument::parse(std::string_view json) +{ + m_json = json; + m_ok = false; + + // jsmn needs a token buffer sized up-front; grow-and-retry on NOMEM rather + // than relying on a separate counting pass. + size_t capacity = 32; + for (;;) { + m_tokens.resize(capacity); + + jsmn_parser parser; + jsmn_init(&parser); + + int result = jsmn_parse(&parser, json.data(), json.size(), m_tokens.data(), static_cast(capacity)); + if (result >= 0) { + m_tokens.resize(static_cast(result)); + m_ok = result > 0; + return m_ok; + } + + if (result == JSMN_ERROR_NOMEM) { + capacity *= 2; + if (capacity > 16384) { + return false; // unreasonably large / malformed + } + continue; + } + + return false; // JSMN_ERROR_INVAL / JSMN_ERROR_PART + } +} + +JSON::JsonView JSON::JsonDocument::root() const noexcept +{ + if (!m_ok || m_tokens.empty()) { + return {}; + } + return JsonView(m_json.data(), m_tokens.data(), static_cast(m_tokens.size()), 0); +} + +void JSON::StringWriter::flushCb(char* buf, void* priv) +{ + static_cast(priv)->append(buf); +} + +JSON::StringWriter::StringWriter() +{ + json_gen_str_start(&m_gen, m_buf, sizeof(m_buf), &StringWriter::flushCb, &m_out); +} + +std::string JSON::StringWriter::finish() +{ + json_gen_str_end(&m_gen); + return std::move(m_out); +} diff --git a/components/protocols/CMakeLists.txt b/components/protocols/CMakeLists.txt new file mode 100644 index 00000000..68ba78d2 --- /dev/null +++ b/components/protocols/CMakeLists.txt @@ -0,0 +1,24 @@ +# OpenShock RF shocker protocol encoders (RMT-based). +# +# Public surface: +# radio/rmt/Sequence.h — RMT sequence builder +# radio/rmt/CaiXianlinEncoder.h — CaiXianlin protocol +# radio/rmt/PetrainerEncoder.h — Petrainer protocol +# radio/rmt/Petrainer998DREncoder.h +# radio/rmt/D80Encoder.h +# radio/rmt/T330Encoder.h + +idf_component_register( + SRCS "src/CaiXianlinEncoder.cpp" + "src/D80Encoder.cpp" + "src/Petrainer998DREncoder.cpp" + "src/PetrainerEncoder.cpp" + "src/Sequence.cpp" + "src/T330Encoder.cpp" + INCLUDE_DIRS "include" + PRIV_INCLUDE_DIRS "src" # radio/rmt/internal/Shared.h (private) + REQUIRES common # enums/*, OpenShock.h in public headers + esp_driver_rmt # driver/rmt_types.h (rmt_symbol_word_t) in public headers + PRIV_REQUIRES common # Checksum.h (internal) + logging # OS_LOG* (internal) +) diff --git a/components/protocols/host_test/CMakeLists.txt b/components/protocols/host_test/CMakeLists.txt new file mode 100644 index 00000000..ed7cafb7 --- /dev/null +++ b/components/protocols/host_test/CMakeLists.txt @@ -0,0 +1,11 @@ +cmake_minimum_required(VERSION 3.16) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) + +# main compiles protocols' pure encoder sources directly (see main/CMakeLists.txt), +# so no firmware component needs to be registered. +set(COMPONENTS main) + +list(APPEND EXTRA_COMPONENT_DIRS "$ENV{IDF_PATH}/tools/mocks/freertos/") + +project(protocols_host_test) diff --git a/components/protocols/host_test/main/CMakeLists.txt b/components/protocols/host_test/main/CMakeLists.txt new file mode 100644 index 00000000..159d1229 --- /dev/null +++ b/components/protocols/host_test/main/CMakeLists.txt @@ -0,0 +1,33 @@ +# Compile protocols' PURE encoder sources directly (not via the protocols +# component) so we avoid its firmware-only transitive deps (esp_driver_rmt / logging +# pull in IDF components unavailable on the host). Stubs (stubs/) supply a minimal +# (just rmt_symbol_word_t) and a no-op Logging.h. +idf_component_register( + SRCS "test_main.cpp" + "test_encodebits.cpp" + "test_caixianlin.cpp" + "test_sequence.cpp" + "../../src/CaiXianlinEncoder.cpp" + "../../src/D80Encoder.cpp" + "../../src/Petrainer998DREncoder.cpp" + "../../src/PetrainerEncoder.cpp" + "../../src/T330Encoder.cpp" + "../../src/Sequence.cpp" + INCLUDE_DIRS "." + "stubs" # stub + Logging.h + "../../include" # radio/rmt/*Encoder.h, Sequence.h + "../../src" # radio/rmt/internal/Shared.h (EncodeBits) + "../../../common/include" # enums/*, OpenShock.h, StringHelpers.h + "../../../util/include" # Checksum.h (header-only) + REQUIRES unity + WHOLE_ARCHIVE +) + +# OpenShock.h (pulled in via Sequence.h) uses these firmware build-time defines as +# string literals. Supply dummies so the pure logic compiles on the host. +target_compile_definitions(${COMPONENT_LIB} PRIVATE + "OPENSHOCK_API_DOMAIN=\"test\"" + "OPENSHOCK_FW_CDN_DOMAIN=\"test\"" + "OPENSHOCK_FW_VERSION=\"0.0.0-test\"" + "OPENSHOCK_FW_USERAGENT=\"host-test\"" +) diff --git a/components/protocols/host_test/main/stubs/Logging.h b/components/protocols/host_test/main/stubs/Logging.h new file mode 100644 index 00000000..4aa7a8cd --- /dev/null +++ b/components/protocols/host_test/main/stubs/Logging.h @@ -0,0 +1,14 @@ +#pragma once + +// Host-test stub for the firmware Logging.h. The real one writes to the serial +// transport, which doesn't exist on the linux host target. The encoder sources +// only use these macros for diagnostics, so no-op them. +#define OS_LOGV(tag, ...) ((void)0) +#define OS_LOGD(tag, ...) ((void)0) +#define OS_LOGI(tag, ...) ((void)0) +#define OS_LOGW(tag, ...) ((void)0) +#define OS_LOGE(tag, ...) ((void)0) +#define OS_LOGN(tag, ...) ((void)0) +#define OS_LOGWTF(tag, ...) ((void)0) +#define OS_PANIC(tag, ...) ((void)0) +#define OS_PANIC_OTA(tag, ...) ((void)0) diff --git a/components/protocols/host_test/main/stubs/driver/rmt_types.h b/components/protocols/host_test/main/stubs/driver/rmt_types.h new file mode 100644 index 00000000..bf04b8a2 --- /dev/null +++ b/components/protocols/host_test/main/stubs/driver/rmt_types.h @@ -0,0 +1,17 @@ +#pragma once + +// Host-test stub for the IDF . The real header pulls in +// hal/soc types (gpio_types.h etc.) that don't build on the linux target. The RF +// encoders only need rmt_symbol_word_t; replicate it exactly so the encoded +// timing/level bit layout under test matches the firmware. +#include + +typedef union { + struct { + uint16_t duration0 : 15; // Duration of level0 + uint16_t level0 : 1; // Level of the first part + uint16_t duration1 : 15; // Duration of level1 + uint16_t level1 : 1; // Level of the second part + }; + uint32_t val; +} rmt_symbol_word_t; diff --git a/components/protocols/host_test/main/test_caixianlin.cpp b/components/protocols/host_test/main/test_caixianlin.cpp new file mode 100644 index 00000000..9bd24faf --- /dev/null +++ b/components/protocols/host_test/main/test_caixianlin.cpp @@ -0,0 +1,88 @@ +// CaiXianlin frame assembly: [preamble][shockerId:16][channel:4][type:4] +// [intensity:8][checksum:8][postamble:3]. These tests decode the emitted RMT +// symbols back to bits and check the layout, the command-type codes, intensity +// clamping, and the Sound-mutes-intensity quirk. +#include "unity.h" + +#include "radio/rmt/CaiXianlinEncoder.h" +#include "Checksum.h" + +#include + +using namespace OpenShock; + +static bool bitOf(const rmt_symbol_word_t& s) +{ + // One vs zero symbols differ by the leading pulse width (750 vs 250). + return s.duration0 > 500; +} + +// Decode n data symbols (MSB-first) back into an integer. +static uint64_t decode(const rmt_symbol_word_t* seq, size_t n) +{ + uint64_t v = 0; + for (size_t i = 0; i < n; i++) { + v = (v << 1) | (bitOf(seq[i]) ? 1U : 0U); + } + return v; +} + +TEST_CASE("CaiXianlin buffer is 44 symbols", "[protocols][caixianlin]") +{ + TEST_ASSERT_EQUAL(44, Rmt::CaiXianlinEncoder::GetBufferSize()); +} + +TEST_CASE("CaiXianlin rejects unsupported command types", "[protocols][caixianlin]") +{ + rmt_symbol_word_t seq[44]; + TEST_ASSERT_FALSE(Rmt::CaiXianlinEncoder::FillBuffer(seq, 0x1234, 5, ShockerCommandType::Stop, 42)); +} + +TEST_CASE("CaiXianlin encodes id/channel/type/intensity + checksum", "[protocols][caixianlin]") +{ + const uint16_t id = 0x1234; + const uint8_t channel = 5; + const uint8_t intensity = 42; + + rmt_symbol_word_t seq[44]; + TEST_ASSERT_TRUE(Rmt::CaiXianlinEncoder::FillBuffer(seq, id, channel, ShockerCommandType::Shock, intensity)); + + // Symbol 0 is the preamble (1400us high / 750us low). + TEST_ASSERT_EQUAL_UINT16(1400, seq[0].duration0); + TEST_ASSERT_EQUAL_UINT16(1, seq[0].level0); + TEST_ASSERT_EQUAL_UINT16(750, seq[0].duration1); + + // Symbols 1..43 carry 43 data bits: (payload<<8 | checksum) << 3. + const uint64_t tx = decode(seq + 1, 43); + TEST_ASSERT_EQUAL_UINT32(0, tx & 0x7); // 3-bit zero postamble + + const uint64_t frame = tx >> 3; // payload<<8 | checksum + const uint8_t checksum = frame & 0xFF; + const uint32_t payload = static_cast(frame >> 8); + + TEST_ASSERT_EQUAL_UINT16(id, (payload >> 16) & 0xFFFF); + TEST_ASSERT_EQUAL_UINT8(channel, (payload >> 12) & 0xF); + TEST_ASSERT_EQUAL_UINT8(0x01, (payload >> 8) & 0xF); // Shock -> 0x01 + TEST_ASSERT_EQUAL_UINT8(intensity, payload & 0xFF); + TEST_ASSERT_EQUAL_UINT8(Checksum::Sum8(payload), checksum); +} + +TEST_CASE("CaiXianlin clamps intensity to 99", "[protocols][caixianlin]") +{ + rmt_symbol_word_t seq[44]; + TEST_ASSERT_TRUE(Rmt::CaiXianlinEncoder::FillBuffer(seq, 1, 0, ShockerCommandType::Vibrate, 200)); + + const uint32_t payload = static_cast((decode(seq + 1, 43) >> 3) >> 8); + TEST_ASSERT_EQUAL_UINT8(99, payload & 0xFF); + TEST_ASSERT_EQUAL_UINT8(0x02, (payload >> 8) & 0xF); // Vibrate -> 0x02 +} + +TEST_CASE("CaiXianlin forces Sound intensity to zero", "[protocols][caixianlin]") +{ + rmt_symbol_word_t seq[44]; + TEST_ASSERT_TRUE(Rmt::CaiXianlinEncoder::FillBuffer(seq, 1, 0, ShockerCommandType::Sound, 55)); + + const uint32_t payload = static_cast((decode(seq + 1, 43) >> 3) >> 8); + TEST_ASSERT_EQUAL_UINT8(0x03, (payload >> 8) & 0xF); // Sound -> 0x03 + TEST_ASSERT_EQUAL_UINT8(0, payload & 0xFF); // intensity muted +} diff --git a/components/protocols/host_test/main/test_encodebits.cpp b/components/protocols/host_test/main/test_encodebits.cpp new file mode 100644 index 00000000..50cee5ce --- /dev/null +++ b/components/protocols/host_test/main/test_encodebits.cpp @@ -0,0 +1,52 @@ +// EncodeBits is the shared primitive behind every RF encoder: it maps the N +// most-significant meaningful bits of an integer to RMT one/zero symbols, +// MSB-first. A regression here silently corrupts every shocker command. +#include "unity.h" + +#include "radio/rmt/internal/Shared.h" + +#include + +using OpenShock::Rmt::Internal::EncodeBits; + +// Distinct one/zero symbols; only duration0 is inspected to classify a bit. +static const rmt_symbol_word_t ONE = {750, 1, 250, 0}; +static const rmt_symbol_word_t ZERO = {250, 1, 750, 0}; + +static bool isOne(const rmt_symbol_word_t& s) +{ + return s.duration0 == ONE.duration0; +} + +TEST_CASE("EncodeBits emits all N bits MSB-first", "[protocols][encodebits]") +{ + rmt_symbol_word_t seq[8]; + EncodeBits<8>(seq, static_cast(0b10110010), ONE, ZERO); + + const bool expected[8] = {1, 0, 1, 1, 0, 0, 1, 0}; + for (int i = 0; i < 8; i++) { + TEST_ASSERT_EQUAL(expected[i], isOne(seq[i])); + } +} + +TEST_CASE("EncodeBits takes the low N bits, MSB-first", "[protocols][encodebits]") +{ + // N=4 over a uint8: only the low nibble is transmitted, top nibble ignored. + rmt_symbol_word_t seq[4]; + EncodeBits<4>(seq, static_cast(0b1111'1010), ONE, ZERO); + + const bool expected[4] = {1, 0, 1, 0}; + for (int i = 0; i < 4; i++) { + TEST_ASSERT_EQUAL(expected[i], isOne(seq[i])); + } +} + +TEST_CASE("EncodeBits selects the correct symbol object", "[protocols][encodebits]") +{ + rmt_symbol_word_t seq[2]; + EncodeBits<2>(seq, static_cast(0b10), ONE, ZERO); + + // Bit 1 -> ONE symbol verbatim, bit 0 -> ZERO symbol verbatim. + TEST_ASSERT_EQUAL_UINT32(ONE.val, seq[0].val); + TEST_ASSERT_EQUAL_UINT32(ZERO.val, seq[1].val); +} diff --git a/components/protocols/host_test/main/test_main.cpp b/components/protocols/host_test/main/test_main.cpp new file mode 100644 index 00000000..7c6c46fc --- /dev/null +++ b/components/protocols/host_test/main/test_main.cpp @@ -0,0 +1,10 @@ +// Host-only (linux target) test binary for the protocols RF bit-encoders. +// FreeRTOS is mocked, so there is no ESP startup - drive Unity directly. +#include "unity.h" + +extern "C" int main(void) +{ + UNITY_BEGIN(); + unity_run_all_tests(); + return UNITY_END(); +} diff --git a/components/protocols/host_test/main/test_sequence.cpp b/components/protocols/host_test/main/test_sequence.cpp new file mode 100644 index 00000000..dbff04dc --- /dev/null +++ b/components/protocols/host_test/main/test_sequence.cpp @@ -0,0 +1,47 @@ +// Sequence is the model-dispatch layer over the per-protocol encoders: it sizes +// and allocates the RMT buffer for a shocker model and fills command frames. These +// tests exercise the dispatch for every supported model (a missing case would +// return a zero-size, invalid sequence). +#include "unity.h" + +#include "radio/rmt/Sequence.h" + +#include + +using namespace OpenShock; + +TEST_CASE("Sequence builds and fills a CaiXianlin buffer", "[protocols][sequence]") +{ + Rmt::Sequence seq(ShockerModelType::CaiXianlin, 0x4321, 0); + + TEST_ASSERT_TRUE(seq.is_valid()); + TEST_ASSERT_EQUAL(44, seq.size()); + TEST_ASSERT_EQUAL_INT(static_cast(ShockerModelType::CaiXianlin), static_cast(seq.shockerModel())); + TEST_ASSERT_EQUAL_UINT16(0x4321, seq.shockerId()); + TEST_ASSERT_TRUE(seq.fill(ShockerCommandType::Vibrate, 30)); +} + +TEST_CASE("Sequence dispatches every supported model", "[protocols][sequence]") +{ + const ShockerModelType models[] = { + ShockerModelType::CaiXianlin, + ShockerModelType::Petrainer, + ShockerModelType::Petrainer998DR, + ShockerModelType::WellturnT330, + ShockerModelType::D80, + }; + + for (ShockerModelType model : models) { + Rmt::Sequence seq(model, 1, 0); + TEST_ASSERT_TRUE(seq.is_valid()); + TEST_ASSERT_GREATER_THAN(0, seq.size()); + TEST_ASSERT_TRUE(seq.fill(ShockerCommandType::Shock, 50)); + } +} + +TEST_CASE("Default-constructed Sequence is invalid", "[protocols][sequence]") +{ + Rmt::Sequence seq; + TEST_ASSERT_FALSE(seq.is_valid()); + TEST_ASSERT_EQUAL(0, seq.size()); +} diff --git a/components/protocols/host_test/sdkconfig.defaults b/components/protocols/host_test/sdkconfig.defaults new file mode 100644 index 00000000..c3d7cf2c --- /dev/null +++ b/components/protocols/host_test/sdkconfig.defaults @@ -0,0 +1,4 @@ +CONFIG_IDF_TARGET="linux" +CONFIG_IDF_TARGET_LINUX=y +CONFIG_COMPILER_CXX_EXCEPTIONS=y +CONFIG_UNITY_ENABLE_IDF_TEST_RUNNER=y diff --git a/components/protocols/include/radio/rmt/CaiXianlinEncoder.h b/components/protocols/include/radio/rmt/CaiXianlinEncoder.h new file mode 100644 index 00000000..1514fd6f --- /dev/null +++ b/components/protocols/include/radio/rmt/CaiXianlinEncoder.h @@ -0,0 +1,12 @@ +#pragma once + +#include "enums/ShockerCommandType.h" + +#include + +#include + +namespace OpenShock::Rmt::CaiXianlinEncoder { + size_t GetBufferSize(); + bool FillBuffer(rmt_symbol_word_t* data, uint16_t shockerId, uint8_t channelId, ShockerCommandType type, uint8_t intensity); +} diff --git a/components/protocols/include/radio/rmt/D80Encoder.h b/components/protocols/include/radio/rmt/D80Encoder.h new file mode 100644 index 00000000..dc11aba3 --- /dev/null +++ b/components/protocols/include/radio/rmt/D80Encoder.h @@ -0,0 +1,12 @@ +#pragma once + +#include "enums/ShockerCommandType.h" + +#include + +#include + +namespace OpenShock::Rmt::D80Encoder { + size_t GetBufferSize(); + bool FillBuffer(rmt_symbol_word_t* data, uint16_t shockerId, ShockerCommandType type, uint8_t intensity); +} diff --git a/components/protocols/include/radio/rmt/Petrainer998DREncoder.h b/components/protocols/include/radio/rmt/Petrainer998DREncoder.h new file mode 100644 index 00000000..021a5863 --- /dev/null +++ b/components/protocols/include/radio/rmt/Petrainer998DREncoder.h @@ -0,0 +1,12 @@ +#pragma once + +#include "enums/ShockerCommandType.h" + +#include + +#include + +namespace OpenShock::Rmt::Petrainer998DREncoder { + size_t GetBufferSize(); + bool FillBuffer(rmt_symbol_word_t* data, uint16_t shockerId, ShockerCommandType type, uint8_t intensity); +} diff --git a/components/protocols/include/radio/rmt/PetrainerEncoder.h b/components/protocols/include/radio/rmt/PetrainerEncoder.h new file mode 100644 index 00000000..aa7094c8 --- /dev/null +++ b/components/protocols/include/radio/rmt/PetrainerEncoder.h @@ -0,0 +1,12 @@ +#pragma once + +#include "enums/ShockerCommandType.h" + +#include + +#include + +namespace OpenShock::Rmt::PetrainerEncoder { + size_t GetBufferSize(); + bool FillBuffer(rmt_symbol_word_t* data, uint16_t shockerId, ShockerCommandType type, uint8_t intensity); +} diff --git a/include/radio/rmt/Sequence.h b/components/protocols/include/radio/rmt/Sequence.h similarity index 79% rename from include/radio/rmt/Sequence.h rename to components/protocols/include/radio/rmt/Sequence.h index ba043258..dd8e4aac 100644 --- a/include/radio/rmt/Sequence.h +++ b/components/protocols/include/radio/rmt/Sequence.h @@ -1,10 +1,10 @@ #pragma once -#include "Common.h" -#include "ShockerCommandType.h" -#include "ShockerModelType.h" +#include "enums/ShockerCommandType.h" +#include "enums/ShockerModelType.h" +#include "OpenShock.h" -#include +#include #include @@ -41,10 +41,10 @@ namespace OpenShock::Rmt { inline int64_t transmitEnd() const noexcept { return m_transmitEnd; } inline void setTransmitEnd(int64_t transmitEnd) noexcept { m_transmitEnd = transmitEnd; } - inline rmt_data_t* payload() noexcept { return m_data; } - inline const rmt_data_t* payload() const noexcept { return m_data; } - inline rmt_data_t* terminator() noexcept { return m_data + m_size; } - inline const rmt_data_t* terminator() const noexcept { return m_data + m_size; } + inline rmt_symbol_word_t* payload() noexcept { return m_data; } + inline const rmt_symbol_word_t* payload() const noexcept { return m_data; } + inline rmt_symbol_word_t* terminator() noexcept { return m_data + m_size; } + inline const rmt_symbol_word_t* terminator() const noexcept { return m_data + m_size; } inline size_t size() const noexcept { return m_size; } bool fill(ShockerCommandType commandType, uint8_t intensity); @@ -76,7 +76,7 @@ namespace OpenShock::Rmt { m_shockerModel = static_cast(0); } - rmt_data_t* m_data; + rmt_symbol_word_t* m_data; size_t m_size; int64_t m_transmitEnd; uint16_t m_shockerId; diff --git a/components/protocols/include/radio/rmt/T330Encoder.h b/components/protocols/include/radio/rmt/T330Encoder.h new file mode 100644 index 00000000..688e3573 --- /dev/null +++ b/components/protocols/include/radio/rmt/T330Encoder.h @@ -0,0 +1,12 @@ +#pragma once + +#include "enums/ShockerCommandType.h" + +#include + +#include + +namespace OpenShock::Rmt::WellturnT330Encoder { + size_t GetBufferSize(); + bool FillBuffer(rmt_symbol_word_t* data, uint16_t shockerId, ShockerCommandType type, uint8_t intensity); +} diff --git a/src/radio/rmt/CaiXianlinEncoder.cpp b/components/protocols/src/CaiXianlinEncoder.cpp similarity index 84% rename from src/radio/rmt/CaiXianlinEncoder.cpp rename to components/protocols/src/CaiXianlinEncoder.cpp index 72077146..45114367 100644 --- a/src/radio/rmt/CaiXianlinEncoder.cpp +++ b/components/protocols/src/CaiXianlinEncoder.cpp @@ -11,9 +11,9 @@ // It is based on the following documentation: // https://wiki.openshock.org/hardware/shockers/caixianlin/#rf-specification -const rmt_data_t kRmtPreamble = {1400, 1, 750, 0}; -const rmt_data_t kRmtOne = {750, 1, 250, 0}; -const rmt_data_t kRmtZero = {250, 1, 750, 0}; +const rmt_symbol_word_t kRmtPreamble = {1400, 1, 750, 0}; +const rmt_symbol_word_t kRmtOne = {750, 1, 250, 0}; +const rmt_symbol_word_t kRmtZero = {250, 1, 750, 0}; using namespace OpenShock; @@ -22,7 +22,7 @@ size_t Rmt::CaiXianlinEncoder::GetBufferSize() return 44; } -bool Rmt::CaiXianlinEncoder::FillBuffer(rmt_data_t* sequence, uint16_t shockerId, uint8_t channelId, ShockerCommandType type, uint8_t intensity) +bool Rmt::CaiXianlinEncoder::FillBuffer(rmt_symbol_word_t* sequence, uint16_t shockerId, uint8_t channelId, ShockerCommandType type, uint8_t intensity) { // Intensity must be between 0 and 99 intensity = std::min(intensity, static_cast(99)); diff --git a/src/radio/rmt/D80Encoder.cpp b/components/protocols/src/D80Encoder.cpp similarity index 83% rename from src/radio/rmt/D80Encoder.cpp rename to components/protocols/src/D80Encoder.cpp index b015eaef..ec51bffa 100644 --- a/src/radio/rmt/D80Encoder.cpp +++ b/components/protocols/src/D80Encoder.cpp @@ -6,10 +6,10 @@ #include -const rmt_data_t kRmtPreamble = {1900, 1, 4000, 0}; -const rmt_data_t kRmtOne = {900, 1, 300, 0}; -const rmt_data_t kRmtZero = {300, 1, 900, 0}; -const rmt_data_t kRmtPostamble = {200, 1, 2200, 0}; +const rmt_symbol_word_t kRmtPreamble = {1900, 1, 4000, 0}; +const rmt_symbol_word_t kRmtOne = {900, 1, 300, 0}; +const rmt_symbol_word_t kRmtZero = {300, 1, 900, 0}; +const rmt_symbol_word_t kRmtPostamble = {200, 1, 2200, 0}; using namespace OpenShock; @@ -18,7 +18,7 @@ size_t Rmt::D80Encoder::GetBufferSize() return 42; } -bool Rmt::D80Encoder::FillBuffer(rmt_data_t* sequence, uint16_t shockerId, ShockerCommandType type, uint8_t intensity) +bool Rmt::D80Encoder::FillBuffer(rmt_symbol_word_t* sequence, uint16_t shockerId, ShockerCommandType type, uint8_t intensity) { // Intensity must be between 0 and 15, this should mimic the rounding of the original remote which // allows you to select from 1-99 when the protocol only has 4 bits for intensity (0-15). diff --git a/src/radio/rmt/Petrainer998DREncoder.cpp b/components/protocols/src/Petrainer998DREncoder.cpp similarity index 76% rename from src/radio/rmt/Petrainer998DREncoder.cpp rename to components/protocols/src/Petrainer998DREncoder.cpp index 2572bb8b..ecf82fb6 100644 --- a/src/radio/rmt/Petrainer998DREncoder.cpp +++ b/components/protocols/src/Petrainer998DREncoder.cpp @@ -6,10 +6,10 @@ #include -const rmt_data_t kRmtPreamble = {1500, 1, 750, 0}; -const rmt_data_t kRmtOne = {750, 1, 250, 0}; -const rmt_data_t kRmtZero = {250, 1, 750, 0}; -const rmt_data_t kRmtPostamble = {250, 1, 3750, 0}; // Some subvariants expect a quiet period between commands, this is a last 1 bit followed by a very long pause +const rmt_symbol_word_t kRmtPreamble = {1500, 1, 750, 0}; +const rmt_symbol_word_t kRmtOne = {750, 1, 250, 0}; +const rmt_symbol_word_t kRmtZero = {250, 1, 750, 0}; +const rmt_symbol_word_t kRmtPostamble = {250, 1, 3750, 0}; // Some subvariants expect a quiet period between commands, this is a last 1 bit followed by a very long pause using namespace OpenShock; @@ -18,7 +18,7 @@ size_t Rmt::Petrainer998DREncoder::GetBufferSize() return 42; } -bool Rmt::Petrainer998DREncoder::FillBuffer(rmt_data_t* sequence, uint16_t shockerId, ShockerCommandType type, uint8_t intensity) +bool Rmt::Petrainer998DREncoder::FillBuffer(rmt_symbol_word_t* sequence, uint16_t shockerId, ShockerCommandType type, uint8_t intensity) { // Intensity must be between 0 and 100 intensity = std::min(intensity, static_cast(100)); diff --git a/src/radio/rmt/PetrainerEncoder.cpp b/components/protocols/src/PetrainerEncoder.cpp similarity index 77% rename from src/radio/rmt/PetrainerEncoder.cpp rename to components/protocols/src/PetrainerEncoder.cpp index 64afa549..57ffd0d1 100644 --- a/src/radio/rmt/PetrainerEncoder.cpp +++ b/components/protocols/src/PetrainerEncoder.cpp @@ -4,10 +4,10 @@ #include -const rmt_data_t kRmtPreamble = {750, 1, 750, 0}; -const rmt_data_t kRmtOne = {200, 1, 1500, 0}; -const rmt_data_t kRmtZero = {200, 1, 750, 0}; -const rmt_data_t kRmtPostamble = {200, 1, 7000, 0}; +const rmt_symbol_word_t kRmtPreamble = {750, 1, 750, 0}; +const rmt_symbol_word_t kRmtOne = {200, 1, 1500, 0}; +const rmt_symbol_word_t kRmtZero = {200, 1, 750, 0}; +const rmt_symbol_word_t kRmtPostamble = {200, 1, 7000, 0}; using namespace OpenShock; @@ -16,7 +16,7 @@ size_t Rmt::PetrainerEncoder::GetBufferSize() return 42; } -bool Rmt::PetrainerEncoder::FillBuffer(rmt_data_t* sequence, uint16_t shockerId, ShockerCommandType type, uint8_t intensity) +bool Rmt::PetrainerEncoder::FillBuffer(rmt_symbol_word_t* sequence, uint16_t shockerId, ShockerCommandType type, uint8_t intensity) { // Intensity must be between 0 and 100 intensity = std::min(intensity, static_cast(100)); diff --git a/src/radio/rmt/Sequence.cpp b/components/protocols/src/Sequence.cpp similarity index 90% rename from src/radio/rmt/Sequence.cpp rename to components/protocols/src/Sequence.cpp index 9d952cc8..a9046ea8 100644 --- a/src/radio/rmt/Sequence.cpp +++ b/components/protocols/src/Sequence.cpp @@ -29,7 +29,7 @@ inline static size_t getSequenceBufferSize(ShockerModelType shockerModelType) } } -inline static bool fillSequenceImpl(rmt_data_t* data, ShockerModelType modelType, uint16_t shockerId, ShockerCommandType commandType, uint8_t intensity) +inline static bool fillSequenceImpl(rmt_symbol_word_t* data, ShockerModelType modelType, uint16_t shockerId, ShockerCommandType commandType, uint8_t intensity) { switch (modelType) { case ShockerModelType::CaiXianlin: @@ -57,7 +57,7 @@ Rmt::Sequence::Sequence(ShockerModelType shockerModel, uint16_t shockerId, int64 { if (m_size == 0) return; - m_data = static_cast(malloc(m_size * 2 * sizeof(rmt_data_t))); + m_data = static_cast(malloc(m_size * 2 * sizeof(rmt_symbol_word_t))); if (m_data == nullptr) { m_size = 0; return; diff --git a/src/radio/rmt/T330Encoder.cpp b/components/protocols/src/T330Encoder.cpp similarity index 80% rename from src/radio/rmt/T330Encoder.cpp rename to components/protocols/src/T330Encoder.cpp index 59896daa..8c4481e8 100644 --- a/src/radio/rmt/T330Encoder.cpp +++ b/components/protocols/src/T330Encoder.cpp @@ -4,10 +4,10 @@ #include -const rmt_data_t kRmtPreamble = {960, 1, 790, 0}; -const rmt_data_t kRmtOne = {220, 1, 980, 0}; -const rmt_data_t kRmtZero = {220, 1, 580, 0}; -const rmt_data_t kRmtPostamble = {220, 1, 135, 0}; +const rmt_symbol_word_t kRmtPreamble = {960, 1, 790, 0}; +const rmt_symbol_word_t kRmtOne = {220, 1, 980, 0}; +const rmt_symbol_word_t kRmtZero = {220, 1, 580, 0}; +const rmt_symbol_word_t kRmtPostamble = {220, 1, 135, 0}; using namespace OpenShock; @@ -16,7 +16,7 @@ size_t Rmt::WellturnT330Encoder::GetBufferSize() return 43; } -bool Rmt::WellturnT330Encoder::FillBuffer(rmt_data_t* sequence, uint16_t shockerId, ShockerCommandType type, uint8_t intensity) +bool Rmt::WellturnT330Encoder::FillBuffer(rmt_symbol_word_t* sequence, uint16_t shockerId, ShockerCommandType type, uint8_t intensity) { // Intensity must be between 0 and 100 intensity = std::min(intensity, static_cast(100)); diff --git a/include/radio/rmt/internal/Shared.h b/components/protocols/src/radio/rmt/internal/Shared.h similarity index 81% rename from include/radio/rmt/internal/Shared.h rename to components/protocols/src/radio/rmt/internal/Shared.h index a62f73b8..db924fa3 100644 --- a/include/radio/rmt/internal/Shared.h +++ b/components/protocols/src/radio/rmt/internal/Shared.h @@ -1,6 +1,6 @@ #pragma once -#include +#include #include #include @@ -8,7 +8,7 @@ namespace OpenShock::Rmt::Internal { template - constexpr void EncodeBits(rmt_data_t* sequence, T data, const rmt_data_t& rmtOne, const rmt_data_t& rmtZero) + constexpr void EncodeBits(rmt_symbol_word_t* sequence, T data, const rmt_symbol_word_t& rmtOne, const rmt_symbol_word_t& rmtZero) { static_assert(std::is_unsigned_v, "T must be an unsigned integer"); static_assert(N > 0, "N must be greater than 0"); diff --git a/components/rfc8908/CMakeLists.txt b/components/rfc8908/CMakeLists.txt new file mode 100644 index 00000000..c75f0428 --- /dev/null +++ b/components/rfc8908/CMakeLists.txt @@ -0,0 +1,9 @@ +# RFC 8908 + classic captive-detection HTTP handlers for esp_http_server: OS probe +# redirects and the machine-readable /captive-portal/api endpoint. Standalone so it's +# independent of the portal's app logic and the DNS responder. +idf_component_register( + SRCS "src/RFC8908Handler.cpp" + INCLUDE_DIRS "include" + REQUIRES esp_http_server # esp_http_server.h in public header + PRIV_REQUIRES logging osjson # .cpp-only (common was unused) +) diff --git a/components/rfc8908/include/rfc8908/RFC8908Handler.h b/components/rfc8908/include/rfc8908/RFC8908Handler.h new file mode 100644 index 00000000..360c9c89 --- /dev/null +++ b/components/rfc8908/include/rfc8908/RFC8908Handler.h @@ -0,0 +1,21 @@ +#pragma once + +#include + +namespace OpenShock::RFC8908 { + // Registers the OS captive-detection surface on an esp_http_server instance: + // - well-known probe URLs (/gen_204, /hotspot-detect*, /ncsi.txt, ...) → 302 to the portal + // - the RFC 8908 endpoint /captive-portal/api → application/captive+json + // - a 404 fallback that redirects any other unknown request to the portal + // + // `apIpv4` is the portal AP address (e.g. "4.3.2.1"); the pointer must remain valid + // for the lifetime of the server (pass a string literal or long-lived buffer). + // + // The server must be configured with httpd_uri_match_wildcard for the prefix probe + // paths to match. + esp_err_t RegisterProbeHandlers(httpd_handle_t server, const char* apIpv4); + + // Emit the standard captive redirect (302 → http:///). Exposed so a static + // file handler can share the exact same fallthrough behaviour on unknown paths. + esp_err_t EmitRedirect(httpd_req_t* req); +} // namespace OpenShock::RFC8908 diff --git a/components/rfc8908/src/RFC8908Handler.cpp b/components/rfc8908/src/RFC8908Handler.cpp new file mode 100644 index 00000000..b5541e85 --- /dev/null +++ b/components/rfc8908/src/RFC8908Handler.cpp @@ -0,0 +1,106 @@ +#include "rfc8908/RFC8908Handler.h" + +const char* const TAG = "RFC8908Handler"; + +#include "Logging.h" + +#include "json/Json.h" + +#include + +using namespace OpenShock; + +// The portal AP IP, captured at registration. Handlers are static C callbacks, so we +// keep it here; there is only ever one captive portal server at a time. +static const char* s_apIpv4 = "4.3.2.1"; + +static const char* const captivePortalApiPath = "/captive-portal/api"; + +esp_err_t RFC8908::EmitRedirect(httpd_req_t* req) +{ + std::string location = std::string("http://") + s_apIpv4 + "/"; + + httpd_resp_set_status(req, "302 Found"); + httpd_resp_set_hdr(req, "Location", location.c_str()); + httpd_resp_set_hdr(req, "Cache-Control", "no-store, no-cache, must-revalidate"); + httpd_resp_set_hdr(req, "Pragma", "no-cache"); + httpd_resp_set_hdr(req, "Expires", "0"); + return httpd_resp_send(req, nullptr, 0); +} + +// Catch-all handler for OS connectivity probes. Operating systems attempt to detect +// internet access by requesting well-known URLs; redirecting these to the portal root +// intentionally fails the connectivity check and activates captive-portal mode. +static esp_err_t probeHandler(httpd_req_t* req) +{ + return RFC8908::EmitRedirect(req); +} + +// RFC 8908 Captive Portal API endpoint — machine-readable captive state. Complements +// the classic redirect-based detection used by Android, iOS/macOS, and Windows. +static esp_err_t captiveApiHandler(httpd_req_t* req) +{ + OS_LOGI(TAG, "Got Captive API request"); + + std::string portalUrl = std::string("http://") + s_apIpv4 + "/"; + + OpenShock::JSON::StringWriter writer; + json_gen_str_t* gen = writer.gen(); + json_gen_start_object(gen); + json_gen_obj_set_bool(gen, "captive", true); + JSON::objSetString(gen, "user-portal-url", portalUrl.c_str()); + JSON::objSetString(gen, "venue-info-url", "https://openshock.org"); + json_gen_end_object(gen); + std::string jsonStr = writer.finish(); + + httpd_resp_set_status(req, "200 OK"); + httpd_resp_set_type(req, "application/captive+json"); + httpd_resp_set_hdr(req, "Cache-Control", "private"); + return httpd_resp_send(req, jsonStr.data(), jsonStr.size()); +} + +static esp_err_t err404Handler(httpd_req_t* req, httpd_err_code_t) +{ + return RFC8908::EmitRedirect(req); +} + +esp_err_t RFC8908::RegisterProbeHandlers(httpd_handle_t server, const char* apIpv4) +{ + if (apIpv4 != nullptr) { + s_apIpv4 = apIpv4; + } + + // Well-known OS probe paths. Prefix forms (e.g. "/hotspot-detect*") require the + // server to be configured with httpd_uri_match_wildcard. + static const char* const probePaths[] = { + captivePortalApiPath, // handled specially below + "/gen_204", + "/generate_204", + "/ncsi.txt", + "/hotspot-detect*", + "/success*", + "/connecttest*", + "/check_network_status*", + "/canonical*", + }; + + esp_err_t result = ESP_OK; + for (const char* path : probePaths) { + httpd_uri_t def = {}; + def.uri = path; + def.method = HTTP_GET; + def.handler = (path == captivePortalApiPath) ? captiveApiHandler : probeHandler; + def.user_ctx = nullptr; + + esp_err_t err = httpd_register_uri_handler(server, &def); + if (err != ESP_OK) { + OS_LOGE(TAG, "Failed to register probe handler %s: %s", path, esp_err_to_name(err)); + result = err; + } + } + + // Any other unknown request also redirects to the portal. + httpd_register_err_handler(server, HTTPD_404_NOT_FOUND, err404Handler); + + return result; +} diff --git a/components/serial/CMakeLists.txt b/components/serial/CMakeLists.txt new file mode 100644 index 00000000..ea15574a --- /dev/null +++ b/components/serial/CMakeLists.txt @@ -0,0 +1,11 @@ +# OpenShock low-level serial transport — raw byte I/O to the device console +# (UART0 / USB-Serial-JTAG, selected by the ESP-IDF console sdkconfig). +# +# Foundation layer: depends only on the IDF console drivers so `logging` can write +# through it (Serial::Write) without a dependency cycle. It must NOT depend on +# `logging` — hence no OS_LOG here; Init() reports failure via its return value. +idf_component_register( + SRCS "src/Serial.cpp" + INCLUDE_DIRS "include" + REQUIRES esp_driver_uart esp_driver_usb_serial_jtag +) diff --git a/components/serial/include/serial/Serial.h b/components/serial/include/serial/Serial.h new file mode 100644 index 00000000..2c75fd72 --- /dev/null +++ b/components/serial/include/serial/Serial.h @@ -0,0 +1,38 @@ +#pragma once + +#include +#include + +// Low-level serial transport for the device console (UART0 or USB-Serial-JTAG, +// selected by the ESP-IDF console sdkconfig). This is a foundation-layer +// component: it owns the raw byte I/O to the port and depends on nothing above +// the IDF drivers, so `logging` can write through it without a dependency cycle. +namespace OpenShock::Serial { + /** + * @brief Installs the console driver (UART0 / USB-Serial-JTAG) and routes stdout + * through it, unbuffered so interactive prompts flush immediately. + * Idempotent. + * + * @return true on success, false if the driver could not be installed. + */ + [[nodiscard]] bool Init(); + + /** + * @brief Non-blocking read of up to @p len bytes from the console. + * + * @param buffer Destination buffer. + * @param len Maximum number of bytes to read. + * @return Number of bytes read (0 if none available). + */ + int Read(uint8_t* buffer, std::size_t len); + + /** + * @brief Writes @p len raw bytes to the console. Before Init() the bytes go + * straight to the ROM serial output (early-boot logging), never C stdio. + * + * @param data Source buffer. + * @param len Number of bytes to write. + * @return Number of bytes written. + */ + int Write(const uint8_t* data, std::size_t len); +} // namespace OpenShock::Serial diff --git a/components/serial/src/Serial.cpp b/components/serial/src/Serial.cpp new file mode 100644 index 00000000..fb3cf034 --- /dev/null +++ b/components/serial/src/Serial.cpp @@ -0,0 +1,110 @@ +#include + +#include "serial/Serial.h" + +#include + +#include +#include + +#include + +// Select the console backend from the configured primary console. USB-CDC console +// (native TinyUSB) is not handled here yet; I/O falls back to the ROM output. +#if defined(CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG) +#define OS_CONSOLE_USJ 1 +#include +#include +#elif defined(CONFIG_ESP_CONSOLE_UART) || defined(CONFIG_ESP_CONSOLE_UART_DEFAULT) || defined(CONFIG_ESP_CONSOLE_UART_CUSTOM) +#define OS_CONSOLE_UART 1 +#include +#include +#endif + +using namespace OpenShock; + +static constexpr int k_consoleBufferSize = 256; + +static bool s_initialized = false; + +// Byte-for-byte write to the ROM serial output. Used before the driver is +// installed (early-boot logging) or when no console driver is configured. This +// is a raw serial write, not C stdio. +static void romWrite(const uint8_t* data, std::size_t len) +{ + for (std::size_t i = 0; i < len; i++) { + esp_rom_output_putc(static_cast(data[i])); + } +} + +bool Serial::Init() +{ + if (s_initialized) { + return true; + } + +#if defined(OS_CONSOLE_USJ) + usb_serial_jtag_driver_config_t cfg = {.tx_buffer_size = k_consoleBufferSize, .rx_buffer_size = k_consoleBufferSize}; + + esp_err_t err = usb_serial_jtag_driver_install(&cfg); + if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) { + return false; + } + usb_serial_jtag_vfs_use_driver(); +#elif defined(OS_CONSOLE_UART) + // TX buffer 0: uart_write_bytes blocks until the bytes are on the wire, so log + // lines (incl. a final panic message before reset) are flushed synchronously. + esp_err_t err = uart_driver_install(static_cast(CONFIG_ESP_CONSOLE_UART_NUM), k_consoleBufferSize, 0, 0, nullptr, 0); + if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) { + return false; + } + uart_vfs_dev_use_driver(CONFIG_ESP_CONSOLE_UART_NUM); +#endif + + // The command handler echoes prompts without trailing newlines, so make stdout + // unbuffered to flush them immediately. + std::setvbuf(stdout, nullptr, _IONBF, 0); + + s_initialized = true; + return true; +} + +int Serial::Read(uint8_t* buffer, std::size_t len) +{ + if (buffer == nullptr || len == 0) { + return 0; + } + +#if defined(OS_CONSOLE_USJ) + int read = usb_serial_jtag_read_bytes(buffer, static_cast(len), 0); +#elif defined(OS_CONSOLE_UART) + int read = uart_read_bytes(static_cast(CONFIG_ESP_CONSOLE_UART_NUM), buffer, static_cast(len), 0); +#else + int read = 0; +#endif + + return read < 0 ? 0 : read; +} + +int Serial::Write(const uint8_t* data, std::size_t len) +{ + if (data == nullptr || len == 0) { + return 0; + } + + if (!s_initialized) { + romWrite(data, len); + return static_cast(len); + } + +#if defined(OS_CONSOLE_USJ) + int written = usb_serial_jtag_write_bytes(data, len, pdMS_TO_TICKS(50)); +#elif defined(OS_CONSOLE_UART) + int written = uart_write_bytes(static_cast(CONFIG_ESP_CONSOLE_UART_NUM), data, len); +#else + romWrite(data, len); + int written = static_cast(len); +#endif + + return written < 0 ? 0 : written; +} diff --git a/components/serial_console/CMakeLists.txt b/components/serial_console/CMakeLists.txt new file mode 100644 index 00000000..05b1116d --- /dev/null +++ b/components/serial_console/CMakeLists.txt @@ -0,0 +1,16 @@ +# OpenShock interactive console — SerialInputHandler + command handlers. +# +# The app-level command console: reads lines from the low-level `serial` transport +# and dispatches them against `core` (config, wifi, RF, OTA, ...). Entry-layer; +# nothing in core depends on it (only the top-level app entry does). Its public +# surface is just SerialInputHandler.h, so all deps are private. +FILE(GLOB_RECURSE console_sources + ${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp) + +idf_component_register( + SRCS ${console_sources} + INCLUDE_DIRS "include" + PRIV_REQUIRES core serial device_control crypto # core: config/wifi/etc. · serial: Serial::Init/Read · device_control: estop/CommandHandler/visual/radio · crypto: Base64 +) + +target_compile_options(${COMPONENT_LIB} PRIVATE -Wno-error) diff --git a/components/serial_console/include/serial_console/SerialInputHandler.h b/components/serial_console/include/serial_console/SerialInputHandler.h new file mode 100644 index 00000000..0cc13f02 --- /dev/null +++ b/components/serial_console/include/serial_console/SerialInputHandler.h @@ -0,0 +1,26 @@ +#pragma once + +#include +#include + +// Serial console output. TX is written to stdout, which the IDF console driver +// routes to the primary console (UART0) and any configured secondary channel +// (USB-Serial-JTAG / USB-CDC). RX is handled separately by the `serial` transport +// (Serial::Read), which also routes stdout through that same driver. +// +// OS_SERIAL_PRINT takes a runtime string (never a format string, so '%' in the +// data is safe); OS_SERIAL_PRINTF takes a literal format + args; OS_SERIAL_PRINTLN +// takes an optional string literal (the "" concatenation appends the CRLF). +#define OS_SERIAL_PRINT(str) std::fputs(str, stdout) +#define OS_SERIAL_PRINTF(...) std::printf(__VA_ARGS__) +#define OS_SERIAL_PRINTLN(...) std::fputs("" __VA_ARGS__ "\r\n", stdout) + +namespace OpenShock::SerialInputHandler { + [[nodiscard]] bool Init(); + + bool SerialEchoEnabled(); + void SetSerialEchoEnabled(bool enabled); + + void PrintWelcomeHeader(); + void PrintVersionInfo(); +} // namespace OpenShock::SerialInputHandler diff --git a/include/serial/command_handlers/CommandEntry.h b/components/serial_console/include/serial_console/command_handlers/CommandEntry.h similarity index 96% rename from include/serial/command_handlers/CommandEntry.h rename to components/serial_console/include/serial_console/command_handlers/CommandEntry.h index 4c254336..040e6096 100644 --- a/include/serial/command_handlers/CommandEntry.h +++ b/components/serial_console/include/serial_console/command_handlers/CommandEntry.h @@ -3,7 +3,7 @@ #include #include -namespace OpenShock::Serial { +namespace OpenShock::SerialCmds { typedef void (*CommandHandler)(std::string_view arg, bool isAutomated); class CommandArgument { @@ -53,4 +53,4 @@ namespace OpenShock::Serial { std::string_view m_name; std::vector m_commands; }; -} // namespace OpenShock::Serial +} // namespace OpenShock::SerialCmds diff --git a/components/serial_console/include/serial_console/command_handlers/common.h b/components/serial_console/include/serial_console/command_handlers/common.h new file mode 100644 index 00000000..06f479b1 --- /dev/null +++ b/components/serial_console/include/serial_console/command_handlers/common.h @@ -0,0 +1,14 @@ +#pragma once + +#include "serial_console/command_handlers/index.h" +#include "serial_console/SerialInputHandler.h" + +#include "Logging.h" + +#define SERPR_SYS(format, ...) OS_SERIAL_PRINTF("$SYS$|" format "\r\n", ##__VA_ARGS__) + +#define SERPR_RESPONSE(format, ...) SERPR_SYS("Response|" format, ##__VA_ARGS__) +#define SERPR_SUCCESS(format, ...) SERPR_SYS("Success|" format, ##__VA_ARGS__) +#define SERPR_ERROR(format, ...) SERPR_SYS("Error|" format, ##__VA_ARGS__) + +using namespace std::string_view_literals; diff --git a/components/serial_console/include/serial_console/command_handlers/index.h b/components/serial_console/include/serial_console/command_handlers/index.h new file mode 100644 index 00000000..b8ebde50 --- /dev/null +++ b/components/serial_console/include/serial_console/command_handlers/index.h @@ -0,0 +1,48 @@ +#pragma once + +#include "serial_console/command_handlers/CommandEntry.h" + +#include + +namespace OpenShock::SerialCmds::CommandHandlers { + OpenShock::SerialCmds::CommandGroup VersionHandler(); + OpenShock::SerialCmds::CommandGroup RestartHandler(); + OpenShock::SerialCmds::CommandGroup SysInfoHandler(); + OpenShock::SerialCmds::CommandGroup EchoHandler(); + OpenShock::SerialCmds::CommandGroup ValidGpiosHandler(); + OpenShock::SerialCmds::CommandGroup RfTxPinHandler(); + OpenShock::SerialCmds::CommandGroup EStopHandler(); + OpenShock::SerialCmds::CommandGroup DomainHandler(); + OpenShock::SerialCmds::CommandGroup AuthTokenHandler(); + OpenShock::SerialCmds::CommandGroup HostnameHandler(); + OpenShock::SerialCmds::CommandGroup NetworksHandler(); + OpenShock::SerialCmds::CommandGroup KeepAliveHandler(); + OpenShock::SerialCmds::CommandGroup JsonConfigHandler(); + OpenShock::SerialCmds::CommandGroup RawConfigHandler(); + OpenShock::SerialCmds::CommandGroup RfTransmitHandler(); + OpenShock::SerialCmds::CommandGroup FactoryResetHandler(); + OpenShock::SerialCmds::CommandGroup LedTestHandler(); + + inline std::vector AllCommandHandlers() + { + return { + VersionHandler(), + RestartHandler(), + SysInfoHandler(), + EchoHandler(), + ValidGpiosHandler(), + RfTxPinHandler(), + EStopHandler(), + DomainHandler(), + AuthTokenHandler(), + HostnameHandler(), + NetworksHandler(), + KeepAliveHandler(), + JsonConfigHandler(), + RawConfigHandler(), + RfTransmitHandler(), + FactoryResetHandler(), + LedTestHandler(), + }; + } +} // namespace OpenShock::SerialCmds::CommandHandlers diff --git a/src/serial/SerialInputHandler.cpp b/components/serial_console/src/SerialInputHandler.cpp similarity index 75% rename from src/serial/SerialInputHandler.cpp rename to components/serial_console/src/SerialInputHandler.cpp index 5682d5a5..62229d3a 100644 --- a/src/serial/SerialInputHandler.cpp +++ b/components/serial_console/src/SerialInputHandler.cpp @@ -1,4 +1,4 @@ -#include "serial/SerialInputHandler.h" +#include "serial_console/SerialInputHandler.h" const char* const TAG = "SerialInputHandler"; @@ -7,26 +7,22 @@ const char* const TAG = "SerialInputHandler"; #include "config/Config.h" #include "config/SerialInputConfig.h" #include "Convert.h" -#include "Core.h" #include "estop/EStopManager.h" #include "FormatHelpers.h" #include "http/HTTPRequestManager.h" #include "Logging.h" -#include "serial/command_handlers/CommandEntry.h" -#include "serial/command_handlers/common.h" -#include "serial/command_handlers/index.h" +#include "serial_console/command_handlers/CommandEntry.h" +#include "serial_console/command_handlers/common.h" +#include "serial_console/command_handlers/index.h" +#include "serial/Serial.h" #include "serialization/JsonAPI.h" #include "serialization/JsonSerial.h" -#include "util/Base64Utils.h" -#include "util/StringUtils.h" +#include "Temporal.h" +#include "Base64.h" +#include "StringHelpers.h" #include "util/TaskUtils.h" #include "wifi/WiFiManager.h" -#include - -#include -#include - #include #include #include @@ -63,8 +59,8 @@ const int64_t PASTE_INTERVAL_THRESHOLD_MS = 20; const std::size_t SERIAL_BUFFER_MAX_CAPACITY = 4096; static bool s_echoEnabled = true; -static std::vector s_commandGroups; -static std::unordered_map s_commandHandlers; +static std::vector s_commandGroups; +static std::unordered_map s_commandHandlers; static void printCompleteHelp() { @@ -127,7 +123,7 @@ static void printCompleteHelp() OS_SERIAL_PRINT(buffer.data()); } -static void printCommandHelp(Serial::CommandGroup& group) +static void printCommandHelp(SerialCmds::CommandGroup& group) { std::size_t size = 0; for (const auto& command : group.commands()) { @@ -275,10 +271,10 @@ static void handleHelpCommand(std::string_view arg, bool isAutomated) return; } - SERPR_ERROR("Command \"%.*s\" not found", arg.length(), arg.data()); + SERPR_ERROR("Command \"%.*s\" not found", static_cast(arg.length()), arg.data()); } -void RegisterCommandHandler(const OpenShock::Serial::CommandGroup& handler) +void RegisterCommandHandler(const OpenShock::SerialCmds::CommandGroup& handler) { s_commandHandlers[handler.name()] = handler; } @@ -351,6 +347,26 @@ class SerialBuffer { m_data[m_size++] = c; } + inline void append(const char* data, std::size_t len) + { + if (len == 0) { + return; + } + + if (m_size + len > m_capacity) { + reserve(m_size + len); + } + + // reserve() clamps to SERIAL_BUFFER_MAX_CAPACITY and may have reset m_size; + // never write past the allocation. + if (len > m_capacity - m_size) { + len = m_capacity - m_size; + } + + std::memcpy(m_data + m_size, data, len); + m_size += len; + } + constexpr void pop_back() noexcept { if (m_size > 0) { @@ -373,133 +389,123 @@ enum class SerialReadResult { AutoCompleteRequest, }; -static SerialReadResult tryReadSerialLine(SerialBuffer& buffer) +// Staging buffer: the console is read in chunks (Serial::Read returns the +// number of bytes read) into this buffer, and the parser scans the chunk in +// place. Bytes left over after an early return (line end / autocomplete) are +// carried across polls via s_rxStagingHead. +static uint8_t s_rxStaging[128]; +static std::size_t s_rxStagingHead = 0; +static std::size_t s_rxStagingLen = 0; + +// Ensures the staging buffer holds unconsumed bytes, refilling from the console +// when it has been drained. Returns false if no bytes are available. +static bool fillRxStaging() { - // Check if there's any data available - int available = OS_SERIAL.available(); - if (available <= 0) { - return SerialReadResult::NoData; + if (s_rxStagingHead < s_rxStagingLen) { + return true; } - // Reserve space for the new data - buffer.reserve(buffer.size() + available); - - // Read the data into the buffer - while (available-- > 0) { - char c = OS_SERIAL.read(); - - // Handle backspace - if (c == '\b') { - buffer.pop_back(); // Remove the last character from the buffer if it exists - continue; - } - - // Handle newline - if (c == '\r' || c == '\n') { - if (!buffer.empty()) { - return SerialReadResult::LineEnd; - } - continue; - } - - // Handle leading whitespace - if (c == ' ' && buffer.empty()) { - continue; - } - - if (c == '\t') { - return SerialReadResult::AutoCompleteRequest; - } - - // If character is printable, add it to the buffer - if (c > 31 && c < 127) { - buffer.push_back(c); - } + int read = Serial::Read(s_rxStaging, sizeof(s_rxStaging)); + if (read <= 0) { + return false; } - return SerialReadResult::Data; + s_rxStagingHead = 0; + s_rxStagingLen = static_cast(read); + return true; } -static void skipSerialWhitespaces(SerialBuffer& buffer) +static SerialReadResult tryReadSerialLine(SerialBuffer& buffer) { - int available = OS_SERIAL.available(); + bool gotData = false; - while (available-- > 0) { - char c = OS_SERIAL.read(); + while (fillRxStaging()) { + gotData = true; - if (c != ' ' && c != '\r' && c != '\n') { - buffer.push_back(c); - break; - } - } -} + const char* chunk = reinterpret_cast(s_rxStaging); + const std::size_t end = s_rxStagingLen; + std::size_t i = s_rxStagingHead; -#if ARDUINO_USB_MODE -static SerialReadResult tryReadUSBSerialLine(SerialBuffer& buffer) -{ - // Check if there's any data available - int available = OS_SERIAL_USB.available(); - if (available <= 0) { - return SerialReadResult::NoData; - } + while (i < end) { + char c = chunk[i]; - // Reserve space for the new data - buffer.reserve(buffer.size() + available); + // Backspace: erase the last buffered character. + if (c == '\b') { + buffer.pop_back(); + ++i; + continue; + } - // Read the data into the buffer - while (available-- > 0) { - char c = OS_SERIAL_USB.read(); + // Line end: dispatch the line, or swallow blank lines. + if (c == '\r' || c == '\n') { + ++i; + if (!buffer.empty()) { + s_rxStagingHead = i; + return SerialReadResult::LineEnd; + } + continue; + } - // Handle backspace - if (c == '\b') { - buffer.pop_back(); // Remove the last character from the buffer if it exists - continue; - } + // Tab: autocomplete request. + if (c == '\t') { + ++i; + s_rxStagingHead = i; + return SerialReadResult::AutoCompleteRequest; + } - // Handle newline - if (c == '\r' || c == '\n') { - if (!buffer.empty()) { - return SerialReadResult::LineEnd; + // Strip leading whitespace. + if (c == ' ' && buffer.empty()) { + ++i; + continue; } - continue; - } - // Handle leading whitespace - if (c == ' ' && buffer.empty()) { - continue; - } + // Bulk-append the maximal run of printable characters. Bytes we don't + // handle above (control codes, >= 0x7F) are dropped one at a time. + std::size_t runStart = i; + while (i < end) { + char d = chunk[i]; + if (d <= 31 || d >= 127) { + break; + } + ++i; + } - if (c == '\t') { - return SerialReadResult::AutoCompleteRequest; + if (i > runStart) { + buffer.append(chunk + runStart, i - runStart); + } else { + ++i; // drop the unhandled byte + } } - // If character is printable, add it to the buffer - if (c > 31 && c < 127) { - buffer.push_back(c); - } + s_rxStagingHead = i; // chunk fully consumed } - return SerialReadResult::Data; + return gotData ? SerialReadResult::Data : SerialReadResult::NoData; } -static void skipUSBSerialWhitespaces(SerialBuffer& buffer) +static void skipSerialWhitespaces(SerialBuffer& buffer) { - int available = OS_SERIAL_USB.available(); - - while (available-- > 0) { - char c = OS_SERIAL_USB.read(); - - if (c != ' ' && c != '\r' && c != '\n') { - buffer.push_back(c); - break; + while (fillRxStaging()) { + const char* chunk = reinterpret_cast(s_rxStaging); + const std::size_t end = s_rxStagingLen; + std::size_t i = s_rxStagingHead; + + while (i < end) { + char c = chunk[i++]; + if (c != ' ' && c != '\r' && c != '\n') { + buffer.push_back(c); + s_rxStagingHead = i; + return; + } } + + s_rxStagingHead = i; // drained; loop refills } } -#endif static void echoBuffer(std::string_view buffer) { - OS_SERIAL_PRINTF(CLEAR_LINE "> %.*s", buffer.size(), buffer.data()); + OS_SERIAL_PRINTF(CLEAR_LINE "> %.*s", static_cast(buffer.size()), buffer.data()); } static void echoHandleSerialInput(std::string_view buffer, bool hasData) @@ -560,7 +566,7 @@ static void processSerialLine(std::string_view line) auto it = s_commandHandlers.find(command); if (it == s_commandHandlers.end()) { - SERPR_ERROR("Command \"%.*s\" not found", command.size(), command.data()); + SERPR_ERROR("Command \"%.*s\" not found", static_cast(command.size()), command.data()); return; } @@ -575,7 +581,7 @@ static void processSerialLine(std::string_view line) // If the first argument is not empty, try to find a subcommand that matches if (!firstArg.empty()) { - for (Serial::CommandEntry& cmd : it->second.commands()) { + for (SerialCmds::CommandEntry& cmd : it->second.commands()) { // Check subcommand name if (cmd.name() != firstArg) { continue; @@ -597,7 +603,7 @@ static void processSerialLine(std::string_view line) } // If no subcommand was found, try to find a default command - for (Serial::CommandEntry& cmd : it->second.commands()) { + for (SerialCmds::CommandEntry& cmd : it->second.commands()) { // Skip subcommands if (!cmd.name().empty()) { continue; @@ -614,7 +620,7 @@ static void processSerialLine(std::string_view line) return; } - SERPR_ERROR("Command \"%.*s\" not found", command.size(), command.data()); + SERPR_ERROR("Command \"%.*s\" not found", static_cast(command.size()), command.data()); } static void serialRxTask(void*) @@ -637,33 +643,7 @@ static void serialRxTask(void*) skipSerialWhitespaces(buffer); break; case SerialReadResult::AutoCompleteRequest: - OS_SERIAL_PRINTF(CLEAR_LINE "> %.*s [AutoComplete is not implemented]", buffer.size(), buffer.data()); - break; - case SerialReadResult::Data: - echoHandleSerialInput(buffer, true); - break; - default: - echoHandleSerialInput(buffer, false); - break; - } - -#if ARDUINO_USB_MODE - switch (tryReadUSBSerialLine(buffer)) { - case SerialReadResult::LineEnd: - processSerialLine(buffer); - - // Deallocate memory if the buffer is too large - if (buffer.capacity() > SERIAL_BUFFER_MAX_CAPACITY) { - buffer.destroy(); - } else { - buffer.clear(); - } - - // Skip any remaining trailing whitespaces - skipUSBSerialWhitespaces(buffer); - break; - case SerialReadResult::AutoCompleteRequest: - OS_SERIAL_PRINTF(CLEAR_LINE "> %.*s [AutoComplete is not implemented]", buffer.size(), buffer.data()); + OS_SERIAL_PRINTF(CLEAR_LINE "> %.*s [AutoComplete is not implemented]", static_cast(buffer.size()), buffer.data()); break; case SerialReadResult::Data: echoHandleSerialInput(buffer, true); @@ -672,7 +652,6 @@ static void serialRxTask(void*) echoHandleSerialInput(buffer, false); break; } -#endif vTaskDelay(pdMS_TO_TICKS(20)); // 50 Hz update rate } @@ -682,15 +661,21 @@ bool SerialInputHandler::Init() { static bool s_initialized = false; if (s_initialized) { - OS_LOGW(TAG, "Serial input handler already initialized"); + OS_LOGW(TAG, "SerialCmds input handler already initialized"); return false; } s_initialized = true; + // Install the console RX driver and make stdout unbuffered. + if (!Serial::Init()) { + OS_LOGE(TAG, "Failed to initialize serial console"); + return false; + } + // Register command handlers - s_commandGroups = OpenShock::Serial::CommandHandlers::AllCommandHandlers(); + s_commandGroups = OpenShock::SerialCmds::CommandHandlers::AllCommandHandlers(); for (const auto& handler : s_commandGroups) { - OS_LOGV(TAG, "Registering command handler: %.*s", handler.name().size(), handler.name().data()); + OS_LOGV(TAG, "Registering command handler: %.*s", static_cast(handler.name().size()), handler.name().data()); RegisterCommandHandler(handler); } diff --git a/src/serial/command_handlers/CommandEntry.cpp b/components/serial_console/src/command_handlers/CommandEntry.cpp similarity index 92% rename from src/serial/command_handlers/CommandEntry.cpp rename to components/serial_console/src/command_handlers/CommandEntry.cpp index 67080b46..7a882266 100644 --- a/src/serial/command_handlers/CommandEntry.cpp +++ b/components/serial_console/src/command_handlers/CommandEntry.cpp @@ -1,6 +1,6 @@ -#include "serial/command_handlers/CommandEntry.h" +#include "serial_console/command_handlers/CommandEntry.h" -using namespace OpenShock::Serial; +using namespace OpenShock::SerialCmds; CommandEntry::CommandEntry(std::string_view description, CommandHandler commandHandler) noexcept : m_description(description) diff --git a/src/serial/command_handlers/authtoken.cpp b/components/serial_console/src/command_handlers/authtoken.cpp similarity index 84% rename from src/serial/command_handlers/authtoken.cpp rename to components/serial_console/src/command_handlers/authtoken.cpp index 28d878d3..88b6d1cd 100644 --- a/src/serial/command_handlers/authtoken.cpp +++ b/components/serial_console/src/command_handlers/authtoken.cpp @@ -1,4 +1,4 @@ -#include "serial/command_handlers/common.h" +#include "serial_console/command_handlers/common.h" #include "config/Config.h" #include "http/JsonAPI.h" @@ -36,9 +36,9 @@ static void handleAuthtokenCommand(std::string_view arg, bool isAutomated) } } -OpenShock::Serial::CommandGroup OpenShock::Serial::CommandHandlers::AuthTokenHandler() +OpenShock::SerialCmds::CommandGroup OpenShock::SerialCmds::CommandHandlers::AuthTokenHandler() { - auto group = OpenShock::Serial::CommandGroup("authtoken"sv); + auto group = OpenShock::SerialCmds::CommandGroup("authtoken"sv); auto& getCommand = group.addCommand("Get the backend auth token"sv, handleAuthtokenCommand); diff --git a/src/serial/command_handlers/domain.cpp b/components/serial_console/src/command_handlers/domain.cpp similarity index 66% rename from src/serial/command_handlers/domain.cpp rename to components/serial_console/src/command_handlers/domain.cpp index 4d292691..63c622a0 100644 --- a/src/serial/command_handlers/domain.cpp +++ b/components/serial_console/src/command_handlers/domain.cpp @@ -1,4 +1,4 @@ -#include "serial/command_handlers/common.h" +#include "serial_console/command_handlers/common.h" #include "config/Config.h" #include "http/HTTPRequestManager.h" @@ -8,7 +8,7 @@ #include -const char* const TAG = "Serial::CommandHandlers::Domain"; +const char* const TAG = "SerialCmds::CommandHandlers::Domain"; static void handleDomainCommand(std::string_view arg, bool isAutomated) { @@ -26,13 +26,13 @@ static void handleDomainCommand(std::string_view arg, bool isAutomated) // Check if the domain is too long // TODO: Remove magic number - if (arg.length() + 40 >= OPENSHOCK_URI_BUFFER_SIZE) { - SERPR_ERROR("Domain name too long, please try increasing the \"OPENSHOCK_URI_BUFFER_SIZE\" constant in source code"); + if (arg.length() + 40 >= CONFIG_OPENSHOCK_URI_BUFFER_SIZE) { + SERPR_ERROR("Domain name too long, please try increasing the \"CONFIG_OPENSHOCK_URI_BUFFER_SIZE\" constant in source code"); return; } - char uri[OPENSHOCK_URI_BUFFER_SIZE]; - sprintf(uri, "https://%.*s/1", arg.length(), arg.data()); + char uri[CONFIG_OPENSHOCK_URI_BUFFER_SIZE]; + sprintf(uri, "https://%.*s/1", static_cast(arg.length()), arg.data()); auto resp = OpenShock::HTTP::GetJSON( uri, @@ -44,11 +44,11 @@ static void handleDomainCommand(std::string_view arg, bool isAutomated) ); if (resp.result != OpenShock::HTTP::RequestResult::Success) { - SERPR_ERROR("Tried to connect to \"%.*s\", but failed with status [%d] (%s), refusing to save domain to config", arg.length(), arg.data(), resp.code, resp.ResultToString()); + SERPR_ERROR("Tried to connect to \"%.*s\", but failed with status [%d] (%s), refusing to save domain to config", static_cast(arg.length()), arg.data(), resp.code, resp.ResultToString()); return; } - OS_LOGI(TAG, "Successfully connected to \"%.*s\", version: %s, commit: %s, current time: %s", arg.length(), arg.data(), resp.data.version.c_str(), resp.data.commit.c_str(), resp.data.currentTime.c_str()); + OS_LOGI(TAG, "Successfully connected to \"%.*s\", version: %s, commit: %s, current time: %s", static_cast(arg.length()), arg.data(), resp.data.version.c_str(), resp.data.commit.c_str(), resp.data.currentTime.c_str()); bool result = OpenShock::Config::SetBackendDomain(std::string(arg)); @@ -63,9 +63,9 @@ static void handleDomainCommand(std::string_view arg, bool isAutomated) esp_restart(); } -OpenShock::Serial::CommandGroup OpenShock::Serial::CommandHandlers::DomainHandler() +OpenShock::SerialCmds::CommandGroup OpenShock::SerialCmds::CommandHandlers::DomainHandler() { - auto group = OpenShock::Serial::CommandGroup("domain"sv); + auto group = OpenShock::SerialCmds::CommandGroup("domain"sv); auto& getCommand = group.addCommand("Get the backend domain."sv, handleDomainCommand); diff --git a/src/serial/command_handlers/echo.cpp b/components/serial_console/src/command_handlers/echo.cpp similarity index 78% rename from src/serial/command_handlers/echo.cpp rename to components/serial_console/src/command_handlers/echo.cpp index 935329cb..94860c39 100644 --- a/src/serial/command_handlers/echo.cpp +++ b/components/serial_console/src/command_handlers/echo.cpp @@ -1,10 +1,10 @@ -#include "serial/command_handlers/common.h" +#include "serial_console/command_handlers/common.h" -#include "serial/SerialInputHandler.h" +#include "serial_console/SerialInputHandler.h" #include "config/Config.h" #include "Convert.h" -#include "util/StringUtils.h" +#include "StringHelpers.h" static void handleSerialEchoCommand(std::string_view arg, bool isAutomated) { @@ -30,9 +30,9 @@ static void handleSerialEchoCommand(std::string_view arg, bool isAutomated) } } -OpenShock::Serial::CommandGroup OpenShock::Serial::CommandHandlers::EchoHandler() +OpenShock::SerialCmds::CommandGroup OpenShock::SerialCmds::CommandHandlers::EchoHandler() { - auto group = OpenShock::Serial::CommandGroup("echo"sv); + auto group = OpenShock::SerialCmds::CommandGroup("echo"sv); auto& getCommand = group.addCommand("Get the serial echo status"sv, handleSerialEchoCommand); diff --git a/src/serial/command_handlers/estop.cpp b/components/serial_console/src/command_handlers/estop.cpp similarity index 91% rename from src/serial/command_handlers/estop.cpp rename to components/serial_console/src/command_handlers/estop.cpp index 859bc63b..f208b7c9 100644 --- a/src/serial/command_handlers/estop.cpp +++ b/components/serial_console/src/command_handlers/estop.cpp @@ -1,4 +1,4 @@ -#include "serial/command_handlers/common.h" +#include "serial_console/command_handlers/common.h" #include "config/Config.h" #include "Convert.h" @@ -68,9 +68,9 @@ static void handleEStopPinCommand(std::string_view arg, bool isAutomated) SERPR_SUCCESS("Saved config"); } -OpenShock::Serial::CommandGroup OpenShock::Serial::CommandHandlers::EStopHandler() +OpenShock::SerialCmds::CommandGroup OpenShock::SerialCmds::CommandHandlers::EStopHandler() { - auto group = OpenShock::Serial::CommandGroup("estop"sv); + auto group = OpenShock::SerialCmds::CommandGroup("estop"sv); auto& getEnabledCommand = group.addCommand("enabled"sv, "Get the E-Stop enabled state."sv, handleEStopEnabledCommand); auto& setEnabledCommand = group.addCommand("enabled"sv, "Set the E-Stop enabled state."sv, handleEStopEnabledCommand); diff --git a/src/serial/command_handlers/factoryreset.cpp b/components/serial_console/src/command_handlers/factoryreset.cpp similarity index 61% rename from src/serial/command_handlers/factoryreset.cpp rename to components/serial_console/src/command_handlers/factoryreset.cpp index e76226e3..d03f9a1e 100644 --- a/src/serial/command_handlers/factoryreset.cpp +++ b/components/serial_console/src/command_handlers/factoryreset.cpp @@ -1,5 +1,5 @@ -#include "serial/command_handlers/common.h" -#include "serial/SerialInputHandler.h" +#include "serial_console/command_handlers/common.h" +#include "serial_console/SerialInputHandler.h" #include "config/Config.h" @@ -15,9 +15,9 @@ static void handleFactoryResetCommand(std::string_view arg, bool isAutomated) esp_restart(); } -OpenShock::Serial::CommandGroup OpenShock::Serial::CommandHandlers::FactoryResetHandler() +OpenShock::SerialCmds::CommandGroup OpenShock::SerialCmds::CommandHandlers::FactoryResetHandler() { - auto group = OpenShock::Serial::CommandGroup("factoryreset"sv); + auto group = OpenShock::SerialCmds::CommandGroup("factoryreset"sv); auto& cmd = group.addCommand("Reset the hub to factory defaults and restart"sv, handleFactoryResetCommand); diff --git a/src/serial/command_handlers/hostname.cpp b/components/serial_console/src/command_handlers/hostname.cpp similarity index 77% rename from src/serial/command_handlers/hostname.cpp rename to components/serial_console/src/command_handlers/hostname.cpp index 976993c8..22f056b5 100644 --- a/src/serial/command_handlers/hostname.cpp +++ b/components/serial_console/src/command_handlers/hostname.cpp @@ -1,4 +1,4 @@ -#include "serial/command_handlers/common.h" +#include "serial_console/command_handlers/common.h" #include "config/Config.h" @@ -6,7 +6,7 @@ #include -const char* const TAG = "Serial::CommandHandlers::Domain"; +const char* const TAG = "SerialCmds::CommandHandlers::Domain"; static void handleHostnameCommand(std::string_view arg, bool isAutomated) { @@ -30,9 +30,9 @@ static void handleHostnameCommand(std::string_view arg, bool isAutomated) } } -OpenShock::Serial::CommandGroup OpenShock::Serial::CommandHandlers::HostnameHandler() +OpenShock::SerialCmds::CommandGroup OpenShock::SerialCmds::CommandHandlers::HostnameHandler() { - auto group = OpenShock::Serial::CommandGroup("hostname"sv); + auto group = OpenShock::SerialCmds::CommandGroup("hostname"sv); auto& getCommand = group.addCommand("Get the network hostname."sv, handleHostnameCommand); diff --git a/src/serial/command_handlers/jsonconfig.cpp b/components/serial_console/src/command_handlers/jsonconfig.cpp similarity index 78% rename from src/serial/command_handlers/jsonconfig.cpp rename to components/serial_console/src/command_handlers/jsonconfig.cpp index e090ee89..e46b3d90 100644 --- a/src/serial/command_handlers/jsonconfig.cpp +++ b/components/serial_console/src/command_handlers/jsonconfig.cpp @@ -1,4 +1,4 @@ -#include "serial/command_handlers/common.h" +#include "serial_console/command_handlers/common.h" #include "config/Config.h" @@ -24,9 +24,9 @@ static void handleJsonConfigCommand(std::string_view arg, bool isAutomated) esp_restart(); } -OpenShock::Serial::CommandGroup OpenShock::Serial::CommandHandlers::JsonConfigHandler() +OpenShock::SerialCmds::CommandGroup OpenShock::SerialCmds::CommandHandlers::JsonConfigHandler() { - auto group = OpenShock::Serial::CommandGroup("jsonconfig"sv); + auto group = OpenShock::SerialCmds::CommandGroup("jsonconfig"sv); auto& getCommand = group.addCommand("Get the configuration as JSON"sv, handleJsonConfigCommand); diff --git a/src/serial/command_handlers/keepalive.cpp b/components/serial_console/src/command_handlers/keepalive.cpp similarity index 82% rename from src/serial/command_handlers/keepalive.cpp rename to components/serial_console/src/command_handlers/keepalive.cpp index baebdb58..bc239458 100644 --- a/src/serial/command_handlers/keepalive.cpp +++ b/components/serial_console/src/command_handlers/keepalive.cpp @@ -1,9 +1,9 @@ -#include "serial/command_handlers/common.h" +#include "serial_console/command_handlers/common.h" #include "CommandHandler.h" #include "config/Config.h" #include "Convert.h" -#include "util/StringUtils.h" +#include "StringHelpers.h" static void handleKeepAliveCommand(std::string_view arg, bool isAutomated) { @@ -34,9 +34,9 @@ static void handleKeepAliveCommand(std::string_view arg, bool isAutomated) } } -OpenShock::Serial::CommandGroup OpenShock::Serial::CommandHandlers::KeepAliveHandler() +OpenShock::SerialCmds::CommandGroup OpenShock::SerialCmds::CommandHandlers::KeepAliveHandler() { - auto group = OpenShock::Serial::CommandGroup("keepalive"sv); + auto group = OpenShock::SerialCmds::CommandGroup("keepalive"sv); auto& getCommand = group.addCommand("Get the shocker keep-alive status"sv, handleKeepAliveCommand); diff --git a/src/serial/command_handlers/ledtest.cpp b/components/serial_console/src/command_handlers/ledtest.cpp similarity index 67% rename from src/serial/command_handlers/ledtest.cpp rename to components/serial_console/src/command_handlers/ledtest.cpp index 3d902d54..c01a2639 100644 --- a/src/serial/command_handlers/ledtest.cpp +++ b/components/serial_console/src/command_handlers/ledtest.cpp @@ -1,4 +1,4 @@ -#include "serial/command_handlers/common.h" +#include "serial_console/command_handlers/common.h" #include "visual/VisualStateManager.h" @@ -14,9 +14,9 @@ static void handleSerialLedTestCommand(std::string_view arg, bool isAutomated) SERPR_SUCCESS("LedTest|Complete"); } -OpenShock::Serial::CommandGroup OpenShock::Serial::CommandHandlers::LedTestHandler() +OpenShock::SerialCmds::CommandGroup OpenShock::SerialCmds::CommandHandlers::LedTestHandler() { - auto group = OpenShock::Serial::CommandGroup("ledtest"sv); + auto group = OpenShock::SerialCmds::CommandGroup("ledtest"sv); group.addCommand("Cycle through all LED patterns for visual verification"sv, handleSerialLedTestCommand); diff --git a/src/serial/command_handlers/networks.cpp b/components/serial_console/src/command_handlers/networks.cpp similarity index 57% rename from src/serial/command_handlers/networks.cpp rename to components/serial_console/src/command_handlers/networks.cpp index 4ac01233..317ca4e1 100644 --- a/src/serial/command_handlers/networks.cpp +++ b/components/serial_console/src/command_handlers/networks.cpp @@ -1,67 +1,53 @@ -#include "serial/command_handlers/common.h" +#include "serial_console/command_handlers/common.h" #include "config/Config.h" #include "wifi/WiFiManager.h" - -#include +#include "json/Json.h" #include -const char* const TAG = "Serial::CommandHandlers::Networks"; +const char* const TAG = "SerialCmds::CommandHandlers::Networks"; static void handleNetworksCommand(std::string_view arg, bool isAutomated) { - cJSON* root; - if (arg.empty()) { - root = cJSON_CreateArray(); - if (root == nullptr) { - SERPR_ERROR("Failed to create JSON array"); - return; - } + OpenShock::JSON::StringWriter writer; + json_gen_str_t* gen = writer.gen(); - if (!OpenShock::Config::GetWiFiCredentials(root, true)) { + json_gen_start_array(gen); + if (!OpenShock::Config::GetWiFiCredentials(gen, true)) { SERPR_ERROR("Failed to get WiFi credentials from config"); - cJSON_Delete(root); return; } + json_gen_end_array(gen); - char* out = cJSON_PrintUnformatted(root); - cJSON_Delete(root); - if (out == nullptr) { - SERPR_ERROR("Failed to print JSON"); - return; - } + std::string out = writer.finish(); - SERPR_RESPONSE("Networks|%s", out); - - cJSON_free(out); + SERPR_RESPONSE("Networks|%s", out.c_str()); return; } - root = cJSON_ParseWithLength(arg.data(), arg.length()); - if (root == nullptr) { - SERPR_ERROR("Failed to parse JSON: %s", cJSON_GetErrorPtr()); + OpenShock::JSON::JsonDocument doc; + if (!doc.parse(arg)) { + SERPR_ERROR("Failed to parse JSON"); return; } - if (cJSON_IsArray(root) == 0) { + OpenShock::JSON::JsonView root = doc.root(); + if (!root.isArray()) { SERPR_ERROR("Invalid argument (not an array)"); - cJSON_Delete(root); return; } std::vector creds; - uint8_t id = 1; - cJSON* network = nullptr; - cJSON_ArrayForEach(network, root) - { + uint8_t id = 1; + const int count = root.count(); + for (int i = 0; i < count; ++i) { OpenShock::Config::WiFiCredentials cred; - if (!cred.FromJSON(network)) { + if (!cred.FromJSON(root.at(i))) { SERPR_ERROR("Failed to parse network"); - cJSON_Delete(root); return; } @@ -74,8 +60,6 @@ static void handleNetworksCommand(std::string_view arg, bool isAutomated) creds.push_back(std::move(cred)); } - cJSON_Delete(root); - if (!OpenShock::Config::SetWiFiCredentials(creds)) { SERPR_ERROR("Failed to save config"); return; @@ -86,9 +70,9 @@ static void handleNetworksCommand(std::string_view arg, bool isAutomated) OpenShock::WiFiManager::RefreshNetworkCredentials(); } -OpenShock::Serial::CommandGroup OpenShock::Serial::CommandHandlers::NetworksHandler() +OpenShock::SerialCmds::CommandGroup OpenShock::SerialCmds::CommandHandlers::NetworksHandler() { - auto group = OpenShock::Serial::CommandGroup("networks"sv); + auto group = OpenShock::SerialCmds::CommandGroup("networks"sv); auto& getCommand = group.addCommand("Get all saved networks."sv, handleNetworksCommand); diff --git a/src/serial/command_handlers/rawconfig.cpp b/components/serial_console/src/command_handlers/rawconfig.cpp similarity index 77% rename from src/serial/command_handlers/rawconfig.cpp rename to components/serial_console/src/command_handlers/rawconfig.cpp index 4fbd1921..bd66e6ef 100644 --- a/src/serial/command_handlers/rawconfig.cpp +++ b/components/serial_console/src/command_handlers/rawconfig.cpp @@ -1,8 +1,8 @@ -#include "serial/command_handlers/common.h" +#include "serial_console/command_handlers/common.h" #include "config/Config.h" #include "TinyVec.h" -#include "util/Base64Utils.h" +#include "Base64.h" #include @@ -20,7 +20,7 @@ static void handleRawConfigCommand(std::string_view arg, bool isAutomated) } std::string base64; - if (!OpenShock::Base64Utils::Encode(buffer, base64)) { + if (!OpenShock::Base64::Encode(buffer, base64)) { SERPR_ERROR("Failed to encode raw config to base64"); return; } @@ -30,7 +30,7 @@ static void handleRawConfigCommand(std::string_view arg, bool isAutomated) } TinyVec buffer; - if (!OpenShock::Base64Utils::Decode(arg, buffer)) { + if (!OpenShock::Base64::Decode(arg, buffer)) { SERPR_ERROR("Failed to decode base64"); return; } @@ -45,9 +45,9 @@ static void handleRawConfigCommand(std::string_view arg, bool isAutomated) esp_restart(); } -OpenShock::Serial::CommandGroup OpenShock::Serial::CommandHandlers::RawConfigHandler() +OpenShock::SerialCmds::CommandGroup OpenShock::SerialCmds::CommandHandlers::RawConfigHandler() { - auto group = OpenShock::Serial::CommandGroup("rawconfig"sv); + auto group = OpenShock::SerialCmds::CommandGroup("rawconfig"sv); auto& getCommand = group.addCommand("Get the raw binary config"sv, handleRawConfigCommand); diff --git a/src/serial/command_handlers/restart.cpp b/components/serial_console/src/command_handlers/restart.cpp similarity index 51% rename from src/serial/command_handlers/restart.cpp rename to components/serial_console/src/command_handlers/restart.cpp index 209ce8ab..8b183c45 100644 --- a/src/serial/command_handlers/restart.cpp +++ b/components/serial_console/src/command_handlers/restart.cpp @@ -1,5 +1,5 @@ -#include "serial/command_handlers/common.h" -#include "serial/SerialInputHandler.h" +#include "serial_console/command_handlers/common.h" +#include "serial_console/SerialInputHandler.h" #include @@ -11,9 +11,9 @@ static void handleRestartCommand(std::string_view arg, bool isAutomated) esp_restart(); } -OpenShock::Serial::CommandGroup OpenShock::Serial::CommandHandlers::RestartHandler() +OpenShock::SerialCmds::CommandGroup OpenShock::SerialCmds::CommandHandlers::RestartHandler() { - auto group = OpenShock::Serial::CommandGroup("restart"sv); + auto group = OpenShock::SerialCmds::CommandGroup("restart"sv); auto& cmd = group.addCommand("Restart the board"sv, handleRestartCommand); diff --git a/src/serial/command_handlers/rftransmit.cpp b/components/serial_console/src/command_handlers/rftransmit.cpp similarity index 78% rename from src/serial/command_handlers/rftransmit.cpp rename to components/serial_console/src/command_handlers/rftransmit.cpp index 3a4f86db..b5c99efd 100644 --- a/src/serial/command_handlers/rftransmit.cpp +++ b/components/serial_console/src/command_handlers/rftransmit.cpp @@ -1,7 +1,8 @@ -#include "serial/command_handlers/common.h" +#include "serial_console/command_handlers/common.h" #include "CommandHandler.h" #include "serialization/JsonSerial.h" +#include "json/Json.h" static void handleRFTransmitCommand(std::string_view arg, bool isAutomated) { @@ -9,16 +10,14 @@ static void handleRFTransmitCommand(std::string_view arg, bool isAutomated) SERPR_ERROR("No command"); return; } - cJSON* root = cJSON_ParseWithLength(arg.data(), arg.length()); - if (root == nullptr) { - SERPR_ERROR("Failed to parse JSON: %s", cJSON_GetErrorPtr()); + OpenShock::JSON::JsonDocument doc; + if (!doc.parse(arg)) { + SERPR_ERROR("Failed to parse JSON"); return; } OpenShock::Serialization::JsonSerial::ShockerCommand cmd; - bool parsed = OpenShock::Serialization::JsonSerial::ParseShockerCommand(root, cmd); - - cJSON_Delete(root); + bool parsed = OpenShock::Serialization::JsonSerial::ParseShockerCommand(doc.root(), cmd); if (!parsed) { SERPR_ERROR("Failed to parse shocker command"); @@ -33,9 +32,9 @@ static void handleRFTransmitCommand(std::string_view arg, bool isAutomated) SERPR_SUCCESS("Command sent"); } -OpenShock::Serial::CommandGroup OpenShock::Serial::CommandHandlers::RfTransmitHandler() +OpenShock::SerialCmds::CommandGroup OpenShock::SerialCmds::CommandHandlers::RfTransmitHandler() { - auto group = OpenShock::Serial::CommandGroup("rftransmit"sv); + auto group = OpenShock::SerialCmds::CommandGroup("rftransmit"sv); auto& cmd = group.addCommand("Transmit a RF command"sv, handleRFTransmitCommand); cmd.addArgument( diff --git a/src/serial/command_handlers/rftxpin.cpp b/components/serial_console/src/command_handlers/rftxpin.cpp similarity index 85% rename from src/serial/command_handlers/rftxpin.cpp rename to components/serial_console/src/command_handlers/rftxpin.cpp index 2e6dec8c..5ff8ba53 100644 --- a/src/serial/command_handlers/rftxpin.cpp +++ b/components/serial_console/src/command_handlers/rftxpin.cpp @@ -1,9 +1,9 @@ -#include "serial/command_handlers/common.h" +#include "serial_console/command_handlers/common.h" #include "CommandHandler.h" #include "config/Config.h" #include "Convert.h" -#include "SetGPIOResultCode.h" +#include "enums/SetGPIOResultCode.h" static void handleRfTxPinCommand(std::string_view arg, bool isAutomated) { @@ -45,9 +45,9 @@ static void handleRfTxPinCommand(std::string_view arg, bool isAutomated) } } -OpenShock::Serial::CommandGroup OpenShock::Serial::CommandHandlers::RfTxPinHandler() +OpenShock::SerialCmds::CommandGroup OpenShock::SerialCmds::CommandHandlers::RfTxPinHandler() { - auto group = OpenShock::Serial::CommandGroup("rftxpin"sv); + auto group = OpenShock::SerialCmds::CommandGroup("rftxpin"sv); auto& getCommand = group.addCommand("Get the GPIO pin used for the radio transmitter"sv, handleRfTxPinCommand); diff --git a/src/serial/command_handlers/sysinfo.cpp b/components/serial_console/src/command_handlers/sysinfo.cpp similarity index 79% rename from src/serial/command_handlers/sysinfo.cpp rename to components/serial_console/src/command_handlers/sysinfo.cpp index 3d224388..d0fc2710 100644 --- a/src/serial/command_handlers/sysinfo.cpp +++ b/components/serial_console/src/command_handlers/sysinfo.cpp @@ -1,7 +1,9 @@ -#include "serial/command_handlers/common.h" +#include + +#include "serial_console/command_handlers/common.h" -#include "Core.h" #include "FormatHelpers.h" +#include "Temporal.h" #include "wifi/WiFiManager.h" #include "wifi/WiFiNetwork.h" @@ -31,14 +33,12 @@ static void handleDebugInfoCommand(std::string_view arg, bool isAutomated) char ipAddressBuffer[64]; OpenShock::WiFiManager::GetIPAddress(ipAddressBuffer); SERPR_RESPONSE("WiFiInfo|IPv4|%s", ipAddressBuffer); - OpenShock::WiFiManager::GetIPv6Address(ipAddressBuffer); - SERPR_RESPONSE("WiFiInfo|IPv6|%s", ipAddressBuffer); } } -OpenShock::Serial::CommandGroup OpenShock::Serial::CommandHandlers::SysInfoHandler() +OpenShock::SerialCmds::CommandGroup OpenShock::SerialCmds::CommandHandlers::SysInfoHandler() { - auto group = OpenShock::Serial::CommandGroup("sysinfo"sv); + auto group = OpenShock::SerialCmds::CommandGroup("sysinfo"sv); auto& cmd = group.addCommand("Get system information from RTOS, WiFi, etc."sv, handleDebugInfoCommand); diff --git a/src/serial/command_handlers/validgpios.cpp b/components/serial_console/src/command_handlers/validgpios.cpp similarity index 76% rename from src/serial/command_handlers/validgpios.cpp rename to components/serial_console/src/command_handlers/validgpios.cpp index 3c5676d2..80ef3c63 100644 --- a/src/serial/command_handlers/validgpios.cpp +++ b/components/serial_console/src/command_handlers/validgpios.cpp @@ -1,4 +1,4 @@ -#include "serial/command_handlers/common.h" +#include "serial_console/command_handlers/common.h" #include "Chipset.h" @@ -30,9 +30,9 @@ static void handleValidGpiosCommand(std::string_view arg, bool isAutomated) SERPR_RESPONSE("ValidGPIOs|%s", buffer.c_str()); } -OpenShock::Serial::CommandGroup OpenShock::Serial::CommandHandlers::ValidGpiosHandler() +OpenShock::SerialCmds::CommandGroup OpenShock::SerialCmds::CommandHandlers::ValidGpiosHandler() { - auto group = OpenShock::Serial::CommandGroup("validgpios"sv); + auto group = OpenShock::SerialCmds::CommandGroup("validgpios"sv); auto& cmd = group.addCommand("List all valid GPIO pins"sv, handleValidGpiosCommand); diff --git a/src/serial/command_handlers/version.cpp b/components/serial_console/src/command_handlers/version.cpp similarity index 53% rename from src/serial/command_handlers/version.cpp rename to components/serial_console/src/command_handlers/version.cpp index 53e72798..e25b9436 100644 --- a/src/serial/command_handlers/version.cpp +++ b/components/serial_console/src/command_handlers/version.cpp @@ -1,6 +1,6 @@ -#include "serial/command_handlers/common.h" +#include "serial_console/command_handlers/common.h" -#include "serial/SerialInputHandler.h" +#include "serial_console/SerialInputHandler.h" #include @@ -12,9 +12,9 @@ static void handleVersionCommand(std::string_view arg, bool isAutomated) OpenShock::SerialInputHandler::PrintVersionInfo(); } -OpenShock::Serial::CommandGroup OpenShock::Serial::CommandHandlers::VersionHandler() +OpenShock::SerialCmds::CommandGroup OpenShock::SerialCmds::CommandHandlers::VersionHandler() { - auto group = OpenShock::Serial::CommandGroup("version"sv); + auto group = OpenShock::SerialCmds::CommandGroup("version"sv); auto cmd = group.addCommand("Print version information"sv, handleVersionCommand); diff --git a/components/serialization/CMakeLists.txt b/components/serialization/CMakeLists.txt new file mode 100644 index 00000000..f5d1ca30 --- /dev/null +++ b/components/serialization/CMakeLists.txt @@ -0,0 +1,11 @@ +# OpenShock serialization layer. +# +# For now this is the generated flatbuffer wire types (serialization/_fbs/*_generated.h, +# produced by scripts/generate_schemas.py from schemas/*.fbs) — a single flatbuffers-only +# layer that config, wifi, the message codecs and the handlers all depend on one-way. +# Extracting these out of `core` is what lets config (and later the codecs themselves: +# JsonAPI/JsonSerial/WSGateway/WSLocal) leave core without a dependency cycle. +idf_component_register( + INCLUDE_DIRS "include" + REQUIRES flatbuffers # flatbuffers/flatbuffers.h in the generated headers +) diff --git a/include/serialization/_fbs/FirmwareBootType_generated.h b/components/serialization/include/serialization/_fbs/FirmwareBootType_generated.h similarity index 100% rename from include/serialization/_fbs/FirmwareBootType_generated.h rename to components/serialization/include/serialization/_fbs/FirmwareBootType_generated.h diff --git a/include/serialization/_fbs/GatewayToHubMessage_generated.h b/components/serialization/include/serialization/_fbs/GatewayToHubMessage_generated.h similarity index 100% rename from include/serialization/_fbs/GatewayToHubMessage_generated.h rename to components/serialization/include/serialization/_fbs/GatewayToHubMessage_generated.h diff --git a/include/serialization/_fbs/HubConfig_generated.h b/components/serialization/include/serialization/_fbs/HubConfig_generated.h similarity index 100% rename from include/serialization/_fbs/HubConfig_generated.h rename to components/serialization/include/serialization/_fbs/HubConfig_generated.h diff --git a/include/serialization/_fbs/HubToGatewayMessage_generated.h b/components/serialization/include/serialization/_fbs/HubToGatewayMessage_generated.h similarity index 100% rename from include/serialization/_fbs/HubToGatewayMessage_generated.h rename to components/serialization/include/serialization/_fbs/HubToGatewayMessage_generated.h diff --git a/include/serialization/_fbs/HubToLocalMessage_generated.h b/components/serialization/include/serialization/_fbs/HubToLocalMessage_generated.h similarity index 100% rename from include/serialization/_fbs/HubToLocalMessage_generated.h rename to components/serialization/include/serialization/_fbs/HubToLocalMessage_generated.h diff --git a/include/serialization/_fbs/LocalToHubMessage_generated.h b/components/serialization/include/serialization/_fbs/LocalToHubMessage_generated.h similarity index 100% rename from include/serialization/_fbs/LocalToHubMessage_generated.h rename to components/serialization/include/serialization/_fbs/LocalToHubMessage_generated.h diff --git a/include/serialization/_fbs/OtaUpdateProgressTask_generated.h b/components/serialization/include/serialization/_fbs/OtaUpdateProgressTask_generated.h similarity index 100% rename from include/serialization/_fbs/OtaUpdateProgressTask_generated.h rename to components/serialization/include/serialization/_fbs/OtaUpdateProgressTask_generated.h diff --git a/include/serialization/_fbs/SemVer_generated.h b/components/serialization/include/serialization/_fbs/SemVer_generated.h similarity index 100% rename from include/serialization/_fbs/SemVer_generated.h rename to components/serialization/include/serialization/_fbs/SemVer_generated.h diff --git a/include/serialization/_fbs/ShockerCommandType_generated.h b/components/serialization/include/serialization/_fbs/ShockerCommandType_generated.h similarity index 100% rename from include/serialization/_fbs/ShockerCommandType_generated.h rename to components/serialization/include/serialization/_fbs/ShockerCommandType_generated.h diff --git a/include/serialization/_fbs/ShockerCommand_generated.h b/components/serialization/include/serialization/_fbs/ShockerCommand_generated.h similarity index 100% rename from include/serialization/_fbs/ShockerCommand_generated.h rename to components/serialization/include/serialization/_fbs/ShockerCommand_generated.h diff --git a/include/serialization/_fbs/ShockerModelType_generated.h b/components/serialization/include/serialization/_fbs/ShockerModelType_generated.h similarity index 100% rename from include/serialization/_fbs/ShockerModelType_generated.h rename to components/serialization/include/serialization/_fbs/ShockerModelType_generated.h diff --git a/include/serialization/_fbs/WifiAuthMode_generated.h b/components/serialization/include/serialization/_fbs/WifiAuthMode_generated.h similarity index 100% rename from include/serialization/_fbs/WifiAuthMode_generated.h rename to components/serialization/include/serialization/_fbs/WifiAuthMode_generated.h diff --git a/include/serialization/_fbs/WifiNetworkEventType_generated.h b/components/serialization/include/serialization/_fbs/WifiNetworkEventType_generated.h similarity index 100% rename from include/serialization/_fbs/WifiNetworkEventType_generated.h rename to components/serialization/include/serialization/_fbs/WifiNetworkEventType_generated.h diff --git a/include/serialization/_fbs/WifiNetwork_generated.h b/components/serialization/include/serialization/_fbs/WifiNetwork_generated.h similarity index 100% rename from include/serialization/_fbs/WifiNetwork_generated.h rename to components/serialization/include/serialization/_fbs/WifiNetwork_generated.h diff --git a/include/serialization/_fbs/WifiScanStatus_generated.h b/components/serialization/include/serialization/_fbs/WifiScanStatus_generated.h similarity index 100% rename from include/serialization/_fbs/WifiScanStatus_generated.h rename to components/serialization/include/serialization/_fbs/WifiScanStatus_generated.h diff --git a/components/temporal/CMakeLists.txt b/components/temporal/CMakeLists.txt new file mode 100644 index 00000000..dd9a6a1a --- /dev/null +++ b/components/temporal/CMakeLists.txt @@ -0,0 +1,9 @@ +# OpenShock time helpers — OpenShock::millis(), OpenShock::micros(). +# +# Thin header-only wrapper over esp_timer. Split from common so that logging +# and other components can depend on it without pulling everything else in. + +idf_component_register( + INCLUDE_DIRS "include" + REQUIRES esp_timer +) diff --git a/include/Core.h b/components/temporal/include/Temporal.h similarity index 100% rename from include/Core.h rename to components/temporal/include/Temporal.h diff --git a/include/Checksum.h b/include/Checksum.h deleted file mode 100644 index 99842699..00000000 --- a/include/Checksum.h +++ /dev/null @@ -1,61 +0,0 @@ -#pragma once - -#include -#include - -#if __cplusplus >= 202'002L -#error Take into use C++20 std::integral instead of this macro logic -#endif - -#define SUM8_INT_FN(Type) \ - constexpr uint8_t Sum8(Type data) \ - { \ - uint8_t result = 0; \ - for (int i = 0; i < sizeof(Type); ++i) { \ - result += static_cast((data >> (i * 8)) & 0xFF); \ - } \ - return result; \ - } - -namespace OpenShock::Checksum { - constexpr uint8_t Sum8(const uint8_t* data, std::size_t size) - { - uint8_t checksum = 0; - for (std::size_t i = 0; i < size; ++i) { - checksum += data[i]; - } - return checksum; - } - template - constexpr uint8_t Sum8(const T& data) - { - static_assert(std::is_trivially_copyable_v, "Sum8 only supports trivially copyable types"); - - return Sum8(reinterpret_cast(std::addressof(data)), sizeof(T)); - } - - SUM8_INT_FN(int16_t) - SUM8_INT_FN(uint16_t) - SUM8_INT_FN(int32_t) - SUM8_INT_FN(uint32_t) - SUM8_INT_FN(int64_t) - SUM8_INT_FN(uint64_t) - - /** - * Make sure the uint8 only has its high bits (0x0F) set before using this function - */ - constexpr uint8_t ReverseNibble(uint8_t b) - { - return (0xF7B3D591E6A2C480ull >> (b * 4)) & 0xF; // Trust me bro - } - - /** - * Make sure the uint8 only has its high bits (0x0F) set before using this function - */ - constexpr uint8_t ReverseInverseNibble(uint8_t b) - { - return (0x084C2A6E195D3B7Full >> (b * 4)) & 0xF; // Trust me bro - } -} // namespace OpenShock::Checksum - -#undef SUM8_INT_FN diff --git a/include/Common.h b/include/Common.h deleted file mode 100644 index 435565f4..00000000 --- a/include/Common.h +++ /dev/null @@ -1,68 +0,0 @@ -#pragma once - -#include -#include - -#define DISABLE_DEFAULT(TypeName) TypeName() = delete -#define DISABLE_COPY(TypeName) \ - TypeName(const TypeName&) = delete; \ - TypeName& operator=(const TypeName&) = delete -#define DISABLE_MOVE(TypeName) \ - TypeName(TypeName&&) = delete; \ - TypeName& operator=(TypeName&&) = delete - -#ifndef OPENSHOCK_API_DOMAIN -#error "OPENSHOCK_API_DOMAIN must be defined" -#endif -#ifndef OPENSHOCK_FW_CDN_DOMAIN -#error "OPENSHOCK_FW_CDN_DOMAIN must be defined" -#endif -#ifndef OPENSHOCK_FW_VERSION -#error "OPENSHOCK_FW_VERSION must be defined" -#endif - -#define OPENSHOCK_FW_CDN_URL(path) "https://" OPENSHOCK_FW_CDN_DOMAIN path - -#define OPENSHOCK_GPIO_INVALID -1 - -#ifndef OPENSHOCK_RF_TX_GPIO -#warning "OPENSHOCK_RF_TX_GPIO is not defined, setting to OPENSHOCK_GPIO_INVALID" -#define OPENSHOCK_RF_TX_GPIO OPENSHOCK_GPIO_INVALID -#endif - -#ifndef OPENSHOCK_ESTOP_PIN -#define OPENSHOCK_ESTOP_PIN OPENSHOCK_GPIO_INVALID -#endif - -// Check if OPENSHOCK_FW_USERAGENT is overridden trough compiler flags, if not, generate a default useragent. -#ifndef OPENSHOCK_FW_USERAGENT -#define OPENSHOCK_FW_USERAGENT OPENSHOCK_FW_HOSTNAME "/" OPENSHOCK_FW_VERSION " (arduino-esp32; " OPENSHOCK_FW_BOARD "; " OPENSHOCK_FW_CHIP "; Espressif)" -#endif - -// Check if Arduino.h exists, if not instruct the developer to remove "arduino-esp32" from the useragent and replace it with "ESP-IDF", after which the developer may remove this warning. -#if defined(__has_include) && !__has_include("Arduino.h") -#warning \ - "Let it be known that Arduino hath finally been cast aside in favor of the noble ESP-IDF! I beseech thee, kind sir or madam, wouldst thou kindly partake in the honors of expunging 'arduino-esp32' from yonder useragent aloft, and in its stead, bestow the illustrious 'ESP-IDF'?" -#endif - -#if __cplusplus >= 202302L -#warning "C++23 compiler detected" -#elif __cplusplus >= 202002L -#warning "C++20 compiler detected" -#elif __cplusplus >= 201703L -// C++17 :3 -#elif __cplusplus >= 201402L -#error "C++14 compiler detected, OpenShock requires a C++17 compliant compiler" -#elif __cplusplus >= 201103L -#error "C++11 compiler detected, OpenShock requires a C++17 compliant compiler" -#elif __cplusplus >= 199711L -#error "C++98 compiler detected, OpenShock requires a C++17 compliant compiler" -#elif __cplusplus == 1 -#error "Pre-C++98 compiler detected, OpenShock requires a C++17 compliant compiler" -#else -#error "Unknown C++ standard detected, OpenShock requires a C++17 compliant compiler" -#endif - -namespace OpenShock::Constants { - const char* const FW_USERAGENT = OPENSHOCK_FW_USERAGENT; -} // namespace OpenShock::Constants diff --git a/include/FirmwareBootType.h b/include/FirmwareBootType.h deleted file mode 100644 index c13aedcf..00000000 --- a/include/FirmwareBootType.h +++ /dev/null @@ -1,30 +0,0 @@ -#pragma once - -#include "serialization/_fbs/FirmwareBootType_generated.h" - -#include -#include - -namespace OpenShock { - typedef OpenShock::Serialization::Types::FirmwareBootType FirmwareBootType; - - inline bool TryParseFirmwareBootType(FirmwareBootType& bootType, const char* str) - { - if (strcasecmp(str, "normal") == 0) { - bootType = FirmwareBootType::Normal; - return true; - } - - if (strcasecmp(str, "newfirmware") == 0 || strcasecmp(str, "new_firmware") == 0) { - bootType = FirmwareBootType::NewFirmware; - return true; - } - - if (strcasecmp(str, "rollback") == 0) { - bootType = FirmwareBootType::Rollback; - return true; - } - - return false; - } -} // namespace OpenShock diff --git a/include/Hashing.h b/include/Hashing.h deleted file mode 100644 index 709cab69..00000000 --- a/include/Hashing.h +++ /dev/null @@ -1,62 +0,0 @@ -#pragma once - -#include "Common.h" - -#include -#include -#include - -#include // TODO: When we use C++20, change this to -#include -#include - -namespace OpenShock { - class MD5 { - DISABLE_COPY(MD5); - DISABLE_MOVE(MD5); - - public: - MD5() { mbedtls_md5_init(&ctx); } - ~MD5() { mbedtls_md5_free(&ctx); } - - inline bool begin() { return mbedtls_md5_starts_ret(&ctx) == 0; } - inline bool update(const uint8_t* data, std::size_t dataLen) { return mbedtls_md5_update_ret(&ctx, data, dataLen) == 0; } - inline bool update(std::string_view data) { return update(reinterpret_cast(data.data()), data.length()); } - inline bool finish(std::array& hash) { return mbedtls_md5_finish_ret(&ctx, hash.data()) == 0; } - - private: - mbedtls_md5_context ctx; - }; - class SHA1 { - DISABLE_COPY(SHA1); - DISABLE_MOVE(SHA1); - - public: - SHA1() { mbedtls_sha1_init(&ctx); } - ~SHA1() { mbedtls_sha1_free(&ctx); } - - inline bool begin() { return mbedtls_sha1_starts_ret(&ctx) == 0; } - inline bool update(const uint8_t* data, std::size_t dataLen) { return mbedtls_sha1_update_ret(&ctx, data, dataLen) == 0; } - inline bool update(std::string_view data) { return update(reinterpret_cast(data.data()), data.length()); } - inline bool finish(std::array& hash) { return mbedtls_sha1_finish_ret(&ctx, hash.data()) == 0; } - - private: - mbedtls_sha1_context ctx; - }; - class SHA256 { - DISABLE_COPY(SHA256); - DISABLE_MOVE(SHA256); - - public: - SHA256() { mbedtls_sha256_init(&ctx); } - ~SHA256() { mbedtls_sha256_free(&ctx); } - - inline bool begin() { return mbedtls_sha256_starts_ret(&ctx, 0) == 0; } - inline bool update(const uint8_t* data, std::size_t dataLen) { return mbedtls_sha256_update_ret(&ctx, data, dataLen) == 0; } - inline bool update(std::string_view data) { return update(reinterpret_cast(data.data()), data.length()); } - inline bool finish(std::array& hash) { return mbedtls_sha256_finish_ret(&ctx, hash.data()) == 0; } - - private: - mbedtls_sha256_context ctx; - }; -} // namespace OpenShock diff --git a/include/WebSocketDeFragger.h b/include/WebSocketDeFragger.h deleted file mode 100644 index 2954a1a7..00000000 --- a/include/WebSocketDeFragger.h +++ /dev/null @@ -1,43 +0,0 @@ -#pragma once - -#include "Common.h" -#include "span.h" -#include "TinyVec.h" -#include "WebSocketMessageType.h" - -#include - -#include -#include -#include - -namespace OpenShock { - class WebSocketDeFragger { - DISABLE_COPY(WebSocketDeFragger); - DISABLE_MOVE(WebSocketDeFragger); - - public: - typedef std::function data)> EventCallback; - - WebSocketDeFragger(EventCallback callback); - ~WebSocketDeFragger(); - - void handler(uint8_t socketId, WStype_t type, const uint8_t* payload, std::size_t length); - void onEvent(const EventCallback& callback); - void clear(uint8_t socketId); - void clear(); - - private: - void start(uint8_t socketId, WebSocketMessageType type, const uint8_t* data, uint32_t length); - void append(uint8_t socketId, const uint8_t* data, uint32_t length); - void finish(uint8_t socketId, const uint8_t* data, uint32_t length); - - struct Message { - TinyVec data; - WebSocketMessageType type; - }; - - std::map m_messages; - EventCallback m_callback; - }; -} // namespace OpenShock diff --git a/include/captiveportal/CaptivePortalInstance.h b/include/captiveportal/CaptivePortalInstance.h deleted file mode 100644 index 09f3a53c..00000000 --- a/include/captiveportal/CaptivePortalInstance.h +++ /dev/null @@ -1,47 +0,0 @@ -#pragma once - -#include "Common.h" -#include "span.h" -#include "WebSocketDeFragger.h" - -#include -#include -#include -#include - -#include - -#include -#include -#include - -namespace OpenShock::CaptivePortal { - class CaptivePortalInstance { - DISABLE_COPY(CaptivePortalInstance); - DISABLE_MOVE(CaptivePortalInstance); - - public: - CaptivePortalInstance(); - ~CaptivePortalInstance(); - - bool sendMessageTXT(uint8_t socketId, std::string_view data) { return m_socketServer.sendTXT(socketId, data.data(), data.length()); } - bool sendMessageBIN(uint8_t socketId, tcb::span data) { return m_socketServer.sendBIN(socketId, data.data(), data.size()); } - bool broadcastMessageTXT(std::string_view data) { return m_socketServer.broadcastTXT(data.data(), data.length()); } - bool broadcastMessageBIN(tcb::span data) { return m_socketServer.broadcastBIN(data.data(), data.size()); } - bool hasClients() { return m_socketServer.connectedClients() > 0; } - - private: - void task(); - void handleWebSocketClientConnected(uint8_t socketId); - void handleWebSocketClientDisconnected(uint8_t socketId); - void handleWebSocketEvent(uint8_t socketId, WebSocketMessageType type, tcb::span payload); - - AsyncWebServer m_webServer; - WebSocketsServer m_socketServer; - WebSocketDeFragger m_socketDeFragger; - fs::LittleFSFS m_fileSystem; - DNSServer m_dnsServer; - TaskHandle_t m_taskHandle; - std::atomic m_stopRequested {false}; - }; -} // namespace OpenShock::CaptivePortal diff --git a/include/captiveportal/RFC8908Handler.h b/include/captiveportal/RFC8908Handler.h deleted file mode 100644 index 36c6e2c6..00000000 --- a/include/captiveportal/RFC8908Handler.h +++ /dev/null @@ -1,17 +0,0 @@ -#pragma once - -#include - -namespace OpenShock::CaptivePortal { - class RFC8908Handler : public AsyncWebHandler { - public: - RFC8908Handler() = default; - virtual ~RFC8908Handler() = default; - - // Static helper for fallback NotFound behavior - static void CatchAll(AsyncWebServerRequest* request); - - bool canHandle(AsyncWebServerRequest* request) const override; - void handleRequest(AsyncWebServerRequest* request) override; - }; -} // namespace OpenShock::CaptivePortal diff --git a/include/config/internal/utils.h b/include/config/internal/utils.h deleted file mode 100644 index cb0144cc..00000000 --- a/include/config/internal/utils.h +++ /dev/null @@ -1,86 +0,0 @@ -#pragma once - -#include "config/ConfigBase.h" - -#include -#include -#include - -#include -#include - -namespace OpenShock::Config::Internal::Utils { - bool FromU8GpioNum(gpio_num_t& val, uint8_t u8Val); - void FromU8GpioNum(gpio_num_t& val, uint8_t u8Val, gpio_num_t defaultVal); - - void FromFbsStr(std::string& str, const flatbuffers::String* fbsStr, const char* defaultStr); - bool FromFbsIPAddress(IPAddress& ip, const flatbuffers::String* fbsIP, const IPAddress& defaultIP); - bool FromJsonBool(bool& val, const cJSON* json, const char* name, bool defaultVal); - bool FromJsonU8(uint8_t& val, const cJSON* json, const char* name, uint8_t defaultVal); - bool FromJsonU16(uint16_t& val, const cJSON* json, const char* name, uint16_t defaultVal); - bool FromJsonI32(int32_t& val, const cJSON* json, const char* name, int32_t defaultVal); - - bool FromJsonStr(std::string& str, const cJSON* json, const char* name); - void FromJsonStr(std::string& str, const cJSON* json, const char* name, const char* defaultStr); - - bool FromJsonIPAddress(IPAddress& ip, const cJSON* json, const char* name); - void FromJsonIPAddress(IPAddress& ip, const cJSON* json, const char* name, const IPAddress& defaultIP); - - bool FromJsonGpioNum(gpio_num_t& val, const cJSON* json, const char* name); - void FromJsonGpioNum(gpio_num_t& val, const cJSON* json, const char* name, gpio_num_t defaultVal); - - template // T inherits from ConfigBase - void FromFbsVec(std::vector& vec, const flatbuffers::Vector>* fbsVec) - { - vec.clear(); - - if (fbsVec == nullptr) { - return; - } - - for (auto fbsItem : *fbsVec) { - T item; - if (item.FromFlatbuffers(fbsItem)) { - vec.push_back(std::move(item)); - } - } - } - template // T inherits from ConfigBase - bool FromJsonStrParsed(T& val, const cJSON* json, const char* name, bool (*StringParser)(T&, const char*), T defaultVal) - { - const cJSON* jsonVal = cJSON_GetObjectItemCaseSensitive(json, name); - if (jsonVal == nullptr) { - val = defaultVal; - return true; - } - - if (cJSON_IsString(jsonVal) == 0) { - return false; - } - - if (!StringParser(val, jsonVal->valuestring)) { - return false; - } - - return true; - } - template // T inherits from ConfigBase - bool FromJsonArray(std::vector& vec, const cJSON* jsonArray) - { - vec.clear(); - if (jsonArray == nullptr) { - return true; - } - - const cJSON* jsonItem = nullptr; - cJSON_ArrayForEach(jsonItem, jsonArray) - { - T item; - if (item.FromJSON(jsonItem)) { - vec.push_back(std::move(item)); - } - } - - return true; - } -} // namespace OpenShock::Config::Internal::Utils diff --git a/include/events/Events.h b/include/events/Events.h deleted file mode 100644 index a47706a3..00000000 --- a/include/events/Events.h +++ /dev/null @@ -1,22 +0,0 @@ -#pragma once - -#include - -#ifdef __cplusplus -extern "C" { -#endif - -ESP_EVENT_DECLARE_BASE(OPENSHOCK_EVENTS); - -enum { - OPENSHOCK_EVENT_ESTOP_STATE_CHANGED, // Event for when the EStop activation state changes - OPENSHOCK_EVENT_GATEWAY_CLIENT_STATE_CHANGED, // Event for when the gateway connection state changes -}; - -#ifdef __cplusplus -} -#endif - -namespace OpenShock::Events { - bool Init(); -} diff --git a/include/http/HTTPRequestManager.h b/include/http/HTTPRequestManager.h deleted file mode 100644 index bed93fd9..00000000 --- a/include/http/HTTPRequestManager.h +++ /dev/null @@ -1,90 +0,0 @@ -#pragma once - -#include - -#include - -#include -#include -#include - -#include "span.h" - -namespace OpenShock::HTTP { - enum class RequestResult : uint8_t { - InternalError, // Internal error - InvalidURL, // Invalid URL - RequestFailed, // Failed to start request - TimedOut, // Request timed out - RateLimited, // Rate limited (can be both local and global) - CodeRejected, // Request completed, but response code was not OK - ParseFailed, // Request completed, but JSON parsing failed - Cancelled, // Request was cancelled - Success, // Request completed successfully - }; - - template - struct [[nodiscard]] Response { - RequestResult result; - int code; - T data; - - inline const char* ResultToString() const - { - switch (result) { - case RequestResult::InternalError: - return "Internal error"; - case RequestResult::InvalidURL: - return "Requested url was invalid"; - case RequestResult::RequestFailed: - return "Request failed"; - case RequestResult::TimedOut: - return "Request timed out"; - case RequestResult::RateLimited: - return "Client was ratelimited"; - case RequestResult::CodeRejected: - return "Unexpected response code"; - case RequestResult::ParseFailed: - return "Parsing the response failed"; - case RequestResult::Cancelled: - return "Request was cancelled"; - case RequestResult::Success: - return "Success"; - default: - return "Unknown reason"; - } - } - }; - - template - using JsonParser = std::function; - using GotContentLengthCallback = std::function; - using DownloadCallback = std::function; - - Response Download(std::string_view url, const std::map& headers, GotContentLengthCallback contentLengthCallback, DownloadCallback downloadCallback, tcb::span acceptedCodes, uint32_t timeoutMs = 10'000); - Response GetString(std::string_view url, const std::map& headers, tcb::span acceptedCodes, uint32_t timeoutMs = 10'000); - - template - Response GetJSON(std::string_view url, const std::map& headers, JsonParser jsonParser, tcb::span acceptedCodes, uint32_t timeoutMs = 10'000) - { - auto response = GetString(url, headers, acceptedCodes, timeoutMs); - if (response.result != RequestResult::Success) { - return {response.result, response.code, {}}; - } - - cJSON* json = cJSON_ParseWithLength(response.data.c_str(), response.data.length()); - if (json == nullptr) { - return {RequestResult::ParseFailed, response.code, {}}; - } - - T data; - if (!jsonParser(response.code, json, data)) { - cJSON_Delete(json); - return {RequestResult::ParseFailed, response.code, {}}; - } - - cJSON_Delete(json); - - return {response.result, response.code, std::move(data)}; - } -} // namespace OpenShock::HTTP diff --git a/include/message_handlers/WebSocket.h b/include/message_handlers/WebSocket.h deleted file mode 100644 index 5b00c4c5..00000000 --- a/include/message_handlers/WebSocket.h +++ /dev/null @@ -1,10 +0,0 @@ -#pragma once - -#include "span.h" - -#include - -namespace OpenShock::MessageHandlers::WebSocket { - void HandleGatewayBinary(tcb::span data); - void HandleLocalBinary(uint8_t socketId, tcb::span data); -} diff --git a/include/radio/rmt/CaiXianlinEncoder.h b/include/radio/rmt/CaiXianlinEncoder.h deleted file mode 100644 index 36ef4da6..00000000 --- a/include/radio/rmt/CaiXianlinEncoder.h +++ /dev/null @@ -1,12 +0,0 @@ -#pragma once - -#include "ShockerCommandType.h" - -#include - -#include - -namespace OpenShock::Rmt::CaiXianlinEncoder { - size_t GetBufferSize(); - bool FillBuffer(rmt_data_t* data, uint16_t shockerId, uint8_t channelId, ShockerCommandType type, uint8_t intensity); -} diff --git a/include/radio/rmt/D80Encoder.h b/include/radio/rmt/D80Encoder.h deleted file mode 100644 index e3c313c8..00000000 --- a/include/radio/rmt/D80Encoder.h +++ /dev/null @@ -1,12 +0,0 @@ -#pragma once - -#include "ShockerCommandType.h" - -#include - -#include - -namespace OpenShock::Rmt::D80Encoder { - size_t GetBufferSize(); - bool FillBuffer(rmt_data_t* data, uint16_t shockerId, ShockerCommandType type, uint8_t intensity); -} diff --git a/include/radio/rmt/Petrainer998DREncoder.h b/include/radio/rmt/Petrainer998DREncoder.h deleted file mode 100644 index 717966fd..00000000 --- a/include/radio/rmt/Petrainer998DREncoder.h +++ /dev/null @@ -1,12 +0,0 @@ -#pragma once - -#include "ShockerCommandType.h" - -#include - -#include - -namespace OpenShock::Rmt::Petrainer998DREncoder { - size_t GetBufferSize(); - bool FillBuffer(rmt_data_t* data, uint16_t shockerId, ShockerCommandType type, uint8_t intensity); -} diff --git a/include/radio/rmt/PetrainerEncoder.h b/include/radio/rmt/PetrainerEncoder.h deleted file mode 100644 index e55b8275..00000000 --- a/include/radio/rmt/PetrainerEncoder.h +++ /dev/null @@ -1,12 +0,0 @@ -#pragma once - -#include "ShockerCommandType.h" - -#include - -#include - -namespace OpenShock::Rmt::PetrainerEncoder { - size_t GetBufferSize(); - bool FillBuffer(rmt_data_t* data, uint16_t shockerId, ShockerCommandType type, uint8_t intensity); -} diff --git a/include/radio/rmt/T330Encoder.h b/include/radio/rmt/T330Encoder.h deleted file mode 100644 index 782a8eaa..00000000 --- a/include/radio/rmt/T330Encoder.h +++ /dev/null @@ -1,12 +0,0 @@ -#pragma once - -#include "ShockerCommandType.h" - -#include - -#include - -namespace OpenShock::Rmt::WellturnT330Encoder { - size_t GetBufferSize(); - bool FillBuffer(rmt_data_t* data, uint16_t shockerId, ShockerCommandType type, uint8_t intensity); -} diff --git a/include/serial/SerialInputHandler.h b/include/serial/SerialInputHandler.h deleted file mode 100644 index 99aeaa38..00000000 --- a/include/serial/SerialInputHandler.h +++ /dev/null @@ -1,47 +0,0 @@ -#pragma once - -#include - -// Making sense of the ESP32 Arduino core #defines -#if ARDUINO_USB_MODE && ARDUINO_USB_CDC_ON_BOOT -#define OS_SERIAL ::Serial0 -#define OS_SERIAL_USB ::Serial -#elif ARDUINO_USB_MODE -#define OS_SERIAL ::Serial -#define OS_SERIAL_USB ::USBSerial -#else -#define OS_SERIAL ::Serial -// Variant lacks USB Serial -#endif - -#if ARDUINO_USB_MODE -#define OS_SERIAL_PRINT(...) \ - { \ - OS_SERIAL.print(__VA_ARGS__); \ - OS_SERIAL_USB.print(__VA_ARGS__); \ - } -#define OS_SERIAL_PRINTF(...) \ - { \ - OS_SERIAL.printf(__VA_ARGS__); \ - OS_SERIAL_USB.printf(__VA_ARGS__); \ - } -#define OS_SERIAL_PRINTLN(...) \ - { \ - OS_SERIAL.println(__VA_ARGS__); \ - OS_SERIAL_USB.println(__VA_ARGS__); \ - } -#else -#define OS_SERIAL_PRINT(...) OS_SERIAL.print(__VA_ARGS__) -#define OS_SERIAL_PRINTF(...) OS_SERIAL.printf(__VA_ARGS__) -#define OS_SERIAL_PRINTLN(...) OS_SERIAL.println(__VA_ARGS__) -#endif - -namespace OpenShock::SerialInputHandler { - [[nodiscard]] bool Init(); - - bool SerialEchoEnabled(); - void SetSerialEchoEnabled(bool enabled); - - void PrintWelcomeHeader(); - void PrintVersionInfo(); -} // namespace OpenShock::SerialInputHandler diff --git a/include/serial/command_handlers/common.h b/include/serial/command_handlers/common.h deleted file mode 100644 index 17ab3563..00000000 --- a/include/serial/command_handlers/common.h +++ /dev/null @@ -1,24 +0,0 @@ -#pragma once - -#include "serial/command_handlers/index.h" -#include "serial/SerialInputHandler.h" - -#include "Logging.h" - -#include - -#if ARDUINO_USB_MODE -#define SERPR_SYS(format, ...) \ - { \ - OS_SERIAL.printf("$SYS$|" format "\r\n", ##__VA_ARGS__); \ - OS_SERIAL_USB.printf("$SYS$|" format "\r\n", ##__VA_ARGS__); \ - } -#else -#define SERPR_SYS(format, ...) OS_SERIAL.printf("$SYS$|" format "\r\n", ##__VA_ARGS__) -#endif - -#define SERPR_RESPONSE(format, ...) SERPR_SYS("Response|" format, ##__VA_ARGS__) -#define SERPR_SUCCESS(format, ...) SERPR_SYS("Success|" format, ##__VA_ARGS__) -#define SERPR_ERROR(format, ...) SERPR_SYS("Error|" format, ##__VA_ARGS__) - -using namespace std::string_view_literals; diff --git a/include/serial/command_handlers/index.h b/include/serial/command_handlers/index.h deleted file mode 100644 index ecfdac5e..00000000 --- a/include/serial/command_handlers/index.h +++ /dev/null @@ -1,48 +0,0 @@ -#pragma once - -#include "serial/command_handlers/CommandEntry.h" - -#include - -namespace OpenShock::Serial::CommandHandlers { - OpenShock::Serial::CommandGroup VersionHandler(); - OpenShock::Serial::CommandGroup RestartHandler(); - OpenShock::Serial::CommandGroup SysInfoHandler(); - OpenShock::Serial::CommandGroup EchoHandler(); - OpenShock::Serial::CommandGroup ValidGpiosHandler(); - OpenShock::Serial::CommandGroup RfTxPinHandler(); - OpenShock::Serial::CommandGroup EStopHandler(); - OpenShock::Serial::CommandGroup DomainHandler(); - OpenShock::Serial::CommandGroup AuthTokenHandler(); - OpenShock::Serial::CommandGroup HostnameHandler(); - OpenShock::Serial::CommandGroup NetworksHandler(); - OpenShock::Serial::CommandGroup KeepAliveHandler(); - OpenShock::Serial::CommandGroup JsonConfigHandler(); - OpenShock::Serial::CommandGroup RawConfigHandler(); - OpenShock::Serial::CommandGroup RfTransmitHandler(); - OpenShock::Serial::CommandGroup FactoryResetHandler(); - OpenShock::Serial::CommandGroup LedTestHandler(); - - inline std::vector AllCommandHandlers() - { - return { - VersionHandler(), - RestartHandler(), - SysInfoHandler(), - EchoHandler(), - ValidGpiosHandler(), - RfTxPinHandler(), - EStopHandler(), - DomainHandler(), - AuthTokenHandler(), - HostnameHandler(), - NetworksHandler(), - KeepAliveHandler(), - JsonConfigHandler(), - RawConfigHandler(), - RfTransmitHandler(), - FactoryResetHandler(), - LedTestHandler(), - }; - } -} // namespace OpenShock::Serial::CommandHandlers diff --git a/include/span.h b/include/span.h deleted file mode 100644 index c26b7f5e..00000000 --- a/include/span.h +++ /dev/null @@ -1,539 +0,0 @@ -#pragma once - -/* -This is an implementation of C++20's std::span -http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2019/n4820.pdf -*/ - -// Copyright Tristan Brindle 2018. -// Distributed under the Boost Software License, Version 1.0. -// (See accompanying file ../../LICENSE_1_0.txt or copy at -// https://www.boost.org/LICENSE_1_0.txt) - -#include -#include -#include -#include - -#ifndef TCB_SPAN_NO_EXCEPTIONS -// Attempt to discover whether we're being compiled with exception support -#if !(defined(__cpp_exceptions) || defined(__EXCEPTIONS) || defined(_CPPUNWIND)) -#define TCB_SPAN_NO_EXCEPTIONS -#endif -#endif - -#ifndef TCB_SPAN_NO_EXCEPTIONS -#include -#include -#endif - -// Various feature test macros - -#ifndef TCB_SPAN_NAMESPACE_NAME -#define TCB_SPAN_NAMESPACE_NAME tcb -#endif - -#if __cplusplus >= 201'703L || (defined(_MSVC_LANG) && _MSVC_LANG >= 201'703L) -#define TCB_SPAN_HAVE_CPP17 -#endif - -#if __cplusplus >= 201'402L || (defined(_MSVC_LANG) && _MSVC_LANG >= 201'402L) -#define TCB_SPAN_HAVE_CPP14 -#endif - -namespace TCB_SPAN_NAMESPACE_NAME { - -// Establish default contract checking behavior -#if !defined(TCB_SPAN_THROW_ON_CONTRACT_VIOLATION) && !defined(TCB_SPAN_TERMINATE_ON_CONTRACT_VIOLATION) && !defined(TCB_SPAN_NO_CONTRACT_CHECKING) -#if defined(NDEBUG) || !defined(TCB_SPAN_HAVE_CPP14) -#define TCB_SPAN_NO_CONTRACT_CHECKING -#else -#define TCB_SPAN_TERMINATE_ON_CONTRACT_VIOLATION -#endif -#endif - -#if defined(TCB_SPAN_THROW_ON_CONTRACT_VIOLATION) - struct contract_violation_error : std::logic_error { - explicit contract_violation_error(const char* msg) - : std::logic_error(msg) - { - } - }; - - inline void contract_violation(const char* msg) - { - throw contract_violation_error(msg); - } - -#elif defined(TCB_SPAN_TERMINATE_ON_CONTRACT_VIOLATION) - [[noreturn]] inline void contract_violation(const char* /*unused*/) - { - std::terminate(); - } -#endif - -#if !defined(TCB_SPAN_NO_CONTRACT_CHECKING) -#define TCB_SPAN_STRINGIFY(cond) #cond -#define TCB_SPAN_EXPECT(cond) cond ? (void)0 : contract_violation("Expected " TCB_SPAN_STRINGIFY(cond)) -#else -#define TCB_SPAN_EXPECT(cond) -#endif - -#if defined(TCB_SPAN_HAVE_CPP17) || defined(__cpp_inline_variables) -#define TCB_SPAN_INLINE_VAR inline -#else -#define TCB_SPAN_INLINE_VAR -#endif - -#if defined(TCB_SPAN_HAVE_CPP14) || (defined(__cpp_constexpr) && __cpp_constexpr >= 201'304) -#define TCB_SPAN_HAVE_CPP14_CONSTEXPR -#endif - -#if defined(TCB_SPAN_HAVE_CPP14_CONSTEXPR) -#define TCB_SPAN_CONSTEXPR14 constexpr -#else -#define TCB_SPAN_CONSTEXPR14 -#endif - -#if defined(TCB_SPAN_HAVE_CPP14_CONSTEXPR) && (!defined(_MSC_VER) || _MSC_VER > 1900) -#define TCB_SPAN_CONSTEXPR_ASSIGN constexpr -#else -#define TCB_SPAN_CONSTEXPR_ASSIGN -#endif - -#if defined(TCB_SPAN_NO_CONTRACT_CHECKING) -#define TCB_SPAN_CONSTEXPR11 constexpr -#else -#define TCB_SPAN_CONSTEXPR11 TCB_SPAN_CONSTEXPR14 -#endif - -#if defined(TCB_SPAN_HAVE_CPP17) || defined(__cpp_deduction_guides) -#define TCB_SPAN_HAVE_DEDUCTION_GUIDES -#endif - -#if defined(TCB_SPAN_HAVE_CPP17) || defined(__cpp_lib_byte) -#define TCB_SPAN_HAVE_STD_BYTE -#endif - -#if defined(TCB_SPAN_HAVE_CPP17) || defined(__cpp_lib_array_constexpr) -#define TCB_SPAN_HAVE_CONSTEXPR_STD_ARRAY_ETC -#endif - -#if defined(TCB_SPAN_HAVE_CONSTEXPR_STD_ARRAY_ETC) -#define TCB_SPAN_ARRAY_CONSTEXPR constexpr -#else -#define TCB_SPAN_ARRAY_CONSTEXPR -#endif - -#ifdef TCB_SPAN_HAVE_STD_BYTE - using byte = std::byte; -#else - using byte = unsigned char; -#endif - -#if defined(TCB_SPAN_HAVE_CPP17) -#define TCB_SPAN_NODISCARD [[nodiscard]] -#else -#define TCB_SPAN_NODISCARD -#endif - - TCB_SPAN_INLINE_VAR constexpr std::size_t dynamic_extent = SIZE_MAX; - - template - class span; - - namespace detail { - - template - struct span_storage { - constexpr span_storage() noexcept = default; - - constexpr span_storage(E* p_ptr, std::size_t /*unused*/) noexcept - : ptr(p_ptr) - { - } - - E* ptr = nullptr; - static constexpr std::size_t size = S; - }; - - template - struct span_storage { - constexpr span_storage() noexcept = default; - - constexpr span_storage(E* p_ptr, std::size_t p_size) noexcept - : ptr(p_ptr) - , size(p_size) - { - } - - E* ptr = nullptr; - std::size_t size = 0; - }; - -// Reimplementation of C++17 std::size() and std::data() -#if defined(TCB_SPAN_HAVE_CPP17) || defined(__cpp_lib_nonmember_container_access) - using std::data; - using std::size; -#else - template - constexpr auto size(const C& c) -> decltype(c.size()) - { - return c.size(); - } - - template - constexpr std::size_t size(const T (&)[N]) noexcept - { - return N; - } - - template - constexpr auto data(C& c) -> decltype(c.data()) - { - return c.data(); - } - - template - constexpr auto data(const C& c) -> decltype(c.data()) - { - return c.data(); - } - - template - constexpr T* data(T (&array)[N]) noexcept - { - return array; - } - - template - constexpr const E* data(std::initializer_list il) noexcept - { - return il.begin(); - } -#endif // TCB_SPAN_HAVE_CPP17 - -#if defined(TCB_SPAN_HAVE_CPP17) || defined(__cpp_lib_void_t) - using std::void_t; -#else - template - using void_t = void; -#endif - - template - using uncvref_t = typename std::remove_cv::type>::type; - - template - struct is_span : std::false_type { }; - - template - struct is_span> : std::true_type { }; - - template - struct is_std_array : std::false_type { }; - - template - struct is_std_array> : std::true_type { }; - - template - struct has_size_and_data : std::false_type { }; - - template - struct has_size_and_data())), decltype(detail::data(std::declval()))>> : std::true_type { }; - - template> - struct is_container { - static constexpr bool value = !is_span::value && !is_std_array::value && !std::is_array::value && has_size_and_data::value; - }; - - template - using remove_pointer_t = typename std::remove_pointer::type; - - template - struct is_container_element_type_compatible : std::false_type { }; - - template - struct is_container_element_type_compatible< - T, - E, - typename std::enable_if()))>::type, void>::value && std::is_convertible()))> (*)[], E (*)[]>::value>::type> - : std::true_type { }; - - template - struct is_complete : std::false_type { }; - - template - struct is_complete : std::true_type { }; - - } // namespace detail - - template - class span { - static_assert( - std::is_object::value, - "A span's ElementType must be an object type (not a " - "reference type or void)" - ); - static_assert( - detail::is_complete::value, - "A span's ElementType must be a complete type (not a forward " - "declaration)" - ); - static_assert(!std::is_abstract::value, "A span's ElementType cannot be an abstract class type"); - - using storage_type = detail::span_storage; - - public: - // constants and types - using element_type = ElementType; - using value_type = typename std::remove_cv::type; - using size_type = std::size_t; - using difference_type = std::ptrdiff_t; - using pointer = element_type*; - using const_pointer = const element_type*; - using reference = element_type&; - using const_reference = const element_type&; - using iterator = pointer; - using reverse_iterator = std::reverse_iterator; - - static constexpr size_type extent = Extent; - - // [span.cons], span constructors, copy, assignment, and destructor - template::type = 0> - constexpr span() noexcept - { - } - - TCB_SPAN_CONSTEXPR11 span(pointer ptr, size_type count) - : storage_(ptr, count) - { - TCB_SPAN_EXPECT(extent == dynamic_extent || count == extent); - } - - TCB_SPAN_CONSTEXPR11 span(pointer first_elem, pointer last_elem) - : storage_(first_elem, last_elem - first_elem) - { - TCB_SPAN_EXPECT(extent == dynamic_extent || last_elem - first_elem == static_cast(extent)); - } - - template::value, int>::type = 0> - constexpr span(element_type (&arr)[N]) noexcept - : storage_(arr, N) - { - } - - template&, ElementType>::value, int>::type = 0> - TCB_SPAN_ARRAY_CONSTEXPR span(std::array& arr) noexcept - : storage_(arr.data(), N) - { - } - - template&, ElementType>::value, int>::type = 0> - TCB_SPAN_ARRAY_CONSTEXPR span(const std::array& arr) noexcept - : storage_(arr.data(), N) - { - } - - template::value && detail::is_container_element_type_compatible::value, int>::type = 0> - constexpr span(Container& cont) - : storage_(detail::data(cont), detail::size(cont)) - { - } - - template::value && detail::is_container_element_type_compatible::value, int>::type = 0> - constexpr span(const Container& cont) - : storage_(detail::data(cont), detail::size(cont)) - { - } - - constexpr span(const span& other) noexcept = default; - - template< - typename OtherElementType, - std::size_t OtherExtent, - typename std::enable_if<(Extent == dynamic_extent || OtherExtent == dynamic_extent || Extent == OtherExtent) && std::is_convertible::value, int>::type = 0> - constexpr span(const span& other) noexcept - : storage_(other.data(), other.size()) - { - } - - ~span() noexcept = default; - - TCB_SPAN_CONSTEXPR_ASSIGN span& operator=(const span& other) noexcept = default; - - // [span.sub], span subviews - template - TCB_SPAN_CONSTEXPR11 span first() const - { - TCB_SPAN_EXPECT(Count <= size()); - return {data(), Count}; - } - - template - TCB_SPAN_CONSTEXPR11 span last() const - { - TCB_SPAN_EXPECT(Count <= size()); - return {data() + (size() - Count), Count}; - } - - template - using subspan_return_t = span; - - template - TCB_SPAN_CONSTEXPR11 subspan_return_t subspan() const - { - TCB_SPAN_EXPECT(Offset <= size() && (Count == dynamic_extent || Offset + Count <= size())); - return {data() + Offset, Count != dynamic_extent ? Count : size() - Offset}; - } - - TCB_SPAN_CONSTEXPR11 span first(size_type count) const - { - TCB_SPAN_EXPECT(count <= size()); - return {data(), count}; - } - - TCB_SPAN_CONSTEXPR11 span last(size_type count) const - { - TCB_SPAN_EXPECT(count <= size()); - return {data() + (size() - count), count}; - } - - TCB_SPAN_CONSTEXPR11 span subspan(size_type offset, size_type count = dynamic_extent) const - { - TCB_SPAN_EXPECT(offset <= size() && (count == dynamic_extent || offset + count <= size())); - return {data() + offset, count == dynamic_extent ? size() - offset : count}; - } - - // [span.obs], span observers - constexpr size_type size() const noexcept { return storage_.size; } - - constexpr size_type size_bytes() const noexcept { return size() * sizeof(element_type); } - - TCB_SPAN_NODISCARD constexpr bool empty() const noexcept { return size() == 0; } - - // [span.elem], span element access - TCB_SPAN_CONSTEXPR11 reference operator[](size_type idx) const - { - TCB_SPAN_EXPECT(idx < size()); - return *(data() + idx); - } - - TCB_SPAN_CONSTEXPR11 reference front() const - { - TCB_SPAN_EXPECT(!empty()); - return *data(); - } - - TCB_SPAN_CONSTEXPR11 reference back() const - { - TCB_SPAN_EXPECT(!empty()); - return *(data() + (size() - 1)); - } - - constexpr pointer data() const noexcept { return storage_.ptr; } - - // [span.iterators], span iterator support - constexpr iterator begin() const noexcept { return data(); } - - constexpr iterator end() const noexcept { return data() + size(); } - - TCB_SPAN_ARRAY_CONSTEXPR reverse_iterator rbegin() const noexcept { return reverse_iterator(end()); } - - TCB_SPAN_ARRAY_CONSTEXPR reverse_iterator rend() const noexcept { return reverse_iterator(begin()); } - - private: - storage_type storage_ {}; - }; - -#ifdef TCB_SPAN_HAVE_DEDUCTION_GUIDES - - /* Deduction Guides */ - template - span(T (&)[N]) -> span; - - template - span(std::array&) -> span; - - template - span(const std::array&) -> span; - - template - span(Container&) -> span()))>::type>; - - template - span(const Container&) -> span; - -#endif // TCB_HAVE_DEDUCTION_GUIDES - - template - constexpr span make_span(span s) noexcept - { - return s; - } - - template - constexpr span make_span(T (&arr)[N]) noexcept - { - return {arr}; - } - - template - TCB_SPAN_ARRAY_CONSTEXPR span make_span(std::array& arr) noexcept - { - return {arr}; - } - - template - TCB_SPAN_ARRAY_CONSTEXPR span make_span(const std::array& arr) noexcept - { - return {arr}; - } - - template - constexpr span()))>::type> make_span(Container& cont) - { - return {cont}; - } - - template - constexpr span make_span(const Container& cont) - { - return {cont}; - } - - template - span as_bytes(span s) noexcept - { - return {reinterpret_cast(s.data()), s.size_bytes()}; - } - - template::value, int>::type = 0> - span as_writable_bytes(span s) noexcept - { - return {reinterpret_cast(s.data()), s.size_bytes()}; - } - - template - constexpr auto get(span s) -> decltype(s[N]) - { - return s[N]; - } - -} // namespace TCB_SPAN_NAMESPACE_NAME - -namespace std { - - template - class tuple_size> : public integral_constant { }; - - template - class tuple_size>; // not defined - - template - class tuple_element> { - public: - static_assert(Extent != TCB_SPAN_NAMESPACE_NAME::dynamic_extent && I < Extent, ""); - using type = ElementType; - }; - -} // end namespace std diff --git a/include/util/DigitCounter.h b/include/util/DigitCounter.h deleted file mode 100644 index 31d792a5..00000000 --- a/include/util/DigitCounter.h +++ /dev/null @@ -1,44 +0,0 @@ -#pragma once - -#include -#include -#include - -namespace OpenShock::Util { - template - inline constexpr int Digits10CountMax = std::numeric_limits::digits10 + (std::is_signed_v ? 2 : 1); - - template - constexpr std::size_t Digits10Count(T val) - { - using Decayed = std::remove_cv_t>; - static_assert(std::is_integral_v); - - using U = std::make_unsigned_t; - - std::size_t digits = 1; - U u; - - if constexpr (std::is_signed_v) { - if (val < 0) { - // Account for the sign - digits++; - - // Safe magnitude via unsigned modular negation - u = U(0) - static_cast(val); - } else { - u = static_cast(val); - } - } else { - u = static_cast(val); - } - - // Now count digits of u (magnitude), base-10 - while (u >= 10) { - u /= 10; - digits++; - } - - return digits; - } -} // namespace OpenShock::Util diff --git a/include/util/FnProxy.h b/include/util/FnProxy.h deleted file mode 100644 index 926832e0..00000000 --- a/include/util/FnProxy.h +++ /dev/null @@ -1,47 +0,0 @@ -#pragma once - -#include - -namespace OpenShock::Util { - - namespace detail { - - template - struct FnProxyImpl; - - // Non-const, non-noexcept - template - struct FnProxyImpl { - static R Call(void* p, Ts... args) noexcept(noexcept((static_cast(p)->*MF)(std::move(args)...))) { return (static_cast(p)->*MF)(std::move(args)...); } - }; - - // Const, non-noexcept - template - struct FnProxyImpl { - static R Call(void* p, Ts... args) noexcept(noexcept((static_cast(p)->*MF)(std::move(args)...))) { return (static_cast(p)->*MF)(std::move(args)...); } - }; - - // Non-const, noexcept - template - struct FnProxyImpl { - static R Call(void* p, Ts... args) noexcept { return (static_cast(p)->*MF)(std::move(args)...); } - }; - - // Const, noexcept - template - struct FnProxyImpl { - static R Call(void* p, Ts... args) noexcept { return (static_cast(p)->*MF)(std::move(args)...); } - }; - - } // namespace detail - - /// Variable template: FnProxy<&Class::method> is directly a function pointer. - /// - /// Usage: - /// auto cb = FnProxy<&Sensor::read>; // R(*)(void*, Ts...) - /// Sensor s; - /// cb(&s, 1.5f); - template - inline constexpr auto FnProxy = &detail::FnProxyImpl::Call; - -} // namespace OpenShock::Util diff --git a/include/util/IPAddressUtils.h b/include/util/IPAddressUtils.h deleted file mode 100644 index d9cfe9c9..00000000 --- a/include/util/IPAddressUtils.h +++ /dev/null @@ -1,9 +0,0 @@ -#pragma once - -#include - -#include - -namespace OpenShock { - bool IPV4AddressFromStringView(IPAddress& ip, std::string_view sv); -} diff --git a/include/util/PartitionUtils.h b/include/util/PartitionUtils.h deleted file mode 100644 index 22669020..00000000 --- a/include/util/PartitionUtils.h +++ /dev/null @@ -1,12 +0,0 @@ -#pragma once - -#include - -#include -#include -#include - -namespace OpenShock { - bool TryGetPartitionHash(const esp_partition_t* partition, char (&hash)[65]); - bool FlashPartitionFromUrl(const esp_partition_t* partition, std::string_view remoteUrl, const uint8_t (&remoteHash)[32], std::function progressCallback = nullptr); -} diff --git a/lib/README b/lib/README deleted file mode 100644 index 6debab1e..00000000 --- a/lib/README +++ /dev/null @@ -1,46 +0,0 @@ - -This directory is intended for project specific (private) libraries. -PlatformIO will compile them to static libraries and link into executable file. - -The source code of each library should be placed in a an own separate directory -("lib/your_library_name/[here are source files]"). - -For example, see a structure of the following two libraries `Foo` and `Bar`: - -|--lib -| | -| |--Bar -| | |--docs -| | |--examples -| | |--src -| | |- Bar.c -| | |- Bar.h -| | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html -| | -| |--Foo -| | |- Foo.c -| | |- Foo.h -| | -| |- README --> THIS FILE -| -|- platformio.ini -|--src - |- main.c - -and a contents of `src/main.c`: -``` -#include -#include - -int main (void) -{ - ... -} - -``` - -PlatformIO Library Dependency Finder will find automatically dependent -libraries scanning project source files. - -More information about PlatformIO Library Dependency Finder -- https://docs.platformio.org/page/librarymanager/ldf.html diff --git a/main/CMakeLists.txt b/main/CMakeLists.txt new file mode 100644 index 00000000..4e3bb37b --- /dev/null +++ b/main/CMakeLists.txt @@ -0,0 +1,9 @@ +# Application entry point only. All firmware logic lives in components/core (the +# interconnected subsystem tangle) and components/serial_console (the interactive +# console); main just wires them together in app_main. +idf_component_register( + SRCS "main.cpp" + REQUIRES core serial_console device_control events logging common nvs_flash +) + +target_compile_options(${COMPONENT_LIB} PRIVATE -Wno-error) diff --git a/src/main.cpp b/main/main.cpp similarity index 68% rename from src/main.cpp rename to main/main.cpp index 70b6d53f..d4e4bfa9 100644 --- a/src/main.cpp +++ b/main/main.cpp @@ -4,20 +4,20 @@ const char* const TAG = "main"; #include "captiveportal/Manager.h" #include "CommandHandler.h" -#include "Common.h" #include "config/Config.h" #include "estop/EStopManager.h" #include "events/Events.h" #include "GatewayConnectionManager.h" #include "Logging.h" +#include "OpenShock.h" #include "OtaUpdateManager.h" -#include "serial/SerialInputHandler.h" -#include "util/TaskUtils.h" +#include "serial_console/SerialInputHandler.h" #include "visual/VisualStateManager.h" #include "wifi/WiFiManager.h" #include "wifi/WiFiScanManager.h" -#include +#include +#include #include @@ -89,14 +89,26 @@ void appSetup() } } -// Arduino setup function -void setup() +// ESP-IDF application entry point. Runs the one-time setup, then spawns the +// long-lived main task. When app_main returns, its own task is torn down by IDF +// while the main_app task keeps the firmware running. +extern "C" void app_main() { - OS_SERIAL.begin(115'200); - -#if ARDUINO_USB_MODE - OS_SERIAL_USB.begin(115'200); -#endif + // WiFi persists calibration/config in the default NVS partition, so NVS must be + // initialized before esp_wifi_init() (or any other NVS user) runs. Arduino used + // to do this implicitly during startup; under the IDF entry point we do it + // explicitly. If the partition is from an incompatible/older layout, reformat it. + esp_err_t nvsResult = nvs_flash_init(); + if (nvsResult == ESP_ERR_NVS_NO_FREE_PAGES || nvsResult == ESP_ERR_NVS_NEW_VERSION_FOUND) { + OS_LOGW(TAG, "NVS partition unusable (%s), erasing and reformatting", esp_err_to_name(nvsResult)); + if (nvs_flash_erase() != ESP_OK) { + OS_PANIC(TAG, "Failed to erase NVS partition"); + } + nvsResult = nvs_flash_init(); + } + if (nvsResult != ESP_OK) { + OS_PANIC(TAG, "Failed to initialize NVS: %s", esp_err_to_name(nvsResult)); + } OpenShock::Config::Init(); @@ -113,25 +125,10 @@ void setup() } else { appSetup(); } -} -void main_app(void* arg) -{ while (true) { OpenShock::GatewayConnectionManager::Update(); vTaskDelay(5); // 5 ticks update interval } } - -void loop() -{ - // Start the main task - if (OpenShock::TaskUtils::TaskCreateExpensive(main_app, "main_app", 8192, nullptr, 1, nullptr) != pdPASS) { // PROFILED: 6KB stack usage - OS_PANIC(TAG, "Failed to create main_app task"); - return; - } - - // Kill the loop task (Arduino is stinky) - vTaskDelete(nullptr); -} diff --git a/chips/partitions_4MB_OTA.csv b/partitions/ota_4mb.csv similarity index 100% rename from chips/partitions_4MB_OTA.csv rename to partitions/ota_4mb.csv diff --git a/chips/partitions_4MB.csv b/partitions/single_4mb.csv similarity index 100% rename from chips/partitions_4MB.csv rename to partitions/single_4mb.csv diff --git a/platformio.ini b/platformio.ini deleted file mode 100644 index 42c9be42..00000000 --- a/platformio.ini +++ /dev/null @@ -1,205 +0,0 @@ -; PlatformIO Project Configuration File -; -; Build options: build flags, source filter -; Upload options: custom upload port, speed and extra flags -; Library options: dependencies, extra library storages -; Advanced options: extra scripting -; -; Please visit documentation for the other options and examples -; https://docs.platformio.org/page/projectconf.html - -[env] -platform = espressif32 @ 7.0.1 -board = az-delivery-devkit-v4 ; Overridden per board -framework = arduino - -build_flags = - -std=c++2a - -std=gnu++2a - -fno-exceptions - -DCONFIG_ASYNC_TCP_QUEUE_SIZE=256 -build_unflags = - -std=gnu++11 - -; Most warnings here are commented out because the compiler detects warnings outside own code, build_src_flags is meant to only be for our code but doesnt seem to work in this instance... -build_src_flags = - -Wall - -Wextra - ;-Wpedantic - ;-Werror - ;-Wcast-align - ;-Wcast-qual - ;-Wctor-dtor-privacy - -Wdisabled-optimization - -Wformat=2 - -Winit-self - -Wlogical-op - ;-Wmissing-declarations - -Wmissing-include-dirs - -Wnoexcept - ;-Wold-style-cast - ;-Woverloaded-virtual - ;-Wredundant-decls - ;-Wshadow - ;-Wsign-conversion - -Wsign-promo - -Wstrict-null-sentinel - -Wstrict-overflow=5 - -Wswitch-default - ;-Wundef - -Wno-unused - -Wno-unknown-pragmas - ;-Weffc++ - -lib_deps = - https://github.com/google/flatbuffers#81edeb17d9118143f2c81caf27edfb0df401279e - https://github.com/OpenShock/ESPAsyncWebServer#7a13c34784ae385aabd45cc02f2e1d174ccc0601 - https://github.com/OpenShock/BadWebSockets#c2ad2c21d39781748af9b4675eb65f7a5c51f40c -custom_openshock.flash_size = 4MB; Can be overridden per board -board_build.filesystem = littlefs -board_build.embed_files = certificates/x509_crt_bundle -extra_scripts = - pre:scripts/install_dependencies.py - pre:scripts/use_openshock_params.py - pre:scripts/embed_env_vars.py - post:scripts/build_frontend.py - -; Serial Monitor options -upload_speed = 921600 -monitor_speed = 115200 -monitor_filters = esp32_exception_decoder - -; Static code analysis -check_tool = cppcheck -check_skip_packages = true -check_flags = - cppcheck: --std=c++20 -j 8 --suppress=*:*/.pio/* --suppress=*:*/include/serialization/_fbs/* - -; https://docs.platformio.org/en/stable/boards/espressif32/wemos_d1_mini32.html -[env:Wemos-D1-Mini-ESP32] -board = Wemos-D1-Mini-ESP32 -custom_openshock.chip = ESP32 -build_flags = ${env.build_flags} - -DOPENSHOCK_LED_GPIO=2 - -DOPENSHOCK_RF_TX_GPIO=15 - -; https://docs.platformio.org/en/latest/boards/espressif32/lolin_s2_mini.html -[env:Wemos-Lolin-S2-Mini] -board = Wemos-Lolin-S2-Mini ; override -custom_openshock.chip = ESP32-S2 -build_flags = ${env.build_flags} - -DOPENSHOCK_LED_GPIO=15 - -; https://docs.platformio.org/en/latest/boards/espressif32/lolin_s3.html -[env:Wemos-Lolin-S3] -board = Wemos-Lolin-S3 ; override -custom_openshock.chip = ESP32-S3 -custom_openshock.flash_size = 16MB -build_flags = ${env.build_flags} - -DOPENSHOCK_LED_WS2812B=38 - -; https://docs.platformio.org/en/latest/boards/espressif32/lolin_s3_mini.html -[env:Wemos-Lolin-S3-Mini] -board = lolin_s3_mini ; builtin -custom_openshock.chip = ESP32-S3 -custom_openshock.flash_size = 4MB -build_flags = ${env.build_flags} - -DOPENSHOCK_LED_WS2812B=47 - -DOPENSHOCK_LED_SWAP_RG_CHANNELS=1 - -DARDUINO_USB_CDC_ON_BOOT=1 - -; https://www.waveshare.com/wiki/ESP32-S3-Zero -[env:Waveshare_esp32_s3_zero] -board = esp32-s3-devkitc-1 -custom_openshock.chip = ESP32-S3 -custom_openshock.flash_size = 4MB -build_flags = ${env.build_flags} - -DOPENSHOCK_RF_TX_GPIO=1 - -DOPENSHOCK_LED_WS2812B=21 - -DOPENSHOCK_LED_SWAP_RG_CHANNELS=1 - -DARDUINO_USB_CDC_ON_BOOT=1 - -[env:Pishock-2023] -board = Wemos-D1-Mini-ESP32 ; override -custom_openshock.chip = ESP32 -build_flags = ${env.build_flags} - -DOPENSHOCK_LED_GPIO=2 - -DOPENSHOCK_RF_TX_GPIO=12 - -[env:Pishock-Lite-2021] -board = Wemos-D1-Mini-ESP32 ; override -custom_openshock.chip = ESP32 -build_flags = ${env.build_flags} - -DOPENSHOCK_LED_GPIO=2 - -DOPENSHOCK_RF_TX_GPIO=15 - -; https://docs.platformio.org/en/latest//boards/espressif32/seeed_xiao_esp32c3.html -[env:Seeed-Xiao-ESP32C3] -board = seeed_xiao_esp32c3 ; builtin -custom_openshock.chip = ESP32-C3 -custom_openshock.flash_size = 4MB - -; https://docs.platformio.org/en/latest//boards/espressif32/seeed_xiao_esp32s3.html -[env:Seeed-Xiao-ESP32S3] -board = seeed_xiao_esp32s3 ; builtin -custom_openshock.chip = ESP32-S3 -custom_openshock.flash_size = 8MB -build_flags = ${env.build_flags} - -DOPENSHOCK_LED_GPIO=21 - -; https://docs.platformio.org/en/latest//boards/espressif32/dfrobot_firebeetle2_esp32e.html -[env:DFRobot-Firebeetle2-ESP32E] -board = dfrobot_firebeetle2_esp32e ; builtin -custom_openshock.chip = ESP32 -custom_openshock.flash_size = 4MB -build_flags = ${env.build_flags} - -DOPENSHOCK_RF_TX_GPIO=13 - -DOPENSHOCK_LED_WS2812B=5 - -DOPENSHOCK_LED_GPIO=2 - -; https://github.com/OpenShock/Hardware/tree/main/Core -; 8MB Flash, assume no PSRAM. -; Uses now-blacklisted pins for RF TX and Status LED. -[env:OpenShock-Core-V1] -board = esp32-s3-devkitc-1 ; builtin -custom_openshock.chip = ESP32-S3 -custom_openshock.flash_size = 8MB -build_flags = ${env.build_flags} - -DOPENSHOCK_LED_WS2812B=48 - -DOPENSHOCK_LED_GPIO=35 - -DOPENSHOCK_RF_TX_GPIO=15 - -DOPENSHOCK_ESTOP_PIN=13 - -; https://github.com/OpenShock/Hardware/tree/main/Core%20v2 -; 8MB Flash, assume no PSRAM. -[env:OpenShock-Core-V2] -board = esp32-s3-devkitc-1 ; builtin -custom_openshock.chip = ESP32-S3 -custom_openshock.flash_size = 8MB -build_flags = ${env.build_flags} - -DOPENSHOCK_LED_WS2812B=14 - -DOPENSHOCK_LED_GPIO=13 - -DOPENSHOCK_RF_TX_GPIO=1 - -DOPENSHOCK_ESTOP_PIN=38 - -; https://docs.platformio.org/en/stable/boards/espressif32/nodemcu-32s.html -[env:NodeMCU-32S] -board = nodemcu-32s -custom_openshock.chip = ESP32 -build_flags = ${env.build_flags} - -DOPENSHOCK_LED_GPIO=2 - -; TODO: -; https://docs.platformio.org/en/latest/boards/espressif32/upesy_wroom.html;upesy-esp32-wroom-devkit - -[env:fs] -custom_openshock.chip = ESP32 -custom_openshock.flash_size = 4MB -; This exists so we don't build individual filesystems per board. - -; Build for CI CodeQL and cppcheck -[env:ci-build] -board = esp32-s3-devkitc-1 ; builtin -custom_openshock.chip = ESP32-S3 -custom_openshock.flash_size = 8MB -build_flags = ${env:OpenShock-Core-V2.build_flags} diff --git a/requirements.txt b/requirements.txt index 66053ad5..2fb66c22 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,9 @@ -platformio==6.1.19 -esptool==5.3.1 cryptography==49.0.0 +esptool==5.3.1 +# Used by scripts/embed_env_vars.py (import git). The old espressif32 platform +# pulled GitPython in transitively; the pioarduino platform does not, so pin it +# explicitly or the SCons pre-build script fails with ModuleNotFoundError. +GitPython==3.1.50 +# scripts/gen_staticfs.py packs the captive-portal assets into a littlefs image +# matching the runtime mount geometry (components/fs/src/LfsPartition.cpp). +littlefs-python==0.14.0 diff --git a/scripts/build.py b/scripts/build.py new file mode 100644 index 00000000..df0d4418 --- /dev/null +++ b/scripts/build.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python3 +"""Native ESP-IDF build driver. + +Usage: + python scripts/build.py [idf.py targets/args ...] + +Resolves the IDF target from boards/.defaults, sets OPENSHOCK_BOARD (so the +root CMakeLists generates the env header), layers sdkconfig.defaults + the board +fragment, and runs idf.py into build//. Replaces the PlatformIO per-env +build so CI and local builds share one entry point. + +Examples: + python scripts/build.py OpenShock-Core-V2 # full build + python scripts/build.py OpenShock-Core-V2 app bootloader partition-table +""" + +import os +import re +import shutil +import subprocess +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent + + +def board_fragment(board: str) -> Path: + fragment = ROOT / 'boards' / f'{board}.defaults' + if not fragment.is_file(): + avail = ', '.join(sorted(p.stem for p in (ROOT / 'boards').glob('*.defaults'))) + raise SystemExit(f'error: unknown board {board!r}\n available: {avail}') + return fragment + + +def resolve_target(fragment: Path) -> str: + for line in fragment.read_text(encoding='utf-8').splitlines(): + m = re.match(r'\s*CONFIG_IDF_TARGET\s*=\s*"([^"]+)"', line) + if m: + return m.group(1) + raise SystemExit(f'error: {fragment} does not set CONFIG_IDF_TARGET') + + +def main(argv: list[str]) -> int: + if not argv: + raise SystemExit(__doc__) + + board = argv[0] + targets = argv[1:] or ['build'] + + fragment = board_fragment(board) + target = resolve_target(fragment) + build_dir = ROOT / 'build' / board + frag_rel = fragment.relative_to(ROOT).as_posix() + + idf_args = [ + '-C', str(ROOT), + '-B', str(build_dir), + f'-DIDF_TARGET={target}', + f'-DSDKCONFIG_DEFAULTS=sdkconfig.defaults;{frag_rel}', + f'-DSDKCONFIG={build_dir / "sdkconfig"}', + *targets, + ] + + # Prefer an executable `idf.py` wrapper on PATH. install-esp-idf-action (and + # an activated ESP-IDF shell) provide one that sets up the tool + venv + # environment itself - the EIM install is not visible to a plain + # `python tools/idf.py`. Restricted to POSIX: on Windows a bare idf.py isn't + # directly executable, so fall through to the interpreter form there. + idf_wrapper = shutil.which('idf.py') if os.name == 'posix' else None + if idf_wrapper: + cmd = [idf_wrapper, *idf_args] + else: + # Fall back to running tools/idf.py with the current interpreter, which + # requires the ESP-IDF venv to already be active (IDF_PATH set). + idf_path = os.environ.get('IDF_PATH') + if not idf_path: + raise SystemExit('error: IDF_PATH is not set - run the ESP-IDF export script first') + cmd = [sys.executable, str(Path(idf_path) / 'tools' / 'idf.py'), *idf_args] + + env = dict(os.environ, OPENSHOCK_BOARD=board) + print(f'+ OPENSHOCK_BOARD={board} {" ".join(cmd)}', flush=True) + return subprocess.call(cmd, env=env) + + +if __name__ == '__main__': + raise SystemExit(main(sys.argv[1:])) diff --git a/scripts/build_frontend.py b/scripts/build_frontend.py deleted file mode 100644 index 9dda64fe..00000000 --- a/scripts/build_frontend.py +++ /dev/null @@ -1,149 +0,0 @@ -import os -import gzip -import shutil -import hashlib -from utils import sysenv - -Import('env') # type: ignore - - -def dir_ensure(dir): - if not os.path.exists(dir): - os.makedirs(dir) - - -def dir_delete(dir): - if os.path.exists(dir): - shutil.rmtree(dir) - - -def file_delete(file): - if os.path.exists(file): - os.remove(file) - - -def file_gzip(file_path, gzip_path): - size_before = os.path.getsize(file_path) - dir_ensure(os.path.dirname(gzip_path)) - file_delete(gzip_path) - with open(file_path, 'rb') as f_in, gzip.open(gzip_path, 'wb') as f_out: - f_out.write(f_in.read()) - size_after = os.path.getsize(gzip_path) - print(f'Gzipped {file_path}: {size_before} => {size_after} bytes') - - -def file_write_bin(file, data): - try: - with open(file, 'wb') as f: - f.write(data) - return True - except: - print('Error writing to ' + file) - return False - - -def file_read_text(file): - try: - with open(file, 'r', encoding='utf-8') as f: - return (f.read(), 'utf-8') - except: - pass - try: - with open(file, 'r') as f: - return (f.read(), None) - except: - print('Error reading ' + file) - return (None, None) - - -def file_write_text(file, text, enc): - try: - with open(file, 'w', encoding=enc) as f: - f.write(text) - return True - except: - print('Error writing to ' + file) - return False - - -def build_frontend(source, target, env): - # Change working directory to frontend. - os.chdir('frontend') - - # Build the frontend only if it wasn't already built. - # This is to avoid rebuilding the frontend every time the firmware is built. - # This could also lead to some annoying behaviour where the frontend is not updated when the firmware is built. - if not sysenv.get_bool('CI', False): - print('Building frontend...') - os.system('pnpm i') - os.system('pnpm run build') - print('Frontend build complete.') - - # Change working directory back to root. - os.chdir('..') - - # Shorten all the filenames in the data/www/_app/immutable directory. - copy_actions = [] - for root, _, files in os.walk('frontend/build'): - root = root.replace('\\', '/') - newroot = root.replace('frontend/build', 'data/www') - - for filename in files: - filepath = os.path.join(root, filename) - - newfilepath = os.path.join(newroot, filename) - - copy_actions.append((filepath, newfilepath)) - - # Delete the data/www directory if it exists. - dir_delete('data/www') - - for src_path, dst_path in copy_actions: - dir_ensure(os.path.dirname(dst_path)) - if src_path.endswith('.gz'): - shutil.copyfile(src_path, dst_path) - else: - file_gzip(src_path, dst_path + '.gz') - - -def hash_file(filepath): - md5 = hashlib.md5() - sha1 = hashlib.sha1() - sha256 = hashlib.sha256() - - with open(filepath, 'rb') as f: - while True: - chunk = f.read(65536) - if not chunk: - break - md5.update(chunk) - sha1.update(chunk) - sha256.update(chunk) - - return { - 'MD5': md5.hexdigest(), - 'SHA1': sha1.hexdigest(), - 'SHA256': sha256.hexdigest(), - } - - -def process_littlefs_binary(source, target, env): - nTargets = len(target) - if nTargets != 1: - raise Exception(f'Expected 1 target, got {nTargets}') - - # Get the path to the binary and its directory. - littlefs_path = target[0].get_abspath() - - bin_size = os.path.getsize(littlefs_path) - bin_hashes = hash_file(littlefs_path) - - print(f'FileSystem Size: {bin_size} bytes') - print('FileSystem Hashes:') - print('MD5: ' + bin_hashes['MD5']) - print('SHA1: ' + bin_hashes['SHA1']) - print('SHA256: ' + bin_hashes['SHA256']) - - -env.AddPreAction('$BUILD_DIR/littlefs.bin', build_frontend) -env.AddPostAction('$BUILD_DIR/littlefs.bin', process_littlefs_binary) diff --git a/scripts/embed_env_vars.py b/scripts/embed_env_vars.py deleted file mode 100644 index 11d3aadf..00000000 --- a/scripts/embed_env_vars.py +++ /dev/null @@ -1,270 +0,0 @@ -from typing import Mapping -from utils import pioenv, sysenv, dotenv, shorthands -import git -import re - -# This file is invoked by PlatformIO during build. -# See 'extra_scripts' in 'platformio.ini'. - -####################################################### -# DETERMINING THE ENVIRONMENT # -####################################################### - -# Import the PlatformIO env and initialize our PioEnv wrapper. -Import('env') # type: ignore -pio = pioenv.PioEnv(env) # type: ignore - -# Get PIO variables. -project_dir = pio.get_string('PROJECT_DIR') - -# By default, the build is run in DEVELOP mode. -# If we are running in CI and either building the master branch, beta branch, or a tag, then we are in RELEASE mode. -is_ci = shorthands.is_github_ci() -branch_name = shorthands.get_github_ref_name() -is_stable = is_ci and ( - branch_name == 'master' - or shorthands.is_github_pr_into('master') -) -is_beta = is_ci and ( - branch_name == 'beta' - or shorthands.is_github_pr_into('beta') -) -is_tag = is_ci and shorthands.is_github_tag() - -is_release_build = is_stable or is_beta or is_tag - -# Get the build type string. -pio_build_type = 'release' if is_release_build else 'debug' -dotenv_type = 'production' if is_release_build else 'development' - -# Read the correct .env file. -dot = dotenv.DotEnv(project_dir, dotenv_type) - -def get_git_repo(): - try: - return git.Repo(search_parent_directories=True) - except git.exc.InvalidGitRepositoryError: - return None - -def sort_semver(versions): - if not versions or len(versions) == 0: - return [] - - def parse_semver(v): - # Split version into main, prerelease, and build metadata parts - match = re.match(r'^v?(\d+(?:\.\d+)*)(?:-([0-9A-Za-z-.]+))?(?:\+([0-9A-Za-z-.]+))?$', v) - if not match: - raise ValueError(f"Invalid version: {v}") - main_version, prerelease, build = match.groups() - - return (main_version, prerelease, build) - - def version_key(version): - main_version, _, _ = parse_semver(version) - - # Convert the main version into a tuple of integers for natural sorting - main_version_tuple = tuple(map(int, main_version.split('.'))) - - return main_version_tuple - - # Remove versions with prerelease/build suffixes - clean_only = [v for v in versions if not ('-' in v or '+' in v)] - - # Sort by version key - return sorted(clean_only, key=lambda v: version_key(v)) - -def last_element(arr): - return arr[-1] if len(arr) > 0 else None - -git_repo = get_git_repo() -git_commit = git_repo.head.object.hexsha if git_repo is not None else None -git_tags = [tag.name for tag in git_repo.tags] if git_repo is not None else [] -git_latest_clean_tag = last_element(sort_semver(git_tags)) - -# Find env variables based on only the pioenv and sysenv. -def get_pio_firmware_vars() -> dict[str, str | int | bool]: - fw_board = pio.get_string('PIOENV') - fw_chip = pio.get_string('BOARD_MCU') - - def macroify(s: str) -> str: - return s.upper().replace('-', '').replace('_', '') - - vars = {} - vars['OPENSHOCK_FW_BOARD'] = fw_board - vars['OPENSHOCK_FW_BOARD_' + macroify(fw_board)] = True # Used for conditional compilation. - vars['OPENSHOCK_FW_CHIP'] = fw_chip.upper() - vars['OPENSHOCK_FW_CHIP_' + macroify(fw_chip)] = True # Used for conditional compilation. - vars['OPENSHOCK_FW_MODE'] = pio_build_type - if git_commit is not None: - vars['OPENSHOCK_FW_GIT_COMMIT'] = git_commit - return vars - - -####################################################### -# UPDATING BUILD PARAMETERS # -####################################################### - - -# Parse PIO build flags. -# All CPP Defines, i.e. flags starting with "-D", are parsed for key/value and stored in a dictionary (first value). -# All other build flags are returned in a list (second value) -def parse_pio_build_flags(raw_flags: list[str]) -> tuple[dict[str, str | int | bool], list[str]]: - flag_dict = {} - leftover_flags = [] - for flag in raw_flags: - if flag.startswith('-D'): - flag = flag[2::] - if '=' in flag: - (k, v) = flag.split('=', 1) - flag_dict[k] = v - else: - flag_dict[flag] = True - else: - leftover_flags.append(flag) - return (flag_dict, leftover_flags) - - -def transform_cpp_define_string(k: str, v: str) -> str: - # Remove surrounding quotes if present. - if v.startswith('"') and v.endswith('"'): - v = v[1:-1] - - return env.StringifyMacro(v) - - -def serialize_cpp_define(k: str, v: str | int | bool) -> str | int: - # Special case for variables that can sometimes be fully made up of numeric characters, breaking builds in specific situations. - if k in ['OPENSHOCK_FW_GIT_COMMIT', 'OPENSHOCK_FW_VERSION_BUILD']: - return transform_cpp_define_string(k, str(v)) - - try: - return int(v) - except ValueError: - pass - if isinstance(v, str): - return transform_cpp_define_string(k, v) - - # TODO: Handle booleans. - - return v - - -def split_semver(version): - # Match the semver pattern (with optional 'v' prefix) - pattern = r'^v?(?P\d+)\.(?P\d+)\.(?P\d+)(?:-(?P[0-9A-Za-z.-]+))?(?:\+(?P[0-9A-Za-z.-]+))?$' - match = re.match(pattern, version) - - if not match: - raise ValueError("Invalid semver format") - - # Extract components and convert major, minor, patch to integers - major = int(match.group('major')) - minor = int(match.group('minor')) - patch = int(match.group('patch')) - prerelease = match.group('prerelease') # None if not present - build = match.group('build') # None if not present - - return major, minor, patch, prerelease, build - - -# Serialize CPP Defines. -# Strings are escaped to be a correct CPP macro. -# Booleans are turned into integers, True => 1 and False => 0. -# Integers are kept as-is. -def serialize_cpp_defines(raw_defines: dict[str, str | int | bool]) -> dict[str, str | int]: - result_defines = {} - for k, v in raw_defines.items(): - result_defines[k] = serialize_cpp_define(k, v) - return result_defines - - -# Copy key/value pairs from "src" into "dest" only if those keys don't exist in "dest" yet. -def merge_missing_keys(dest: dict[str, str | int | bool], src: Mapping[str, str | int | bool]): - for k, v in src.items(): - if k not in dest: - dest[k] = v - - -# Print a dictionary for debugging purposes. -def print_dump(name: str, map: Mapping[str, str | int | bool]) -> None: - print('%s:' % name) - for k, v in map.items(): - print(' %s = %s' % (k, v)) - - -# Fetch the current build flags and group them into (CPP Defines, Other Flags). -raw_build_flags = pio.get_string_array('BUILD_FLAGS', []) -(cpp_defines, remaining_build_flags) = parse_pio_build_flags(raw_build_flags) - -# Gets all the environment variables prefixed with 'OPENSHOCK_' and add them as CPP Defines. -sys_openshock_vars = sysenv.get_all_prefixed('OPENSHOCK_') -pio_openshock_vars = get_pio_firmware_vars() -dot_openshock_vars = dot.get_all_prefixed('OPENSHOCK_') - -print_dump('Sys OpenShock vars', sys_openshock_vars) -print_dump('PIO OpenShock vars', pio_openshock_vars) -print_dump('Dotenv OpenShock vars', dot_openshock_vars) - -merge_missing_keys(cpp_defines, sys_openshock_vars) -merge_missing_keys(cpp_defines, pio_openshock_vars) -merge_missing_keys(cpp_defines, dot_openshock_vars) - -# Check if OPENSHOCK_FW_VERSION is set. -if 'OPENSHOCK_FW_VERSION' not in cpp_defines: - if is_ci: - raise ValueError('OPENSHOCK_FW_VERSION must be set in environment variables for CI builds.') - - # If latest_git_tag is set, use it (stripped of 'v' prefix), else use 0.0.0, assign to variable. - clean_tag = git_latest_clean_tag.lstrip('v') if git_latest_clean_tag is not None else '0.0.0' - version = clean_tag + '-local' - - # If git_commit is set, append it to the version. - if git_commit is not None: - version += '+' + git_commit[0:7] - - # If not set, get the latest tag. - cpp_defines['OPENSHOCK_FW_VERSION'] = version - - -version_major, version_minor, version_patch, version_prerelease, version_build = split_semver( - cpp_defines['OPENSHOCK_FW_VERSION'] -) - -cpp_defines['OPENSHOCK_FW_VERSION_MAJOR'] = version_major -cpp_defines['OPENSHOCK_FW_VERSION_MINOR'] = version_minor -cpp_defines['OPENSHOCK_FW_VERSION_PATCH'] = version_patch -if version_prerelease is not None: - cpp_defines['OPENSHOCK_FW_VERSION_PRERELEASE'] = version_prerelease -if version_build is not None: - cpp_defines['OPENSHOCK_FW_VERSION_BUILD'] = version_build - -# Gets the log level from environment variables. -# TODO: Delete get_loglevel and use... something more generic. -log_level_int = dot.get_loglevel('LOG_LEVEL') -if log_level_int is None: - raise ValueError('LOG_LEVEL must be set in environment variables.') - -# Beta builds are release builds but with debug-level logging. -if is_beta: - log_level_int = 4 # Debug level. - -cpp_defines['OPENSHOCK_LOG_LEVEL'] = log_level_int -cpp_defines['CORE_DEBUG_LEVEL'] = log_level_int if not is_release_build or is_beta else 2 # Warning level for release, match log level for dev/beta. - -# Serialize and inject CPP Defines. -print_dump('CPP Defines', cpp_defines) - -cpp_defines = serialize_cpp_defines(cpp_defines) - -print('Build type: ' + pio_build_type + (' (beta, debug logging)' if is_beta else '')) - -# Set PIO variables. -env['BUILD_TYPE'] = pio_build_type -env['BUILD_FLAGS'] = remaining_build_flags -env.Append(CPPDEFINES=list(cpp_defines.items())) -env.Append(CXXFLAGS=['-fno-rtti', '-fno-threadsafe-statics', '-fno-use-cxa-atexit']) - -# Rename the firmware.bin to app.bin. -env.Replace(PROGNAME='app') - -print(env.Dump()) \ No newline at end of file diff --git a/scripts/flatc b/scripts/flatc index dc55abcd..63f734db 100755 Binary files a/scripts/flatc and b/scripts/flatc differ diff --git a/scripts/flatc.exe b/scripts/flatc.exe index 183b26c5..0d666d7b 100644 Binary files a/scripts/flatc.exe and b/scripts/flatc.exe differ diff --git a/scripts/gen_dep_graph.py b/scripts/gen_dep_graph.py new file mode 100644 index 00000000..e2dc077d --- /dev/null +++ b/scripts/gen_dep_graph.py @@ -0,0 +1,197 @@ +#!/usr/bin/env python3 +"""Generate a Mermaid dependency graph from the ESP-IDF component manifests. + +Scans every `components//CMakeLists.txt`, parses the REQUIRES and +PRIV_REQUIRES lists out of its idf_component_register() call, and emits a Mermaid +`flowchart` where each edge points from a dependency to the component that needs +it (i.e. `X --> Y` reads "Y requires X"). + +By default only edges between local components (those with a directory under +components/) are drawn. Pass --external to also include external components +(IDF built-ins, managed_components, etc.) as nodes. + +Usage: + python scripts/gen_dep_graph.py # local components only + python scripts/gen_dep_graph.py --external # include IDF/managed deps + python scripts/gen_dep_graph.py -o graph.mmd # write to a file + +Paste the output into any Mermaid renderer (GitHub, mermaid.live, VS Code). +""" + +import argparse +import re +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent + +# idf_component_register() argument keywords. A token equal to one of these ends +# the current value list and (for REQUIRES/PRIV_REQUIRES) starts a new one. +CMAKE_KEYWORDS = { + 'SRCS', 'SRC_DIRS', 'EXCLUDE_SRCS', + 'INCLUDE_DIRS', 'PRIV_INCLUDE_DIRS', + 'REQUIRES', 'PRIV_REQUIRES', 'REQUIRED_IDF_TARGETS', + 'EMBED_FILES', 'EMBED_TXTFILES', 'LDFRAGMENTS', + 'KCONFIG', 'KCONFIG_PROJBUILD', 'WHOLE_ARCHIVE', +} + +# Component name -> display label. Anything not listed falls back to PascalCase +# of the snake_case directory name (e.g. led_drivers -> LedDrivers). +LABEL_OVERRIDES = { + 'osjson': 'OSJson', + 'http': 'HTTP', + 'fs': 'FS', + 'dns_server': 'DnsServer', + 'rfc8908': 'RFC8908', + 'hwutil': 'HwUtil', +} + + +def label_for(name: str) -> str: + if name in LABEL_OVERRIDES: + return LABEL_OVERRIDES[name] + return ''.join(word.capitalize() for word in name.split('_')) + + +def col_id(index: int) -> str: + """Spreadsheet-style id: 0->A, 25->Z, 26->AA, ...""" + s = '' + index += 1 + while index > 0: + index, rem = divmod(index - 1, 26) + s = chr(ord('A') + rem) + s + return s + + +def strip_comments(text: str) -> str: + return '\n'.join(line.split('#', 1)[0] for line in text.splitlines()) + + +def extract_register_block(text: str) -> str: + """Return the contents inside the idf_component_register( ... ) call.""" + m = re.search(r'idf_component_register\s*\(', text) + if not m: + return '' + depth = 0 + start = m.end() - 1 # at the '(' + for i in range(start, len(text)): + c = text[i] + if c == '(': + depth += 1 + elif c == ')': + depth -= 1 + if depth == 0: + return text[start + 1:i] + return text[start + 1:] # unbalanced; take the rest + + +def parse_requires(cmake_text: str, include_priv: bool) -> list[str]: + """Pull the REQUIRES (+ optionally PRIV_REQUIRES) token lists.""" + block = extract_register_block(strip_comments(cmake_text)) + tokens = block.split() + + deps: list[str] = [] + active = None # None | 'collect' + for tok in tokens: + if tok in CMAKE_KEYWORDS: + if tok == 'REQUIRES' or (tok == 'PRIV_REQUIRES' and include_priv): + active = 'collect' + else: + active = None + continue + if active == 'collect': + deps.append(tok) + + # de-dupe while preserving order + seen = set() + ordered = [] + for d in deps: + if d not in seen: + seen.add(d) + ordered.append(d) + return ordered + + +def topo_order(nodes: set[str], edges: set[tuple[str, str]]) -> list[str]: + """Dependencies first. Edge (dep, dependent). Alphabetical tie-break; any + nodes left in a cycle are appended in sorted order so output stays total.""" + indeg = {n: 0 for n in nodes} + adj = {n: [] for n in nodes} + for dep, dependent in edges: + adj[dep].append(dependent) + indeg[dependent] += 1 + + ready = sorted(n for n in nodes if indeg[n] == 0) + order = [] + while ready: + n = ready.pop(0) + order.append(n) + for m in sorted(adj[n]): + indeg[m] -= 1 + if indeg[m] == 0: + ready.append(m) + ready.sort() + + if len(order) < len(nodes): + order.extend(sorted(nodes - set(order))) + return order + + +def main(argv: list[str]) -> int: + parser = argparse.ArgumentParser(description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument('--components-dir', default=str(ROOT / 'components'), + help='directory holding the component subdirs (default: ./components)') + parser.add_argument('-e', '--external', action='store_true', + help='also include external components (IDF/managed) as nodes') + parser.add_argument('--no-priv', action='store_true', + help='ignore PRIV_REQUIRES, use only public REQUIRES') + parser.add_argument('--direction', default='TD', choices=['TD', 'TB', 'LR', 'RL', 'BT'], + help='Mermaid flowchart direction (default: TD)') + parser.add_argument('-o', '--output', help='write to this file instead of stdout') + args = parser.parse_args(argv) + + comp_dir = Path(args.components_dir) + if not comp_dir.is_dir(): + raise SystemExit(f'error: {comp_dir} is not a directory') + + # Local components: direct children of components/ with a CMakeLists.txt. + local = {} + for cml in sorted(comp_dir.glob('*/CMakeLists.txt')): + name = cml.parent.name + local[name] = parse_requires(cml.read_text(encoding='utf-8'), + include_priv=not args.no_priv) + + edges: set[tuple[str, str]] = set() + nodes: set[str] = set(local) + for name, deps in local.items(): + for dep in deps: + is_local = dep in local + if not is_local and not args.external: + continue + edges.add((dep, name)) # dependency -> dependent + nodes.add(dep) + + order = topo_order(nodes, edges) + ids = {name: col_id(i) for i, name in enumerate(order)} + + lines = [f'flowchart {args.direction}'] + for name in order: + lines.append(f' {ids[name]}({label_for(name)})') + # Edges grouped by source in node order for stable, readable output. + for src in order: + for dst in order: + if (src, dst) in edges: + lines.append(f' {ids[src]} --> {ids[dst]}') + + text = '\n'.join(lines) + '\n' + if args.output: + Path(args.output).write_text(text, encoding='utf-8') + print(f'wrote {args.output} ({len(nodes)} nodes, {len(edges)} edges)', file=sys.stderr) + else: + sys.stdout.write(text) + return 0 + + +if __name__ == '__main__': + raise SystemExit(main(sys.argv[1:])) diff --git a/scripts/gen_env_header.py b/scripts/gen_env_header.py new file mode 100644 index 00000000..6372b978 --- /dev/null +++ b/scripts/gen_env_header.py @@ -0,0 +1,171 @@ +#!/usr/bin/env python3 +"""Generate the OpenShock environment header, force-included into every TU. + +Native ESP-IDF replacement for the PlatformIO ``embed_env_vars.py`` extra_script. +It emits the firmware version, API/CDN domains, board/chip identity, build mode +and log level as preprocessor ``#define``s. The header is force-included by the +build (see the root ``CMakeLists.txt``), so ``OpenShock.h`` and friends see the +same macros they used to get from PlatformIO ``-D`` flags. + +Values come from (highest priority first): + 1. ``OPENSHOCK_*`` process environment variables (CI provides the version etc.) + 2. the ``.env`` / ``.env.`` files (API/CDN domains, hostname, log level) + 3. computed defaults (local dev version fallback, board/chip macros) +""" + +import argparse +import os +import re +import subprocess +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +from utils import shorthands # noqa: E402 + +# Keys whose value must always be emitted as a C string, even when it happens to +# be all-numeric (a bare numeric macro breaks string concatenation / #if usage). +FORCE_STRING_KEYS = { + 'OPENSHOCK_FW_GIT_COMMIT', + 'OPENSHOCK_FW_VERSION_BUILD', + # Semver allows purely-numeric prerelease identifiers (e.g. 1.2.3-1); keep it + # a string so it isn't emitted as a bare int macro. + 'OPENSHOCK_FW_VERSION_PRERELEASE', +} + + +def macroify(s: str) -> str: + return s.upper().replace('-', '').replace('_', '') + + +def split_semver(version: str): + pattern = ( + r'^v?(?P\d+)\.(?P\d+)\.(?P\d+)' + r'(?:-(?P[0-9A-Za-z.-]+))?(?:\+(?P[0-9A-Za-z.-]+))?$' + ) + m = re.match(pattern, version) + if not m: + raise ValueError(f'Invalid semver version: {version!r}') + return ( + int(m.group('major')), + int(m.group('minor')), + int(m.group('patch')), + m.group('prerelease'), + m.group('build'), + ) + + +def c_string(value: str) -> str: + escaped = value.replace('\\', '\\\\').replace('"', '\\"') + return f'"{escaped}"' + + +def serialize(key: str, value) -> str: + """Render a macro value: raw int when numeric, else a C string.""" + if key in FORCE_STRING_KEYS: + return c_string(str(value)) + s = str(value) + try: + return str(int(s)) + except ValueError: + return c_string(s) + + +def git_commit() -> str | None: + commit = os.environ.get('OPENSHOCK_FW_GIT_COMMIT') + if commit: + return commit + try: + return subprocess.check_output( + ['git', 'rev-parse', 'HEAD'], text=True, stderr=subprocess.DEVNULL + ).strip() + except Exception: + return None + + +def resolve_version() -> str: + version = os.environ.get('OPENSHOCK_FW_VERSION') + if version: + return version + if shorthands.is_github_ci(): + raise SystemExit('error: OPENSHOCK_FW_VERSION must be set for CI builds') + # Local dev fallback — a valid semver so split_semver() is happy. + commit = git_commit() + return '0.0.0-local' + (f'+{commit[:7]}' if commit else '') + + +def main() -> int: + ap = argparse.ArgumentParser(description=__doc__) + ap.add_argument('--output', required=True, help='header file to write') + ap.add_argument('--board', required=True, help='board name, e.g. OpenShock-Core-V2') + ap.add_argument('--chip', required=True, help='chip, e.g. ESP32-S3') + args = ap.parse_args() + + # Build mode: release for CI builds of master/beta/tags, debug otherwise. + is_ci = shorthands.is_github_ci() + is_release = is_ci and ( + shorthands.get_github_ref_name() in ('master', 'beta') + or shorthands.is_github_pr_into('master') + or shorthands.is_github_pr_into('beta') + or shorthands.is_github_tag() + ) + is_beta = is_ci and ( + shorthands.get_github_ref_name() == 'beta' or shorthands.is_github_pr_into('beta') + ) + mode = 'release' if is_release else 'debug' + + defines: dict[str, object] = {} + + # Version. + version = resolve_version() + major, minor, patch, prerelease, build = split_semver(version) + defines['OPENSHOCK_FW_VERSION'] = version + defines['OPENSHOCK_FW_VERSION_MAJOR'] = major + defines['OPENSHOCK_FW_VERSION_MINOR'] = minor + defines['OPENSHOCK_FW_VERSION_PATCH'] = patch + if prerelease is not None: + defines['OPENSHOCK_FW_VERSION_PRERELEASE'] = prerelease + if build is not None: + defines['OPENSHOCK_FW_VERSION_BUILD'] = build + + # Board / chip identity (+ macros used for conditional compilation). + defines['OPENSHOCK_FW_BOARD'] = args.board + defines['OPENSHOCK_FW_BOARD_' + macroify(args.board)] = 1 + defines['OPENSHOCK_FW_CHIP'] = args.chip.upper() + defines['OPENSHOCK_FW_CHIP_' + macroify(args.chip)] = 1 + + # Build mode + git commit. + defines['OPENSHOCK_FW_MODE'] = mode + commit = git_commit() + if commit is not None: + defines['OPENSHOCK_FW_GIT_COMMIT'] = commit + + # Log level by build mode (formerly .env.development / .env.production): + # debug -> VERBOSE(5), release -> INFO(3), beta -> DEBUG(4). + if is_beta: + log_level = 4 + elif is_release: + log_level = 3 + else: + log_level = 5 + defines['OPENSHOCK_LOG_LEVEL'] = log_level + defines['CORE_DEBUG_LEVEL'] = log_level if (not is_release or is_beta) else 2 + + lines = [ + '// Generated by scripts/gen_env_header.py — do not edit.', + '#pragma once', + '', + ] + for key in sorted(defines): + lines.append(f'#define {key} {serialize(key, defines[key])}') + lines.append('') + + out = Path(args.output) + out.parent.mkdir(parents=True, exist_ok=True) + out.write_text('\n'.join(lines), encoding='utf-8') + print(f'gen_env_header: wrote {len(defines)} defines to {out}') + return 0 + + +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/scripts/gen_staticfs.py b/scripts/gen_staticfs.py new file mode 100644 index 00000000..62588f5a --- /dev/null +++ b/scripts/gen_staticfs.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python3 +"""Build the captive-portal static filesystem image (native ESP-IDF). + +Pipeline: + 1. Build the SvelteKit frontend (pnpm), unless already built / running in CI. + 2. Gzip every built asset into /www/.gz. + 3. Pack / into a littlefs image sized for the `static0` partition. + +The littlefs geometry MUST match how the firmware mounts the partition at +runtime (components/fs/src/LfsPartition.cpp): 4 KiB erase blocks, 128-byte +read/prog. Only block_size and block_count are fixed on-disk and validated +against the superblock at mount; the rest are runtime buffer sizes. + +Invoked by CMake (see the root CMakeLists.txt). This replaces the old +PlatformIO SCons extra_script scripts/build_frontend.py. +""" + +import argparse +import gzip +import os +import shutil +import subprocess +import sys + + +def log(msg): + print(f'[gen_staticfs] {msg}', flush=True) + + +def build_frontend(frontend_dir): + """Run the SvelteKit production build unless it already exists or we're in CI. + + In CI the frontend is built once in a separate job and downloaded as an + artifact into frontend/build, so we must not rebuild it here. + """ + build_dir = os.path.join(frontend_dir, 'build') + + if os.environ.get('CI'): + if not os.path.isdir(build_dir): + raise SystemExit(f'CI build: expected prebuilt frontend at {build_dir}') + log('CI detected — using prebuilt frontend/build') + return build_dir + + log('Building frontend (pnpm i && pnpm run build)...') + subprocess.run(['pnpm', 'i'], cwd=frontend_dir, check=True, shell=os.name == 'nt') + subprocess.run(['pnpm', 'run', 'build'], cwd=frontend_dir, check=True, shell=os.name == 'nt') + log('Frontend build complete') + return build_dir + + +def stage_assets(build_dir, staging_dir): + """Mirror build_dir into /www, gzipping everything that isn't already .gz.""" + www_dir = os.path.join(staging_dir, 'www') + if os.path.exists(www_dir): + shutil.rmtree(www_dir) + + count = 0 + for root, _, files in os.walk(build_dir): + rel = os.path.relpath(root, build_dir) + dst_root = www_dir if rel == '.' else os.path.join(www_dir, rel) + os.makedirs(dst_root, exist_ok=True) + + for name in files: + src = os.path.join(root, name) + if name.endswith('.gz'): + dst = os.path.join(dst_root, name) + shutil.copyfile(src, dst) + else: + dst = os.path.join(dst_root, name + '.gz') + with open(src, 'rb') as f_in, gzip.open(dst, 'wb') as f_out: + f_out.write(f_in.read()) + count += 1 + + log(f'Staged {count} gzipped assets into {www_dir}') + if not os.path.exists(os.path.join(www_dir, 'index.html.gz')): + raise SystemExit(f'{www_dir}/index.html.gz missing — frontend build produced no index.html') + return www_dir + + +def pack_image(staging_dir, output, partition_size, block_size, read_size, prog_size): + """Pack staging_dir into a littlefs image of exactly partition_size bytes.""" + try: + from littlefs import LittleFS + except ImportError: + raise SystemExit("littlefs-python is required (pip install -r requirements.txt)") + + if partition_size % block_size != 0: + raise SystemExit(f'partition size {partition_size} is not a multiple of block size {block_size}') + block_count = partition_size // block_size + + # cache_size must be a multiple of read/prog and divide block_size; 512 matches + # the runtime mount (LfsPartition.cpp). lookahead just needs to be a multiple of 8. + fs = LittleFS( + block_size=block_size, + block_count=block_count, + read_size=read_size, + prog_size=prog_size, + cache_size=512, + lookahead_size=128, + ) + + file_count = 0 + for root, _, files in os.walk(staging_dir): + rel_root = os.path.relpath(root, staging_dir).replace(os.sep, '/') + if rel_root != '.': + fs.makedirs(rel_root, exist_ok=True) + for name in files: + rel_path = name if rel_root == '.' else f'{rel_root}/{name}' + with open(os.path.join(root, name), 'rb') as f_in: + with fs.open(rel_path, 'wb') as f_out: + f_out.write(f_in.read()) + file_count += 1 + + os.makedirs(os.path.dirname(os.path.abspath(output)), exist_ok=True) + with open(output, 'wb') as f: + f.write(bytes(fs.context.buffer)) + + log(f'Packed {file_count} files into {output} ' + f'({partition_size} bytes, {block_count} x {block_size}-byte blocks)') + + +def main(): + p = argparse.ArgumentParser(description='Build the OpenShock static filesystem image') + p.add_argument('--frontend-dir', required=True, help='Path to the frontend/ project') + p.add_argument('--staging-dir', required=True, help='Where to stage gzipped assets (contains www/)') + p.add_argument('--output', required=True, help='Output littlefs image path') + p.add_argument('--partition-size', required=True, type=lambda v: int(v, 0), help='static0 partition size') + p.add_argument('--block-size', type=int, default=4096) + p.add_argument('--read-size', type=int, default=128) + p.add_argument('--prog-size', type=int, default=128) + p.add_argument('--skip-frontend-build', action='store_true', + help='Reuse existing frontend/build without invoking pnpm') + args = p.parse_args() + + if args.skip_frontend_build: + build_dir = os.path.join(args.frontend_dir, 'build') + if not os.path.isdir(build_dir): + raise SystemExit(f'--skip-frontend-build set but {build_dir} does not exist') + else: + build_dir = build_frontend(args.frontend_dir) + + stage_assets(build_dir, args.staging_dir) + pack_image(args.staging_dir, args.output, args.partition_size, + args.block_size, args.read_size, args.prog_size) + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/scripts/generate_schemas.py b/scripts/generate_schemas.py index 1d809d00..787ff74f 100644 --- a/scripts/generate_schemas.py +++ b/scripts/generate_schemas.py @@ -51,7 +51,7 @@ def resolve_path(path): # resolve paths schemas_path = resolve_path('../schemas') ts_output_path = resolve_path('../frontend/src/lib/_fbs') -cpp_output_path = resolve_path('../include/serialization/_fbs') +cpp_output_path = resolve_path('../components/serialization/include/serialization/_fbs') # Get all the schema files. schema_files = [] diff --git a/scripts/install_dependencies.py b/scripts/install_dependencies.py deleted file mode 100644 index 60d9960d..00000000 --- a/scripts/install_dependencies.py +++ /dev/null @@ -1,59 +0,0 @@ -import importlib.util -import sys -import subprocess - -class Package: - def __init__(self, pip_name, pip_options, import_name): - self.pip_name = pip_name - self.pip_options = pip_options - self.import_name = import_name - - def pip_name(self): - return self.pip_name - - def pip_package(self): - if len(self.pip_options) == 0: - return self.pip_name - - return self.pip_name + '[' + ','.join(self.pip_options) + ']' - - def import_name(self): - return self.import_name - - def is_installed(self): - if self.pip_name in sys.modules: - return True - - if importlib.util.find_spec(self.pip_name) is not None: - return True - - try: - __import__(self.import_name) - return True - except ImportError: - pass - - return False - -# List of packages to install (pip name, package name) -required = [ - # Captive Portal building - Package('fonttools', ['woff', 'unicode'], 'fontTools'), - Package('brotli', [], 'brotli'), - Package('gitpython', [], 'git'), - # esptool (while using custom version) - Package('intelhex', [], "IntelHex"), -] - -# Get all packages that are not installed -to_install = [ p.pip_package() for p in required if not p.is_installed() ] - -# Install all packages that are not installed -if len(to_install) > 0: - print('Installing: ' + ', '.join(to_install)) - - # Get the python executable path - python_path = sys.executable - - # Install the packages - subprocess.check_call([python_path, '-m', 'pip', 'install', *to_install]) diff --git a/scripts/merge_image.py b/scripts/merge_image.py index 58b0ad10..3e589dd4 100644 --- a/scripts/merge_image.py +++ b/scripts/merge_image.py @@ -1,41 +1,77 @@ -#!/bin/python3 +#!/usr/bin/env python3 +"""Merge a board's partition images into one flashable binary (native ESP-IDF). -import os -import sys +Combines the discrete images produced by the C++ build (app, bootloader, +partition table) with the shared static0 web-assets image into a single binary +flashable at offset 0 - the web-flasher / GitHub-release artifact. Runs esptool +directly so the merge CI job needs esptool only, not a full ESP-IDF install. + +Offsets mirror partitions/ota_4mb.csv (partition table 0x8000, app 0x10000, +static0 0x353000); the bootloader offset is the only chip-specific value. +Replaces the old chips//merge-image.py scripts. +""" + +import argparse +import re from pathlib import Path -from configparser import ConfigParser -from utils.boardconf import BoardConf, from_pio_file, validate, print_header, print_footer +import esptool + +ROOT = Path(__file__).resolve().parent.parent -def call_script(file): - print('Calling script: %s' % file) - print() - with open(file) as f: - exec(f.read()) - sys.exit(0) +# The 2nd-stage bootloader flashes at 0x0 on chips whose ROM loader expects it +# there, and at 0x1000 on the original ESP32 and ESP32-S2. +BOOTLOADER_OFFSET = { + 'esp32': '0x1000', + 'esp32s2': '0x1000', + 'esp32s3': '0x0', + 'esp32c3': '0x0', +} +# Fixed by partitions/ota_4mb.csv + the ESP-IDF default partition-table offset. +PARTITION_TABLE_OFFSET = '0x8000' +APP_OFFSET = '0x10000' +STATICFS_OFFSET = '0x353000' -# Get pio env name from CLI args -env_name = sys.argv[1] -# Read board configuration from platformio.ini -boardconf: BoardConf = from_pio_file(env_name) +def resolve_target(board: str) -> str: + fragment = ROOT / 'boards' / f'{board}.defaults' + if not fragment.is_file(): + raise SystemExit(f'error: unknown board {board!r} ({fragment} missing)') + for line in fragment.read_text(encoding='utf-8').splitlines(): + m = re.match(r'\s*CONFIG_IDF_TARGET\s*=\s*"([^"]+)"', line) + if m: + return m.group(1) + raise SystemExit(f'error: {fragment} does not set CONFIG_IDF_TARGET') -def merge_image(): - if not validate(boardconf): - return +def main() -> None: + ap = argparse.ArgumentParser(description=__doc__) + ap.add_argument('--board', required=True, help='board name (matches boards/.defaults)') + ap.add_argument('--bindir', required=True, help='dir with app.bin, bootloader.bin, partition-table.bin') + ap.add_argument('--staticfs', required=True, help='path to the static0 image (staticfs.bin)') + ap.add_argument('--output', required=True, help='merged output binary') + args = ap.parse_args() - # If the board exists directly, call `merge-image.py` in that directory. - if not boardconf.does_merge_script_exist(): - print('ERROR: MERGE SCRIPT DOES NOT EXIST: %s' % boardconf.get_merge_script()) - print('FAILED TO FIND merge-image.py') - sys.exit(1) + chip = resolve_target(args.board) + boot_offset = BOOTLOADER_OFFSET.get(chip) + if boot_offset is None: + raise SystemExit(f'error: no bootloader offset known for chip {chip!r} - add it to merge_image.py') - # Call the chips/{chip}/merge-image.py script - call_script(boardconf.get_merge_script()) + bindir = Path(args.bindir) + argv = [ + '--chip', chip, + 'merge-bin', + '--output', args.output, + '--flash-size', '4MB', + boot_offset, str(bindir / 'bootloader.bin'), + PARTITION_TABLE_OFFSET, str(bindir / 'partition-table.bin'), + APP_OFFSET, str(bindir / 'app.bin'), + STATICFS_OFFSET, str(args.staticfs), + ] + print(f'+ esptool {" ".join(argv)}', flush=True) + esptool.main(argv) -print_header() -merge_image() -print_footer() +if __name__ == '__main__': + main() diff --git a/scripts/use_openshock_params.py b/scripts/use_openshock_params.py deleted file mode 100644 index 75e10ec3..00000000 --- a/scripts/use_openshock_params.py +++ /dev/null @@ -1,57 +0,0 @@ -import os -from pathlib import Path -from utils.boardconf import BoardConf, from_pio_env, validate, print_header, print_footer, fallback_flash_size -import csv - -# -# This file is responsible for processing the "custom_openshock.*" -# parameters declared in `platformio.ini`. -# -# And for updating the `partitions.csv` file to include the -# `config` partition. -# -# This file is invoked by PlatformIO during build. -# See 'extra_scripts' in 'platformio.ini'. -# - -Import('env') # type: ignore -env = env # type: ignore - -# Parse the board/chip specific configuration from the current pio `env` variable. -boardconf: BoardConf = from_pio_env(env) - -# In case we need to blacklist certain flash sizes from OTA support. -blacklisted_OTA_sizes = [ - # '4MB' - ] - -def use_openshock_params(): - if not validate(boardconf): - print('Failed to validate OpenShock board configuration.') - return - - # Handle partitions file not existing - if not boardconf.do_partitions_files_exists(boardconf.get_flash_size()): - if boardconf.do_partitions_files_exists(fallback_flash_size): - print(f'WARNING: PARTITIONS FILES DON\'T EXIST FOR THIS FLASH SIZE, FALLING BACK TO {fallback_flash_size}.') - boardconf.override_flash_size(fallback_flash_size) - - # Set partition file to chip dir. - # https://docs.platformio.org/en/latest/scripting/examples/override_board_configuration.html - board_config = env.BoardConfig() - - partitions, partitions_ota = boardconf.get_partitions_files(boardconf.get_flash_size()) - - if boardconf.get_flash_size() in blacklisted_OTA_sizes: - print('WARNING: OTA NOT SUPPORTED FOR THIS FLASH SIZE.') - print('Using Non-OTA partition layout.') - board_config.update('build.partitions', str(partitions)) - board_config.update('upload.flash_size', boardconf.get_flash_size()) - else: - board_config.update('build.partitions', str(partitions_ota)) - board_config.update('upload.flash_size', boardconf.get_flash_size()) - - -print_header() -use_openshock_params() -print_footer() diff --git a/scripts/utils/boardconf.py b/scripts/utils/boardconf.py deleted file mode 100644 index 6fe35397..00000000 --- a/scripts/utils/boardconf.py +++ /dev/null @@ -1,148 +0,0 @@ -import os -from pathlib import Path -from configparser import ConfigParser - -fallback_flash_size = '4MB' - -class BoardConf: - def __init__(self, chip: str, flash_size: str): - self.chip = chip - self.flash_size = flash_size - if flash_size == '': - self.flash_size = fallback_flash_size - - # Set dirs - self.root_dir = Path().absolute() - self.chips_dir = self.root_dir / 'chips' - self.chip_base_dir = self.chips_dir / self.chip - self.flash_size_dir = ( - (self.chip_base_dir / self.flash_size) if self.flash_size != '' else self.chip_base_dir - ) - - def get_chip(self) -> str: - return self.chip - - def get_flash_size(self) -> str: - return self.flash_size - - def get_chips_dir(self) -> Path: - return self.chips_dir - - def get_chip_dir(self) -> Path: - return self.chip_base_dir - - def get_flash_size_dir(self) -> Path: - return self.flash_size_dir - - def override_flash_size(self, flash_size: str): - self.flash_size = flash_size - self.flash_size_dir = self.chip_base_dir / self.flash_size - - def get_merge_script(self) -> Path: - return self.flash_size_dir / 'merge-image.py' - - def get_partitions_files(self, size: str) -> tuple[Path, Path]: - return self.chips_dir / f'partitions_{size}.csv', self.chips_dir / f'partitions_{size}_OTA.csv', - - def is_chip_specified(self) -> bool: - return self.chip != '' - - def is_flash_size_specified(self) -> bool: - return self.flash_size != '' - - # Whether the chip folder exists. - def does_chip_exist(self) -> bool: - return os.path.exists(self.get_chip_dir()) - - # Whether the flash size folder exists. - def does_flash_size_exist(self, size: str = "") -> bool: - if size == "": - return os.path.exists(self.get_flash_size_dir()) - else: - return os.path.exists(self.chip_base_dir / size) - - # Whether the merge script for the current flash size exists. - def does_merge_script_exist(self) -> bool: - return os.path.exists(self.get_merge_script()) - - # Whether the partitions file for the current flash size exists. - def do_partitions_files_exists(self, size: str) -> bool: - partitions, partitions_ota = self.get_partitions_files(size) - return os.path.exists(partitions) and os.path.exists(partitions_ota) - - -def from_pio_file(env_name: str) -> BoardConf: - config = ConfigParser() - config.read('platformio.ini') - - # Grab env - env = config['env:' + env_name] - - # Grab strings from platformio.ini env declaration - # TODO: Simplify platformio.ini to only have one env (Flash Size) - # Need to either pass in or figure out chip - chip = env.get('custom_openshock.chip', '') - flash_size = env.get('custom_openshock.flash_size', '') - - return BoardConf(chip, flash_size) - - -def from_pio_env(env): - # Grab strings from current pio env - # TODO: Simplify platformio.ini to only have one env (Flash Size) - # env.BoardConfig().get('build.mcu') - # can be used to get chip - chip = env.GetProjectOption('custom_openshock.chip', '') - flash_size = env.GetProjectOption('custom_openshock.flash_size', '') - - return BoardConf(chip, flash_size) - - -def print_header(): - print('========================') - print(' OpenShock ') - print('========================') - print('') - - -def print_footer(): - print('========================') - print('') - - -def validate(boardconf) -> bool: - # Handle missing chip declaration. - if not boardconf.is_chip_specified(): - print('No chip specified.') - print('Skipping further chip/flash size-specific initialization.') - return False - - # Print info about determined chip/flash size. - print('Chip Variant: %s' % boardconf.get_chip()) - print('Flash Size: %s' % boardconf.get_flash_size()) - print('') - - # Handle chip not existing. - if not boardconf.does_chip_exist(): - print('Chip directory does not exist: %s' % boardconf.get_chip_dir()) - print('Did you make a typo?') - return False - - # Handle specified flash size not existing. - if not boardconf.does_flash_size_exist(): - print('Flash size directory does not exist: %s' % boardconf.get_flash_size_dir()) - # Attempt to try fallback flash size. - if boardconf.does_flash_size_exist(fallback_flash_size): - print(f'Falling back to {fallback_flash_size} flash size.') - boardconf.override_flash_size(fallback_flash_size) - else: - print(f'Failed to fallback to {fallback_flash_size} flash size.') - print('Did you make a typo?') - return False - - # Print expected location of merge script and partitions file - print('Determined merge script: %s' % boardconf.get_merge_script()) - print('Potential partition files: %s' % str(boardconf.get_partitions_files(boardconf.get_flash_size()))) - print('') - - return True diff --git a/scripts/utils/dotenv.py b/scripts/utils/dotenv.py deleted file mode 100644 index 6f0ed63e..00000000 --- a/scripts/utils/dotenv.py +++ /dev/null @@ -1,141 +0,0 @@ -from pathlib import Path -from typing import Mapping - -LOGLEVEL_MAP: dict[str, tuple[int, str]] = { - 'none': (0, 'LOG_NONE'), - 'log_none': (0, 'LOG_NONE'), - 'error': (1, 'LOG_ERROR'), - 'log_error': (1, 'LOG_ERROR'), - 'warn': (2, 'LOG_WARN'), - 'log_warn': (2, 'LOG_WARN'), - 'warning': (2, 'LOG_WARN'), - 'log_warning': (2, 'LOG_WARN'), - 'info': (3, 'LOG_INFO'), - 'log_info': (3, 'LOG_INFO'), - 'debug': (4, 'LOG_DEBUG'), - 'log_debug': (4, 'LOG_DEBUG'), - 'verbose': (5, 'LOG_VERBOSE'), - 'log_verbose': (5, 'LOG_VERBOSE'), -} - - -def read_text_with_fallback( - path: str | Path, - encodings: list[str] | tuple[str, ...] | None = None, -) -> str: - """ - Read a text file using multiple attempted encodings in order. - - Handles BOM automatically via utf-8-sig and utf-16 encodings. - Raises a clean, descriptive error if all encodings fail. - """ - - if encodings is None: - # You can reorder these depending on what you expect most commonly. - encodings = [ - 'utf-8-sig', # handles UTF-8 BOM automatically - 'utf-16', # auto-detects LE/BE with BOM - 'utf-16-le', - 'utf-16-be', - 'latin-1', # fallback that never fails (for decoding) - ] - - path = Path(path) - raw = path.read_bytes() - - last_error: UnicodeError | None = None - - for encoding in encodings: - try: - text = raw.decode(encoding) - return text - except UnicodeError as e: - last_error = e - continue - - # If we reach here, all decoding attempts failed (only possible if latin-1 is not in encodings). - raise UnicodeDecodeError( - 'multi-encoding-reader', - raw, - 0, - len(raw), - f"failed to decode file '{path}' using encodings: {', '.join(encodings)}", - ) from last_error - - -class DotEnv: - def __read_dotenv(self, path: str | Path): - text_data = read_text_with_fallback(path) - - for line in text_data.splitlines(): - line = line.strip() - - # Skip empty lines and comments - if not line or line.startswith('#'): - continue - - # Ignore lines that don't contain '=' instead of raising - if '=' not in line: - continue - - key, value = line.split('=', 1) - key = key.strip() - value = value.strip() - - # Skip lines with empty keys - if not key: - continue - # Strip optional surrounding quotes (must match) - if len(value) >= 2: - if (value[0] == '"' and value[-1] == '"') or (value[0] == "'" and value[-1] == "'"): - value = value[1:-1] - - self.dotenv_vars[key] = value - - def __init__(self, path: str | Path, environment: str): - self.dotenv_vars: dict[str, str] = {} - - if isinstance(path, str): - path = Path(path) - - # Read .env file(s) into environment variables, start from the root of the drive and work our way down. - paths = list(path.parents[::-1]) + [path] - - # Get the environment specific name. (e.g. .env.develop / .env.production / .env.release) - env_specific_name = '.env.' + environment - - # Read the .env files. - for base in paths: - env_file = base / '.env' - if env_file.exists(): - self.__read_dotenv(env_file) - - env_file = base / env_specific_name - if env_file.exists(): - self.__read_dotenv(env_file) - - env_file = base / '.env.local' - if env_file.exists(): - self.__read_dotenv(env_file) - - def get_string(self, key: str) -> str | None: - return self.dotenv_vars.get(key) - - def get_all_prefixed(self, prefix: str) -> Mapping[str, str]: - return {k: v for k, v in self.dotenv_vars.items() if k.startswith(prefix)} - - def get_loglevel(self, key: str) -> int | None: - value = self.get_string(key) - if value is None: - return None - - normalized = value.strip().lower() - tup = LOGLEVEL_MAP.get(normalized) - if tup is None: - raise ValueError(f'Environment variable {key} ({value}) is not a valid log level.') - - return tup[0] - - -def read(workdir: str | Path, environment_name: str) -> DotEnv: - return DotEnv(workdir, environment=environment_name) diff --git a/scripts/utils/pioenv.py b/scripts/utils/pioenv.py deleted file mode 100644 index 2bc15ad3..00000000 --- a/scripts/utils/pioenv.py +++ /dev/null @@ -1,52 +0,0 @@ -from . import conv - -# def __parse_pio_file(): -# leftover_flags = [] -# for flag in build_flags: -# if flag.startswith('-D'): -# flag = flag[2::] -# if '=' in flag: -# (k, v) = flag.split('=', 1) - -# cpp_defines[k] = v -# else: -# cpp_defines[flag] = True -# else: -# leftover_flags.append(flag) - - -class PioEnv: - def __init__(self, env: dict): - self.env = env - - def get(self, key: str): - return self.env[key] # type: ignore - - def set(self, key: str, value): - self.env[key] = value # type: ignore - - def get_bool(self, key: str, default: bool | None = None) -> bool: - try: - return conv.to_bool(self.get(key)) - except Exception as ex: - if default != None: - return default - raise ValueError('Failed to get pio config boolean: %s' % key) - - # Get a string from the pio env, falling back to the default if provided. - def get_string(self, key: str, default: str | None = None) -> str: - try: - return conv.to_string(self.get(key)) - except Exception as ex: - if default != None: - return default - raise ValueError('Failed to get pio string: %s' % key) from ex - - # Get a string array from the pio env. - def get_string_array(self, key: str, default: list[str] | None = None) -> list[str]: - strings = self.get(key) or default - if strings == None or not isinstance(strings, list): - raise ValueError('Failed to get pio string array: %s' % key) - if any([not isinstance(v, str) for v in strings]): - raise ValueError('Failed to get pio string array (found non-string values): %s' % key) - return strings diff --git a/sdkconfig.defaults b/sdkconfig.defaults new file mode 100644 index 00000000..c1bd23d1 --- /dev/null +++ b/sdkconfig.defaults @@ -0,0 +1,70 @@ +# Required by Arduino-as-IDF-component +CONFIG_FREERTOS_HZ=1000 +CONFIG_AUTOSTART_ARDUINO=y + +# Keep main task large enough for Arduino setup() / existing init chain +CONFIG_ESP_MAIN_TASK_STACK_SIZE=16384 + +# WiFi/IP event handlers (WiFiManager, GatewayConnectionManager) now run directly +# on the default event loop task instead of Arduino's separate 4096-byte event +# task. They do flatbuffer serialization + websocket broadcast, so restore that +# headroom here (default is 2304). +CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=4096 + +# Arduino expects these selected via ENABLE_ARDUINO_DEPENDS, but be explicit +CONFIG_LWIP_SO_RCVBUF=y + +# Let application use exceptions-free C++ (matches build_flags -fno-exceptions) +CONFIG_COMPILER_CXX_EXCEPTIONS=n + +# Arduino NetworkClientSecure (pulled in transitively by WebSockets/WiFiClientSecure) +# refuses to compile its ssl_client.cpp body unless at least one PSK exchange is +# enabled — otherwise all ssl_* symbols become unresolved at link time. We don't +# actually use PSK, but we need MBEDTLS_KEY_EXCHANGE_SOME_PSK_ENABLED to be defined. +CONFIG_MBEDTLS_PSK_MODES=y +CONFIG_MBEDTLS_KEY_EXCHANGE_PSK=y + +# Captive portal serves HTTP + WebSocket from a single esp_http_server. WebSocket +# support is compiled out of esp_http_server by default; enable it or the httpd_ws_* +# symbols don't exist. +CONFIG_HTTPD_WS_SUPPORT=y + +# TLS server-cert verification uses the mbedTLS certificate bundle. We compile in +# our own curated trust store (certificates/cacert-merged.pem) instead of IDF's built-in +# Mozilla list, so DEFAULT_NONE + a custom bundle. esp_http_client and +# esp_websocket_client attach it via config.crt_bundle_attach = esp_crt_bundle_attach. +# +# cacert-merged.pem is generated by certificates/cert_bundle.py: the verbatim curl/Mozilla +# base (certificates/cacert-curl.pem, checksum-verified on download) followed by +# certificates/pinned_certs/*.pem. Pinned roots cover anchors the Mozilla base has retired +# but our infra still chains through (e.g. GlobalSign Root CA R1, which cross-signs GTS +# Root R4). `cert_bundle.py audit` (the pinned-cert-audit CI workflow) monitors their +# expiry and relevance. +CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=y +CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_NONE=y +CONFIG_MBEDTLS_CUSTOM_CERTIFICATE_BUNDLE=y +CONFIG_MBEDTLS_CUSTOM_CERTIFICATE_BUNDLE_PATH="certificates/cacert-merged.pem" + +# Partition table — OpenShock uses a custom one; existing partitions stay valid +# (leave PARTITION_TABLE_* at IDF defaults; per-env sdkconfigs can override) + +# Arduino selective compilation — every CONFIG_ARDUINO_SELECTIVE_ defaults +# to y, so turning on SELECTIVE_COMPILATION and then DISABLING the heavy library +# wrappers we don't use drops their managed components (esp-sr + esp-dsp, +# esp_rainmaker + insights + diag + cbor + qrcode, esp-zigbee/zboss, esp_modem, +# matter, ...) out of the build closure entirely — a large build-time and flash +# saving. None of these are referenced by OpenShock. Re-enable one (=y / delete +# its line) if new code ever needs it. +CONFIG_ARDUINO_SELECTIVE_COMPILATION=y +CONFIG_ARDUINO_SELECTIVE_BLE=n +CONFIG_ARDUINO_SELECTIVE_BluetoothSerial=n +CONFIG_ARDUINO_SELECTIVE_SimpleBLE=n +CONFIG_ARDUINO_SELECTIVE_RainMaker=n +CONFIG_ARDUINO_SELECTIVE_Insights=n +CONFIG_ARDUINO_SELECTIVE_Zigbee=n +CONFIG_ARDUINO_SELECTIVE_OpenThread=n +CONFIG_ARDUINO_SELECTIVE_Matter=n +CONFIG_ARDUINO_SELECTIVE_ESP_SR=n +CONFIG_ARDUINO_SELECTIVE_PPP=n +CONFIG_ARDUINO_SELECTIVE_Ethernet=n +CONFIG_ARDUINO_SELECTIVE_WiFiProv=n diff --git a/src/CompatibilityChecks.cpp b/src/CompatibilityChecks.cpp deleted file mode 100644 index 4e77a35b..00000000 --- a/src/CompatibilityChecks.cpp +++ /dev/null @@ -1,16 +0,0 @@ -#include "Chipset.h" -#include "Common.h" - -const bool kIsValidOrUndefinedRfTxPin = OpenShock::IsValidOutputPin(OPENSHOCK_RF_TX_GPIO) || OPENSHOCK_RF_TX_GPIO == OPENSHOCK_GPIO_INVALID; -static_assert(kIsValidOrUndefinedRfTxPin, "OPENSHOCK_RF_TX_GPIO is not a valid output GPIO, and is not declared as bypassed by board specific definitions, refusing to compile"); - -const bool kIsValidOrUndefinedEStopPin = OpenShock::IsValidInputPin(OPENSHOCK_ESTOP_PIN) || OPENSHOCK_ESTOP_PIN == OPENSHOCK_GPIO_INVALID; -static_assert(kIsValidOrUndefinedEStopPin, "OPENSHOCK_ESTOP_PIN is not a valid input GPIO, and is not declared as bypassed by board specific definitions, refusing to compile"); - -#ifdef OPENSHOCK_LED_GPIO -static_assert(OpenShock::IsValidOutputPin(OPENSHOCK_LED_GPIO), "OPENSHOCK_LED_GPIO is not a valid output GPIO, and is not declared as bypassed by board specific definitions, refusing to compile"); -#endif - -#ifdef OPENSHOCK_LED_WS2812B -static_assert(OpenShock::IsValidOutputPin(OPENSHOCK_LED_WS2812B), "OPENSHOCK_LED_WS2812B is not a valid output GPIO, and is not declared as bypassed by board specific definitions, refusing to compile"); -#endif diff --git a/src/GatewayClient.cpp b/src/GatewayClient.cpp deleted file mode 100644 index 4eca8c26..00000000 --- a/src/GatewayClient.cpp +++ /dev/null @@ -1,210 +0,0 @@ -#include "GatewayClient.h" - -const char* const TAG = "GatewayClient"; - -#include "Common.h" -#include "config/Config.h" -#include "Core.h" -#include "events/Events.h" -#include "Logging.h" -#include "message_handlers/WebSocket.h" -#include "OtaUpdateManager.h" -#include "serialization/WSGateway.h" -#include "visual/VisualStateManager.h" - -using namespace OpenShock; - -const int64_t GATEWAY_PING_TIMEOUT = 90'000; - -static bool s_bootStatusSent = false; - -GatewayClient::GatewayClient(const std::string& authToken) - : m_webSocket() - , m_state(GatewayClientState::Disconnected) - , m_lastPingTimestamp(0) -{ - OS_LOGD(TAG, "Creating GatewayClient"); - - std::string headers = "Firmware-Version: " OPENSHOCK_FW_VERSION "\r\n" - "Device-Token: " - + authToken; - - m_webSocket.setUserAgent(OpenShock::Constants::FW_USERAGENT); - m_webSocket.setExtraHeaders(headers.c_str()); - m_webSocket.onEvent(std::bind(&GatewayClient::_handleEvent, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); -} -GatewayClient::~GatewayClient() -{ - _setState(GatewayClientState::Disconnected); - - OS_LOGD(TAG, "Destroying GatewayClient"); - m_webSocket.disconnect(); -} - -void GatewayClient::connect(const std::string& host, uint16_t port, const std::string& path) -{ - if (m_state != GatewayClientState::Disconnected) { - return; - } - - _setState(GatewayClientState::Connecting); - -// -// ###### ######## ###### ## ## ######## #### ######## ## ## ######## #### ###### ## ## -// ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## -// ## ## ## ## ## ## ## ## ## #### ## ## ## ## ## ## -// ###### ###### ## ## ## ######## ## ## ## ######## ## ###### ##### -// ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## -// ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## ## -// ###### ######## ###### ####### ## ## #### ## ## ## ## #### ###### ## ## -// -// TODO: Implement certificate verification -// -#warning SSL certificate verification is currently not implemented, by RFC definition this is a security risk, and allows for MITM attacks, but the realistic risk is low - - m_webSocket.beginSSL(host.c_str(), port, path.c_str()); - OS_LOGW(TAG, "WEBSOCKET CONNECTION BY RFC DEFINITION IS INSECURE, remote endpoint can not be verified due to lack of CA verification support, theoretically this is a security risk and allows for MITM attacks, but the realistic risk is low"); -} - -void GatewayClient::disconnect() -{ - if (m_state != GatewayClientState::Connected) { - return; - } - _setState(GatewayClientState::Disconnecting); - m_webSocket.disconnect(); -} - -bool GatewayClient::sendMessageTXT(std::string_view data) -{ - if (m_state != GatewayClientState::Connected) { - return false; - } - - return m_webSocket.sendTXT(data.data(), data.length()); -} - -bool GatewayClient::sendMessageBIN(tcb::span data) -{ - if (m_state != GatewayClientState::Connected) { - return false; - } - - return m_webSocket.sendBIN(data.data(), data.size()); -} - -void GatewayClient::markPingReceived() -{ - m_lastPingTimestamp = OpenShock::millis(); -} - -bool GatewayClient::loop() -{ - if (m_state == GatewayClientState::Disconnected) { - return false; - } - - m_webSocket.loop(); - - // We are still in the process of connecting or disconnecting - if (m_state != GatewayClientState::Connected) { - // return true to indicate that we are still busy - return true; - } - - if (m_lastPingTimestamp != 0 && (OpenShock::millis() - m_lastPingTimestamp) > GATEWAY_PING_TIMEOUT) { - OS_LOGW(TAG, "No ping received from gateway for %lld ms, forcing reconnect", GATEWAY_PING_TIMEOUT); - m_webSocket.disconnect(); - _setState(GatewayClientState::Disconnected); - return false; - } - - return true; -} - -void GatewayClient::_setState(GatewayClientState state) -{ - if (m_state == state) { - return; - } - - m_state = state; - - ESP_ERROR_CHECK(esp_event_post(OPENSHOCK_EVENTS, OPENSHOCK_EVENT_GATEWAY_CLIENT_STATE_CHANGED, &m_state, sizeof(m_state), portMAX_DELAY)); -} - -void GatewayClient::_sendBootStatus() -{ - if (s_bootStatusSent) return; - - OS_LOGV(TAG, "Sending Gateway boot status message"); - - int32_t updateId; - if (!Config::GetOtaUpdateId(updateId)) { - OS_LOGE(TAG, "Failed to get OTA update ID"); - return; - } - - OpenShock::OtaUpdateStep updateStep; - if (!Config::GetOtaUpdateStep(updateStep)) { - OS_LOGE(TAG, "Failed to get OTA firmware boot type"); - return; - } - - using namespace std::string_view_literals; - - OpenShock::SemVer version; - if (!OpenShock::TryParseSemVer(OPENSHOCK_FW_VERSION ""sv, version)) { - OS_LOGE(TAG, "Failed to parse firmware version"); - return; - } - - s_bootStatusSent = Serialization::Gateway::SerializeBootStatusMessage(updateId, OtaUpdateManager::GetFirmwareBootType(), [this](tcb::span data) { return m_webSocket.sendBIN(data.data(), data.size()); }); - - if (s_bootStatusSent && updateStep != OpenShock::OtaUpdateStep::None) { - if (!Config::SetOtaUpdateStep(OpenShock::OtaUpdateStep::None)) { - OS_LOGE(TAG, "Failed to reset firmware boot type to normal"); - } - } -} - -void GatewayClient::_handleEvent(WStype_t type, uint8_t* payload, std::size_t length) -{ - switch (type) { - case WStype_DISCONNECTED: - _setState(GatewayClientState::Disconnected); - break; - case WStype_CONNECTED: - m_lastPingTimestamp = 0; - _setState(GatewayClientState::Connected); - _sendBootStatus(); - break; - case WStype_TEXT: - OS_LOGW(TAG, "Received text from API, JSON parsing is not supported anymore :D"); - break; - case WStype_ERROR: - OS_LOGE(TAG, "Received error from API"); - break; - case WStype_FRAGMENT_TEXT_START: - OS_LOGD(TAG, "Received fragment text start from API"); - break; - case WStype_FRAGMENT: - OS_LOGD(TAG, "Received fragment from API"); - break; - case WStype_FRAGMENT_FIN: - OS_LOGD(TAG, "Received fragment fin from API"); - break; - case WStype_BIN: - MessageHandlers::WebSocket::HandleGatewayBinary(tcb::span(payload, length)); - break; - case WStype_FRAGMENT_BIN_START: - OS_LOGE(TAG, "Received binary fragment start from API, this is not supported!"); - break; - case WStype_PING: - case WStype_PONG: - break; - default: - OS_LOGE(TAG, "Received unknown event from API"); - break; - } -} diff --git a/src/WebSocketDeFragger.cpp b/src/WebSocketDeFragger.cpp deleted file mode 100644 index ba6cb227..00000000 --- a/src/WebSocketDeFragger.cpp +++ /dev/null @@ -1,125 +0,0 @@ -#include "WebSocketDeFragger.h" - -const char* const TAG = "WebSocketDeFragger"; - -#include "Logging.h" - -#include - -using namespace OpenShock; - -WebSocketDeFragger::WebSocketDeFragger(EventCallback callback) - : m_messages() - , m_callback(callback) -{ -} - -WebSocketDeFragger::~WebSocketDeFragger() -{ - clear(); -} - -void WebSocketDeFragger::handler(uint8_t socketId, WStype_t type, const uint8_t* payload, std::size_t length) -{ - switch (type) { - case WStype_FRAGMENT_BIN_START: - start(socketId, WebSocketMessageType::Binary, payload, length); - return; - case WStype_FRAGMENT_TEXT_START: - start(socketId, WebSocketMessageType::Text, payload, length); - return; - case WStype_FRAGMENT: - append(socketId, payload, length); - return; - case WStype_FRAGMENT_FIN: - finish(socketId, payload, length); - return; - [[likely]] default: - clear(socketId); - break; - } - - WebSocketMessageType messageType; - switch (type) { - case WStype_DISCONNECTED: - messageType = WebSocketMessageType::Disconnected; - break; - case WStype_CONNECTED: - messageType = WebSocketMessageType::Connected; - break; - case WStype_TEXT: - messageType = WebSocketMessageType::Text; - break; - [[likely]] case WStype_BIN: - messageType = WebSocketMessageType::Binary; - break; - case WStype_PING: - messageType = WebSocketMessageType::Ping; - break; - case WStype_PONG: - messageType = WebSocketMessageType::Pong; - break; - [[unlikely]] default: - OS_LOGE(TAG, "WebSocket client #%u error %u: %s", socketId, length, reinterpret_cast(payload)); - return; - } - - m_callback(socketId, messageType, tcb::span(payload, length)); -} - -void WebSocketDeFragger::onEvent(const EventCallback& callback) -{ - m_callback = callback; -} - -void WebSocketDeFragger::clear(uint8_t socketId) -{ - auto it = m_messages.find(socketId); - if (it != m_messages.end()) { - it->second.data.clear(); - m_messages.erase(it); - } -} - -void WebSocketDeFragger::clear() -{ - m_messages.clear(); -} - -void WebSocketDeFragger::start(uint8_t socketId, WebSocketMessageType type, const uint8_t* data, uint32_t length) -{ - auto it = m_messages.find(socketId); - if (it != m_messages.end()) { - it->second.data.assign(data, length); - it->second.type = type; - return; - } - - m_messages.insert(std::make_pair(socketId, Message {.data = TinyVec(data, length), .type = type})); -} - -void WebSocketDeFragger::append(uint8_t socketId, const uint8_t* data, uint32_t length) -{ - auto it = m_messages.find(socketId); - if (it == m_messages.end()) { - return; - } - - it->second.data.append(data, length); -} - -void WebSocketDeFragger::finish(uint8_t socketId, const uint8_t* data, uint32_t length) -{ - auto it = m_messages.find(socketId); - if (it == m_messages.end()) { - return; - } - - auto& message = it->second; - message.data.append(data, length); - - m_callback(socketId, message.type, message.data); - - message.data.clear(); - m_messages.erase(it); -} diff --git a/src/captiveportal/CaptivePortalInstance.cpp b/src/captiveportal/CaptivePortalInstance.cpp deleted file mode 100644 index d6e3a390..00000000 --- a/src/captiveportal/CaptivePortalInstance.cpp +++ /dev/null @@ -1,595 +0,0 @@ -#include - -#include "captiveportal/CaptivePortalInstance.h" - -const char* const TAG = "CaptivePortalInstance"; - -#include "captiveportal/Manager.h" -#include "captiveportal/RFC8908Handler.h" -#include "Chipset.h" -#include "CommandHandler.h" -#include "config/Config.h" -#include "estop/EStopManager.h" -#include "GatewayConnectionManager.h" -#include "http/ContentTypes.h" -#include "Logging.h" -#include "RateLimiter.h" -#include "message_handlers/WebSocket.h" -#include "OtaUpdateChannel.h" -#include "serialization/WSLocal.h" -#include "util/FnProxy.h" -#include "util/HexUtils.h" -#include "util/PartitionUtils.h" -#include "util/TaskUtils.h" -#include "wifi/WiFiManager.h" -#include "wifi/WiFiScanManager.h" - -#include "serialization/_fbs/HubToLocalMessage_generated.h" - -#include -#include - -const uint16_t HTTP_PORT = 80; -const uint16_t WEBSOCKET_PORT = 81; -const uint16_t DNS_PORT = 53; -const uint32_t WEBSOCKET_PING_INTERVAL = 10'000; -const uint32_t WEBSOCKET_PING_TIMEOUT = 1000; -const uint8_t WEBSOCKET_PING_RETRIES = 3; -const uint32_t WEBSOCKET_UPDATE_INTERVAL = 10; // 10ms / 100Hz - -static const char* const JSON_ERR_INTERNAL = "{\"error\":\"InternalError\"}"; -static const char* const JSON_ERR_MISSING_PARAM = "{\"error\":\"MissingParam\"}"; -static const char* const JSON_ERR_INVALID_PIN = "{\"error\":\"InvalidPin\"}"; -static const char* const JSON_ERR_MISSING_SSID = "{\"error\":\"MissingSsid\"}"; -static const char* const JSON_ERR_INVALID_SSID = "{\"error\":\"InvalidSsid\"}"; -static const char* const JSON_ERR_PASSWORD_SHORT = "{\"error\":\"PasswordTooShort\"}"; -static const char* const JSON_ERR_PASSWORD_LONG = "{\"error\":\"PasswordTooLong\"}"; -static const char* const JSON_ERR_CODE_REQUIRED = "{\"error\":\"CodeRequired\"}"; -static const char* const JSON_ERR_INVALID_CHANNEL = "{\"error\":\"InvalidChannel\"}"; -static const char* const JSON_ERR_RATE_LIMITED = "{\"error\":\"RateLimited\"}"; - -static OpenShock::RateLimiter& getAccountLinkRateLimiter() -{ - static OpenShock::RateLimiter* rl = nullptr; - if (rl == nullptr) { - rl = new OpenShock::RateLimiter(); - rl->addLimit(60'000, 5); // 5 attempts per minute - rl->addLimit(300'000, 10); // 10 attempts per 5 minutes - } - return *rl; -} - -using namespace OpenShock; - -static const esp_partition_t* getStaticPartition() -{ - const esp_partition_t* partition = esp_partition_find_first(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_SPIFFS, "static0"); - if (partition != nullptr) { - return partition; - } - - partition = esp_partition_find_first(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_SPIFFS, "static1"); - if (partition != nullptr) { - return partition; - } - - return nullptr; -} - -static const char* getPartitionHash() -{ - const esp_partition_t* partition = getStaticPartition(); - if (partition == nullptr) { - return nullptr; - } - - static char hash[65]; - if (!OpenShock::TryGetPartitionHash(partition, hash)) { - return nullptr; - } - - return hash; -} - -CaptivePortal::CaptivePortalInstance::CaptivePortalInstance() - : m_webServer(HTTP_PORT) - , m_socketServer(WEBSOCKET_PORT, "/ws", "flatbuffers") // Sec-WebSocket-Protocol = flatbuffers - , m_socketDeFragger(std::bind(&CaptivePortalInstance::handleWebSocketEvent, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)) - , m_fileSystem() - , m_dnsServer() - , m_taskHandle(nullptr) -{ - m_socketServer.onEvent(std::bind(&WebSocketDeFragger::handler, &m_socketDeFragger, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3, std::placeholders::_4)); - m_socketServer.begin(); - - m_socketServer.enableHeartbeat(WEBSOCKET_PING_INTERVAL, WEBSOCKET_PING_TIMEOUT, WEBSOCKET_PING_RETRIES); - - OS_LOGI(TAG, "Setting up DNS server"); - bool dnsStarted = m_dnsServer.start(DNS_PORT, "*", WiFi.softAPIP()); - - if (!dnsStarted) { - OS_LOGE(TAG, "Failed to start DNS server!"); - } - - bool fsOk = true; - - // Get the hash of the filesystem - const char* fsHash = getPartitionHash(); - if (fsHash == nullptr) { - OS_LOGE(TAG, "Failed to get filesystem hash"); - fsOk = false; - } - - if (fsOk) { - // Mounting LittleFS - if (!m_fileSystem.begin(false, "/static", 10U, "static0")) { - OS_LOGE(TAG, "Failed to mount LittleFS"); - fsOk = false; - } else { - fsOk = m_fileSystem.exists("/www/index.html.gz"); - } - } - - if (fsOk) { - OS_LOGI(TAG, "Serving files from LittleFS"); - OS_LOGI(TAG, "Filesystem hash: %s", fsHash); - - m_webServer.addHandler(new RFC8908Handler()); - - // API endpoints — must be registered before serveStatic so they take priority - m_webServer.on("/api/board", HTTP_GET, [](AsyncWebServerRequest* request) { - bool hasPredefinedPins = OPENSHOCK_RF_TX_GPIO != OPENSHOCK_GPIO_INVALID; - request->send(200, HTTP::ContentType::JSON, hasPredefinedPins ? "{\"has_predefined_pins\":true}" : "{\"has_predefined_pins\":false}"); - }); - - m_webServer.on("/api/portal/close", HTTP_POST, [](AsyncWebServerRequest* request) { - CaptivePortal::SetUserDone(); - request->send(200); - }); - - m_webServer.on("/api/wifi/scan", HTTP_POST, [](AsyncWebServerRequest* request) { - bool run = true; - if (request->hasParam("run")) { - run = request->getParam("run")->value().toInt() != 0; - } - if (run) { - WiFiScanManager::StartScan(); - } else { - WiFiScanManager::AbortScan(); - } - request->send(200); - }); - - m_webServer.on("/api/wifi/networks", HTTP_DELETE, [](AsyncWebServerRequest* request) { - if (!request->hasParam("ssid")) { - request->send(400, HTTP::ContentType::JSON, JSON_ERR_MISSING_SSID); - return; - } - String ssid = request->getParam("ssid")->value(); - if (!WiFiManager::Forget(ssid.c_str())) { - request->send(500, HTTP::ContentType::JSON, JSON_ERR_INTERNAL); - return; - } - request->send(200); - }); - - m_webServer.on("/api/account/link", HTTP_POST, [](AsyncWebServerRequest* request) { - if (!getAccountLinkRateLimiter().tryRequest()) { - request->send(429, HTTP::ContentType::JSON, JSON_ERR_RATE_LIMITED); - return; - } - if (!request->hasParam("code")) { - request->send(400, HTTP::ContentType::JSON, JSON_ERR_CODE_REQUIRED); - return; - } - String code = request->getParam("code")->value(); - auto result = GatewayConnectionManager::Link(std::string_view(code.c_str(), code.length())); - using ResultCode = OpenShock::AccountLinkResultCode; - if (result == ResultCode::Success) { - request->send(200); - return; - } - const char* error; - switch (result) { - case ResultCode::CodeRequired: - error = "CodeRequired"; - break; - case ResultCode::InvalidCodeLength: - error = "InvalidCodeLength"; - break; - case ResultCode::NoInternetConnection: - error = "NoInternetConnection"; - break; - case ResultCode::InvalidCode: - error = "InvalidCode"; - break; - case ResultCode::RateLimited: - error = "RateLimited"; - break; - case ResultCode::RequestFailed: - error = "RequestFailed"; - break; - case ResultCode::RequestTimedOut: - error = "RequestTimedOut"; - break; - case ResultCode::ServerError: - error = "ServerError"; - break; - case ResultCode::InvalidResponse: - error = "InvalidResponse"; - break; - case ResultCode::ConfigSaveFailed: - error = "ConfigSaveFailed"; - break; - default: - error = "InternalError"; - break; - } - cJSON* root = cJSON_CreateObject(); - cJSON_AddStringToObject(root, "error", error); - char* json = cJSON_PrintUnformatted(root); - cJSON_Delete(root); - if (json == nullptr) { - request->send(500, HTTP::ContentType::JSON, JSON_ERR_INTERNAL); - } else { - request->send(400, HTTP::ContentType::JSON, json); - cJSON_free(json); - } - }); - - m_webServer.on("/api/account", HTTP_DELETE, [](AsyncWebServerRequest* request) { - GatewayConnectionManager::UnLink(); - request->send(200); - }); - - m_webServer.on("/api/config/rf/pin", HTTP_PUT, [](AsyncWebServerRequest* request) { - if (!request->hasParam("pin")) { - request->send(400, HTTP::ContentType::JSON, JSON_ERR_INVALID_PIN); - return; - } - int pin = request->getParam("pin")->value().toInt(); - auto result = CommandHandler::SetRfTxPin(static_cast(pin)); - using ResultCode = OpenShock::SetGPIOResultCode; - if (result != ResultCode::Success) { - request->send(400, HTTP::ContentType::JSON, (result == ResultCode::InvalidPin) ? JSON_ERR_INVALID_PIN : JSON_ERR_INTERNAL); - return; - } - cJSON* root = cJSON_CreateObject(); - cJSON_AddNumberToObject(root, "pin", pin); - char* json = cJSON_PrintUnformatted(root); - cJSON_Delete(root); - if (json == nullptr) { - request->send(500, HTTP::ContentType::JSON, JSON_ERR_INTERNAL); - } else { - request->send(200, HTTP::ContentType::JSON, json); - cJSON_free(json); - } - }); - - m_webServer.on("/api/config/estop/pin", HTTP_PUT, [](AsyncWebServerRequest* request) { - if (!request->hasParam("pin")) { - request->send(400, HTTP::ContentType::JSON, JSON_ERR_INVALID_PIN); - return; - } - int8_t pin = static_cast(request->getParam("pin")->value().toInt()); - if (!IsValidInputPin(pin)) { - request->send(400, HTTP::ContentType::JSON, JSON_ERR_INVALID_PIN); - return; - } - if (!EStopManager::SetEStopPin(static_cast(pin)) || !Config::SetEStopGpioPin(static_cast(pin))) { - request->send(500, HTTP::ContentType::JSON, JSON_ERR_INTERNAL); - return; - } - cJSON* root = cJSON_CreateObject(); - cJSON_AddNumberToObject(root, "pin", pin); - char* json = cJSON_PrintUnformatted(root); - cJSON_Delete(root); - if (json == nullptr) { - request->send(500, HTTP::ContentType::JSON, JSON_ERR_INTERNAL); - } else { - request->send(200, HTTP::ContentType::JSON, json); - cJSON_free(json); - } - }); - - m_webServer.on("/api/config/estop/enabled", HTTP_PUT, [](AsyncWebServerRequest* request) { - if (!request->hasParam("enabled")) { - request->send(400, HTTP::ContentType::JSON, JSON_ERR_INTERNAL); - return; - } - bool enabled = request->getParam("enabled")->value().toInt() != 0; - bool success = EStopManager::SetEStopEnabled(enabled) && Config::SetEStopEnabled(enabled); - if (success) { - request->send(200); - } else { - request->send(500, HTTP::ContentType::JSON, JSON_ERR_INTERNAL); - } - }); - - m_webServer.on("/api/wifi/networks", HTTP_POST, [](AsyncWebServerRequest* request) { - if (!request->hasParam("ssid", true)) { - request->send(400, HTTP::ContentType::JSON, JSON_ERR_MISSING_SSID); - return; - } - String ssid = request->getParam("ssid", true)->value(); - if (ssid.length() == 0 || ssid.length() > 31) { - request->send(400, HTTP::ContentType::JSON, JSON_ERR_INVALID_SSID); - return; - } - String password; - if (request->hasParam("password", true)) { - password = request->getParam("password", true)->value(); - if (password.length() > 0 && password.length() < 8) { - request->send(400, HTTP::ContentType::JSON, JSON_ERR_PASSWORD_SHORT); - return; - } - if (password.length() > 63) { - request->send(400, HTTP::ContentType::JSON, JSON_ERR_PASSWORD_LONG); - return; - } - } - bool connect = true; - if (request->hasParam("connect", true)) { - connect = request->getParam("connect", true)->value().toInt() != 0; - } - wifi_auth_mode_t authMode = WIFI_AUTH_MAX; - if (request->hasParam("security", true)) { - int sec = request->getParam("security", true)->value().toInt(); - if (sec >= 0 && sec <= static_cast(WIFI_AUTH_MAX)) { - authMode = static_cast(sec); - } - } - if (!WiFiManager::Save(ssid.c_str(), std::string_view(password.c_str(), password.length()), connect, authMode)) { - request->send(500, HTTP::ContentType::JSON, JSON_ERR_INTERNAL); - return; - } - request->send(200); - }); - - m_webServer.on("/api/wifi/connect", HTTP_POST, [](AsyncWebServerRequest* request) { - if (!request->hasParam("ssid", true)) { - request->send(400, HTTP::ContentType::JSON, JSON_ERR_MISSING_SSID); - return; - } - String ssid = request->getParam("ssid", true)->value(); - if (!WiFiManager::Connect(ssid.c_str())) { - request->send(500, HTTP::ContentType::JSON, JSON_ERR_INTERNAL); - return; - } - request->send(200); - }); - - m_webServer.on("/api/wifi/disconnect", HTTP_POST, [](AsyncWebServerRequest* request) { - WiFiManager::Disconnect(); - request->send(200); - }); - - m_webServer.on("/api/ota/enabled", HTTP_PUT, [](AsyncWebServerRequest* request) { - if (!request->hasParam("enabled")) { - request->send(400, HTTP::ContentType::JSON, JSON_ERR_MISSING_PARAM); - return; - } - bool enabled = request->getParam("enabled")->value().toInt() != 0; - Config::OtaUpdateConfig cfg; - if (!Config::GetOtaUpdateConfig(cfg)) { - request->send(500, HTTP::ContentType::JSON, JSON_ERR_INTERNAL); - return; - } - cfg.isEnabled = enabled; - if (!Config::SetOtaUpdateConfig(cfg)) { - request->send(500, HTTP::ContentType::JSON, JSON_ERR_INTERNAL); - return; - } - request->send(200); - }); - - m_webServer.on("/api/ota/domain", HTTP_PUT, [](AsyncWebServerRequest* request) { - if (!request->hasParam("domain")) { - request->send(400, HTTP::ContentType::JSON, JSON_ERR_MISSING_PARAM); - return; - } - String domain = request->getParam("domain")->value(); - Config::OtaUpdateConfig cfg; - if (!Config::GetOtaUpdateConfig(cfg)) { - request->send(500, HTTP::ContentType::JSON, JSON_ERR_INTERNAL); - return; - } - cfg.cdnDomain = std::string(domain.c_str(), domain.length()); - if (!Config::SetOtaUpdateConfig(cfg)) { - request->send(500, HTTP::ContentType::JSON, JSON_ERR_INTERNAL); - return; - } - request->send(200); - }); - - m_webServer.on("/api/ota/channel", HTTP_PUT, [](AsyncWebServerRequest* request) { - if (!request->hasParam("channel")) { - request->send(400, HTTP::ContentType::JSON, JSON_ERR_MISSING_PARAM); - return; - } - String channelStr = request->getParam("channel")->value(); - OtaUpdateChannel channel; - if (!TryParseOtaUpdateChannel(channel, channelStr.c_str())) { - request->send(400, HTTP::ContentType::JSON, JSON_ERR_INVALID_CHANNEL); - return; - } - Config::OtaUpdateConfig cfg; - if (!Config::GetOtaUpdateConfig(cfg)) { - request->send(500, HTTP::ContentType::JSON, JSON_ERR_INTERNAL); - return; - } - cfg.updateChannel = channel; - if (!Config::SetOtaUpdateConfig(cfg)) { - request->send(500, HTTP::ContentType::JSON, JSON_ERR_INTERNAL); - return; - } - request->send(200); - }); - - m_webServer.on("/api/ota/check-interval", HTTP_PUT, [](AsyncWebServerRequest* request) { - if (!request->hasParam("interval")) { - request->send(400, HTTP::ContentType::JSON, JSON_ERR_MISSING_PARAM); - return; - } - uint16_t interval = static_cast(request->getParam("interval")->value().toInt()); - Config::OtaUpdateConfig cfg; - if (!Config::GetOtaUpdateConfig(cfg)) { - request->send(500, HTTP::ContentType::JSON, JSON_ERR_INTERNAL); - return; - } - cfg.checkInterval = interval; - if (!Config::SetOtaUpdateConfig(cfg)) { - request->send(500, HTTP::ContentType::JSON, JSON_ERR_INTERNAL); - return; - } - request->send(200); - }); - - m_webServer.on("/api/ota/allow-backend-management", HTTP_PUT, [](AsyncWebServerRequest* request) { - if (!request->hasParam("allow")) { - request->send(400, HTTP::ContentType::JSON, JSON_ERR_MISSING_PARAM); - return; - } - bool allow = request->getParam("allow")->value().toInt() != 0; - Config::OtaUpdateConfig cfg; - if (!Config::GetOtaUpdateConfig(cfg)) { - request->send(500, HTTP::ContentType::JSON, JSON_ERR_INTERNAL); - return; - } - cfg.allowBackendManagement = allow; - if (!Config::SetOtaUpdateConfig(cfg)) { - request->send(500, HTTP::ContentType::JSON, JSON_ERR_INTERNAL); - return; - } - request->send(200); - }); - - m_webServer.on("/api/ota/require-manual-approval", HTTP_PUT, [](AsyncWebServerRequest* request) { - if (!request->hasParam("require")) { - request->send(400, HTTP::ContentType::JSON, JSON_ERR_MISSING_PARAM); - return; - } - bool require = request->getParam("require")->value().toInt() != 0; - Config::OtaUpdateConfig cfg; - if (!Config::GetOtaUpdateConfig(cfg)) { - request->send(500, HTTP::ContentType::JSON, JSON_ERR_INTERNAL); - return; - } - cfg.requireManualApproval = require; - if (!Config::SetOtaUpdateConfig(cfg)) { - request->send(500, HTTP::ContentType::JSON, JSON_ERR_INTERNAL); - return; - } - request->send(200); - }); - - m_webServer.on("/api/ota/check", HTTP_POST, [](AsyncWebServerRequest* request) { - // TODO: trigger OTA check - OtaUpdateManager does not yet expose a CheckForUpdates method - request->send(200); - }); - - // Serving the captive portal files from LittleFS - m_webServer.serveStatic("/", m_fileSystem, "/www/", "max-age=3600").setDefaultFile("index.html").setSharedEtag(fsHash); - - m_webServer.onNotFound(&RFC8908Handler::CatchAll); - - } else { - OS_LOGE(TAG, "/www/index.html or hash files not found, serving error page"); - - m_webServer.onNotFound([](AsyncWebServerRequest* request) { - request->send( - 200, - HTTP::ContentType::TextPlain, - // Raw string literal (1+ to remove the first newline) - 1 + R"( -You probably forgot to upload the Filesystem with PlatformIO! -Go to PlatformIO -> Platform -> Upload Filesystem Image! -If this happened with a file we provided or you just need help, come to the Discord! - -discord.gg/OpenShock -)" - ); - }); - } - - m_webServer.begin(); - - if (fsOk) { - m_stopRequested.store(false, std::memory_order_relaxed); - if (TaskUtils::TaskCreateExpensive(Util::FnProxy<&CaptivePortal::CaptivePortalInstance::task>, TAG, 8192, this, 1, &m_taskHandle) != pdPASS) { // PROFILED: 4-6KB stack usage - OS_LOGE(TAG, "Failed to create task"); - } - } -} - -CaptivePortal::CaptivePortalInstance::~CaptivePortalInstance() -{ - m_stopRequested.store(true, std::memory_order_relaxed); - if (m_taskHandle != nullptr) { - TaskUtils::StopTask(m_taskHandle, TAG, "CaptivePortal task"); - m_taskHandle = nullptr; - } - m_webServer.end(); - m_socketServer.close(); - m_fileSystem.end(); - m_dnsServer.stop(); -} - -void CaptivePortal::CaptivePortalInstance::task() -{ - while (!m_stopRequested.load(std::memory_order_relaxed)) { - m_socketServer.loop(); - m_dnsServer.processNextRequest(); - vTaskDelay(pdMS_TO_TICKS(WEBSOCKET_UPDATE_INTERVAL)); - } - vTaskDelete(nullptr); -} - -void CaptivePortal::CaptivePortalInstance::handleWebSocketClientConnected(uint8_t socketId) -{ - OS_LOGD(TAG, "WebSocket client #%u connected from %s", socketId, m_socketServer.remoteIP(socketId).toString().c_str()); - - WiFiNetwork connectedNetwork; - WiFiNetwork* connectedNetworkPtr = nullptr; - if (WiFiManager::GetConnectedNetwork(connectedNetwork)) { - connectedNetworkPtr = &connectedNetwork; - } - - Serialization::Local::SerializeReadyMessage(connectedNetworkPtr, GatewayConnectionManager::IsLinked(), std::bind(&CaptivePortal::CaptivePortalInstance::sendMessageBIN, this, socketId, std::placeholders::_1)); - - // Send all previously scanned wifi networks - auto networks = OpenShock::WiFiManager::GetDiscoveredWiFiNetworks(); - - Serialization::Local::SerializeWiFiNetworksEvent(Serialization::Types::WifiNetworkEventType::Discovered, networks, std::bind(&CaptivePortal::CaptivePortalInstance::sendMessageBIN, this, socketId, std::placeholders::_1)); -} - -void CaptivePortal::CaptivePortalInstance::handleWebSocketClientDisconnected(uint8_t socketId) -{ - OS_LOGD(TAG, "WebSocket client #%u disconnected", socketId); -} - -void CaptivePortal::CaptivePortalInstance::handleWebSocketEvent(uint8_t socketId, WebSocketMessageType type, tcb::span payload) -{ - switch (type) { - case WebSocketMessageType::Connected: - handleWebSocketClientConnected(socketId); - break; - case WebSocketMessageType::Disconnected: - handleWebSocketClientDisconnected(socketId); - break; - case WebSocketMessageType::Text: - OS_LOGE(TAG, "Message type is not supported"); - break; - case WebSocketMessageType::Binary: - MessageHandlers::WebSocket::HandleLocalBinary(socketId, payload); - break; - case WebSocketMessageType::Ping: - case WebSocketMessageType::Pong: - // Do nothing - break; - default: - m_socketDeFragger.clear(); - OS_LOGE(TAG, "Unknown WebSocket event type: %u", type); - break; - } -} diff --git a/src/captiveportal/RFC8908Handler.cpp b/src/captiveportal/RFC8908Handler.cpp deleted file mode 100644 index ed275436..00000000 --- a/src/captiveportal/RFC8908Handler.cpp +++ /dev/null @@ -1,108 +0,0 @@ -#include "captiveportal/RFC8908Handler.h" - -const char* const TAG = "RFC8908Handler"; - -#include "http/ContentTypes.h" -#include "Logging.h" - -#include - -#include - -#include - -using namespace OpenShock; - -static const char* const captivePortalApiPath = "/captive-portal/api"; - -static const char* const probeRequestrefixes[] = {"/gen_204", "/generate_204", "/hotspot-detect.", "/success.", "/connecttest.", "/check_network_status.", "/canonical.", "/ncsi.txt", captivePortalApiPath}; - -static String GetCaptivePortalUrl() -{ - return String("http://") + WiFi.softAPIP().toString() + "/"; -} - -// Catch-all handler for OS connectivity probes. -// Operating systems attempt to detect internet access by requesting -// well-known URLs. Redirecting these requests to the portal root -// intentionally fails connectivity checks and activates captive -// portal mode. -// -// This mechanism complements the IETF CAPPORT architecture (RFC 8908), -// which provides a machine-readable API for captive state, while this -// handler ensures broad OS compatibility. -void CaptivePortal::RFC8908Handler::CatchAll(AsyncWebServerRequest* request) -{ - AsyncWebServerResponse* response = request->beginResponse(302); - response->addHeader(asyncsrv::T_LOCATION, GetCaptivePortalUrl()); - response->addHeader(asyncsrv::T_Cache_Control, "no-store, no-cache, must-revalidate"); - response->addHeader("Pragma", "no-cache"); - response->addHeader("Expires", "0"); - request->send(response); -} - -bool CaptivePortal::RFC8908Handler::canHandle(AsyncWebServerRequest* request) const -{ - if (!request->isHTTP() || request->method() != WebRequestMethod::HTTP_GET) { - return false; - } - - auto& url = request->url(); - - for (auto& prefix : probeRequestrefixes) { - if (url.startsWith(prefix)) return true; - } - - return false; -} -void CaptivePortal::RFC8908Handler::handleRequest(AsyncWebServerRequest* request) -{ - auto& url = request->url(); - - // Captive Portal API endpoint (RFC 8908) - // - // Provides a machine-readable JSON interface for clients to query - // their captive portal state. This complements the classic redirect-based - // captive portal detection used by Android, iOS/macOS, and Windows. - // - // Minimal required fields for RFC compliance: - // - "captive": boolean indicating whether the client is still captive - // - "user-portal-url": URL of the portal page the client should visit - // - // Optional fields that can be added later: - // - "venue-info-url", "can-extend-session", "seconds-remaining", "bytes-remaining" - // - // Clients accessing this endpoint should respect "Cache-Control: no-store" - // and only rely on this data over HTTPS if possible. On embedded devices, - // HTTPS may be omitted for practicality, but the redirect-based captive - // portal ensures broad OS compatibility. - if (url.equals(captivePortalApiPath)) { - OS_LOGI(TAG, "Got Captive API request"); - - auto portalUrl = GetCaptivePortalUrl(); - - cJSON* doc = cJSON_CreateObject(); - cJSON_AddBoolToObject(doc, "captive", true); - cJSON_AddStringToObject(doc, "user-portal-url", portalUrl.c_str()); - cJSON_AddStringToObject(doc, "venue-info-url", "https://openshock.org"); - - char* jsonStr = cJSON_Print(doc); - cJSON_Delete(doc); - - if (jsonStr == nullptr) { - OS_LOGE(TAG, "Failed to serialize captive portal JSON response"); - request->send(500, OpenShock::HTTP::ContentType::TextPlain, "Internal Server Error"); - return; - } - - AsyncWebServerResponse* response = request->beginResponse(200, "application/captive+json", jsonStr); - cJSON_free(jsonStr); - - response->addHeader("Cache-Control", "private"); - request->send(response); - return; - } - - // Fallback to 302 - CatchAll(request); -} diff --git a/src/config/OtaUpdateConfig.cpp b/src/config/OtaUpdateConfig.cpp deleted file mode 100644 index 4ae0735a..00000000 --- a/src/config/OtaUpdateConfig.cpp +++ /dev/null @@ -1,124 +0,0 @@ -#include "config/OtaUpdateConfig.h" - -const char* const TAG = "Config::OtaUpdateConfig"; - -#include "config/internal/utils.h" -#include "Logging.h" - -using namespace OpenShock::Config; - -OtaUpdateConfig::OtaUpdateConfig() - : isEnabled(true) - , cdnDomain(OPENSHOCK_FW_CDN_DOMAIN) - , updateChannel(OtaUpdateChannel::Stable) - , checkOnStartup(false) - , checkPeriodically(false) - , checkInterval(30) - , allowBackendManagement(true) - , requireManualApproval(false) - , updateId(0) - , updateStep(OtaUpdateStep::None) -{ -} - -OtaUpdateConfig::OtaUpdateConfig( - bool isEnabled, std::string cdnDomain, OtaUpdateChannel updateChannel, bool checkOnStartup, bool checkPeriodically, uint16_t checkInterval, bool allowBackendManagement, bool requireManualApproval, int32_t updateId, OtaUpdateStep updateStep -) - : isEnabled(isEnabled) - , cdnDomain(std::move(cdnDomain)) - , updateChannel(updateChannel) - , checkOnStartup(checkOnStartup) - , checkPeriodically(checkPeriodically) - , checkInterval(checkInterval) - , allowBackendManagement(allowBackendManagement) - , requireManualApproval(requireManualApproval) - , updateId(updateId) - , updateStep(updateStep) -{ -} - -void OtaUpdateConfig::ToDefault() -{ - isEnabled = true; - cdnDomain = OPENSHOCK_FW_CDN_DOMAIN; - updateChannel = OtaUpdateChannel::Stable; - checkOnStartup = false; - checkPeriodically = false; - checkInterval = 30; // 30 minutes - allowBackendManagement = true; - requireManualApproval = false; - updateId = 0; - updateStep = OtaUpdateStep::None; -} - -bool OtaUpdateConfig::FromFlatbuffers(const Serialization::Configuration::OtaUpdateConfig* config) -{ - if (config == nullptr) { - OS_LOGW(TAG, "Config is null, setting to default"); - ToDefault(); - return true; - } - - isEnabled = config->is_enabled(); - Internal::Utils::FromFbsStr(cdnDomain, config->cdn_domain(), OPENSHOCK_FW_CDN_DOMAIN); - updateChannel = config->update_channel(); - checkOnStartup = config->check_on_startup(); - checkPeriodically = config->check_periodically(); - checkInterval = config->check_interval(); - allowBackendManagement = config->allow_backend_management(); - requireManualApproval = config->require_manual_approval(); - updateId = config->update_id(); - updateStep = config->update_step(); - - return true; -} - -flatbuffers::Offset OtaUpdateConfig::ToFlatbuffers(flatbuffers::FlatBufferBuilder& builder, bool withSensitiveData) const -{ - return Serialization::Configuration::CreateOtaUpdateConfig(builder, isEnabled, builder.CreateString(cdnDomain), updateChannel, checkOnStartup, checkPeriodically, checkInterval, allowBackendManagement, requireManualApproval, updateId, updateStep); -} - -bool OtaUpdateConfig::FromJSON(const cJSON* json) -{ - if (json == nullptr) { - OS_LOGW(TAG, "Config is null, setting to default"); - ToDefault(); - return true; - } - - if (!cJSON_IsObject(json)) { - OS_LOGE(TAG, "json is not an object"); - return false; - } - - Internal::Utils::FromJsonBool(isEnabled, json, "isEnabled", true); - Internal::Utils::FromJsonStr(cdnDomain, json, "cdnDomain", OPENSHOCK_FW_CDN_DOMAIN); - Internal::Utils::FromJsonStrParsed(updateChannel, json, "updateChannel", OpenShock::TryParseOtaUpdateChannel, OpenShock::OtaUpdateChannel::Stable); - Internal::Utils::FromJsonBool(checkOnStartup, json, "checkOnStartup", false); - Internal::Utils::FromJsonBool(checkPeriodically, json, "checkPeriodically", false); - Internal::Utils::FromJsonU16(checkInterval, json, "checkInterval", 30); - Internal::Utils::FromJsonBool(allowBackendManagement, json, "allowBackendManagement", true); - Internal::Utils::FromJsonBool(requireManualApproval, json, "requireManualApproval", false); - Internal::Utils::FromJsonI32(updateId, json, "updateId", 0); - Internal::Utils::FromJsonStrParsed(updateStep, json, "updateStep", OpenShock::TryParseOtaUpdateStep, OpenShock::OtaUpdateStep::None); - - return true; -} - -cJSON* OtaUpdateConfig::ToJSON(bool withSensitiveData) const -{ - cJSON* root = cJSON_CreateObject(); - - cJSON_AddBoolToObject(root, "isEnabled", isEnabled); - cJSON_AddStringToObject(root, "cdnDomain", cdnDomain.c_str()); - cJSON_AddStringToObject(root, "updateChannel", OpenShock::Serialization::Configuration::EnumNameOtaUpdateChannel(updateChannel)); - cJSON_AddBoolToObject(root, "checkOnStartup", checkOnStartup); - cJSON_AddBoolToObject(root, "checkPeriodically", checkPeriodically); - cJSON_AddNumberToObject(root, "checkInterval", checkInterval); - cJSON_AddBoolToObject(root, "allowBackendManagement", allowBackendManagement); - cJSON_AddBoolToObject(root, "requireManualApproval", requireManualApproval); - cJSON_AddNumberToObject(root, "updateId", updateId); - cJSON_AddStringToObject(root, "updateStep", OpenShock::Serialization::Configuration::EnumNameOtaUpdateStep(updateStep)); - - return root; -} diff --git a/src/config/internal/utils.cpp b/src/config/internal/utils.cpp deleted file mode 100644 index 37966a97..00000000 --- a/src/config/internal/utils.cpp +++ /dev/null @@ -1,191 +0,0 @@ -#include "config/internal/utils.h" - -const char* const TAG = "Config::Internal::Utils"; - -#include "Chipset.h" -#include "Logging.h" -#include "util/IPAddressUtils.h" - -using namespace OpenShock; - -template -static bool utilFromJsonInt(T& val, const cJSON* json, const char* name, T defaultVal, int minVal, int maxVal) -{ - static_assert(std::is_integral_v, "T must be an integral type"); - - const cJSON* jsonVal = cJSON_GetObjectItemCaseSensitive(json, name); - if (jsonVal == nullptr) { - return false; - } - - if (cJSON_IsNumber(jsonVal) == 0) { - OS_LOGE(TAG, "value at '%s' is not a number", name); - return false; - } - - int intVal = jsonVal->valueint; - - if (intVal < minVal) { - OS_LOGE(TAG, "value at '%s' is less than %d", name, minVal); - return false; - } - - if (intVal > maxVal) { - OS_LOGE(TAG, "value at '%s' is greater than %d", name, maxVal); - return false; - } - - val = static_cast(intVal); - - return true; -} - -bool Config::Internal::Utils::FromU8GpioNum(gpio_num_t& val, uint8_t u8Val) -{ - if (u8Val >= GPIO_NUM_MAX || !GPIO_IS_VALID_GPIO(u8Val)) { - OS_LOGE(TAG, "invalid GPIO number"); - return false; - } - - val = static_cast(u8Val); - - return true; -} - -void Config::Internal::Utils::FromU8GpioNum(gpio_num_t& val, uint8_t u8Val, gpio_num_t defaultVal) -{ - if (!FromU8GpioNum(val, u8Val)) { - val = defaultVal; - } -} - -void Config::Internal::Utils::FromFbsStr(std::string& str, const flatbuffers::String* fbsStr, const char* defaultStr) -{ - if (fbsStr != nullptr) { - str = fbsStr->c_str(); - } else { - str = defaultStr; - } -} - -bool Config::Internal::Utils::FromFbsIPAddress(IPAddress& ip, const flatbuffers::String* fbsIP, const IPAddress& defaultIP) -{ - if (fbsIP == nullptr) { - ip = defaultIP; - return true; - } - - std::string_view view(*fbsIP); - - if (!OpenShock::IPV4AddressFromStringView(ip, view)) { - OS_LOGE(TAG, "failed to parse IP address"); - return false; - } - - return true; -} - -bool Config::Internal::Utils::FromJsonBool(bool& val, const cJSON* json, const char* name, bool defaultVal) -{ - const cJSON* jsonVal = cJSON_GetObjectItemCaseSensitive(json, name); - if (jsonVal == nullptr) { - val = defaultVal; - return true; - } - - if (cJSON_IsBool(jsonVal) == 0) { - OS_LOGE(TAG, "value at '%s' is not a bool", name); - return false; - } - - val = cJSON_IsTrue(jsonVal); - - return true; -} - -bool Config::Internal::Utils::FromJsonU8(uint8_t& val, const cJSON* json, const char* name, uint8_t defaultVal) -{ - return utilFromJsonInt(val, json, name, defaultVal, 0, UINT8_MAX); -} - -bool Config::Internal::Utils::FromJsonU16(uint16_t& val, const cJSON* json, const char* name, uint16_t defaultVal) -{ - return utilFromJsonInt(val, json, name, defaultVal, 0, UINT16_MAX); -} - -bool Config::Internal::Utils::FromJsonI32(int32_t& val, const cJSON* json, const char* name, int32_t defaultVal) -{ - return utilFromJsonInt(val, json, name, defaultVal, INT32_MIN, INT32_MAX); -} - -bool Config::Internal::Utils::FromJsonStr(std::string& str, const cJSON* json, const char* name) -{ - const cJSON* jsonVal = cJSON_GetObjectItemCaseSensitive(json, name); - if (jsonVal == nullptr) { - OS_LOGE(TAG, "value at '%s' is null", name); - return false; - } - - if (cJSON_IsString(jsonVal) == 0) { - OS_LOGE(TAG, "value at '%s' is not a string", name); - return false; - } - - str = jsonVal->valuestring; - - return true; -} - -void Config::Internal::Utils::FromJsonStr(std::string& str, const cJSON* json, const char* name, const char* defaultStr) -{ - if (!FromJsonStr(str, json, name)) { - str = defaultStr; - } -} - -bool Config::Internal::Utils::FromJsonIPAddress(IPAddress& ip, const cJSON* json, const char* name) -{ - const cJSON* jsonVal = cJSON_GetObjectItemCaseSensitive(json, name); - if (jsonVal == nullptr) { - OS_LOGE(TAG, "value at '%s' is null", name); - return false; - } - - if (cJSON_IsString(jsonVal) == 0) { - OS_LOGE(TAG, "value at '%s' is not a string", name); - return false; - } - - std::string_view view(jsonVal->valuestring); - - if (!OpenShock::IPV4AddressFromStringView(ip, view)) { - OS_LOGE(TAG, "failed to parse IP address at '%s'", name); - return false; - } - - return true; -} - -void Config::Internal::Utils::FromJsonIPAddress(IPAddress& ip, const cJSON* json, const char* name, const IPAddress& defaultIP) -{ - if (!FromJsonIPAddress(ip, json, name)) { - ip = defaultIP; - } -} - -bool Config::Internal::Utils::FromJsonGpioNum(gpio_num_t& val, const cJSON* json, const char* name) -{ - uint8_t u8Val; - if (!FromJsonU8(u8Val, json, name, 0)) { - return false; - } - - return FromU8GpioNum(val, u8Val); -} - -void Config::Internal::Utils::FromJsonGpioNum(gpio_num_t& val, const cJSON* json, const char* name, gpio_num_t defaultVal) -{ - if (!FromJsonGpioNum(val, json, name)) { - val = defaultVal; - } -} diff --git a/src/http/HTTPRequestManager.cpp b/src/http/HTTPRequestManager.cpp deleted file mode 100644 index be18fbae..00000000 --- a/src/http/HTTPRequestManager.cpp +++ /dev/null @@ -1,472 +0,0 @@ -#include "http/HTTPRequestManager.h" - -const char* const TAG = "HTTPRequestManager"; - -#include "Common.h" -#include "Core.h" -#include "Logging.h" -#include "RateLimiter.h" -#include "SimpleMutex.h" -#include "util/HexUtils.h" -#include "util/StringUtils.h" - -#include - -#include -#include -#include -#include -#include - -using namespace std::string_view_literals; - -const std::size_t HTTP_BUFFER_SIZE = 4096LLU; -const int HTTP_DOWNLOAD_SIZE_LIMIT = 200 * 1024 * 1024; // 200 MB - -static OpenShock::SimpleMutex s_rateLimitsMutex = {}; -static std::unordered_map> s_rateLimits = {}; - -using namespace OpenShock; - -static std::string_view getDomainFromURL(std::string_view url) -{ - if (url.empty()) { - return {}; - } - - // Remove the protocol eg. "https://api.example.com:443/path" -> "api.example.com:443/path" - auto seperator = url.find("://"); - if (seperator != std::string_view::npos) { - url = url.substr(seperator + 3); - } - - // Remove the path eg. "api.example.com:443/path" -> "api.example.com:443" - seperator = url.find('/'); - if (seperator != std::string_view::npos) { - url = url.substr(0, seperator); - } - - // Remove the port eg. "api.example.com:443" -> "api.example.com" - seperator = url.rfind(':'); - if (seperator != std::string_view::npos) { - url = url.substr(0, seperator); - } - - // Remove all subdomains eg. "api.example.com" -> "example.com" - seperator = url.rfind('.'); - if (seperator == std::string_view::npos) { - return url; // E.g. "localhost" - } - seperator = url.rfind('.', seperator - 1); - if (seperator != std::string_view::npos) { - url = url.substr(seperator + 1); - } - - return url; -} - -static std::shared_ptr createRateLimiterForDomain(std::string_view domain) -{ - auto rateLimit = std::make_shared(); - - // Add default limits - rateLimit->addLimit(1000, 5); // 5 per second - rateLimit->addLimit(10 * 1000, 10); // 10 per 10 seconds - - // per-domain limits - if (domain == OPENSHOCK_API_DOMAIN) { - rateLimit->addLimit(60 * 1000, 12); // 12 per minute - rateLimit->addLimit(60 * 60 * 1000, 120); // 120 per hour - } - - return rateLimit; -} - -static std::shared_ptr createRateLimiterForURL(std::string_view url) -{ - auto domain = std::string(getDomainFromURL(url)); - if (domain.empty()) { - return nullptr; - } - - OpenShock::ScopedLock lock__(&s_rateLimitsMutex); - - auto it = s_rateLimits.find(domain); - if (it == s_rateLimits.end()) { - it = s_rateLimits.emplace(domain, createRateLimiterForDomain(domain)).first; - } - - return it->second; -} - -struct StreamReaderResult { - HTTP::RequestResult result; - std::size_t nWritten; -}; - -static bool isCRLF(const uint8_t* buffer) -{ - return buffer[0] == '\r' && buffer[1] == '\n'; -} -static bool tryFindCRLF(std::size_t& pos, const uint8_t* buffer, std::size_t len) -{ - const uint8_t* cur = buffer; - const uint8_t* end = buffer + len - 1; - - while (cur < end) { - if (isCRLF(cur)) { - pos = static_cast(cur - buffer); - return true; - } - - ++cur; - } - - return false; -} - -enum class ParserState : uint8_t { - Ok, - NeedMoreData, - Invalid, -}; - -static ParserState tryParseHttpChunkHeader(const uint8_t* buffer, std::size_t bufferLen, std::size_t& headerLen, std::size_t& payloadLen) -{ - if (bufferLen < 5) { // Bare minimum: "0\r\n\r\n" - return ParserState::NeedMoreData; - } - - // Find the first CRLF - if (!tryFindCRLF(headerLen, buffer, bufferLen)) { - return ParserState::NeedMoreData; - } - - // Header must have at least one character - if (headerLen == 0) { - OS_LOGW(TAG, "Invalid chunk header length"); - return ParserState::Invalid; - } - - // Check for end of size field (possibly followed by extensions which is separated by a semicolon) - std::size_t sizeFieldEnd = headerLen; - for (std::size_t i = 0; i < headerLen; ++i) { - if (buffer[i] == ';') { - sizeFieldEnd = i; - break; - } - } - - // Bounds check - if (sizeFieldEnd == 0 || sizeFieldEnd > 16) { - OS_LOGW(TAG, "Invalid chunk size field length"); - return ParserState::Invalid; - } - - std::string_view sizeField(reinterpret_cast(buffer), sizeFieldEnd); - - // Parse the chunk size - if (!HexUtils::TryParseHexToInt(sizeField.data(), sizeField.length(), payloadLen)) { - OS_LOGW(TAG, "Failed to parse chunk size"); - return ParserState::Invalid; - } - - if (payloadLen > HTTP_DOWNLOAD_SIZE_LIMIT) { - OS_LOGW(TAG, "Chunk size too large"); - return ParserState::Invalid; - } - - // Set the header length to the end of the CRLF - headerLen += 2; - - return ParserState::Ok; -} - -static ParserState tryParseHttpChunk(const uint8_t* buffer, std::size_t bufferLen, std::size_t& payloadPos, std::size_t& payloadLen) -{ - if (payloadPos == 0) { - ParserState state = tryParseHttpChunkHeader(buffer, bufferLen, payloadPos, payloadLen); - if (state != ParserState::Ok) { - return state; - } - } - - std::size_t totalLen = payloadPos + payloadLen + 2; // +2 for CRLF - if (bufferLen < totalLen) { - return ParserState::NeedMoreData; - } - - // Check for CRLF - if (!isCRLF(buffer + totalLen - 2)) { - OS_LOGW(TAG, "Invalid chunk payload CRLF"); - return ParserState::Invalid; - } - - return ParserState::Ok; -} - -static void alignHttpChunk(uint8_t* buffer, std::size_t& bufferCursor, std::size_t payloadPos, std::size_t payloadLen) -{ - std::size_t totalLen = payloadPos + payloadLen + 2; // +2 for CRLF - std::size_t remaining = bufferCursor - totalLen; - if (remaining > 0) { - memmove(buffer, buffer + totalLen, remaining); - bufferCursor = remaining; - } else { - bufferCursor = 0; - } -} - -static StreamReaderResult readHttpStreamDataChunked(HTTPClient& client, WiFiClient* stream, HTTP::DownloadCallback downloadCallback, int64_t begin, uint32_t timeoutMs) -{ - std::size_t totalWritten = 0; - HTTP::RequestResult result = HTTP::RequestResult::Success; - - uint8_t* buffer = static_cast(malloc(HTTP_BUFFER_SIZE)); - if (buffer == nullptr) { - OS_LOGE(TAG, "Out of memory"); - return {HTTP::RequestResult::RequestFailed, 0}; - } - - ParserState state = ParserState::NeedMoreData; - std::size_t bufferCursor = 0, payloadPos = 0, payloadSize = 0; - - while (client.connected() && state != ParserState::Invalid) { - if (begin + timeoutMs < OpenShock::millis()) { - OS_LOGW(TAG, "Request timed out"); - result = HTTP::RequestResult::TimedOut; - break; - } - - std::size_t bytesAvailable = stream->available(); - if (bytesAvailable == 0) { - vTaskDelay(pdMS_TO_TICKS(5)); - continue; - } - - std::size_t bytesRead = stream->readBytes(buffer + bufferCursor, HTTP_BUFFER_SIZE - bufferCursor); - if (bytesRead == 0) { - OS_LOGW(TAG, "No bytes read"); - result = HTTP::RequestResult::RequestFailed; - break; - } - - bufferCursor += bytesRead; - - while (bufferCursor > 0) { - state = tryParseHttpChunk(buffer, bufferCursor, payloadPos, payloadSize); - if (state == ParserState::Invalid) { - OS_LOGE(TAG, "Failed to parse chunk"); - result = HTTP::RequestResult::RequestFailed; - state = ParserState::Invalid; // Mark to exit both loops - break; - } - OS_LOGD(TAG, "Chunk parsed: %zu %zu", payloadPos, payloadSize); - - if (state == ParserState::NeedMoreData) { - if (bufferCursor == HTTP_BUFFER_SIZE) { - OS_LOGE(TAG, "Chunk too large"); - result = HTTP::RequestResult::RequestFailed; - state = ParserState::Invalid; // Mark to exit both loops - } - break; // If chunk size good, this only exits one loop - } - - // Check for zero chunk size (end of transfer) - if (payloadSize == 0) { - state = ParserState::Invalid; // Mark to exit both loops - break; - } - - if (!downloadCallback(totalWritten, buffer + payloadPos, payloadSize)) { - result = HTTP::RequestResult::Cancelled; - state = ParserState::Invalid; // Mark to exit both loops - break; - } - - totalWritten += payloadSize; - alignHttpChunk(buffer, bufferCursor, payloadPos, payloadSize); - payloadSize = 0; - payloadPos = 0; - } - - if (state == ParserState::NeedMoreData) { - vTaskDelay(pdMS_TO_TICKS(5)); - } - } - - free(buffer); - - return {result, totalWritten}; -} - -static StreamReaderResult readHttpStreamData(HTTPClient& client, WiFiClient* stream, std::size_t contentLength, HTTP::DownloadCallback downloadCallback, int64_t begin, uint32_t timeoutMs) -{ - std::size_t nWritten = 0; - HTTP::RequestResult result = HTTP::RequestResult::Success; - - uint8_t* buffer = static_cast(malloc(HTTP_BUFFER_SIZE)); - if (buffer == nullptr) { - OS_LOGE(TAG, "Failed to allocate HTTP buffer"); - return {HTTP::RequestResult::InternalError, 0}; - } - - while (client.connected() && nWritten < contentLength) { - if (begin + timeoutMs < OpenShock::millis()) { - OS_LOGW(TAG, "Request timed out"); - result = HTTP::RequestResult::TimedOut; - break; - } - - std::size_t bytesAvailable = stream->available(); - if (bytesAvailable == 0) { - vTaskDelay(pdMS_TO_TICKS(5)); - continue; - } - - std::size_t bytesToRead = std::min(bytesAvailable, HTTP_BUFFER_SIZE); - - std::size_t bytesRead = stream->readBytes(buffer, bytesToRead); - if (bytesRead == 0) { - OS_LOGW(TAG, "No bytes read"); - result = HTTP::RequestResult::RequestFailed; - break; - } - - if (!downloadCallback(nWritten, buffer, bytesRead)) { - OS_LOGW(TAG, "Request cancelled by callback"); - result = HTTP::RequestResult::Cancelled; - break; - } - - nWritten += bytesRead; - - vTaskDelay(pdMS_TO_TICKS(10)); - } - - free(buffer); - - return {result, nWritten}; -} - -HTTP::Response - HTTP::Download(std::string_view url, const std::map& headers, HTTP::GotContentLengthCallback contentLengthCallback, HTTP::DownloadCallback downloadCallback, tcb::span acceptedCodes, uint32_t timeoutMs) -{ - std::shared_ptr rateLimiter = createRateLimiterForURL(url); - if (rateLimiter == nullptr) { - return {RequestResult::InvalidURL, 0, 0}; - } - - if (!rateLimiter->tryRequest()) { - return {RequestResult::RateLimited, 0, 0}; - } - - HTTPClient client; - client.setUserAgent(OpenShock::Constants::FW_USERAGENT); - - int64_t begin = OpenShock::millis(); - - // This method is horribly named, if you call the begin() method with one String parameter its HTTP, but the one with (String, const char*) is HTTPS. - // We pass null here for CAcert parameter to remove erroneous "unexpected protocol: https, expected http" warning, this is what begin(String) does as a fallback. - // This is yet another example of why we need to get rid of Arduino dependency lol - if (!client.begin(OpenShock::StringToArduinoString(url), nullptr)) { - OS_LOGE(TAG, "Failed to begin HTTP request"); - return {HTTP::RequestResult::RequestFailed, 0, 0}; - } - - for (auto& header : headers) { - client.addHeader(header.first, header.second); - } - - int responseCode = client.GET(); - - if (responseCode == HTTP_CODE_REQUEST_TIMEOUT || begin + timeoutMs < OpenShock::millis()) { - OS_LOGW(TAG, "Request timed out"); - return {HTTP::RequestResult::TimedOut, responseCode, 0}; - } - - if (responseCode == HTTP_CODE_TOO_MANY_REQUESTS) { - // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After - - // Get "Retry-After" header - String retryAfterStr = client.header("Retry-After"); - - // Try to parse it as an integer (delay-seconds) - long retryAfter = 0; - if (retryAfterStr.length() > 0 && std::all_of(retryAfterStr.begin(), retryAfterStr.end(), isdigit)) { - retryAfter = retryAfterStr.toInt(); - } - - // If header missing/unparseable, default to 15 seconds - if (retryAfter <= 0) { - retryAfter = 15; - } - - // Apply the block-for time - rateLimiter->blockFor(retryAfter * 1000); - - return {HTTP::RequestResult::RateLimited, responseCode, 0}; - } - - if (responseCode == 418) { - OS_LOGW(TAG, "The server refused to brew coffee because it is, permanently, a teapot."); - } - - if (std::find(acceptedCodes.begin(), acceptedCodes.end(), responseCode) == acceptedCodes.end()) { - OS_LOGD(TAG, "Received unexpected response code %d", responseCode); - return {HTTP::RequestResult::CodeRejected, responseCode, 0}; - } - - int contentLength = client.getSize(); - if (contentLength == 0) { - return {HTTP::RequestResult::Success, responseCode, 0}; - } - - if (contentLength > 0) { - if (contentLength > HTTP_DOWNLOAD_SIZE_LIMIT) { - OS_LOGE(TAG, "Content-Length too large"); - return {HTTP::RequestResult::RequestFailed, responseCode, 0}; - } - - if (!contentLengthCallback(contentLength)) { - OS_LOGW(TAG, "Request cancelled by callback"); - return {HTTP::RequestResult::Cancelled, responseCode, 0}; - } - } - - WiFiClient* stream = client.getStreamPtr(); - if (stream == nullptr) { - OS_LOGE(TAG, "Failed to get stream"); - return {HTTP::RequestResult::RequestFailed, 0, 0}; - } - - StreamReaderResult result; - if (contentLength > 0) { - result = readHttpStreamData(client, stream, contentLength, downloadCallback, begin, timeoutMs); - } else { - result = readHttpStreamDataChunked(client, stream, downloadCallback, begin, timeoutMs); - } - - return {result.result, responseCode, result.nWritten}; -} - -HTTP::Response HTTP::GetString(std::string_view url, const std::map& headers, tcb::span acceptedCodes, uint32_t timeoutMs) -{ - std::string result; - - auto allocator = [&result](std::size_t contentLength) { - result.reserve(contentLength); - return true; - }; - auto writer = [&result](std::size_t offset, const uint8_t* data, std::size_t len) { - result.append(reinterpret_cast(data), len); - return true; - }; - - auto response = Download(url, headers, allocator, writer, acceptedCodes, timeoutMs); - if (response.result != RequestResult::Success) { - return {response.result, response.code, {}}; - } - - return {response.result, response.code, result}; -} diff --git a/src/serialization/JsonAPI.cpp b/src/serialization/JsonAPI.cpp deleted file mode 100644 index 6f0e38f5..00000000 --- a/src/serialization/JsonAPI.cpp +++ /dev/null @@ -1,258 +0,0 @@ -#include "serialization/JsonAPI.h" - -const char* const TAG = "JsonAPI"; - -#include "Logging.h" - -#define ESP_LOGJSONE(err, root) \ - { \ - char* _jsonStr = cJSON_PrintUnformatted(root); \ - OS_LOGE(TAG, "Invalid JSON response (" err "): %s", _jsonStr ? _jsonStr : ""); \ - cJSON_free(_jsonStr); \ - } - -using namespace OpenShock::Serialization; - -bool JsonAPI::ParseLcgInstanceDetailsJsonResponse(int code, const cJSON* root, JsonAPI::LcgInstanceDetailsResponse& out) -{ - (void)code; - - if (cJSON_IsObject(root) == 0) { - ESP_LOGJSONE("not an object", root); - return false; - } - - out = {}; - - const cJSON* name = cJSON_GetObjectItemCaseSensitive(root, "name"); - if (cJSON_IsString(name) == 0) { - ESP_LOGJSONE("value at 'data.name' is not a string", root); - return false; - } - - const cJSON* version = cJSON_GetObjectItemCaseSensitive(root, "version"); - if (cJSON_IsString(version) == 0) { - ESP_LOGJSONE("value at 'data.version' is not a string", root); - return false; - } - - const cJSON* currentTime = cJSON_GetObjectItemCaseSensitive(root, "currentTime"); - if (cJSON_IsString(currentTime) == 0) { - ESP_LOGJSONE("value at 'data.currentTime' is not a string", root); - return false; - } - - const cJSON* countryCode = cJSON_GetObjectItemCaseSensitive(root, "countryCode"); - if (cJSON_IsString(countryCode) == 0) { - ESP_LOGJSONE("value at 'data.countryCode' is not a string", root); - return false; - } - - const cJSON* fqdn = cJSON_GetObjectItemCaseSensitive(root, "fqdn"); - if (cJSON_IsString(fqdn) == 0) { - ESP_LOGJSONE("value at 'data.fqdn' is not a string", root); - return false; - } - - out.name = name->valuestring; - out.version = version->valuestring; - out.currentTime = currentTime->valuestring; - out.countryCode = countryCode->valuestring; - out.fqdn = fqdn->valuestring; - - return true; -} -bool JsonAPI::ParseBackendVersionJsonResponse(int code, const cJSON* root, JsonAPI::BackendVersionResponse& out) -{ - (void)code; - - if (cJSON_IsObject(root) == 0) { - ESP_LOGJSONE("not an object", root); - return false; - } - - const cJSON* data = cJSON_GetObjectItemCaseSensitive(root, "data"); - if (cJSON_IsObject(data) == 0) { - ESP_LOGJSONE("value at 'data' is not an object", root); - return false; - } - - out = {}; - - const cJSON* version = cJSON_GetObjectItemCaseSensitive(data, "version"); - if (cJSON_IsString(version) == 0) { - ESP_LOGJSONE("value at 'data.version' is not a string", root); - return false; - } - - const cJSON* commit = cJSON_GetObjectItemCaseSensitive(data, "commit"); - if (cJSON_IsString(commit) == 0) { - ESP_LOGJSONE("value at 'data.commit' is not a string", root); - return false; - } - - const cJSON* currentTime = cJSON_GetObjectItemCaseSensitive(data, "currentTime"); - if (cJSON_IsString(currentTime) == 0) { - ESP_LOGJSONE("value at 'data.currentTime' is not a string", root); - return false; - } - - out.version = version->valuestring; - out.commit = commit->valuestring; - out.currentTime = currentTime->valuestring; - - return true; -} - -bool JsonAPI::ParseAccountLinkJsonResponse(int code, const cJSON* root, JsonAPI::AccountLinkResponse& out) -{ - (void)code; - - if (cJSON_IsObject(root) == 0) { - ESP_LOGJSONE("not an object", root); - return false; - } - - const cJSON* data = cJSON_GetObjectItemCaseSensitive(root, "data"); - if (cJSON_IsString(data) == 0) { - ESP_LOGJSONE("value at 'data' is not a string", root); - return false; - } - - out = {}; - - out.authToken = data->valuestring; - - return true; -} -bool JsonAPI::ParseHubInfoJsonResponse(int code, const cJSON* root, JsonAPI::HubInfoResponse& out) -{ - (void)code; - - if (cJSON_IsObject(root) == 0) { - ESP_LOGJSONE("not an object", root); - return false; - } - - const cJSON* data = cJSON_GetObjectItemCaseSensitive(root, "data"); - if (cJSON_IsObject(data) == 0) { - ESP_LOGJSONE("value at 'data' is not an object", root); - return false; - } - - const cJSON* hubId = cJSON_GetObjectItemCaseSensitive(data, "id"); - if (cJSON_IsString(hubId) == 0) { - ESP_LOGJSONE("value at 'data.id' is not a string", root); - return false; - } - - const cJSON* hubName = cJSON_GetObjectItemCaseSensitive(data, "name"); - if (cJSON_IsString(hubName) == 0) { - ESP_LOGJSONE("value at 'data.name' is not a string", root); - return false; - } - - const cJSON* hubShockers = cJSON_GetObjectItemCaseSensitive(data, "shockers"); - if (cJSON_IsArray(hubShockers) == 0) { - ESP_LOGJSONE("value at 'data.shockers' is not an array", root); - return false; - } - - out = {}; - - out.hubId = hubId->valuestring; - out.hubName = hubName->valuestring; - - if (out.hubId.empty() || out.hubName.empty()) { - ESP_LOGJSONE("value at 'data.id' or 'data.name' is empty", root); - return false; - } - - cJSON* shocker = nullptr; - cJSON_ArrayForEach(shocker, hubShockers) - { - const cJSON* shockerId = cJSON_GetObjectItemCaseSensitive(shocker, "id"); - if (cJSON_IsString(shockerId) == 0) { - ESP_LOGJSONE("value at 'shocker.id' is not a string", shocker); - return false; - } - const char* shockerIdStr = shockerId->valuestring; - - if (shockerIdStr == nullptr || shockerIdStr[0] == '\0') { - ESP_LOGJSONE("value at 'shocker.id' is empty", shocker); - return false; - } - - const cJSON* shockerRfId = cJSON_GetObjectItemCaseSensitive(shocker, "rfId"); - if (cJSON_IsNumber(shockerRfId) == 0) { - ESP_LOGJSONE("value at 'shocker.rfId' is not a number", shocker); - return false; - } - int shockerRfIdInt = shockerRfId->valueint; - if (shockerRfIdInt < 0 || shockerRfIdInt > UINT16_MAX) { - ESP_LOGJSONE("value at 'shocker.rfId' is not a valid uint16_t", shocker); - return false; - } - uint16_t shockerRfIdU16 = static_cast(shockerRfIdInt); - - const cJSON* shockerModel = cJSON_GetObjectItemCaseSensitive(shocker, "model"); - if (cJSON_IsString(shockerModel) == 0) { - ESP_LOGJSONE("value at 'shocker.model' is not a string", shocker); - return false; - } - const char* shockerModelStr = shockerModel->valuestring; - - if (shockerModelStr == nullptr || shockerModelStr[0] == '\0') { - ESP_LOGJSONE("value at 'shocker.model' is empty", shocker); - return false; - } - - OpenShock::ShockerModelType shockerModelType; - if (!OpenShock::ShockerModelTypeFromString(shockerModelStr, shockerModelType, true)) { // PetTrainer is a typo in the API, we pass true to allow it - ESP_LOGJSONE("value at 'shocker.model' is not a valid shocker model", shocker); - return false; - } - - out.shockers.push_back({.id = shockerIdStr, .rfId = shockerRfIdU16, .model = shockerModelType}); - } - - return true; -} -bool JsonAPI::ParseAssignLcgJsonResponse(int code, const cJSON* root, JsonAPI::AssignLcgResponse& out) -{ - (void)code; - - if (cJSON_IsObject(root) == 0) { - ESP_LOGJSONE("not an object", root); - return false; - } - - const cJSON* host = cJSON_GetObjectItemCaseSensitive(root, "host"); - const cJSON* port = cJSON_GetObjectItemCaseSensitive(root, "port"); - const cJSON* path = cJSON_GetObjectItemCaseSensitive(root, "path"); - const cJSON* country = cJSON_GetObjectItemCaseSensitive(root, "country"); - - if (cJSON_IsString(host) == 0 || cJSON_IsString(path) == 0 || cJSON_IsString(country) == 0) { - ESP_LOGJSONE("value at 'host', 'path' or 'country' is not a string", root); - return false; - } - if (cJSON_IsNumber(port) == 0) { - ESP_LOGJSONE("value at 'port' is not a number", root); - return false; - } - - int portInt = port->valueint; - if (portInt < 0 || portInt > UINT16_MAX) { - ESP_LOGJSONE("value at 'port' is outside UINT16 bounds", root); - return false; - } - - out = {}; - - out.host = host->valuestring; - out.port = static_cast(portInt); - out.path = path->valuestring; - out.country = country->valuestring; - - return true; -} diff --git a/src/util/IPAddressUtils.cpp b/src/util/IPAddressUtils.cpp deleted file mode 100644 index 50b9010d..00000000 --- a/src/util/IPAddressUtils.cpp +++ /dev/null @@ -1,27 +0,0 @@ -#include "util/IPAddressUtils.h" - -#include "Convert.h" -#include "util/StringUtils.h" - -const char* const TAG = "Util::IPAddressUtils"; - -bool OpenShock::IPV4AddressFromStringView(IPAddress& ip, std::string_view sv) -{ - if (sv.empty()) { - return false; - } - - std::string_view parts[4]; - if (!OpenShock::TryStringSplit(sv, '.', parts)) { - return false; // Must have 4 octets - } - - std::uint8_t octets[4]; - if (!Convert::ToUint8(parts[0], octets[0]) || !Convert::ToUint8(parts[1], octets[1]) || !Convert::ToUint8(parts[2], octets[2]) || !Convert::ToUint8(parts[3], octets[3])) { - return false; - } - - ip = IPAddress(octets); - - return true; -} diff --git a/src/wifi/WiFiScanManager.cpp b/src/wifi/WiFiScanManager.cpp deleted file mode 100644 index 57dc463c..00000000 --- a/src/wifi/WiFiScanManager.cpp +++ /dev/null @@ -1,313 +0,0 @@ -#include - -#include "wifi/WiFiScanManager.h" - -const char* const TAG = "WiFiScanManager"; - -#include "Logging.h" -#include "SimpleMutex.h" -#include "util/TaskUtils.h" - -#include - -#include - -const uint8_t OPENSHOCK_WIFI_SCAN_MAX_CHANNEL = 13; -const uint32_t OPENSHOCK_WIFI_SCAN_MAX_MS_PER_CHANNEL = 300; // Adjusting this value will affect the scan rate, but may also affect the scan results -const uint32_t OPENSHOCK_WIFI_SCAN_TIMEOUT_MS = 10 * 1000; - -enum WiFiScanTaskNotificationFlags { - CHANNEL_DONE = 1 << 0, - ERROR = 1 << 1, - WIFI_DISABLED = 1 << 2, - CLEAR_FLAGS = CHANNEL_DONE | ERROR -}; - -static bool s_initialized = false; -static TaskHandle_t s_scanTaskHandle = nullptr; -static OpenShock::SimpleMutex s_scanTaskMutex = {}; -static uint8_t s_currentChannel = 0; -static std::map s_statusChangedHandlers; -static std::map s_networksDiscoveredHandlers; - -using namespace OpenShock; - -static bool notifyTask(WiFiScanTaskNotificationFlags flags) -{ - ScopedLock lock__(&s_scanTaskMutex); - - if (s_scanTaskHandle == nullptr) { - return false; - } - - return xTaskNotify(s_scanTaskHandle, flags, eSetBits) == pdPASS; -} - -static void notifyStatusChangedHandlers(OpenShock::WiFiScanStatus status) -{ - for (auto& it : s_statusChangedHandlers) { - it.second(status); - } -} - -static bool isScanError(int16_t retval) -{ - return retval < 0 && retval != WIFI_SCAN_RUNNING; -} - -static void handleScanError(int16_t retval) -{ - if (retval >= 0) return; - - notifyTask(WiFiScanTaskNotificationFlags::ERROR); - - if (retval == WIFI_SCAN_FAILED) { - OS_LOGE(TAG, "Failed to start scan on channel %u", s_currentChannel); - return; - } - - if (retval == WIFI_SCAN_RUNNING) { - OS_LOGE(TAG, "Scan is running on channel %u", s_currentChannel); - return; - } - - OS_LOGE(TAG, "Scan returned an unknown error"); -} - -static int16_t scanChannel(uint8_t channel) -{ - int16_t retval = WiFi.scanNetworks(true, true, false, OPENSHOCK_WIFI_SCAN_MAX_MS_PER_CHANNEL, channel); - if (!isScanError(retval)) { - return retval; - } - - handleScanError(retval); - - return retval; -} - -static WiFiScanStatus scanningTaskImpl() -{ - // Start the scan on the highest channel and work our way down - uint8_t channel = OPENSHOCK_WIFI_SCAN_MAX_CHANNEL; - - notifyStatusChangedHandlers(WiFiScanStatus::Started); - - // Start the scan on the first channel - int16_t retval = scanChannel(channel); - if (isScanError(retval)) { - return WiFiScanStatus::Error; - } - - // Scan each channel until we're done - while (true) { - uint32_t notificationFlags = 0; - - notifyStatusChangedHandlers(WiFiScanStatus::InProgress); - - // Wait for the scan to complete, evScanCompleted will notify us when it's done - if (xTaskNotifyWait(0, WiFiScanTaskNotificationFlags::CLEAR_FLAGS, ¬ificationFlags, pdMS_TO_TICKS(OPENSHOCK_WIFI_SCAN_TIMEOUT_MS)) != pdTRUE) { - OS_LOGE(TAG, "Scan timed out"); - return WiFiScanStatus::TimedOut; - } - - // Check if we were notified of an error or if WiFi was disabled - if (notificationFlags != WiFiScanTaskNotificationFlags::CHANNEL_DONE) { - if (notificationFlags & WiFiScanTaskNotificationFlags::WIFI_DISABLED) { - OS_LOGE(TAG, "Scan task exiting due to being notified that WiFi was disabled"); - return WiFiScanStatus::Aborted; - } - - if (notificationFlags & WiFiScanTaskNotificationFlags::ERROR) { - OS_LOGE(TAG, "Scan task exiting due to being notified of an error"); - return WiFiScanStatus::Error; - } - - return WiFiScanStatus::Error; - } - - // Select the next channel, or break if we're done - if (--channel <= 0) { - break; - } - - // Start the scan on the next channel - retval = scanChannel(channel); - if (isScanError(retval)) { - return WiFiScanStatus::Error; - } - } - - return WiFiScanStatus::Completed; -} - -static void scanningTask(void* arg) -{ - (void)arg; - - // Start the scan - WiFiScanStatus status = scanningTaskImpl(); - - // Notify the status changed handlers of the scan result - notifyStatusChangedHandlers(status); - - s_scanTaskMutex.lock(portMAX_DELAY); - - // Clear the task handle - s_scanTaskHandle = nullptr; - - s_scanTaskMutex.unlock(); - - // Kill this task - vTaskDelete(nullptr); -} - -static void evScanCompleted(arduino_event_id_t event, arduino_event_info_t info) -{ - (void)event; - (void)info; - - int16_t numNetworks = WiFi.scanComplete(); - if (isScanError(numNetworks)) { - handleScanError(numNetworks); - return; - } - - if (numNetworks == WIFI_SCAN_RUNNING) { - OS_LOGE(TAG, "Scan completed but scan is still running... WTF?"); - return; - } - - std::vector networkRecords; - networkRecords.reserve(numNetworks); - - for (int16_t i = 0; i < numNetworks; i++) { - wifi_ap_record_t* record = static_cast(WiFi.getScanInfoByIndex(i)); - if (record == nullptr) { - OS_LOGE(TAG, "Failed to get scan info for network #%d", i); - return; - } - - networkRecords.push_back(record); - } - - // Notify the networks discovered handlers - for (auto& it : s_networksDiscoveredHandlers) { - it.second(networkRecords); - } - - // Notify the scan task that we're done - notifyTask(WiFiScanTaskNotificationFlags::CHANNEL_DONE); -} -static void evSTAStopped(arduino_event_id_t event, arduino_event_info_t info) -{ - (void)event; - (void)info; - - notifyTask(WiFiScanTaskNotificationFlags::WIFI_DISABLED); -} - -bool WiFiScanManager::Init() -{ - if (s_initialized) { - OS_LOGW(TAG, "WiFiScanManager is already initialized"); - return true; - } - - WiFi.onEvent(evScanCompleted, ARDUINO_EVENT_WIFI_SCAN_DONE); - WiFi.onEvent(evSTAStopped, ARDUINO_EVENT_WIFI_STA_STOP); - - s_initialized = true; - - return true; -} - -bool WiFiScanManager::IsScanning() -{ - // Quick check - if (s_scanTaskHandle == nullptr) { - return false; - } - - // It wasnt null, lock and perform proper check - ScopedLock lock__(&s_scanTaskMutex); - - return s_scanTaskHandle != nullptr && eTaskGetState(s_scanTaskHandle) != eDeleted; -} - -bool WiFiScanManager::StartScan() -{ - ScopedLock lock__(&s_scanTaskMutex); - - // Check if a scan is already in progress - if (s_scanTaskHandle != nullptr && eTaskGetState(s_scanTaskHandle) != eDeleted) { - OS_LOGW(TAG, "Cannot start scan: scan task is already running"); - return false; - } - - // Free the TCB - if (s_scanTaskHandle != nullptr) { - vTaskDelete(s_scanTaskHandle); - } - - // Start the scan task - if (TaskUtils::TaskCreateExpensive(scanningTask, "WiFiScanManager", 4096, nullptr, 1, &s_scanTaskHandle) != pdPASS) { // PROFILED: 1.8KB stack usage - OS_LOGE(TAG, "Failed to create scan task"); - return false; - } - - return true; -} -bool WiFiScanManager::AbortScan() -{ - ScopedLock lock__(&s_scanTaskMutex); - - // Check if a scan is in progress - if (s_scanTaskHandle == nullptr || eTaskGetState(s_scanTaskHandle) == eDeleted) { - OS_LOGW(TAG, "Cannot abort scan: no scan is in progress"); - return false; - } - - // Kill the task - vTaskDelete(s_scanTaskHandle); - s_scanTaskHandle = nullptr; - - // Inform the change handlers that the scan was aborted - for (auto& it : s_statusChangedHandlers) { - it.second(WiFiScanStatus::Aborted); - } - - return true; -} - -uint64_t WiFiScanManager::RegisterStatusChangedHandler(const WiFiScanManager::StatusChangedHandler& handler) -{ - static uint64_t nextHandle = 0; - uint64_t handle = nextHandle++; - s_statusChangedHandlers[handle] = handler; - return handle; -} -void WiFiScanManager::UnregisterStatusChangedHandler(uint64_t handle) -{ - auto it = s_statusChangedHandlers.find(handle); - - if (it != s_statusChangedHandlers.end()) { - s_statusChangedHandlers.erase(it); - } -} - -uint64_t WiFiScanManager::RegisterNetworksDiscoveredHandler(const WiFiScanManager::NetworksDiscoveredHandler& handler) -{ - static uint64_t nextHandle = 0; - uint64_t handle = nextHandle++; - s_networksDiscoveredHandlers[handle] = handler; - return handle; -} -void WiFiScanManager::UnregisterNetworksDiscoveredHandler(uint64_t handle) -{ - auto it = s_networksDiscoveredHandlers.find(handle); - - if (it != s_networksDiscoveredHandlers.end()) { - s_networksDiscoveredHandlers.erase(it); - } -} diff --git a/test/README b/test/README deleted file mode 100644 index 9b1e87bc..00000000 --- a/test/README +++ /dev/null @@ -1,11 +0,0 @@ - -This directory is intended for PlatformIO Test Runner and project tests. - -Unit Testing is a software testing method by which individual units of -source code, sets of one or more MCU program modules together with associated -control data, usage procedures, and operating procedures, are tested to -determine whether they are fit for use. Unit testing finds problems early -in the development cycle. - -More information about PlatformIO Unit Testing: -- https://docs.platformio.org/en/latest/advanced/unit-testing/index.html