diff --git a/.github/workflows/arm64-macos.yml b/.github/workflows/arm64-macos.yml index 79e8569b..19e64d64 100644 --- a/.github/workflows/arm64-macos.yml +++ b/.github/workflows/arm64-macos.yml @@ -47,7 +47,7 @@ jobs: - name: Install dependencies run: | . venv/bin/activate - etc/conan-install.sh Release -pr:a ./etc/conan/profiles/macos${{ steps.retrieve-version.outputs.MACOS_MAJOR_VERSION }} + etc/conan-install.sh Release -pr:a="./etc/conan/profiles/macos${{ steps.retrieve-version.outputs.MACOS_MAJOR_VERSION }}" - name: Build run: | @@ -56,7 +56,7 @@ jobs: - name: Unit-Test C++ run: | - etc/install-uic-keys.sh + cert/install-uic-keys.sh build/Release/bin/ticket-decoder-test - name: Unit-Test Python3 diff --git a/.github/workflows/ubuntu22-gcc11.yml b/.github/workflows/ubuntu22-gcc11.yml index 98bb6d3b..71d9606b 100644 --- a/.github/workflows/ubuntu22-gcc11.yml +++ b/.github/workflows/ubuntu22-gcc11.yml @@ -51,7 +51,7 @@ jobs: - name: Install dependencies run: | . venv/bin/activate - etc/conan-install.sh Release -pr:a ./etc/conan/profiles/ubuntu22 + etc/conan-install.sh Release -pr:a="./etc/conan/profiles/ubuntu22" -o:a="&:with_signature_verifier=False" - name: Build run: | @@ -60,7 +60,7 @@ jobs: - name: Unit-Test C++ run: | - etc/install-uic-keys.sh + cert/install-uic-keys.sh build/Release/bin/ticket-decoder-test - name: Unit-Test Python3 diff --git a/.github/workflows/ubuntu24-clang16.yml b/.github/workflows/ubuntu24-clang16.yml index 0df7b6ab..6f9d29c0 100644 --- a/.github/workflows/ubuntu24-clang16.yml +++ b/.github/workflows/ubuntu24-clang16.yml @@ -55,7 +55,7 @@ jobs: - name: Install dependencies run: | . venv/bin/activate - etc/conan-install.sh Release -pr:a ./etc/conan/profiles/ubuntu24 -pr:a ./etc/conan/profiles/clang$CLANG_VERSION + etc/conan-install.sh Release -pr:a="./etc/conan/profiles/ubuntu24" -pr:a="./etc/conan/profiles/clang$CLANG_VERSION" - name: Build run: | @@ -64,7 +64,7 @@ jobs: - name: Unit-Test C++ run: | - etc/install-uic-keys.sh + cert/install-uic-keys.sh build/Release/bin/ticket-decoder-test - name: Unit-Test Python3 diff --git a/.github/workflows/ubuntu24-gcc13.yml b/.github/workflows/ubuntu24-gcc13.yml index 707f9fab..4b5cb0e2 100644 --- a/.github/workflows/ubuntu24-gcc13.yml +++ b/.github/workflows/ubuntu24-gcc13.yml @@ -51,7 +51,7 @@ jobs: - name: Install dependencies run: | . venv/bin/activate - etc/conan-install.sh Release -pr:a ./etc/conan/profiles/ubuntu24 + etc/conan-install.sh Release -pr:a="./etc/conan/profiles/ubuntu24" - name: Build run: | @@ -60,7 +60,7 @@ jobs: - name: Unit-Test C++ run: | - etc/install-uic-keys.sh + cert/install-uic-keys.sh build/Release/bin/ticket-decoder-test - name: Unit-Test Python3 diff --git a/.github/workflows/x64-macos.yml b/.github/workflows/x64-macos.yml index 71237969..1c236e54 100644 --- a/.github/workflows/x64-macos.yml +++ b/.github/workflows/x64-macos.yml @@ -40,7 +40,7 @@ jobs: - name: Install dependencies run: | . venv/bin/activate - etc/conan-install.sh Release -pr:a ./etc/conan/profiles/macos${{ steps.retrieve-version.outputs.MACOS_MAJOR_VERSION }} + etc/conan-install.sh Release -pr:a="./etc/conan/profiles/macos${{ steps.retrieve-version.outputs.MACOS_MAJOR_VERSION }}" - name: Build run: | @@ -49,7 +49,7 @@ jobs: - name: Unit-Test C++ run: | - etc/install-uic-keys.sh + cert/install-uic-keys.sh build/Release/bin/ticket-decoder-test - name: Unit-Test Python3 diff --git a/.vscode/launch.json b/.vscode/launch.json index e85c3fca..9cc3a57d 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -145,6 +145,18 @@ "cwd": "${workspaceFolder}", "preLaunchTask": "build debug" }, + { + "type": "lldb", + "request": "launch", + "name": "decoder - debug - image (VDV Einzelfahrausweis Ermäßigt)", + "program": "${workspaceFolder}/build/Debug/bin/ticket-decoder", + "args": [ + "-i", + "images/VDV Einzelfahrausweis Ermäßigt Berlin Rheinsberg.png" + ], + "cwd": "${workspaceFolder}", + "preLaunchTask": "build debug" + }, { "type": "lldb", "request": "launch", diff --git a/README.md b/README.md index a1cc24f8..889a1295 100644 --- a/README.md +++ b/README.md @@ -214,7 +214,7 @@ Optional and minimal user interaction methods to support fast interactive experi * Interoperability UIC/VDV codes, UIC918-3 and UIC918-9 example tickets and mappings for ids used in VDV codes https://www.bahn.de/angebot/regio/barcode * [UIC918-3 Muster](https://assets.static-bahn.de/dam/jcr:c362849f-210d-4dbe-bb18-34141b5ba274/mdb_320951_muster-tickets_nach_uic_918-3_2.zip) - * [UIC918-9 Muster](https://assets.static-bahn.de/dam/jcr:3c7a020a-7632-4f23-8716-6ebfc9f93ccb/Muster%20918-9.zip) + * [UIC918-9 Muster](https://assets.static-bahn.de/dam/jcr:ec74454d-557b-438f-8ed9-689abcc276f5/Muster%20918-9.zip) ``` # You can use the following command to convert PDF file into images for further processing, but you don't have to because application is able to precess pdf files directly. But decoding quality might differ depending on parameters like DPI. # brew|apt install imagemagick @@ -277,7 +277,7 @@ It is possible to enable/disable parts of the application or the Python module * * **with_sbb_interpreter=False** skips creation of SBB interpreter module and avoids dependency to protobuf -To enable/disable, please use prepared scripts like [setup.Python.sh](setup.Python.sh) or [setup.Decoder.sh](setup.Decoder.sh) and change desired feature toggles there. Or pass options like `-o "&:with_ticket_analyzer=False"` to conan install script. Check the script mentioned above as a guideline. +To enable/disable, please use prepared scripts like [setup.Python.sh](setup.Python.sh) or [setup.Decoder.sh](setup.Decoder.sh) and change desired feature toggles there. Or pass options like `-o:a="&:with_ticket_analyzer=False"` to conan install script. Check the script mentioned above as a guideline. Following libraries are used by the project. Usually you should not care about it since conan will do that for you. @@ -332,7 +332,7 @@ Take a look into `./build/` folder to discover artifacts. You should be able to When opencv has to be built from source because of missing pre-built package for your arch/os/compiler/config mix, it might be necessary to install some further xorg/system libraries to make highgui stuff building inside conan install process. To get this handled automatically, use the conan config flags shown below in `~/conan2/profiles/default` or pass additional -argument `-pr:a ./etc/conan/profiles/package-manager-config` to conan-install call in `setup.All.sh`. +argument `-pr:a="./etc/conan/profiles/package-manager-config"` to conan-install call in `setup.All.sh`. ``` [conf] tools.system.package_manager:mode=install @@ -351,7 +351,7 @@ pip install -r requirements.txt git clone https://github.com/user4223/ticket-decoder.git && cd ticket-decoder ./setup.All.sh -- -j -etc/install-uic-keys.sh +cert/install-uic-keys.sh build/Release/bin/ticket-decoder-test etc/python-test.sh @@ -375,7 +375,7 @@ pip install -r requirements.txt git clone https://github.com/user4223/ticket-decoder.git && cd ticket-decoder ./setup.All.sh -- -j -etc/install-uic-keys.sh +cert/install-uic-keys.sh build/Release/bin/ticket-decoder-test etc/python-test.sh diff --git a/etc/install-uic-keys.sh b/cert/install-uic-keys.sh similarity index 57% rename from etc/install-uic-keys.sh rename to cert/install-uic-keys.sh index 884a77ba..db2f0cca 100755 --- a/etc/install-uic-keys.sh +++ b/cert/install-uic-keys.sh @@ -9,6 +9,11 @@ readonly WORKSPACE_ROOT="$(readlink -f $(dirname "$0"))"/.. # curl --output ${WORKSPACE_ROOT}/cert/UIC_PublicKeys.xml \ # --show-error --fail 'https://railpublickey.uic.org/download.php' -mkdir -p ${WORKSPACE_ROOT}/cert -wget -nv -O ${WORKSPACE_ROOT}/cert/UIC_PublicKeys.xml \ - 'https://railpublickey.uic.org/download.php' +readonly DESTINATION_FILE="${WORKSPACE_ROOT}/cert/UIC_PublicKeys.xml" + +if [ -f ${DESTINATION_FILE} ]; then + readonly DATE=$(date +%F) + cp ${DESTINATION_FILE} "${WORKSPACE_ROOT}/cert/UIC_PublicKeys_before_${DATE}.xml" +fi + +wget -nv -O ${DESTINATION_FILE} 'https://railpublickey.uic.org/download.php' diff --git a/conanfile.py b/conanfile.py index 4377147c..b3633afa 100644 --- a/conanfile.py +++ b/conanfile.py @@ -62,8 +62,7 @@ def requirements(self): # https://conan.io/center/recipes/pugixml self.requires("pugixml/1.15") # https://conan.io/center/recipes/botan - # - version 3.x is available but has breaking changes - self.requires("botan/2.19.5") + self.requires("botan/3.10.0") if self.options.with_pdf_input: # https://conan.io/center/recipes/poppler diff --git a/etc/conan-install.sh b/etc/conan-install.sh index 137ad39c..9d02c7b8 100755 --- a/etc/conan-install.sh +++ b/etc/conan-install.sh @@ -24,10 +24,13 @@ export CMAKE_BUILD_TYPE=${BUILD_TYPE} conan install ${WORKSPACE_ROOT} \ --build missing \ -of ${WORKSPACE_ROOT}/build/${BUILD_TYPE} \ - -s:a build_type=${BUILD_TYPE} \ - -s:a compiler.cppstd=20 \ + -s:a="build_type=${BUILD_TYPE}" \ + -s:a="compiler.cppstd=20" \ ${@:2} # Remove temporary stuff like source and build folders 2 keep cache folder as small as possible. # This does NOT remove the created binaries. -conan cache clean '*' +# In Debug config, we do need source folders for debugging +if [ "$BUILD_TYPE" = "Release" ]; then + conan cache clean -p="build_type=Release" +fi diff --git a/etc/docker/ubuntu22.gcc.Dockerfile b/etc/docker/ubuntu22.gcc.Dockerfile index 5852fce1..1cd0a634 100644 --- a/etc/docker/ubuntu22.gcc.Dockerfile +++ b/etc/docker/ubuntu22.gcc.Dockerfile @@ -20,9 +20,10 @@ RUN update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-$GCC_VERSION 800 RUN update-alternatives --install /usr/bin/cc cc /usr/bin/gcc-$GCC_VERSION 800 WORKDIR /ticket-decoder -COPY etc/conan-config.sh etc/conan-install.sh etc/cmake-config.sh etc/cmake-build.sh etc/python-test.sh etc/install-uic-keys.sh etc/ +COPY etc/conan-config.sh etc/conan-install.sh etc/cmake-config.sh etc/cmake-build.sh etc/python-test.sh etc/ COPY etc/poppler/ etc/poppler COPY etc/conan/profiles etc/conan/profiles +COPY cert/install-uic-keys.sh cert/ COPY requirements.txt . RUN pip install -r requirements.txt @@ -30,8 +31,9 @@ RUN etc/conan-config.sh gcc $GCC_VERSION COPY conanfile.py . RUN etc/conan-install.sh Release \ - -pr:a ./etc/conan/profiles/ubuntu22 \ - -o libxml2/*:zlib=False + -pr:a="./etc/conan/profiles/ubuntu22" \ + -o:a="libxml2/*:zlib=False" \ + -o:a="with_signature_verifier=False" COPY <( - "k", "keys-file", + auto const uicPublicKeyXmlFileArg = TCLAP::ValueArg( + "K", "keys-file", "Path to file containing public keys from UIC for signature validation", false, "cert/UIC_PublicKeys.xml", "File path [xml]", cmd); + auto const vdvCertificateLdifFileArg = TCLAP::ValueArg( + "C", "certificates-file", + "Path to file containing certificates from VDV for message decoding and signature validation", + false, "cert/VDV_Certificates.ldif", "File path [ldif]", cmd); auto const cameraEnabledArg = TCLAP::SwitchArg( "c", "camera-enabled", "Enable camera at start and try to detect aztec codes in delivered images", @@ -78,7 +82,8 @@ int main(int argc, char **argv) auto decoderFacade = api::DecoderFacade::create(context) .withAsynchronousLoad(true) - .withPublicKeyFile(publicKeyFilePathArg.getValue()) + .withUicPublicKeyXmlFile(uicPublicKeyXmlFileArg.getValue()) + .withVdvCertificateLdifFile(vdvCertificateLdifFileArg.getValue()) .withImageRotation(imageRotationArg.getValue()) .withImageSplit(imageSplitArg.getValue()) .withDetector(detector::api::DetectorType::NOP_DETECTOR) diff --git a/source/app/source/decoder.cpp b/source/app/source/decoder.cpp index b7bbb4e7..4d29259c 100644 --- a/source/app/source/decoder.cpp +++ b/source/app/source/decoder.cpp @@ -49,10 +49,14 @@ int main(int argc, char **argv) "R", "output-base64-raw-data", "Decode aztec code and dump raw data to output after base64 encoding", cmd, false); - auto const publicKeyFilePathArg = TCLAP::ValueArg( - "k", "keys-file", + auto const uicPublicKeyXmlFileArg = TCLAP::ValueArg( + "K", "keys-file", "Path to file containing public keys from UIC for signature validation", false, "cert/UIC_PublicKeys.xml", "File path [xml]", cmd); + auto const vdvCertificateLdifFileArg = TCLAP::ValueArg( + "C", "certificates-file", + "Path to file containing certificates from VDV for message decoding and signature validation", + false, "cert/VDV_Certificates.ldif", "File path [ldif]", cmd); auto const pureBarcodeArg = TCLAP::ValueArg( "P", "pure-barcode", "Input contains the barcode only", @@ -103,7 +107,8 @@ int main(int argc, char **argv) auto decoderFacade = api::DecoderFacade::create(context) .withPureBarcode(pureBarcodeArg.getValue()) .withLocalBinarizer(binarizerEnabledArg.getValue()) - .withPublicKeyFile(publicKeyFilePathArg.getValue()) + .withUicPublicKeyXmlFile(uicPublicKeyXmlFileArg.getValue()) + .withVdvCertificateLdifFile(vdvCertificateLdifFileArg.getValue()) .withImageRotation(imageRotationArg.getValue()) .withImageScale(imageScaleArg.getValue()) .withImageSplit(imageSplitArg.getValue()) diff --git a/source/lib/api/include/DecoderFacade.h b/source/lib/api/include/DecoderFacade.h index 91a5f0a4..35f806dc 100644 --- a/source/lib/api/include/DecoderFacade.h +++ b/source/lib/api/include/DecoderFacade.h @@ -70,7 +70,9 @@ namespace api DecoderFacadeBuilder &withDetector(detector::api::DetectorType type); - DecoderFacadeBuilder &withPublicKeyFile(std::filesystem::path publicKeyFilePath); + DecoderFacadeBuilder &withUicPublicKeyXmlFile(std::filesystem::path uicPublicKeyXmlFile); + + DecoderFacadeBuilder &withVdvCertificateLdifFile(std::filesystem::path vdvCertificateLdifFile); DecoderFacadeBuilder &withPureBarcode(bool pureBarcode); diff --git a/source/lib/api/source/DecoderFacade.cpp b/source/lib/api/source/DecoderFacade.cpp index 76c536fe..7af62005 100644 --- a/source/lib/api/source/DecoderFacade.cpp +++ b/source/lib/api/source/DecoderFacade.cpp @@ -22,6 +22,7 @@ #include "lib/interpreter/api/include/Interpreter.h" #include "lib/interpreter/api/include/SignatureVerifier.h" +#include "lib/interpreter/api/include/CertificateProvider.h" namespace api { @@ -30,7 +31,8 @@ namespace api public: friend DecoderFacadeBuilder; - std::optional publicKeyFilePath; + std::optional uicPublicKeyXmlFile; + std::optional vdvCertificateLdifFile; std::optional classifierFile; std::optional> preProcessorResultVisitor; std::optional> detectorResultVisitor; @@ -159,9 +161,15 @@ namespace api return *this; } - DecoderFacadeBuilder &DecoderFacadeBuilder::withPublicKeyFile(std::filesystem::path publicKeyFilePath) + DecoderFacadeBuilder &DecoderFacadeBuilder::withUicPublicKeyXmlFile(std::filesystem::path uicPublicKeyXmlFile) { - options->publicKeyFilePath = std::make_optional(publicKeyFilePath); + options->uicPublicKeyXmlFile = std::make_optional(uicPublicKeyXmlFile); + return *this; + } + + DecoderFacadeBuilder &DecoderFacadeBuilder::withVdvCertificateLdifFile(std::filesystem::path vdvCertificateLdifFile) + { + options->vdvCertificateLdifFile = std::make_optional(vdvCertificateLdifFile); return *this; } @@ -266,6 +274,7 @@ namespace api std::shared_ptr detector; std::unique_ptr const decoder; std::unique_ptr const signatureChecker; + std::unique_ptr certificateProvider; std::unique_ptr const interpreter; Internal(infrastructure::Context &context, std::shared_ptr o) @@ -282,13 +291,16 @@ namespace api decoder(decoder::api::Decoder::create( context, options->getDecoderOptions())), - signatureChecker( - options->publicKeyFilePath - ? interpreter::api::SignatureVerifier::create(context, *options->publicKeyFilePath) - : interpreter::api::SignatureVerifier::createDummy(context)), + signatureChecker(options->uicPublicKeyXmlFile + ? interpreter::api::SignatureVerifier::create(context, *options->uicPublicKeyXmlFile) + : interpreter::api::SignatureVerifier::createDummy(context)), + certificateProvider(options->vdvCertificateLdifFile + ? interpreter::api::CertificateProvider::create(context, *options->vdvCertificateLdifFile) + : interpreter::api::CertificateProvider::createDummy(context)), interpreter(interpreter::api::Interpreter::create( context, - *signatureChecker)) + *signatureChecker, + *certificateProvider)) { } diff --git a/source/lib/input/detail/CMakeLists.txt b/source/lib/input/detail/CMakeLists.txt index a4e81283..78a1b0dc 100644 --- a/source/lib/input/detail/CMakeLists.txt +++ b/source/lib/input/detail/CMakeLists.txt @@ -5,6 +5,7 @@ PROJECT(ticket-decoder-input-detail) add_subdirectory(api) add_subdirectory(image) + IF (WITH_PDF_INPUT) add_subdirectory(pdf) ENDIF() diff --git a/source/lib/interpreter/api/include/CertificateProvider.h b/source/lib/interpreter/api/include/CertificateProvider.h new file mode 100644 index 00000000..51c19280 --- /dev/null +++ b/source/lib/interpreter/api/include/CertificateProvider.h @@ -0,0 +1,42 @@ +// SPDX-FileCopyrightText: (C) 2025 user4223 and (other) contributors to ticket-decoder +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "lib/infrastructure/include/ContextFwd.h" + +#include "lib/interpreter/detail/vdv/include/Certificate.h" // TODO Replace and remove + +#include +#include +#include +#include +#include +#include + +namespace interpreter::api +{ + + /*struct CertificateData + { + std::string name; + std::string description; + std::span data; + };*/ + + class CertificateProvider + { + public: + virtual ~CertificateProvider() = default; + + static std::unique_ptr create(infrastructure::Context &context, std::filesystem::path const &vdvCertificateLdifFile); + + static std::unique_ptr createDummy(infrastructure::Context &context); + + virtual std::vector getAuthorities() = 0; + + /* TODO This should not use the type out of detail::vdv and should use a locally defined type, see above + */ + virtual std::optional get(std::string authority) = 0; + }; +} diff --git a/source/lib/interpreter/api/include/Interpreter.h b/source/lib/interpreter/api/include/Interpreter.h index 075a88ef..46e1f2ad 100644 --- a/source/lib/interpreter/api/include/Interpreter.h +++ b/source/lib/interpreter/api/include/Interpreter.h @@ -14,6 +14,7 @@ namespace interpreter::api { class SignatureVerifier; + class CertificateProvider; class Interpreter { @@ -24,6 +25,6 @@ namespace interpreter::api */ virtual std::optional interpret(std::vector const &input, std::string origin, int indent = -1) const = 0; - static std::unique_ptr create(infrastructure::Context &context, SignatureVerifier const &signatureChecker); + static std::unique_ptr create(infrastructure::Context &context, SignatureVerifier const &signatureChecker, CertificateProvider &certificateProvider); }; } diff --git a/source/lib/interpreter/api/include/NopCertificateProvider.h b/source/lib/interpreter/api/include/NopCertificateProvider.h new file mode 100644 index 00000000..da617e76 --- /dev/null +++ b/source/lib/interpreter/api/include/NopCertificateProvider.h @@ -0,0 +1,27 @@ +// SPDX-FileCopyrightText: (C) 2025 user4223 and (other) contributors to ticket-decoder +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "CertificateProvider.h" + +#include "lib/infrastructure/include/ContextFwd.h" +#include "lib/infrastructure/include/Logger.h" + +namespace interpreter::api +{ + + class NopCertificateProvider : public CertificateProvider + { + infrastructure::Logger logger; + + public: + NopCertificateProvider(infrastructure::Context &context); + + static std::unique_ptr create(infrastructure::Context &context); + + virtual std::vector getAuthorities() override; + + virtual std::optional get(std::string authority) override; + }; +} diff --git a/source/lib/interpreter/api/include/SignatureVerifier.h b/source/lib/interpreter/api/include/SignatureVerifier.h index b11dbbb5..1c8c2593 100644 --- a/source/lib/interpreter/api/include/SignatureVerifier.h +++ b/source/lib/interpreter/api/include/SignatureVerifier.h @@ -22,7 +22,7 @@ namespace interpreter::api Successful }; - static std::unique_ptr create(infrastructure::Context &context, std::filesystem::path const &uicSignatureXml); + static std::unique_ptr create(infrastructure::Context &context, std::filesystem::path const &uicPublicKeyXmlFile); /* Creates a dummy implementation returning always KeyNotFound */ diff --git a/source/lib/interpreter/api/source/CertificateProvider.cpp b/source/lib/interpreter/api/source/CertificateProvider.cpp new file mode 100644 index 00000000..a0edd15f --- /dev/null +++ b/source/lib/interpreter/api/source/CertificateProvider.cpp @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: (C) 2025 user4223 and (other) contributors to ticket-decoder +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "../include/CertificateProvider.h" +#include "../include/NopCertificateProvider.h" + +#include "lib/interpreter/detail/vdv/include/LDIFFileCertificateProvider.h" + +#include "lib/infrastructure/include/Context.h" + +namespace interpreter::api +{ + + std::unique_ptr CertificateProvider::create(infrastructure::Context &context, std::filesystem::path const &vdvCertificateLdifFile) + { + return std::make_unique(context, vdvCertificateLdifFile); + } + + std::unique_ptr CertificateProvider::createDummy(infrastructure::Context &context) + { + return std::make_unique(context); + } +} diff --git a/source/lib/interpreter/api/source/Interpreter.cpp b/source/lib/interpreter/api/source/Interpreter.cpp index ef05475f..4ef41516 100644 --- a/source/lib/interpreter/api/source/Interpreter.cpp +++ b/source/lib/interpreter/api/source/Interpreter.cpp @@ -3,8 +3,10 @@ #include "../include/Interpreter.h" +#include "lib/interpreter/api/include/CertificateProvider.h" + #include "lib/interpreter/detail/common/include/Context.h" -#include "lib/interpreter/detail/common/include/InterpreterUtility.h" +#include "lib/interpreter/detail/common/include/StringDecoder.h" #include "lib/interpreter/detail/uic918/include/Uic918Interpreter.h" #include "lib/interpreter/detail/vdv/include/VDVInterpreter.h" @@ -27,7 +29,13 @@ namespace interpreter::api return std::make_pair(T::getTypeId(), decltype(interpreterMap)::mapped_type{new T(loggerFactory, signatureChecker)}); } - Internal(infrastructure::Context &c, SignatureVerifier const &signatureChecker) + template + static decltype(interpreterMap)::value_type create(auto &loggerFactory, auto &certificateProvider) + { + return std::make_pair(T::getTypeId(), decltype(interpreterMap)::mapped_type{new T(loggerFactory, certificateProvider)}); + } + + Internal(infrastructure::Context &c, SignatureVerifier const &signatureChecker, CertificateProvider &certificateProvider) : logger(CREATE_LOGGER(c.getLoggerFactory())), interpreterMap() { @@ -35,7 +43,7 @@ namespace interpreter::api interpreterMap.emplace(create(c.getLoggerFactory(), signatureChecker)); #endif #ifdef WITH_VDV_INTERPRETER - interpreterMap.emplace(create(c.getLoggerFactory(), signatureChecker)); + interpreterMap.emplace(create(c.getLoggerFactory(), certificateProvider)); #endif #ifdef WITH_SBB_INTERPRETER interpreterMap.emplace(create(c.getLoggerFactory(), signatureChecker)); @@ -54,7 +62,7 @@ namespace interpreter::api auto const interpreter = interpreterMap.find(detail::common::Interpreter::TypeIdType(typeId.begin(), typeId.end())); if (interpreter == interpreterMap.end()) { - LOG_WARN(logger) << "Unknown message type: " << detail::common::bytesToString(typeId); + LOG_WARN(logger) << "Unknown message type: 0x" << detail::common::StringDecoder::toHexString(typeId); return std::move(context); } @@ -77,8 +85,8 @@ namespace interpreter::api } }; - std::unique_ptr Interpreter::create(infrastructure::Context &context, SignatureVerifier const &signatureChecker) + std::unique_ptr Interpreter::create(infrastructure::Context &context, SignatureVerifier const &signatureChecker, CertificateProvider &certificateProvider) { - return std::make_unique(context, signatureChecker); + return std::make_unique(context, signatureChecker, certificateProvider); } } diff --git a/source/lib/interpreter/api/source/NopCertificateProvider.cpp b/source/lib/interpreter/api/source/NopCertificateProvider.cpp new file mode 100644 index 00000000..29f33e28 --- /dev/null +++ b/source/lib/interpreter/api/source/NopCertificateProvider.cpp @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: (C) 2025 user4223 and (other) contributors to ticket-decoder +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "../include/NopCertificateProvider.h" + +#include "lib/infrastructure/include/Context.h" +#include "lib/infrastructure/include/Logging.h" + +namespace interpreter::api +{ + NopCertificateProvider::NopCertificateProvider(infrastructure::Context &context) + : logger(CREATE_LOGGER(context.getLoggerFactory())) + { + LOG_WARN(logger) << "Using dummy certificate provider"; + } + + std::unique_ptr NopCertificateProvider::create(infrastructure::Context &context) + { + return std::make_unique(context); + } + + std::vector NopCertificateProvider::getAuthorities() + { + return {}; + } + + std::optional NopCertificateProvider::get(std::string authority) + { + return {}; + } +} diff --git a/source/lib/interpreter/api/source/NopSignatureVerifier.cpp b/source/lib/interpreter/api/source/NopSignatureVerifier.cpp index 6fa310ca..16786580 100644 --- a/source/lib/interpreter/api/source/NopSignatureVerifier.cpp +++ b/source/lib/interpreter/api/source/NopSignatureVerifier.cpp @@ -4,7 +4,6 @@ #include "../include/NopSignatureVerifier.h" #include "lib/infrastructure/include/Context.h" - #include "lib/infrastructure/include/Logging.h" namespace interpreter::api @@ -15,6 +14,11 @@ namespace interpreter::api LOG_WARN(logger) << "Using dummy signature checker"; } + std::unique_ptr NopSignatureVerifier::create(infrastructure::Context &context) + { + return std::make_unique(context); + } + NopSignatureVerifier::Result NopSignatureVerifier::check( std::string const &ricsCode, std::string const &keyId, std::span message, @@ -22,9 +26,4 @@ namespace interpreter::api { return Result::KeyNotFound; } - - std::unique_ptr NopSignatureVerifier::create(infrastructure::Context &context) - { - return std::make_unique(context); - } } diff --git a/source/lib/interpreter/api/source/SignatureVerifier.cpp b/source/lib/interpreter/api/source/SignatureVerifier.cpp index 6cc54f6f..a4844b91 100644 --- a/source/lib/interpreter/api/source/SignatureVerifier.cpp +++ b/source/lib/interpreter/api/source/SignatureVerifier.cpp @@ -15,10 +15,10 @@ namespace interpreter::api { std::unique_ptr SignatureVerifier::create( infrastructure::Context &context, - std::filesystem::path const &uicSignatureXml) + std::filesystem::path const &uicPublicKeyXmlFile) { #ifdef WITH_SIGNATURE_VERIFIER - return std::make_unique(context, uicSignatureXml); + return std::make_unique(context, uicPublicKeyXmlFile); #else return createDummy(context); #endif diff --git a/source/lib/interpreter/detail/common/include/BCDDecoder.h b/source/lib/interpreter/detail/common/include/BCDDecoder.h new file mode 100644 index 00000000..5376e114 --- /dev/null +++ b/source/lib/interpreter/detail/common/include/BCDDecoder.h @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: (C) 2025 user4223 and (other) contributors to ticket-decoder +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include + +namespace interpreter::detail::common +{ + class Context; + + /* Decoder for binary coded decimal numbers (BCD). + */ + class BCDDecoder + { + public: + /* Consumes 2 bytes in big endian order and decodes packed binary coded decimal value + */ + static std::uint16_t consumePackedInteger2(Context &context); + + /* Decodes 2 bytes in big endian order and decodes packed binary coded decimal value + */ + static std::uint16_t decodePackedInteger2(std::span bytes); + + /* Consumes 1 byte and decodes packed binary coded decimal value + */ + static std::uint8_t consumePackedInteger1(Context &context); + + /* Decodes 1 byte and decodes packed binary coded decimal value + */ + static std::uint8_t decodePackedInteger1(std::uint8_t byte); + }; +} diff --git a/source/lib/interpreter/detail/common/include/Context.h b/source/lib/interpreter/detail/common/include/Context.h index cbc4f5ae..1ac58faf 100644 --- a/source/lib/interpreter/detail/common/include/Context.h +++ b/source/lib/interpreter/detail/common/include/Context.h @@ -16,110 +16,181 @@ namespace interpreter::detail::common { - struct Context - { - std::size_t inputSize; - std::vector::const_iterator begin; - std::vector::const_iterator position; - std::vector::const_iterator end; - std::map output; - std::map records; + class Context + { + using IteratorType = std::span::iterator; + + std::optional> raw; + std::span data; + IteratorType begin; + IteratorType position; + IteratorType end; + std::map output; + std::map records; + + public: + /* Does NOT take ownership of input data + */ + Context(std::vector const &input, std::string origin); + /* Does NOT take ownership of input data + */ + Context(std::vector const &input, Context &&otherContext); + /* Does NOT take ownership of input data + */ + Context(std::span input); + /* Takes ownership of data + */ + Context(std::vector &&input); + + Context(Context const &) = delete; + Context &operator=(Context const &) = delete; + + Context(Context &&) = default; + Context &operator=(Context &&) = default; + + /* Returns a copy of iterator to the current position. + Attention! The copy gets not updated when the internal position moves on. + */ + IteratorType getPosition() const; + + /* Returns just one byte from current position + to current position + 1 without consumtion. + Throws runtime_error if size exceeds remaining bytes. + */ + std::uint8_t peekByte() const; + + /* Returns just one byte from current position + offset + to current position + offset + 1 without consumtion. + Throws runtime_error if size exceeds remaining bytes. + */ + std::uint8_t peekByte(std::size_t offset) const; + + /* Returns size bytes in a vector from current position + to current position + size without consumtion. + Throws runtime_error if size exceeds remaining bytes. + */ + std::span peekBytes(std::size_t size) const; + + /* Returns size bytes in a span from current position + offset + to current position + offset + size without consumtion. + Throws runtime_error if offset + size exceeds remaining bytes. + */ + std::span peekBytes(std::size_t offset, std::size_t size) const; + + /* Returns and consumes just one byte from current position + to current position + 1. + */ + std::uint8_t consumeByte(); + + /* Returns and consumes size bytes from current position + to current position + size. + Throws runtime_error if size exceeds remaining bytes. + */ + std::span consumeBytes(std::size_t size); + + /* Returns and consumes just one byte from end of all bytes. + Throws runtime_error if size exceeds remaining bytes. + */ + std::uint8_t consumeByteEnd(); + + /* Returns and consumes size bytes from end of all bytes. + Throws runtime_error if size exceeds remaining bytes. + */ + std::span consumeBytesEnd(std::size_t size); + + /* Returns and consumes as a maximum size bytes from current position + to current position + 0...size. + */ + std::span consumeMaximalBytes(std::size_t size); + + /* Returns and consumes all remaining bytes from current + postion to end. + */ + std::span consumeRemainingBytes(); + + /* Consumes and copies all remaining bytes from current + postion to end. + */ + std::vector consumeRemainingBytesCopy(); + + /* Consumes all remaining bytes from current postion to end, + appends given postfix string to the end and returns a copy. + */ + std::vector consumeRemainingBytesAppend(std::span postfix); + + /* Ignores and skips size bytes from current position + to current position + size. + */ + std::size_t ignoreBytes(std::size_t size); - Context(std::vector const &input, std::string origin); - Context(std::vector const &input, std::map &&f); + /* Consumes expected bytes from current position when they + match, otherwise nothing is consumed. + Returns true when matching bytes has been consumed, false otherwise. + */ + bool ignoreBytesIf(std::vector expectedValue); - Context(Context const &) = delete; - Context &operator=(Context const &) = delete; + /* Ignores and skips all remaining bytes from current + position to end. + */ + std::size_t ignoreRemainingBytes(); - Context(Context &&) = default; - Context &operator=(Context &&) = default; + /* Returns all bytes from begin to end as base64 + encoded string. This does NOT consume bytes, it just returns the + entire buffer as base64 encoded string. + */ + std::string getAllBase64Encoded() const; - /* Returns a copy of iterator to the current position. - Attention! The copy gets not updated when the internal position moves on. - */ - std::vector::const_iterator getPosition() const; + bool hasInput() const; - /* Returns size bytes in a vector from current position - to current position + size without consumtion. - Throws runtime_error if size exceeds remaining bytes. - */ - std::span peekBytes(std::size_t size); + bool hasOutput() const; - /* Returns size bytes in a span from current position + offset - to current position + offset + size without consumtion. - Throws runtime_error if offset + size exceeds remaining bytes. - */ - std::span peekBytes(std::size_t offset, std::size_t size); + bool isEmpty() const; - /* Returns and consumes size bytes from current position - to current position + size. - Throws runtime_error if size exceeds remaining bytes. - */ - std::span consumeBytes(std::size_t size); + void ensureEmpty() const; - /* Returns and consumes as a maximum size bytes from current position - to current position + 0...size. - */ - std::span consumeMaximalBytes(std::size_t size); + void ensureRemaining(std::size_t size) const; - /* Returns and consumes all remaining bytes from current - postion to end. - */ - std::span consumeRemainingBytes(); + constexpr std::size_t getOverallSize() const + { + return std::distance(begin, end); + } - /* Ignores and skips size bytes from current position - to current position + size. - */ - std::size_t ignoreBytes(std::size_t size); + constexpr std::size_t getRemainingSize() const + { + return std::distance(position, end); + } - /* Ignores and skips all remaining bytes from current - position to end. - */ - std::size_t ignoreRemainingBytes(); + constexpr std::size_t getConsumedSize() const + { + return std::distance(begin, position); + } - /* Returns all bytes from begin to end as base64 - encoded string. - */ - std::string getAllBase64Encoded() const; + // Fields - bool hasInput() const; + std::map const &getFields() const; - bool hasOutput() const; + std::optional getField(std::string key) const; - bool isEmpty() const; + Context &ifField(std::string key, std::function consumer); - std::size_t getOverallSize() const; + Context &setField(std::string key, Field &&field); - std::size_t getRemainingSize() const; + Context &addField(std::string key, std::string value); - std::size_t getConsumedSize() const; + Context &addField(std::string key, std::string value, std::string description); - // Fields + Context &addField(std::string key, std::string value, std::optional description); - std::map const &getFields() const; + // Json - std::optional getField(std::string key) const; + std::optional getJson(int indent = -1); - Context &ifField(std::string key, std::function consumer); + // Records - Context &setField(std::string key, Field &&field); + Context &addRecord(Record &&record); - Context &addField(std::string key, std::string value); + Record const &getRecord(std::string recordKey) const; - Context &addField(std::string key, std::string value, std::string description); - - Context &addField(std::string key, std::string value, std::optional description); - - // Json - - std::optional getJson(int indent = -1); - - // Records - - Context &addRecord(Record &&record); - - Record const &getRecord(std::string recordKey) const; - - std::map const &getRecords() const; - }; + std::map const &getRecords() const; + }; } diff --git a/source/lib/interpreter/detail/common/include/DateTimeDecoder.h b/source/lib/interpreter/detail/common/include/DateTimeDecoder.h new file mode 100644 index 00000000..0ff7f263 --- /dev/null +++ b/source/lib/interpreter/detail/common/include/DateTimeDecoder.h @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: (C) 2026 user4223 and (other) contributors to ticket-decoder +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include +#include + +namespace interpreter::detail::common +{ + class Context; + + class DateTimeDecoder + { + public: + /* Consumes 4 bytes and decodes date-time to ISO-8601 format + */ + static std::string consumeDateTimeCompact4(Context &context); + static std::string decodeDateTimeCompact4(std::span bytes); + + /* Consumes 12 bytes (ASCII) and decodes date-time to ISO-8601 format + */ + static std::string consumeDateTime12(Context &context); + + /* Consumes 8 bytes (ASCII) and decodes date to ISO-8601 format + */ + static std::string consumeDate8(Context &context); + }; +} diff --git a/source/lib/interpreter/detail/common/include/InterpreterUtility.h b/source/lib/interpreter/detail/common/include/InterpreterUtility.h deleted file mode 100644 index 1015222d..00000000 --- a/source/lib/interpreter/detail/common/include/InterpreterUtility.h +++ /dev/null @@ -1,32 +0,0 @@ -// SPDX-FileCopyrightText: (C) 2022 user4223 and (other) contributors to ticket-decoder -// SPDX-License-Identifier: GPL-3.0-or-later - -#pragma once - -#include "Context.h" - -#include -#include - -namespace interpreter::detail::common -{ - std::string getAlphanumeric(Context &context, std::size_t size); - - std::uint32_t getNumeric32(Context &context); - - std::uint32_t getNumeric24(Context &context); - - std::uint16_t getNumeric16(Context &context); - - std::uint8_t getNumeric8(Context &context); - - std::string getDateTimeCompact(Context &context); - - std::string getDateTime12(Context &context); - - std::string getDate8(Context &context); - - std::string bytesToString(std::span bytes); - - std::string bytesToString(std::vector const &bytes); -} diff --git a/source/lib/interpreter/detail/common/include/NumberDecoder.h b/source/lib/interpreter/detail/common/include/NumberDecoder.h new file mode 100644 index 00000000..066d814c --- /dev/null +++ b/source/lib/interpreter/detail/common/include/NumberDecoder.h @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: (C) 2022 user4223 and (other) contributors to ticket-decoder +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include + +namespace interpreter::detail::common +{ + class Context; + + class NumberDecoder + { + public: + /* Consumes 4 bytes and converts from big-endian to system byte order + */ + static std::uint32_t consumeInteger4(Context &context); + + /* Consumes 3 bytes and converts from big-endian to system byte order + */ + static std::uint32_t consumeInteger3(Context &context); + + /* Consumes 2 bytes and converts from big-endian to system byte order + */ + static std::uint16_t consumeInteger2(Context &context); + + /* Consumes 1 byte + */ + static std::uint8_t consumeInteger1(Context &context); + }; +} diff --git a/source/lib/interpreter/detail/common/include/StringDecoder.h b/source/lib/interpreter/detail/common/include/StringDecoder.h new file mode 100644 index 00000000..6a428763 --- /dev/null +++ b/source/lib/interpreter/detail/common/include/StringDecoder.h @@ -0,0 +1,62 @@ +// SPDX-FileCopyrightText: (C) 2026 user4223 and (other) contributors to ticket-decoder +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace interpreter::detail::common +{ + + class Context; + + class StringDecoder + { + public: + /* Consumes maximumBytes or less bytes (NOT chars) and returns a 0 terminated UTF8 string. + When the buffer is filled by multiple 0 at the end, the returned string might be shorter. + In case you are sure the buffer contains ASCII only, the method works fine and is the + preferred method over consumeLatin1 since it avoids additional conversion. + */ + static std::string consumeUTF8(Context &context, std::size_t maximumBytes); + static std::string decodeUTF8(std::span bytes); + + /* Consumes maximumBytes or less ISO 8859-1 (Latin-1) encoded bytes and returns a 0 + terminated UTF8 string. + When the buffer is filled by multiple 0 at the end, the returned string might be shorter. + */ + static std::string consumeLatin1(Context &context, std::size_t maximumBytes); + static std::string decodeLatin1(std::span bytes); + + /* Consumes maximumBytes or less bytes and ensures it's plain ASCII (<128) and returns a + 0 terminated UTF8 string (which is the same as ASCII in this case). + */ + static std::string consumeASCII(Context &context, std::size_t maximumBytes, bool ensurePrintable = false); + static std::string decodeASCII(std::span bytes, bool ensurePrintable = false); + + /* Returns a string containing the hexadecimal representation of the given byte buffer. + */ + static std::string toHexString(std::span bytes); + static std::string toHexString(std::vector const &bytes); + + template + static std::string toHexString(std::array const &bytes) + { + return toHexString(std::span(bytes.data(), S)); + } + + template + static std::string toHexString(T const &bytes) + { + auto const raw = std::span((std::uint8_t const *const)&bytes, sizeof(T)); + return std::endian::native == std::endian::big + ? toHexString(raw) + : toHexString(std::vector(raw.rbegin(), raw.rend())); + } + }; +} diff --git a/source/lib/interpreter/detail/common/include/TLVDecoder.h b/source/lib/interpreter/detail/common/include/TLVDecoder.h new file mode 100644 index 00000000..ad89895d --- /dev/null +++ b/source/lib/interpreter/detail/common/include/TLVDecoder.h @@ -0,0 +1,97 @@ +// SPDX-FileCopyrightText: (C) 2025 user4223 and (other) contributors to ticket-decoder +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +namespace interpreter::detail::common +{ + class Context; + + class TLVTag + { + std::uint32_t value; // this is used in big endian byte order, so do not access it directly or be confused on little endian machines + std::size_t currentSize; + + constexpr std::uint8_t *getByte(std::size_t index) const { return ((std::uint8_t *)&value) + index; } + + public: + constexpr TLVTag() : value(0), currentSize(0) {} + constexpr TLVTag(std::initializer_list const bytes) : TLVTag::TLVTag() + { + auto const *source = bytes.begin(); + auto *destination = getByte(0); + std::size_t index = 0; + for (; index < bytes.size() && index < maximumSize(); ++index) + { + destination[index] = source[index]; + } + currentSize = index; + } + + TLVTag(TLVTag const &) = default; + TLVTag(TLVTag &&) = default; + + TLVTag &operator=(TLVTag const &) = default; + TLVTag &operator=(TLVTag &&) = default; + + constexpr void assign(std::size_t index, std::uint8_t value) + { + if (index + 1 > currentSize) + { + currentSize = index + 1; + } + *getByte(index) = value; + } + + constexpr std::uint8_t const &operator[](std::size_t index) const { return *getByte(index); } + + constexpr std::size_t size() const { return currentSize; } + + constexpr std::size_t maximumSize() const { return sizeof(decltype(value)); } + + constexpr bool operator==(TLVTag const &rhs) const { return currentSize == rhs.currentSize && value == rhs.value; } + + constexpr bool operator!=(TLVTag const &rhs) const { return currentSize != rhs.currentSize || value != rhs.value; } + + constexpr bool operator<(TLVTag const &rhs) const + { + return std::tie(value, currentSize) < std::tie(rhs.value, rhs.currentSize); + }; + + void ensureEqual(TLVTag const &rhs) const; + + std::string toHexString() const; + }; + + class TLVDecoder + { + public: + using TagMapType = std::map)>>; + + private: + TagMapType const tagMap; + + public: + TLVDecoder(TagMapType tagMap); + + std::tuple consume(common::Context &context) const; + + std::tuple consume(std::span bytes) const; + + static TLVTag consumeTag(common::Context &context); + + static common::Context &consumeExpectedTag(common::Context &context, common::TLVTag const &expectedTag); + + static std::size_t consumeLength(common::Context &context); + + static std::span consumeExpectedElement(common::Context &context, common::TLVTag const &expectedTag); + }; +} diff --git a/source/lib/interpreter/detail/common/source/BCDDecoder.cpp b/source/lib/interpreter/detail/common/source/BCDDecoder.cpp new file mode 100644 index 00000000..10dbdf7e --- /dev/null +++ b/source/lib/interpreter/detail/common/source/BCDDecoder.cpp @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: (C) 2025 user4223 and (other) contributors to ticket-decoder +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "../include/BCDDecoder.h" + +#include "lib/interpreter/detail/common/include/Context.h" + +#include + +namespace interpreter::detail::common +{ + + std::uint16_t BCDDecoder::consumePackedInteger2(Context &context) + { + return decodePackedInteger2(context.consumeBytes(2)); + } + + std::uint16_t BCDDecoder::decodePackedInteger2(std::span bytes) + { + if (bytes.size() < 2) { + throw std::runtime_error(std::string("Less than expected bytes available, expecting at least: 2")); + } + std::uint16_t const high = decodePackedInteger1(bytes[0]); + std::uint16_t const low = decodePackedInteger1(bytes[1]); + return high * 100 + low; + } + + std::uint8_t BCDDecoder::consumePackedInteger1(Context &context) + { + return decodePackedInteger1(context.consumeByte()); + } + + std::uint8_t BCDDecoder::decodePackedInteger1(std::uint8_t byte) + { + std::uint8_t const high = byte >> 4 & 0x0F; + std::uint8_t const low = byte & 0x0F; + return high * 10 + low; + } +} diff --git a/source/lib/interpreter/detail/common/source/Context.cpp b/source/lib/interpreter/detail/common/source/Context.cpp index 8404cfde..3a4ded62 100644 --- a/source/lib/interpreter/detail/common/source/Context.cpp +++ b/source/lib/interpreter/detail/common/source/Context.cpp @@ -12,60 +12,109 @@ namespace interpreter::detail::common { Context::Context(std::vector const &input, std::string origin) - : inputSize(input.size()), - begin(input.cbegin()), + : raw(std::nullopt), + data(input.data(), input.size()), + begin(std::begin(data)), position(begin), - end(input.cend()) + end(std::end(data)), + output(), + records() { addField("origin", origin); } - Context::Context(std::vector const &input, std::map &&fields) - : inputSize(input.size()), - begin(input.cbegin()), + Context::Context(std::vector const &input, Context &&otherContext) + : raw(std::nullopt), + data(input.data(), input.size()), + begin(std::begin(data)), position(begin), - end(input.cend()), - output(std::move(fields)) + end(std::end(data)), + output(std::move(otherContext.output)), + records(std::move(otherContext.records)) { } - std::vector::const_iterator Context::getPosition() const + Context::Context(std::span input) + : raw(std::nullopt), + data(std::move(input)), + begin(std::begin(data)), + position(begin), + end(std::end(data)), + output(), + records() + { + } + + Context::Context(std::vector &&input) + : raw(std::make_optional(std::move(input))), + data(std::begin(*raw), std::end(*raw)), + begin(std::begin(data)), + position(begin), + end(std::end(data)), + output(), + records() + { + } + + Context::IteratorType Context::getPosition() const { return position; } - std::span Context::peekBytes(std::size_t size) + std::uint8_t Context::peekByte() const { - if (getRemainingSize() < size) - { - throw std::runtime_error("Not enough bytes available to peek"); - } + ensureRemaining(1); + return *position; + } - return std::span(position, size); + std::uint8_t Context::peekByte(std::size_t offset) const + { + ensureRemaining(offset + 1); + return *(position + offset); } - std::span Context::peekBytes(std::size_t offset, std::size_t size) + std::span Context::peekBytes(std::size_t size) const { - if (getRemainingSize() < (offset + size)) - { - throw std::runtime_error("Not enough bytes available to peek"); - } + ensureRemaining(size); + return std::span(position, size); + } + std::span Context::peekBytes(std::size_t offset, std::size_t size) const + { + ensureRemaining(offset + size); return std::span(position + offset, size); } - std::span Context::consumeBytes(std::size_t size) + std::uint8_t Context::consumeByte() { - if (getRemainingSize() < size) - { - throw std::runtime_error("Not enough bytes available to consume"); - } + ensureRemaining(1); + auto value = *position; + position += 1; + return value; + } + std::span Context::consumeBytes(std::size_t size) + { + ensureRemaining(size); auto result = std::span(position, size); position += size; return result; } + std::uint8_t Context::consumeByteEnd() + { + ensureRemaining(1); + end -= 1; + return *end; + } + + std::span Context::consumeBytesEnd(std::size_t size) + { + ensureRemaining(size); + end -= size; + return std::span(end, size); + } + std::span Context::consumeMaximalBytes(std::size_t size) { return consumeBytes(std::min(getRemainingSize(), size)); @@ -76,15 +125,44 @@ namespace interpreter::detail::common return consumeBytes(getRemainingSize()); } + std::vector Context::consumeRemainingBytesCopy() + { + auto const data = consumeRemainingBytes(); + return {data.begin(), data.end()}; + } + + std::vector Context::consumeRemainingBytesAppend(std::span postfix) + { + auto data = consumeRemainingBytesCopy(); + data.insert(data.end(), postfix.begin(), postfix.end()); + return data; + } + std::size_t Context::ignoreBytes(std::size_t size) { - if (getRemainingSize() < size) + ensureRemaining(size); + std::advance(position, size); + return size; + } + + bool Context::ignoreBytesIf(std::vector expectedValue) + { + if (getRemainingSize() < expectedValue.size()) { - throw std::runtime_error("Not enough bytes available to ignore"); + return false; } - std::advance(position, size); - return size; + auto index = std::size_t(0); + for (auto const value : expectedValue) + { + if (value != peekByte(index++)) + { + return false; + } + } + + ignoreBytes(expectedValue.size()); + return true; } std::size_t Context::ignoreRemainingBytes() @@ -99,7 +177,7 @@ namespace interpreter::detail::common bool Context::hasInput() const { - return inputSize > 0; + return data.size() > 0; } bool Context::hasOutput() const @@ -112,19 +190,20 @@ namespace interpreter::detail::common return position == end; } - std::size_t Context::getOverallSize() const - { - return std::distance(begin, end); - } - - std::size_t Context::getRemainingSize() const + void Context::ensureEmpty() const { - return std::distance(position, end); + if (!isEmpty()) + { + throw std::runtime_error(std::string("Expecting fully consumed context, but found remaining bytes: ") + std::to_string(getRemainingSize())); + } } - std::size_t Context::getConsumedSize() const + void Context::ensureRemaining(std::size_t size) const { - return std::distance(begin, position); + if (getRemainingSize() < size) + { + throw std::runtime_error(std::string("Less than expected bytes available, expecting at least: ") + std::to_string(size)); + } } std::map const &Context::getFields() const diff --git a/source/lib/interpreter/detail/common/source/DateTimeDecoder.cpp b/source/lib/interpreter/detail/common/source/DateTimeDecoder.cpp new file mode 100644 index 00000000..e365857d --- /dev/null +++ b/source/lib/interpreter/detail/common/source/DateTimeDecoder.cpp @@ -0,0 +1,68 @@ +// SPDX-FileCopyrightText: (C) 2026 user4223 and (other) contributors to ticket-decoder +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "../include/DateTimeDecoder.h" +#include "../include/StringDecoder.h" +#include "../include/NumberDecoder.h" +#include "../include/Context.h" + +#include +#include + +namespace interpreter::detail::common +{ + + std::string DateTimeDecoder::consumeDateTimeCompact4(Context &context) + { + auto const date = NumberDecoder::consumeInteger2(context); + auto const time = NumberDecoder::consumeInteger2(context); + + if (date == 0 && time == 0) + { + return {"0000-00-00T00:00:00"}; + } + + // TODO Use chrono parse or apply validation for all values + std::ostringstream os; + os << std::setw(4) << std::setfill('0') << std::to_string(((date & 0xFE00) >> 9) + 1990) << "-" + << std::setw(2) << std::setfill('0') << std::to_string(((date & 0x01E0) >> 5)) << "-" + << std::setw(2) << std::setfill('0') << std::to_string(((date & 0x001F) >> 0)) << "T" + << std::setw(2) << std::setfill('0') << std::to_string(((time & 0xF800) >> 11)) << ":" + << std::setw(2) << std::setfill('0') << std::to_string(((time & 0x07E0) >> 5)) << ":" + << std::setw(2) << std::setfill('0') << std::to_string(((time & 0x001F) >> 0)); + return os.str(); + } + + std::string DateTimeDecoder::decodeDateTimeCompact4(std::span bytes) + { + auto context = Context(bytes); + return consumeDateTimeCompact4(context); + } + + std::string DateTimeDecoder::consumeDateTime12(Context &context) + { + auto const input = StringDecoder::consumeASCII(context, 12, true); + auto const p = input.begin(); + // TODO Use chrono parse or apply validation for all values + std::ostringstream os; // DDMMYYYYHHMM + os << std::string(p + 4, p + 8) << "-" + << std::string(p + 2, p + 4) << "-" + << std::string(p + 0, p + 2) << "T" + << std::string(p + 8, p + 10) << ":" + << std::string(p + 10, p + 12) << ":" + << "00"; + return os.str(); + } + + std::string DateTimeDecoder::consumeDate8(Context &context) + { + auto const input = StringDecoder::consumeASCII(context, 8, true); + auto const p = input.begin(); + // TODO Use chrono parse or apply validation for all values + std::ostringstream os; // DDMMYYYY + os << std::string(p + 4, p + 8) << "-" + << std::string(p + 2, p + 4) << "-" + << std::string(p + 0, p + 2); + return os.str(); + } +} diff --git a/source/lib/interpreter/detail/common/source/InterpreterUtility.cpp b/source/lib/interpreter/detail/common/source/InterpreterUtility.cpp deleted file mode 100644 index c29fcb31..00000000 --- a/source/lib/interpreter/detail/common/source/InterpreterUtility.cpp +++ /dev/null @@ -1,132 +0,0 @@ -// SPDX-FileCopyrightText: (C) 2022 user4223 and (other) contributors to ticket-decoder -// SPDX-License-Identifier: GPL-3.0-or-later - -#include "../include/InterpreterUtility.h" - -#include -#include -#include -#include -#include -#include -#include - -namespace interpreter::detail::common -{ - std::string getAlphanumeric(Context &context, std::size_t size) - { - auto const data = context.consumeMaximalBytes(size); - auto result = std::string{std::begin(data), std::find(std::begin(data), std::end(data), '\0')}; - result.erase(std::find_if(std::rbegin(result), std::rend(result), [](unsigned char ch) - { return !std::isspace(ch); }) - .base(), - std::end(result)); - return result; - } - - template - T getNumeric(Context &context, std::size_t sourceLength = sizeof(T)) - { - if (sizeof(T) < sourceLength) - { - throw std::runtime_error("Destination size must be equal or greater than source size"); - } - - auto const source = context.consumeBytes(sourceLength); - auto result = T(); - auto destination = std::span(reinterpret_cast(&result), sizeof(T)); - - // TODO This depends on endianess, test and verify big-endian style conversion - if constexpr (std::endian::native == std::endian::big) - { - std::copy(source.begin(), source.end(), destination.begin()); - } - else - { - std::copy(source.rbegin(), source.rend(), destination.begin()); - } - - return result; - } - - std::uint32_t getNumeric32(Context &context) - { - return getNumeric(context); - } - - std::uint32_t getNumeric24(Context &context) - { - return getNumeric(context, 3); - } - - std::uint16_t getNumeric16(Context &context) - { - return getNumeric(context); - } - - std::uint8_t getNumeric8(Context &context) - { - return getNumeric(context); - } - - std::string getDateTimeCompact(Context &context) - { - auto const date = common::getNumeric16(context); - auto const time = common::getNumeric16(context); - // TODO Use chrono parse or apply validation for all values - std::ostringstream os; - os << std::setw(4) << std::setfill('0') << std::to_string(((date & 0xFE00) >> 9) + 1990) << "-" - << std::setw(2) << std::setfill('0') << std::to_string(((date & 0x01E0) >> 5)) << "-" - << std::setw(2) << std::setfill('0') << std::to_string(((date & 0x001F) >> 0)) << "T" - << std::setw(2) << std::setfill('0') << std::to_string(((time & 0xF800) >> 11)) << ":" - << std::setw(2) << std::setfill('0') << std::to_string(((time & 0x07E0) >> 5)) << ":" - << std::setw(2) << std::setfill('0') << std::to_string(((time & 0x001F) >> 0)); - return os.str(); - } - - std::string getDateTime12(Context &context) - { - auto const input = getAlphanumeric(context, 12); - auto const p = input.begin(); - // TODO Use chrono parse or apply validation for all values - std::ostringstream os; // DDMMYYYYHHMM - os << std::string(p + 4, p + 8) << "-" - << std::string(p + 2, p + 4) << "-" - << std::string(p + 0, p + 2) << "T" - << std::string(p + 8, p + 10) << ":" - << std::string(p + 10, p + 12) << ":" - << "00"; - return os.str(); - } - - std::string getDate8(Context &context) - { - auto const input = getAlphanumeric(context, 8); - auto const p = input.begin(); - // TODO Use chrono parse or apply validation for all values - std::ostringstream os; // DDMMYYYY - os << std::string(p + 4, p + 8) << "-" - << std::string(p + 2, p + 4) << "-" - << std::string(p + 0, p + 2); - return os.str(); - } - - std::string bytesToString(std::span typeId) - { - if (typeId.empty()) - { - return ""; - } - - std::stringstream os; - os << "0x"; - std::for_each(std::begin(typeId), std::end(typeId), [&](auto const &byte) - { os << std::hex << std::uppercase << std::setw(2) << std::setfill('0') << (int)byte; }); - return os.str(); - } - - std::string bytesToString(std::vector const &typeId) - { - return bytesToString(std::span(typeId.data(), typeId.size())); - } -} \ No newline at end of file diff --git a/source/lib/interpreter/detail/common/source/NumberDecoder.cpp b/source/lib/interpreter/detail/common/source/NumberDecoder.cpp new file mode 100644 index 00000000..2c685815 --- /dev/null +++ b/source/lib/interpreter/detail/common/source/NumberDecoder.cpp @@ -0,0 +1,59 @@ +// SPDX-FileCopyrightText: (C) 2022 user4223 and (other) contributors to ticket-decoder +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "../include/NumberDecoder.h" + +#include "lib/interpreter/detail/common/include/StringDecoder.h" +#include "lib/interpreter/detail/common/include/Context.h" + +#include +#include +#include + +namespace interpreter::detail::common +{ + + template + T getInteger(Context &context, std::size_t sourceLength = sizeof(T)) + { + if (sizeof(T) < sourceLength) + { + throw std::runtime_error("Destination size must be equal or greater than source size"); + } + + auto const source = context.consumeBytes(sourceLength); + auto result = T(); + auto destination = std::span(reinterpret_cast(&result), sizeof(T)); + + if constexpr (std::endian::native == std::endian::big) + { + std::copy(source.begin(), source.end(), destination.begin() + sizeof(T) - sourceLength); + } + else + { + std::copy(source.rbegin(), source.rend(), destination.begin()); + } + + return result; + } + + std::uint32_t NumberDecoder::consumeInteger4(Context &context) + { + return getInteger(context); + } + + std::uint32_t NumberDecoder::consumeInteger3(Context &context) + { + return getInteger(context, 3); + } + + std::uint16_t NumberDecoder::consumeInteger2(Context &context) + { + return getInteger(context); + } + + std::uint8_t NumberDecoder::consumeInteger1(Context &context) + { + return context.consumeByte(); + } +} \ No newline at end of file diff --git a/source/lib/interpreter/detail/common/source/StringDecoder.cpp b/source/lib/interpreter/detail/common/source/StringDecoder.cpp new file mode 100644 index 00000000..3bc8bc02 --- /dev/null +++ b/source/lib/interpreter/detail/common/source/StringDecoder.cpp @@ -0,0 +1,114 @@ +// SPDX-FileCopyrightText: (C) 2026 user4223 and (other) contributors to ticket-decoder +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "../include/StringDecoder.h" + +#include "lib/interpreter/detail/common/include/Context.h" + +#include +#include +#include +#include +#include + +namespace interpreter::detail::common +{ + static std::string latin1ToUtf8(std::string &&latin1) + { + auto utf8 = std::string{}; + utf8.reserve(latin1.capacity()); + for (std::uint8_t const &ch : latin1) + { + if (ch < 0x80) + { + utf8.push_back(ch); + } + else + { + utf8.push_back(0xc0 | ch >> 6); + utf8.push_back(0x80 | (ch & 0x3f)); + } + } + return utf8; + } + + static constexpr std::string toString(std::span bytes) + { + auto const first = std::begin(bytes); + return std::string{first, std::find(first, std::end(bytes), '\0')}; + } + + static std::string removeTrailingSpaces(std::string &&input) + { + auto const last = std::rbegin(input); + auto const position = std::find_if(last, std::rend(input), [](unsigned char ch) + { return !std::isspace(ch); }) + .base(); + + auto const end = std::end(input); + if (position != end) + { + input.erase(position, end); + } + return input; + } + + std::string StringDecoder::consumeUTF8(Context &context, std::size_t maximumBytes) + { + return decodeUTF8(context.consumeMaximalBytes(maximumBytes)); + } + + std::string StringDecoder::decodeUTF8(std::span bytes) + { + return removeTrailingSpaces(toString(bytes)); + } + + std::string StringDecoder::consumeLatin1(Context &context, std::size_t maximumBytes) + { + return decodeLatin1(context.consumeMaximalBytes(maximumBytes)); + } + + std::string StringDecoder::decodeLatin1(std::span bytes) + { + return removeTrailingSpaces(latin1ToUtf8(toString(bytes))); + } + + std::string StringDecoder::consumeASCII(Context &context, std::size_t maximumBytes, bool ensurePrintable) + { + return decodeASCII(context.consumeMaximalBytes(maximumBytes), ensurePrintable); + } + + std::string StringDecoder::decodeASCII(std::span bytes, bool ensurePrintable) + { + auto const comparator = ensurePrintable // clang-format off + ? [](std::uint8_t const &ch) -> bool { return ch > 0x7E || ch < 0x20; } + : [](std::uint8_t const &ch) -> bool { return ch > 0x7F; }; // clang-format on + + auto const end = std::end(bytes); + auto const match = std::find_if(std::begin(bytes), end, comparator); + if (match != end) + { + throw std::runtime_error(std::string("Unexpected (non-ascii or non-printable) character found: ") + toHexString(*match)); + } + + return removeTrailingSpaces(toString(bytes)); + } + + std::string StringDecoder::toHexString(std::span bytes) + { + if (bytes.empty()) + { + return {}; + } + + std::stringstream os; + std::for_each(std::begin(bytes), std::end(bytes), [&](auto const &byte) + { os << std::hex << std::uppercase << std::setw(2) << std::setfill('0') << (std::uint32_t)byte; }); + return os.str(); + } + + std::string StringDecoder::toHexString(std::vector const &bytes) + { + return toHexString(std::span(bytes.data(), bytes.size())); + } +} diff --git a/source/lib/interpreter/detail/common/source/TLVDecoder.cpp b/source/lib/interpreter/detail/common/source/TLVDecoder.cpp new file mode 100644 index 00000000..ab2107f0 --- /dev/null +++ b/source/lib/interpreter/detail/common/source/TLVDecoder.cpp @@ -0,0 +1,127 @@ +// SPDX-FileCopyrightText: (C) 2025 user4223 and (other) contributors to ticket-decoder +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "../include/TLVDecoder.h" + +#include "lib/interpreter/detail/common/include/Context.h" + +#include + +namespace interpreter::detail::common +{ + void TLVTag::ensureEqual(TLVTag const &rhs) const + { + if (*this != rhs) + { + throw std::runtime_error(std::string("Unexpected tag found: ") + toHexString()); + } + } + + std::string TLVTag::toHexString() const + { + std::stringstream os; + auto const bytes = std::span(getByte(0), size()); + std::for_each(std::begin(bytes), std::end(bytes), [&](auto const &byte) + { os << std::hex << std::uppercase << std::setw(2) << std::setfill('0') << (int)byte; }); + return os.str(); + } + + TLVDecoder::TLVDecoder(TagMapType tm) + : tagMap(std::move(tm)) + { + } + + std::tuple TLVDecoder::consume(common::Context &context) const + { + auto matchCount = std::size_t{0}; + auto ignoreCount = std::size_t{0}; + while (!context.isEmpty()) + { + auto const tag = consumeTag(context); + auto const length = consumeLength(context); + auto const entry = tagMap.find(tag); + if (entry != tagMap.end()) + { + auto const value = context.consumeBytes(length); + entry->second(value); + matchCount++; + } + else + { + context.ignoreBytes(length); + ignoreCount++; + } + } + return std::make_tuple(matchCount, ignoreCount); + } + + std::tuple TLVDecoder::consume(std::span bytes) const + { + auto context = common::Context(bytes); + return consume(context); + } + + TLVTag TLVDecoder::consumeTag(common::Context &context) + { + auto tag = TLVTag{}; + tag.assign(0, context.consumeByte()); + // auto const usage = (first & 0xC0) >> 6; // 0 universal, 1 application, 2 context-specific, 3 private + // auto const type = (first & 0x20) >> 5; // 0 primitive, 1 constructed + // auto const tag = (first & 0x1F); // 0b11111 (31) see further bytes or else single byte tag value + + if ((tag[0] & 0x1F) == 0x1F) + { + for (int index = 1; index < tag.maximumSize(); index++) + { + auto const byte = context.consumeByte(); + tag.assign(index, byte); + if ((byte & 0x80) == 0) + { + break; + } + } + } + + return tag; + } + + common::Context &TLVDecoder::consumeExpectedTag(common::Context &context, common::TLVTag const &expectedTag) + { + consumeTag(context).ensureEqual(expectedTag); + return context; + } + + std::size_t TLVDecoder::consumeLength(common::Context &context) + { + auto const first = context.consumeByte(); + if (first < 0x80) + { + return first; + } + + auto const maxSize = sizeof(std::size_t); + auto const length = first & 0x7f; + if (length > maxSize) + { + throw std::runtime_error(std::string("Expecting ") + std::to_string(maxSize) + " as a max no of successor bytes for length, found unsupported length: " + std::to_string(length)); + } + + auto const source = context.consumeBytes(length); + auto result = std::size_t(0); + auto const destination = std::span(reinterpret_cast(&result), maxSize); + if constexpr (std::endian::native == std::endian::little) + { + std::copy(source.rbegin(), source.rend(), destination.begin()); + } + else + { + std::copy(source.begin(), source.end(), destination.begin() + maxSize - length); + } + return result; + } + + std::span TLVDecoder::consumeExpectedElement(common::Context &context, common::TLVTag const &expectedTag) + { + return context.consumeBytes(consumeLength(consumeExpectedTag(context, expectedTag))); + } +} diff --git a/source/lib/interpreter/detail/sbb/source/SBBInterpreter.cpp b/source/lib/interpreter/detail/sbb/source/SBBInterpreter.cpp index 230312a7..cbe8b711 100644 --- a/source/lib/interpreter/detail/sbb/source/SBBInterpreter.cpp +++ b/source/lib/interpreter/detail/sbb/source/SBBInterpreter.cpp @@ -32,7 +32,7 @@ namespace interpreter::detail::sbb LOG_WARN(logger) << "Failed to parse SBB protobuf message, trying to continue..."; // return context; } - std::string json; + auto json = std::string{}; auto const status = google::protobuf::util::MessageToJsonString(sbb, &json); if (!status.ok()) { diff --git a/source/lib/interpreter/detail/uic918/include/RecordHeader.h b/source/lib/interpreter/detail/uic918/include/RecordHeader.h index f07c4a91..a9329f82 100644 --- a/source/lib/interpreter/detail/uic918/include/RecordHeader.h +++ b/source/lib/interpreter/detail/uic918/include/RecordHeader.h @@ -13,7 +13,7 @@ namespace interpreter::detail::uic { struct RecordHeader { - std::vector::const_iterator const start; + std::span::iterator const start; std::string const recordId; std::string const recordVersion; unsigned int const recordLength; diff --git a/source/lib/interpreter/detail/uic918/source/Record0080BL.cpp b/source/lib/interpreter/detail/uic918/source/Record0080BL.cpp index 8b380edc..f782ba23 100644 --- a/source/lib/interpreter/detail/uic918/source/Record0080BL.cpp +++ b/source/lib/interpreter/detail/uic918/source/Record0080BL.cpp @@ -3,7 +3,9 @@ #include "../include/Record0080BL.h" -#include "lib/interpreter/detail/common/include/InterpreterUtility.h" +#include "lib/interpreter/detail/common/include/NumberDecoder.h" +#include "lib/interpreter/detail/common/include/StringDecoder.h" +#include "lib/interpreter/detail/common/include/DateTimeDecoder.h" #include "lib/interpreter/detail/common/include/Record.h" #include "lib/utility/include/JsonBuilder.h" @@ -74,16 +76,16 @@ namespace interpreter::detail::uic auto const certificate2 = context.consumeBytes(11); builder - .add("validFrom", common::getDate8(context)) - .add("validTo", common::getDate8(context)) - .add("serial", common::getAlphanumeric(context, 8)); + .add("validFrom", common::DateTimeDecoder::consumeDate8(context)) + .add("validTo", common::DateTimeDecoder::consumeDate8(context)) + .add("serial", common::StringDecoder::consumeUTF8(context, 8)); }}, {std::string("03"), [](auto &context, auto &builder) { builder - .add("validFrom", common::getDate8(context)) - .add("validTo", common::getDate8(context)) - .add("serial", common::getAlphanumeric(context, 10)); + .add("validFrom", common::DateTimeDecoder::consumeDate8(context)) + .add("validTo", common::DateTimeDecoder::consumeDate8(context)) + .add("serial", common::StringDecoder::consumeUTF8(context, 10)); }}}; Record0080BL::Record0080BL(infrastructure::LoggerFactory &loggerFactory, RecordHeader &&h) @@ -98,14 +100,14 @@ namespace interpreter::detail::uic auto recordJson = ::utility::JsonBuilder::object(); // clang-format off recordJson - .add("ticketType", common::getAlphanumeric(context, 2)) - .add("trips", ::utility::toArray(std::stoi(common::getAlphanumeric(context, 1)), [&](auto &builder) + .add("ticketType", common::StringDecoder::consumeUTF8(context, 2)) + .add("trips", ::utility::toArray(std::stoi(common::StringDecoder::consumeUTF8(context, 1)), [&](auto &builder) { tripInterpreter(context, builder); })) - .add("fields", ::utility::toObject(std::stoi(common::getAlphanumeric(context, 2)), [&](auto & builder) + .add("fields", ::utility::toObject(std::stoi(common::StringDecoder::consumeUTF8(context, 2)), [&](auto & builder) { - auto const type = common::getAlphanumeric(context, 4); - auto const length = std::stoi(common::getAlphanumeric(context, 4)); - auto const content = common::getAlphanumeric(context, length); + auto const type = common::StringDecoder::consumeUTF8(context, 4); + auto const length = std::stoi(common::StringDecoder::consumeUTF8(context, 4)); + auto const content = common::StringDecoder::consumeUTF8(context, length); auto const annotation = annotationMap.find(type); builder diff --git a/source/lib/interpreter/detail/uic918/source/Record0080VU.cpp b/source/lib/interpreter/detail/uic918/source/Record0080VU.cpp index 40c73dd7..f6d792c9 100644 --- a/source/lib/interpreter/detail/uic918/source/Record0080VU.cpp +++ b/source/lib/interpreter/detail/uic918/source/Record0080VU.cpp @@ -3,7 +3,8 @@ #include "../include/Record0080VU.h" -#include "lib/interpreter/detail/common/include/InterpreterUtility.h" +#include "lib/interpreter/detail/common/include/NumberDecoder.h" +#include "lib/interpreter/detail/common/include/DateTimeDecoder.h" #include "lib/interpreter/detail/common/include/Record.h" #include "lib/utility/include/JsonBuilder.h" @@ -13,6 +14,9 @@ namespace interpreter::detail::uic { + + using namespace common; + Record0080VU::Record0080VU(infrastructure::LoggerFactory &loggerFactory, RecordHeader &&h) : AbstractRecord(CREATE_LOGGER(loggerFactory), std::move(h)) { @@ -23,16 +27,16 @@ namespace interpreter::detail::uic { auto recordJson = ::utility::JsonBuilder::object(); // clang-format off recordJson - .add("terminalNummer", std::to_string(common::getNumeric16(context))) - .add("samNummer", std::to_string(common::getNumeric24(context))) - .add("anzahlPersonen", std::to_string(common::getNumeric8(context))) - .add("efs", ::utility::toArray(common::getNumeric8(context), [&context](auto &builder) + .add("terminalNummer", std::to_string(NumberDecoder::consumeInteger2(context))) + .add("samNummer", std::to_string(NumberDecoder::consumeInteger3(context))) + .add("anzahlPersonen", std::to_string(NumberDecoder::consumeInteger1(context))) + .add("efs", ::utility::toArray(NumberDecoder::consumeInteger1(context), [&context](auto &builder) { // TODO Unsure if numeric is the proper interpretation of berechtigungsNummer - auto const berechtigungsNummer = common::getNumeric32(context); - auto const kvpOrganisationsId = common::getNumeric16(context); - auto const pvProduktnummer = common::getNumeric16(context); - auto const pvOrganisationsId = common::getNumeric16(context); + auto const berechtigungsNummer = NumberDecoder::consumeInteger4(context); + auto const kvpOrganisationsId = NumberDecoder::consumeInteger2(context); + auto const pvProduktnummer = NumberDecoder::consumeInteger2(context); + auto const pvOrganisationsId = NumberDecoder::consumeInteger2(context); auto const pvProduktbezeichnung = getProduktbezeichnung(pvOrganisationsId, pvProduktnummer); builder @@ -43,27 +47,27 @@ namespace interpreter::detail::uic .add("pvProduktnummer", std::to_string(pvProduktnummer)) .add("pvProduktbezeichnung", pvProduktbezeichnung) .add("pvOrganisationsId", std::to_string(pvOrganisationsId)) - .add("gueltigAb", common::getDateTimeCompact(context)) - .add("gueltigBis", common::getDateTimeCompact(context)) - .add("preis", common::getNumeric24(context)) - .add("samSequenznummer", std::to_string(common::getNumeric32(context))) - .add("flaechenelemente", ::utility::toDynamicArray(common::getNumeric8(context), [&context](auto &builder) + .add("gueltigAb", DateTimeDecoder::consumeDateTimeCompact4(context)) + .add("gueltigBis", DateTimeDecoder::consumeDateTimeCompact4(context)) + .add("preis", NumberDecoder::consumeInteger3(context)) + .add("samSequenznummer", std::to_string(NumberDecoder::consumeInteger4(context))) + .add("flaechenelemente", ::utility::toDynamicArray(NumberDecoder::consumeInteger1(context), [&context](auto &builder) { auto tagStream = std::ostringstream(); - auto const tagValue = int(common::getNumeric8(context)); + auto const tagValue = int(NumberDecoder::consumeInteger1(context)); tagStream << std::hex << std::noshowbase << tagValue; auto const tag = tagStream.str(); - auto const elementLength = common::getNumeric8(context); - auto const typ = std::to_string(common::getNumeric8(context)); - auto const organisationsId = std::to_string(common::getNumeric16(context)); + auto const elementLength = NumberDecoder::consumeInteger1(context); + auto const typ = std::to_string(NumberDecoder::consumeInteger1(context)); + auto const organisationsId = std::to_string(NumberDecoder::consumeInteger2(context)); auto const flaechenIdLength = elementLength - 3; if (flaechenIdLength != 2 && flaechenIdLength != 3) { throw std::runtime_error(std::string("Unexpected FlaechenelementId length: ") + std::to_string(flaechenIdLength)); } auto const flaechenId = std::to_string(flaechenIdLength == 2 - ? common::getNumeric16(context) - : common::getNumeric24(context)); + ? NumberDecoder::consumeInteger2(context) + : NumberDecoder::consumeInteger3(context)); builder .add("tag", tag) @@ -73,7 +77,7 @@ namespace interpreter::detail::uic return elementLength + 2; })); })); // clang-format on - context.addRecord(common::Record(header.recordId, header.recordVersion, std::move(recordJson))); + context.addRecord(Record(header.recordId, header.recordVersion, std::move(recordJson))); return std::move(context); } diff --git a/source/lib/interpreter/detail/uic918/source/Record118199.cpp b/source/lib/interpreter/detail/uic918/source/Record118199.cpp index f44db1be..b681122b 100644 --- a/source/lib/interpreter/detail/uic918/source/Record118199.cpp +++ b/source/lib/interpreter/detail/uic918/source/Record118199.cpp @@ -3,7 +3,7 @@ #include "../include/Record118199.h" -#include "lib/interpreter/detail/common/include/InterpreterUtility.h" +#include "lib/interpreter/detail/common/include/StringDecoder.h" #include "lib/interpreter/detail/common/include/Record.h" #include "lib/utility/include/JsonBuilder.h" @@ -20,7 +20,7 @@ namespace interpreter::detail::uic common::Context Record118199::interpret(common::Context &&context) { - auto const jsonString = common::getAlphanumeric(context, context.getRemainingSize()); + auto const jsonString = common::StringDecoder::consumeUTF8(context, context.getRemainingSize()); context.addRecord(common::Record(header.recordId, header.recordVersion, ::utility::JsonBuilder(jsonString))); return std::move(context); diff --git a/source/lib/interpreter/detail/uic918/source/RecordHeader.cpp b/source/lib/interpreter/detail/uic918/source/RecordHeader.cpp index 5034429b..d190ff24 100644 --- a/source/lib/interpreter/detail/uic918/source/RecordHeader.cpp +++ b/source/lib/interpreter/detail/uic918/source/RecordHeader.cpp @@ -3,7 +3,7 @@ #include "../include/RecordHeader.h" -#include "lib/interpreter/detail/common/include/InterpreterUtility.h" +#include "lib/interpreter/detail/common/include/StringDecoder.h" #include #include @@ -12,9 +12,9 @@ namespace interpreter::detail::uic { RecordHeader::RecordHeader(common::Context &context) : start(context.getPosition()), - recordId(common::getAlphanumeric(context, 6)), - recordVersion(common::getAlphanumeric(context, 2)), - recordLength(std::stoi(common::getAlphanumeric(context, 4))) + recordId(common::StringDecoder::consumeUTF8(context, 6)), + recordVersion(common::StringDecoder::consumeUTF8(context, 2)), + recordLength(std::stoi(common::StringDecoder::consumeUTF8(context, 4))) { context.addField(recordId + ".recordId", recordId); context.addField(recordId + ".recordVersion", recordVersion); diff --git a/source/lib/interpreter/detail/uic918/source/RecordU_FLEX.cpp b/source/lib/interpreter/detail/uic918/source/RecordU_FLEX.cpp index 2cd28baf..166501de 100644 --- a/source/lib/interpreter/detail/uic918/source/RecordU_FLEX.cpp +++ b/source/lib/interpreter/detail/uic918/source/RecordU_FLEX.cpp @@ -3,7 +3,7 @@ #include "../include/RecordU_FLEX.h" -#include "lib/interpreter/detail/common/include/InterpreterUtility.h" +#include "lib/interpreter/detail/common/include/NumberDecoder.h" #include "lib/interpreter/detail/common/include/Record.h" #include "../u_flex/v1.3/include/RecordU_FLEX_13.h" diff --git a/source/lib/interpreter/detail/uic918/source/RecordU_HEAD.cpp b/source/lib/interpreter/detail/uic918/source/RecordU_HEAD.cpp index 9fe48bb6..c6aea043 100644 --- a/source/lib/interpreter/detail/uic918/source/RecordU_HEAD.cpp +++ b/source/lib/interpreter/detail/uic918/source/RecordU_HEAD.cpp @@ -3,7 +3,8 @@ #include "../include/RecordU_HEAD.h" -#include "lib/interpreter/detail/common/include/InterpreterUtility.h" +#include "lib/interpreter/detail/common/include/StringDecoder.h" +#include "lib/interpreter/detail/common/include/DateTimeDecoder.h" #include "lib/interpreter/detail/common/include/Record.h" #include "lib/utility/include/JsonBuilder.h" @@ -21,12 +22,12 @@ namespace interpreter::detail::uic { auto recordJson = ::utility::JsonBuilder::object(); recordJson - .add("companyCode", common::getAlphanumeric(context, 4)) - .add("uniqueTicketKey", common::getAlphanumeric(context, 20)) - .add("editionTime", common::getDateTime12(context)) - .add("flags", common::getAlphanumeric(context, 1)) - .add("editionLanguageOfTicket", common::getAlphanumeric(context, 2)) - .add("secondLanguageOfContract", common::getAlphanumeric(context, 2)); + .add("companyCode", common::StringDecoder::consumeUTF8(context, 4)) + .add("uniqueTicketKey", common::StringDecoder::consumeUTF8(context, 20)) + .add("editionTime", common::DateTimeDecoder::consumeDateTime12(context)) + .add("flags", common::StringDecoder::consumeUTF8(context, 1)) + .add("editionLanguageOfTicket", common::StringDecoder::consumeUTF8(context, 2)) + .add("secondLanguageOfContract", common::StringDecoder::consumeUTF8(context, 2)); context.addRecord(common::Record(header.recordId, header.recordVersion, std::move(recordJson))); return std::move(context); diff --git a/source/lib/interpreter/detail/uic918/source/RecordU_TLAY.cpp b/source/lib/interpreter/detail/uic918/source/RecordU_TLAY.cpp index f61324f0..8e07e9f1 100644 --- a/source/lib/interpreter/detail/uic918/source/RecordU_TLAY.cpp +++ b/source/lib/interpreter/detail/uic918/source/RecordU_TLAY.cpp @@ -3,7 +3,7 @@ #include "../include/RecordU_TLAY.h" -#include "lib/interpreter/detail/common/include/InterpreterUtility.h" +#include "lib/interpreter/detail/common/include/StringDecoder.h" #include "lib/interpreter/detail/common/include/Record.h" #include "lib/utility/include/JsonBuilder.h" @@ -24,7 +24,7 @@ namespace interpreter::detail::uic common::Context RecordU_TLAY::interpret(common::Context &&context) { - auto const layoutStandard = common::getAlphanumeric(context, 4); + auto const layoutStandard = common::StringDecoder::consumeUTF8(context, 4); context.addField("U_TLAY.layoutStandard", layoutStandard); if (layoutStandard.compare("RCT2") != 0 && layoutStandard.compare("PLAI") != 0) { @@ -35,17 +35,17 @@ namespace interpreter::detail::uic auto recordJson = ::utility::JsonBuilder::object(); // clang-format off recordJson - .add("fields", ::utility::toArray(std::stoi(common::getAlphanumeric(context, 4)), [&](auto &builder) + .add("fields", ::utility::toArray(std::stoi(common::StringDecoder::consumeUTF8(context, 4)), [&](auto &builder) { builder - .add("line", std::stoi(common::getAlphanumeric(context, 2))) - .add("column", std::stoi(common::getAlphanumeric(context, 2))) - .add("height", std::stoi(common::getAlphanumeric(context, 2))) - .add("width", std::stoi(common::getAlphanumeric(context, 2))) - .add("formatting", common::getAlphanumeric(context, 1)); + .add("line", std::stoi(common::StringDecoder::consumeUTF8(context, 2))) + .add("column", std::stoi(common::StringDecoder::consumeUTF8(context, 2))) + .add("height", std::stoi(common::StringDecoder::consumeUTF8(context, 2))) + .add("width", std::stoi(common::StringDecoder::consumeUTF8(context, 2))) + .add("formatting", common::StringDecoder::consumeUTF8(context, 1)); - auto const length = std::stoi(common::getAlphanumeric(context, 4)); + auto const length = std::stoi(common::StringDecoder::consumeUTF8(context, 4)); builder - .add("text", common::getAlphanumeric(context, length)); + .add("text", common::StringDecoder::consumeUTF8(context, length)); })); // clang-format on context.addRecord(common::Record(header.recordId, header.recordVersion, std::move(recordJson))); diff --git a/source/lib/interpreter/detail/uic918/source/Uic918Interpreter.cpp b/source/lib/interpreter/detail/uic918/source/Uic918Interpreter.cpp index 36aab70c..9c78d417 100644 --- a/source/lib/interpreter/detail/uic918/source/Uic918Interpreter.cpp +++ b/source/lib/interpreter/detail/uic918/source/Uic918Interpreter.cpp @@ -12,7 +12,7 @@ #include "../include/Record118199.h" #include "../include/Deflator.h" -#include "lib/interpreter/detail/common/include/InterpreterUtility.h" +#include "lib/interpreter/detail/common/include/StringDecoder.h" #include "lib/interpreter/detail/common/include/Field.h" #include "lib/interpreter/detail/common/include/Record.h" @@ -57,7 +57,7 @@ namespace interpreter::detail::uic auto const tid = context.consumeBytes(typeId.size()); if (Uic918Interpreter::TypeIdType(tid.begin(), tid.end()) != typeId) { - throw std::runtime_error("Unexpected UIC918 type ID, expecting " + common::bytesToString(typeId) + ", got: " + common::bytesToString(tid)); + throw std::runtime_error("Unexpected UIC918 type ID, expecting 0x" + common::StringDecoder::toHexString(typeId) + ", got: 0x" + common::StringDecoder::toHexString(tid)); } if (context.getRemainingSize() < 2) @@ -66,7 +66,7 @@ namespace interpreter::detail::uic return std::move(context); } - auto const messageTypeVersion = common::getAlphanumeric(context, 2); + auto const messageTypeVersion = common::StringDecoder::consumeUTF8(context, 2); auto const version = std::stoi(messageTypeVersion); // Might be "OTI" as well if (version != 1 && version != 2) @@ -78,15 +78,15 @@ namespace interpreter::detail::uic context.addField("raw", context.getAllBase64Encoded()); context.addField("uniqueMessageTypeId", "#UT"); context.addField("messageTypeVersion", messageTypeVersion); - auto const ricsCode = common::getAlphanumeric(context, 4); + auto const ricsCode = common::StringDecoder::consumeUTF8(context, 4); context.addField("companyCode", ricsCode); - auto const keyId = common::getAlphanumeric(context, 5); + auto const keyId = common::StringDecoder::consumeUTF8(context, 5); context.addField("signatureKeyId", keyId); auto const signatureLength = version == 2 ? 64 : 50; auto const signature = context.consumeBytes(signatureLength); auto const consumed = context.getConsumedSize(); - auto const messageLengthString = common::getAlphanumeric(context, 4); + auto const messageLengthString = common::StringDecoder::consumeUTF8(context, 4); auto const messageLength = std::stoi(messageLengthString); context.addField("compressedMessageLength", std::to_string(messageLength)); if (messageLength < 0 || messageLength > context.getRemainingSize()) @@ -112,7 +112,7 @@ namespace interpreter::detail::uic auto const uncompressedMessage = deflate(compressedMessage); context.addField("uncompressedMessageLength", std::to_string(uncompressedMessage.size())); - auto messageContext = common::Context(uncompressedMessage, std::move(context.output)); + auto messageContext = common::Context(uncompressedMessage, std::move(context)); while (!messageContext.isEmpty()) { LOG_DEBUG(logger) << "Overall remaining bytes: " << messageContext.getRemainingSize(); diff --git a/source/lib/interpreter/detail/vdv/CMakeLists.txt b/source/lib/interpreter/detail/vdv/CMakeLists.txt index 1e9da26b..aee1f8a8 100644 --- a/source/lib/interpreter/detail/vdv/CMakeLists.txt +++ b/source/lib/interpreter/detail/vdv/CMakeLists.txt @@ -3,6 +3,9 @@ PROJECT(ticket-decoder-interpreter-detail-vdv) +find_package(Boost REQUIRED COMPONENTS headers) +find_package(botan REQUIRED) + AUX_SOURCE_DIRECTORY("source" PROJECT_SOURCE) file(GLOB PROJECT_INCLUDES "include/*.h") @@ -11,4 +14,20 @@ target_include_directories(${PROJECT_NAME} PRIVATE) target_link_libraries(${PROJECT_NAME} PRIVATE easyloggingpp::easyloggingpp nlohmann_json::nlohmann_json - ticket-decoder-interpreter-detail-common) + ticket-decoder-interpreter-detail-common + ticket-decoder-utility + Boost::headers + botan::botan) + +# TODO This is just a temporary solution to solve compiling issues with botan3 and gcc11. +# gcc11.4 is the default compiler coming with ubuntu22, but conan build of botan3 +# is complaining about missing c++20 support and states a minimum required gcc11.2. +# Maybe this is just an issue in conan recipe of botan3. But it could be a failing +# test during build about a specific c++20 feature as well, just failing with gcc11.4 +# and the message about minimum required version is just outdated. +# IF (WITH_SIGNATURE_VERIFIER) +# +# find_package(botan REQUIRED) +# target_link_libraries(${PROJECT_NAME} PRIVATE botan::botan) +# +# ENDIF() diff --git a/source/lib/interpreter/detail/vdv/include/BotanMessageDecoder.h b/source/lib/interpreter/detail/vdv/include/BotanMessageDecoder.h new file mode 100644 index 00000000..a77ac2aa --- /dev/null +++ b/source/lib/interpreter/detail/vdv/include/BotanMessageDecoder.h @@ -0,0 +1,36 @@ +// SPDX-FileCopyrightText: (C) 2025 user4223 and (other) contributors to ticket-decoder +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "MessageDecoder.h" +#include "Certificate.h" + +#include "lib/interpreter/api/include/CertificateProvider.h" + +#include "lib/infrastructure/include/LoggingFwd.h" + +#include +#include +#include + +namespace interpreter::detail::vdv +{ + class BotanMessageDecoder : public MessageDecoder + { + class Internal; + + infrastructure::Logger logger; + std::shared_ptr internal; + std::map> issuingCertificates; + + std::optional getIssuingCertificate(std::string authority); + + public: + BotanMessageDecoder(infrastructure::LoggerFactory &loggerFactory, api::CertificateProvider &certificateProvider); + + virtual std::optional> decodeMessage( + Certificate const &envelopeCertificate, + Signature const &envelopeSignature) override; + }; +} diff --git a/source/lib/interpreter/detail/vdv/include/Certificate.h b/source/lib/interpreter/detail/vdv/include/Certificate.h new file mode 100644 index 00000000..842b7dcd --- /dev/null +++ b/source/lib/interpreter/detail/vdv/include/Certificate.h @@ -0,0 +1,153 @@ +// SPDX-FileCopyrightText: (C) 2025 user4223 and (other) contributors to ticket-decoder +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "lib/interpreter/detail/common/include/Context.h" + +#include +#include +#include +#include + +namespace interpreter::detail::vdv +{ + + struct Signature + { + std::span value; + std::span remainder; + + static Signature consumeFrom(common::Context &context); + + static Signature consumeFromEnvelope(common::Context &context); + }; + + struct PublicKey + { + std::span exponent; + std::span modulus; + + static PublicKey consumeFrom(common::Context &context); + }; + + /* This object does not own the data, it's just a view. + */ + struct Certificate + { + std::string authority; + std::string description; + Signature signature; + std::span content; + + static Certificate consumeFromEnvelope(common::Context &context); + }; + + /* Has no fixed lengh, right now we are passing a length argument but this may break for any other ticket + TODO Find a way to determine length automatically or to discover termination 2 avoid length argument! + */ + struct CertificateOID + { + std::vector parts; + + std::string toString() const; + + static CertificateOID consumeFrom(common::Context &context, std::size_t length); + }; + + /* 8 bytes long on wire + */ + struct CertificateParticipant + { + std::string region; + std::string name; + int serviceIdenticator = 0; + int algorithmReference = 0; + std::string year; + + std::string toString() const; + + static CertificateParticipant consumeFrom(common::Context &context); + }; + + /* 4 or 3 or 2 bytes long on wire + */ + struct CertificateDate + { + std::uint16_t year = 0; + std::uint8_t month = 0; + std::uint8_t day = 0; + + std::string toString() const; + + static CertificateDate consumeFrom4(common::Context &context); + + static CertificateDate consumeFrom3(common::Context &context); + + static CertificateDate consumeFrom2(common::Context &context); + }; + + /* 12 bytes long on wire + */ + struct CertificateReference + { + std::string organisationId; + CertificateDate samValidUntil; + CertificateDate samValidFrom; + std::string ownerOrganisationId; + std::string samId; + + std::string toString() const; + + static CertificateReference consumeFrom(common::Context &context); + }; + + /* 7 bytes long on wire + */ + struct CertificateAuthorization + { + std::string name; + int serviceIndicator = 0; + + std::string toString() const; + + static CertificateAuthorization consumeFrom(common::Context &context); + }; + + /* 1 byte long on wire + */ + struct CertificateProfile + { + std::string identifier; + + std::string toString() const; + + static CertificateProfile consumeFrom(common::Context &context); + }; + + struct CertificateIdentity + { + CertificateProfile profile; + CertificateParticipant authority; + std::optional holder; + std::optional reference; + CertificateAuthorization authorization; + CertificateDate expiryDate; + CertificateOID algorithm; + + std::string toString() const; + + static CertificateIdentity consumeFrom(common::Context &context, std::size_t oidLength); + }; + + struct DecodedCertificate + { + std::optional> rawData; + CertificateIdentity identity; + PublicKey publicKey; + + static DecodedCertificate decodeRootFrom(std::span content); + + static DecodedCertificate decodeFrom(std::vector &&content); + }; +} diff --git a/source/lib/interpreter/detail/vdv/include/LDIFFileCertificateProvider.h b/source/lib/interpreter/detail/vdv/include/LDIFFileCertificateProvider.h new file mode 100644 index 00000000..e1b42d77 --- /dev/null +++ b/source/lib/interpreter/detail/vdv/include/LDIFFileCertificateProvider.h @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: (C) 2025 user4223 and (other) contributors to ticket-decoder +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "lib/interpreter/api/include/CertificateProvider.h" + +#include "lib/infrastructure/include/ContextFwd.h" +#include "lib/infrastructure/include/Logger.h" + +#include +#include + +namespace interpreter::detail::vdv +{ + /* Use 'Apache Directory Studio' and connect to public ldap 'ldaps://ldap-vdv-ion.telesec.de:636' + to get company and root certificates. + - Install 'Apache Directory Studio' or another useful tool + - on macos via 'brew install apache-directory-studio' + - on linux see: https://directory.apache.org/studio/download/download-linux.html + - Create a new connection by using the host:port from above + - Navigate to 'c=de,o=VDV Kernapplikations GmbH,ou=VDV KA' + - Right click to 'ou=VDV KA' and select 'Export' and 'LDIF Export' + - Click continue and select a file name and continue until the export starts, + use '${workspaceFolder}/cert/VDV_Certificates.ldif' to make it working with default used below + + See: https://www.telesec.de/assets/downloads/Public-Key-Service/PKS-LDAP_Schnittstelle-V2.1-DE.pdf + */ + class LDIFFileCertificateProvider : public api::CertificateProvider + { + infrastructure::Logger logger; + struct Internal; + std::shared_ptr internal; + + public: + LDIFFileCertificateProvider(infrastructure::Context &context, std::filesystem::path vdvCertificateLdifFile); + + virtual std::vector getAuthorities() override; + + /* The value in 'authority' should match exactly one very specific entry in + the list of exported certificates from public LDAP server identified by + 'cn=,ou=VDV KA,o=VDV Kernapplikations GmbH,c=de' + 4555564456xxxxxx -> EUVDVxxxxxx + 4445564456xxxxxx -> DEVDVxxxxxx + */ + virtual std::optional get(std::string authority) override; + }; +} diff --git a/source/lib/interpreter/detail/vdv/include/MessageDecoder.h b/source/lib/interpreter/detail/vdv/include/MessageDecoder.h new file mode 100644 index 00000000..689f04b1 --- /dev/null +++ b/source/lib/interpreter/detail/vdv/include/MessageDecoder.h @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: (C) 2025 user4223 and (other) contributors to ticket-decoder +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include "Certificate.h" + +#include +#include +#include +#include +#include + +namespace interpreter::detail::vdv +{ + class MessageDecoder + { + public: + virtual ~MessageDecoder() = default; + + /* Takes certificate from envelope and signature and decodes the + message by using root + issuing certificate internally. + */ + virtual std::optional> decodeMessage( + Certificate const &envelopeCertificate, + Signature const &envelopeSignature) = 0; + }; +} diff --git a/source/lib/interpreter/detail/vdv/include/VDVInterpreter.h b/source/lib/interpreter/detail/vdv/include/VDVInterpreter.h index 11d7f4bb..f80fb8d4 100644 --- a/source/lib/interpreter/detail/vdv/include/VDVInterpreter.h +++ b/source/lib/interpreter/detail/vdv/include/VDVInterpreter.h @@ -3,25 +3,27 @@ #pragma once +#include "lib/interpreter/api/include/CertificateProvider.h" #include "lib/interpreter/detail/common/include/Interpreter.h" #include "lib/infrastructure/include/Logger.h" -namespace interpreter::api -{ - class SignatureVerifier; -} +#include "MessageDecoder.h" + +#include namespace interpreter::detail::vdv { class VDVInterpreter : public common::Interpreter { infrastructure::Logger logger; + api::CertificateProvider &certificateProvider; + std::unique_ptr messageDecoder; public: static TypeIdType getTypeId(); - VDVInterpreter(infrastructure::LoggerFactory &loggerFactory, api::SignatureVerifier const &signatureChecker); + VDVInterpreter(infrastructure::LoggerFactory &loggerFactory, api::CertificateProvider &certificateProvider); virtual common::Context interpret(common::Context &&context) override; }; diff --git a/source/lib/interpreter/detail/vdv/include/VDVUtility.h b/source/lib/interpreter/detail/vdv/include/VDVUtility.h new file mode 100644 index 00000000..aa3a43df --- /dev/null +++ b/source/lib/interpreter/detail/vdv/include/VDVUtility.h @@ -0,0 +1,8 @@ +// SPDX-FileCopyrightText: (C) 2025 user4223 and (other) contributors to ticket-decoder +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +namespace interpreter::detail::vdv +{ +} diff --git a/source/lib/interpreter/detail/vdv/source/BotanMessageDecoder.cpp b/source/lib/interpreter/detail/vdv/source/BotanMessageDecoder.cpp new file mode 100644 index 00000000..e3b84b15 --- /dev/null +++ b/source/lib/interpreter/detail/vdv/source/BotanMessageDecoder.cpp @@ -0,0 +1,131 @@ +// SPDX-FileCopyrightText: (C) 2025 user4223 and (other) contributors to ticket-decoder +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "../include/BotanMessageDecoder.h" +#include "../include/VDVUtility.h" + +#include "lib/interpreter/detail/common/include/Context.h" +#include "lib/interpreter/detail/common/include/NumberDecoder.h" + +#include "lib/infrastructure/include/Logging.h" + +#include +#include +#include + +namespace interpreter::detail::vdv +{ + + class BotanMessageDecoder::Internal + { + std::unique_ptr const sha1HashFunction = Botan::HashFunction::create_or_throw("SHA-1"); + + api::CertificateProvider &certificateProvider; + std::optional rootCertificate; + + public: + Internal(api::CertificateProvider &cp) + : certificateProvider(cp), + rootCertificate() + { + auto const certificate = certificateProvider.get("4555564456100106"); // EUVDV, 16, 01, 1996 - self signed root certificate + rootCertificate = certificate + ? std::make_optional(DecodedCertificate::decodeRootFrom(certificate->content)) + : std::nullopt; + } + + bool isEnabled() const + { + return rootCertificate.has_value(); + } + + std::optional decryptIssuingCertificate(std::string authority) + { + if (!isEnabled()) + { + return std::nullopt; + } + + auto const certificate = certificateProvider.get(authority); + return certificate + ? std::make_optional(DecodedCertificate::decodeFrom(decryptVerify(certificate->signature, rootCertificate->publicKey))) + : std::nullopt; + } + + std::vector decryptVerify(Signature const &signature, PublicKey const &publicKey) + { + auto context = common::Context(Botan::power_mod(Botan::BigInt(signature.value), + Botan::BigInt(publicKey.exponent), + Botan::BigInt(publicKey.modulus)) + .serialize()); + + auto const head = context.consumeByte(); + auto const tail = context.consumeByteEnd(); + if (head != 0x6A || tail != 0xBC) + { + throw std::runtime_error(std::string("Expected head 0x6A / tail 0xBC bytes not found") + std::to_string(head) + "/" + std::to_string(tail)); + } + auto const expectedHash = context.consumeBytesEnd(20); + auto content = context.consumeRemainingBytesAppend(signature.remainder); + context.ensureEmpty(); + + auto const actualHash = sha1HashFunction->process(content); + if (!std::equal(actualHash.begin(), actualHash.end(), expectedHash.begin(), expectedHash.end())) + { + throw std::runtime_error("SHA1 hash value for content does not match expected hash value"); + } + return content; // Copy of vector owning the data and moving out here + } + }; + + BotanMessageDecoder::BotanMessageDecoder(infrastructure::LoggerFactory &lf, api::CertificateProvider &cp) + : logger(CREATE_LOGGER(lf)), + internal(std::make_shared(cp)), + issuingCertificates() + { + if (!internal->isEnabled()) + { + LOG_INFO(logger) << "Decryption disabled, certificates not found or unable to read"; + } + } + + std::optional BotanMessageDecoder::getIssuingCertificate(std::string authority) + { + auto cacheEntry = issuingCertificates.find(authority); + if (cacheEntry != issuingCertificates.end()) + { + return cacheEntry->second; + } + + auto certificate = internal->decryptIssuingCertificate(authority); + return issuingCertificates.emplace(std::make_pair(authority, std::move(certificate))).first->second; + } + + std::optional> BotanMessageDecoder::decodeMessage( + Certificate const &certificate, + Signature const &signature) + { + if (!internal->isEnabled()) + { + return std::nullopt; + } + + /* TODO Verify issuer and holder identiy for the entire chain of certificates + */ + + auto const issuingCertificate = getIssuingCertificate(certificate.authority); + if (!issuingCertificate) + { + LOG_INFO(logger) << "Decryption faild, issuing certificate not found: " << certificate.authority; + return std::nullopt; + } + + auto const envelopeCertificate = DecodedCertificate::decodeFrom( + internal->decryptVerify(certificate.signature, issuingCertificate->publicKey)); + + LOG_DEBUG(logger) << "Using envelope certificate " << envelopeCertificate.identity.toString(); + + return std::make_optional( + internal->decryptVerify(signature, envelopeCertificate.publicKey)); + } +} diff --git a/source/lib/interpreter/detail/vdv/source/Certificate.cpp b/source/lib/interpreter/detail/vdv/source/Certificate.cpp new file mode 100644 index 00000000..9c76f441 --- /dev/null +++ b/source/lib/interpreter/detail/vdv/source/Certificate.cpp @@ -0,0 +1,251 @@ +// SPDX-FileCopyrightText: (C) 2025 user4223 and (other) contributors to ticket-decoder +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "../include/Certificate.h" + +#include "lib/interpreter/detail/common/include/NumberDecoder.h" +#include "lib/interpreter/detail/common/include/TLVDecoder.h" +#include "lib/interpreter/detail/common/include/BCDDecoder.h" +#include "lib/interpreter/detail/common/include/StringDecoder.h" + +#include +#include + +#include + +namespace interpreter::detail::vdv +{ + + using namespace common; + + Signature Signature::consumeFrom(common::Context &context) + { + auto value = TLVDecoder::consumeExpectedElement(context, {0x5f, 0x37}); + auto remainder = TLVDecoder::consumeExpectedElement(context, {0x5f, 0x38}); + return Signature{std::move(value), std::move(remainder)}; + } + + Signature Signature::consumeFromEnvelope(common::Context &context) + { + auto value = TLVDecoder::consumeExpectedElement(context, {0x9e}); + auto remainder = TLVDecoder::consumeExpectedElement(context, {0x9a}); + return Signature{std::move(value), std::move(remainder)}; + } + + PublicKey PublicKey::consumeFrom(common::Context &context) + { // 65537 = 2^16+1 is the fifth Fermat, used commonly as exponent in RSA algorithms + auto exponent = context.consumeBytesEnd(4); // TODO Length unsure, always 4 bytes for exponent? + auto modulus = context.consumeRemainingBytes(); + return PublicKey{std::move(exponent), std::move(modulus)}; + } + + Certificate Certificate::consumeFromEnvelope(common::Context &context) + { + auto const signatureData = TLVDecoder::consumeExpectedElement(context, {0x7f, 0x21}); + auto authority = StringDecoder::toHexString(TLVDecoder::consumeExpectedElement(context, {0x42})); + + auto signatureContext = Context(signatureData); + auto signature = Signature::consumeFrom(signatureContext); + context.ensureEmpty(); + + return Certificate{std::move(authority), "envelope", std::move(signature), {}}; + } + + CertificateOID CertificateOID::consumeFrom(common::Context &inputContext, std::size_t length) + { + /* There is no 0x06 tag in the beginning and there is also no byte-length encoded. + So it should be a fixed length or there is a termination indicator, but + the specification does not mention a terminator. + */ + auto context = Context(inputContext.consumeBytes(length)); + auto parts = std::vector{}; + + auto const header = NumberDecoder::consumeInteger1(context); + if (header < 40) { // ITU-T + parts.insert(parts.begin(), {0, header}); + } else if (header < 80) { // ISO + parts.insert(parts.begin(), {1, header - 40u}); + } else { // joint-iso-itu-t + parts.insert(parts.begin(), {2, header - 80u}); + } + + for (;length > 1 && !context.isEmpty(); length -= 1u) + { + auto part = std::uint32_t{0}; + auto chunk = std::uint32_t{0}; // MSB = 1 means more bytes + for (chunk = NumberDecoder::consumeInteger1(context); chunk & 0x80; chunk = NumberDecoder::consumeInteger1(context)) + { + part = (part | (chunk & 0x7f)) << 7; // Drop MSB, OR it to what we have already and shift left to ensure space 4 next chunk + } + + parts.push_back(part | chunk); + } + + return CertificateOID{std::accumulate(std::begin(parts), std::end(parts), std::vector{}, [](auto &&result, auto value) + { + result.emplace_back(std::to_string(value)); + return std::move(result); })}; + } + + std::string CertificateOID::toString() const + { + return boost::algorithm::join(parts, "."); + } + + CertificateParticipant CertificateParticipant::consumeFrom(common::Context &context) + { + auto region = StringDecoder::decodeLatin1(context.consumeBytes(2)); + auto name = StringDecoder::decodeLatin1(context.consumeBytes(3)); + auto serviceIdenticator = NumberDecoder::consumeInteger1(context); + auto algorithmReference = NumberDecoder::consumeInteger1(context); + auto year = std::to_string(1990 + NumberDecoder::consumeInteger1(context)); + return CertificateParticipant{std::move(region), std::move(name), serviceIdenticator, algorithmReference, std::move(year)}; + } + + std::string CertificateParticipant::toString() const + { + auto out = std::ostringstream(); + out << region << "-" + << name << "-" + << year << "-" + << "SVC(" << serviceIdenticator << ")-" + << "ALG(" << algorithmReference << ")"; + return out.str(); + } + + std::string CertificateReference::toString() const + { + auto out = std::ostringstream(); + out << organisationId << "/" + << ownerOrganisationId << "-" + << "SAM(" << samId << ")-" + << "FROM(" << samValidFrom.toString() << ")-" + << "UNTIL(" << samValidUntil.toString() << ")"; + return out.str(); + } + + CertificateReference CertificateReference::consumeFrom(common::Context &context) + { + auto orgId = std::to_string(NumberDecoder::consumeInteger2(context)); + auto samValidUntil = CertificateDate::consumeFrom2(context); + auto samValidFrom = CertificateDate::consumeFrom3(context); + auto ownerOrgId = std::to_string(NumberDecoder::consumeInteger2(context)); + auto samId = std::to_string(NumberDecoder::consumeInteger3(context)); + return CertificateReference{std::move(orgId), std::move(samValidUntil), std::move(samValidFrom), std::move(ownerOrgId), std::move(samId)}; + } + + std::string CertificateDate::toString() const + { + auto out = std::ostringstream(); + out << std::setw(2) << std::setfill('0') << (int)day << "-" + << std::setw(2) << std::setfill('0') << (int)month << "-" + << std::setw(4) << std::setfill('0') << (int)year; + return out.str(); + } + + CertificateDate CertificateDate::consumeFrom4(common::Context &context) + { + auto const year = BCDDecoder::consumePackedInteger2(context); + auto const month = BCDDecoder::consumePackedInteger1(context); + auto const day = BCDDecoder::consumePackedInteger1(context); + return CertificateDate{year, month, day}; + } + + CertificateDate CertificateDate::consumeFrom3(common::Context &context) + { + auto const year = static_cast(2000 + BCDDecoder::consumePackedInteger1(context)); + auto const month = BCDDecoder::consumePackedInteger1(context); + auto const day = BCDDecoder::consumePackedInteger1(context); + return CertificateDate{year, month, day}; + } + + CertificateDate CertificateDate::consumeFrom2(common::Context &context) + { + auto const year = static_cast(2000 + BCDDecoder::consumePackedInteger1(context)); + auto const month = BCDDecoder::consumePackedInteger1(context); + return CertificateDate{year, month, 1}; + } + + std::string CertificateAuthorization::toString() const + { + auto out = std::ostringstream(); + out << name << "-" + << "SVC(" << serviceIndicator << ")"; + return out.str(); + } + + CertificateAuthorization CertificateAuthorization::consumeFrom(common::Context &context) + { + auto name = StringDecoder::consumeUTF8(context, 6); + auto const serviceIndicator = NumberDecoder::consumeInteger1(context); + return CertificateAuthorization{std::move(name), serviceIndicator}; + } + + std::string CertificateProfile::toString() const + { + return identifier; + } + + CertificateProfile CertificateProfile::consumeFrom(common::Context &context) + { + auto identifier = std::to_string(NumberDecoder::consumeInteger1(context)); + return CertificateProfile{std::move(identifier)}; + } + + std::string CertificateIdentity::toString() const + { + auto out = std::ostringstream(); + out << "PROFILE(" << profile.toString() << ")-" + << "AUTHORITY(" << authority.toString() << ")-"; + if (holder) + { + out << "HOLDER(" << holder->toString() << ")-"; + } + if (reference) + { + out << "REFERENCE(" << reference->toString() << ")-"; + } + out << "AUTHORIZATION(" << authorization.toString() << ")-" + << "EXPIRY(" << expiryDate.toString() << ")-" + << "ALG(" << algorithm.toString() << ")"; + return out.str(); + } + + CertificateIdentity CertificateIdentity::consumeFrom(common::Context &context, std::size_t oidLength) + { + auto profile = CertificateProfile::consumeFrom(context); + auto authority = CertificateParticipant::consumeFrom(context); + auto holder = std::optional{}; + auto reference = std::optional{}; + if (context.ignoreBytesIf({0, 0, 0, 0})) + { + holder = std::make_optional(CertificateParticipant::consumeFrom(context)); + } + else + { + reference = std::make_optional(CertificateReference::consumeFrom(context)); + } + auto authorization = CertificateAuthorization::consumeFrom(context); + auto expiryDate = CertificateDate::consumeFrom4(context); + auto algorithm = CertificateOID::consumeFrom(context, oidLength); + return CertificateIdentity{std::move(profile), std::move(authority), std::move(holder), std::move(reference), std::move(authorization), std::move(expiryDate), std::move(algorithm)}; + } + + DecodedCertificate DecodedCertificate::decodeRootFrom(std::span content) + { + auto context = Context(content); + auto identity = CertificateIdentity::consumeFrom(context, 9); + auto publicKey = PublicKey::consumeFrom(context); + context.ensureEmpty(); + return DecodedCertificate{std::nullopt, std::move(identity), std::move(publicKey)}; + } + + DecodedCertificate DecodedCertificate::decodeFrom(std::vector &&content) + { + auto context = Context(content); + auto identity = CertificateIdentity::consumeFrom(context, 7); // TODO OID length is probably not always 7 for all sub-certificates + auto publicKey = PublicKey::consumeFrom(context); + context.ensureEmpty(); + return DecodedCertificate{std::make_optional(std::move(content)), std::move(identity), std::move(publicKey)}; + } +} diff --git a/source/lib/interpreter/detail/vdv/source/LDIFFileCertificateProvider.cpp b/source/lib/interpreter/detail/vdv/source/LDIFFileCertificateProvider.cpp new file mode 100644 index 00000000..db23d77b --- /dev/null +++ b/source/lib/interpreter/detail/vdv/source/LDIFFileCertificateProvider.cpp @@ -0,0 +1,175 @@ +// SPDX-FileCopyrightText: (C) 2025 user4223 and (other) contributors to ticket-decoder +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "../include/LDIFFileCertificateProvider.h" + +#include "lib/interpreter/detail/common/include/Context.h" +#include "lib/interpreter/detail/common/include/TLVDecoder.h" + +#include "lib/utility/include/Base64.h" + +#include "lib/infrastructure/include/Logging.h" +#include "lib/infrastructure/include/Context.h" + +#include +#include +#include +#include +#include + +namespace interpreter::detail::vdv +{ + struct LDIFCertificate + { + std::string commonName; + std::string distinguishedName; + std::string description; + std::vector data; + std::optional mutable certificate; // Cache a certificate extracted once + + std::optional get() const + { + if (certificate) + { + return certificate; + } + + auto context = common::Context(data); + auto payload = common::TLVDecoder::consumeExpectedElement(context, {0x7f, 0x21}); + context.ensureEmpty(); + + auto content = std::span{}; + auto signature = std::span{}; + auto remainder = std::span{}; + + common::TLVDecoder({// clang-format off + {{0x5f, 0x4e}, [&](auto bytes) { content = bytes; }}, + {{0x5f, 0x37}, [&](auto bytes) { signature = bytes; }}, + {{0x5f, 0x38}, [&](auto bytes) { remainder = bytes; }} + }).consume(payload); // clang-format on + + certificate = std::make_optional(Certificate{commonName, description, Signature{signature, remainder}, content}); + return certificate; + } + }; + + struct LDIFFileCertificateProvider::Internal + { + std::optional> const entries; + + static std::string getValue(std::vector const &entryLines, std::string key) + { + auto const result = std::find_if(std::begin(entryLines), std::end(entryLines), [&](auto const &line) + { return line.starts_with(key); }); + return result == entryLines.end() + ? std::string{} + : result->substr(key.size(), result->size() - key.size()); + } + + static std::optional> import(std::filesystem::path file) + { + if (!std::filesystem::exists(file) || !std::filesystem::is_regular_file(file)) + { + return std::nullopt; + } + + auto entryLines = std::vector>(); + auto entry = std::vector(); + auto stream = std::ifstream(file); + auto count = 1; + for (auto line = std::string(); std::getline(stream, line); count++) + { + if (line.starts_with('#')) // Ignore comments + { + continue; + } + + if (line.empty()) // An empty line (2 directly following newlines) indicate a new entry + { + entryLines.emplace_back(std::move(entry)); + entry = std::vector(); + continue; + } + + if (line.starts_with(' ')) // Line starting with space indicates line-folding, that means the following line belongs the the line before + { + if (entry.empty()) + { + throw std::runtime_error(std::string("Expect line-folding after regular line only but found it as a starting line: ") + std::to_string(count)); + } + (*entry.rbegin()) += line.erase(0, 1); + continue; + } + + entry.push_back(line); + } + + /* Remove all entries not having a distinguished name matching the following pattern + */ + auto const entryStart = std::regex("dn[:] cn[=]\\w+,ou[=]VDV KA,o[=]VDV Kernapplikations GmbH,c[=]de", std::regex::icase); + std::erase_if(entryLines, [&](auto const &lines) + { return lines.end() == std::find_if(std::begin(lines), std::end(lines), + [&](auto const &line) + { return std::regex_match(line, entryStart); }); }); + + /* Given file might be in LDIF format but might not contain desired entries matching the distinguished name above + */ + if (entryLines.empty()) + { + return std::nullopt; + } + + std::map entries; + std::transform(std::begin(entryLines), std::end(entryLines), std::inserter(entries, entries.begin()), [](auto const &lines) + { + auto commonName = getValue(lines, "cn: "); + return std::make_pair(commonName, LDIFCertificate{ + commonName, + getValue(lines, "dn: "), + getValue(lines, "description: "), + utility::base64::decode(getValue(lines, "cACertificate:: "))}); }); + + return entries; + } + }; + + LDIFFileCertificateProvider::LDIFFileCertificateProvider(infrastructure::Context &context, std::filesystem::path vdvCertificateLdifFile) + : logger(CREATE_LOGGER(context.getLoggerFactory())), + internal(std::make_shared(Internal::import(vdvCertificateLdifFile))) + { + if (!internal->entries) + { + LOG_WARN(logger) << "Failed to import certificates from given LDIF file: " << vdvCertificateLdifFile; + } + else + { + LOG_DEBUG(logger) << "Imported no of certificates: " << internal->entries->size(); + } + } + + std::vector LDIFFileCertificateProvider::getAuthorities() + { + if (!internal->entries) + { + return {}; + } + + auto keys = std::vector{}; + std::transform(std::begin(*(internal->entries)), std::end(*(internal->entries)), std::back_inserter(keys), [](auto const &entry) + { return entry.first; }); + return keys; + } + + std::optional LDIFFileCertificateProvider::get(std::string authority) + { + if (!internal->entries) + { + return std::nullopt; + } + + auto const entry = internal->entries->find(authority); + return entry == internal->entries->end() + ? std::nullopt + : entry->second.get(); + } +} diff --git a/source/lib/interpreter/detail/vdv/source/VDVInterpreter.cpp b/source/lib/interpreter/detail/vdv/source/VDVInterpreter.cpp index f69a1321..8797deb6 100644 --- a/source/lib/interpreter/detail/vdv/source/VDVInterpreter.cpp +++ b/source/lib/interpreter/detail/vdv/source/VDVInterpreter.cpp @@ -2,14 +2,26 @@ // SPDX-License-Identifier: GPL-3.0-or-later #include "../include/VDVInterpreter.h" +#include "../include/BotanMessageDecoder.h" +#include "../include/VDVUtility.h" -#include "lib/interpreter/detail/common/include/InterpreterUtility.h" +#include "lib/interpreter/detail/common/include/NumberDecoder.h" +#include "lib/interpreter/detail/common/include/TLVDecoder.h" +#include "lib/interpreter/detail/common/include/StringDecoder.h" +#include "lib/interpreter/detail/common/include/DateTimeDecoder.h" +#include "lib/interpreter/detail/common/include/BCDDecoder.h" + +#include "lib/utility/include/Base64.h" #include "lib/infrastructure/include/Logging.h" namespace interpreter::detail::vdv { + using namespace common; + /* This is actually not a fixed ident. 0x9e is a BER-TLV tag (signature) and 0x81+0x80 a length (128 bytes). + But it should be sufficient for now to identify VDV tickets. + */ static std::vector const typeId = {0x9E, 0x81, 0x80}; VDVInterpreter::TypeIdType VDVInterpreter::getTypeId() @@ -17,29 +29,91 @@ namespace interpreter::detail::vdv return typeId; } - VDVInterpreter::VDVInterpreter(infrastructure::LoggerFactory &lf, api::SignatureVerifier const &sc) - : logger(CREATE_LOGGER(lf)) + VDVInterpreter::VDVInterpreter(infrastructure::LoggerFactory &lf, api::CertificateProvider &cp) + : logger(CREATE_LOGGER(lf)), + certificateProvider(cp), + messageDecoder(std::make_unique(lf, certificateProvider)) + { + } + + static void decodePrimaryData(std::span bytes, utility::JsonBuilder &jsonResult) + { + auto context = Context(bytes); + context.ignoreBytes(7); + auto const price = NumberDecoder::consumeInteger4(context); + jsonResult + .add("price", price); + } + + static void decodePassengerData(std::span bytes, utility::JsonBuilder &jsonResult) + { + auto context = Context(bytes); + auto const gender = std::to_string(context.consumeByte()); + auto const dateOfBirth = DateTimeDecoder::consumeDateTimeCompact4(context); + auto const name = StringDecoder::decodeLatin1(context.consumeRemainingBytes()); + jsonResult + .add("gender", gender) + .add("name", name) + .add("dateOfBirth", dateOfBirth); + } + + static void decodeIdentificationData(std::span bytes, utility::JsonBuilder &jsonResult) { } common::Context VDVInterpreter::interpret(common::Context &&context) { - // Documentation: https://www.kcd-nrw.de/fileadmin/03_KC_Seiten/KCD/Downloads/Technische_Dokumente/Archiv/2010_02_12_kompendiumvrrfa2dvdv_1_4.pdf - // Reference-Impl: https://sourceforge.net/projects/dbuic2vdvbc/ - auto const tag = common::getNumeric8(context); - auto const signatureLength = common::getNumeric16(context); - auto const signature = context.consumeBytes(128); - - auto const ticketDataTag = common::getNumeric8(context); - auto const ticketDataLength = common::getNumeric8(context); - auto const ticketData = context.consumeBytes(128); - - auto const keyTag = common::getNumeric16(context); - auto const keyLength = common::getNumeric8(context); - auto const key = context.consumeBytes(12); - - auto const ignored = context.ignoreRemainingBytes(); - LOG_WARN(logger) << "Unsupported VDV barcode detected of size: " << ignored; + // Documentation (not fully matching anymore): + // - https://www.kcd-nrw.de/fileadmin/03_KC_Seiten/KCD/Downloads/Technische_Dokumente/Archiv/2010_02_12_kompendiumvrrfa2dvdv_1_4.pdf + // - https://www.kcd-nrw.de/fileadmin/user_upload/Abbildung_und_Kontrolle_in_NRW_1_5_5.pdf + // Quite old reference impl for encoding: + // - https://sourceforge.net/projects/dbuic2vdvbc/ + // Some more up-to-date hints: + // - https://magicalcodewit.ch/38c3-slides/#/32 + // - https://github.com/TheEnbyperor/zuegli/tree/root/main/vdv + // - https://github.com/akorb/deutschlandticket_parser/blob/main/main.py + // - https://github.com/RWTH-i5-IDSG/ticketserver/blob/master/barti-check/src/main/java/de/rwth/idsg/barti/check/Decode.java + + auto const signature = Signature::consumeFromEnvelope(context); + auto const certificate = Certificate::consumeFromEnvelope(context); + context.ensureEmpty(); + + auto const remainderTail = Context(signature.remainder).consumeBytesEnd(5); + auto const signatureIdent = StringDecoder::decodeLatin1(remainderTail.subspan(0, 3)); + auto const signatureVersion = std::to_string(BCDDecoder::decodePackedInteger2(remainderTail.subspan(3, 2))); + + auto jsonBuilder = utility::JsonBuilder::object(); + jsonBuilder + .add("signatureIdent", signatureIdent) + .add("signatureVersion", signatureVersion) + .add("certificateAuthority", certificate.authority); + + auto message = messageDecoder->decodeMessage(certificate, signature); + if (message) + { + auto messageContext = Context(*message); + auto const messageTail = messageContext.consumeBytesEnd(5); + auto const messageIdent = StringDecoder::decodeLatin1(messageTail.subspan(0, 3)); + auto const messageVersion = BCDDecoder::decodePackedInteger2(messageTail.subspan(3, 2)); + jsonBuilder + .add("ticketId", std::to_string(NumberDecoder::consumeInteger4(messageContext))) + .add("ticketOrganisationId", std::to_string(NumberDecoder::consumeInteger2(messageContext))) + .add("productNumber", std::to_string(NumberDecoder::consumeInteger2(messageContext))) + .add("productOrganisationId", std::to_string(NumberDecoder::consumeInteger2(messageContext))) + .add("validFrom", DateTimeDecoder::consumeDateTimeCompact4(messageContext)) + .add("validTo", DateTimeDecoder::consumeDateTimeCompact4(messageContext)); + + auto const efsDecoder = TLVDecoder({// clang-format off + {{0xDA}, [&](auto bytes) { decodePrimaryData(std::move(bytes), jsonBuilder); }}, + {{0xDB}, [&](auto bytes) { decodePassengerData(std::move(bytes), jsonBuilder); }}, + {{0xD7}, [&](auto bytes) { decodeIdentificationData(std::move(bytes), jsonBuilder); }} + }); // clang-format on + efsDecoder.consume(TLVDecoder::consumeExpectedElement(messageContext, {0x85})); + } + + context.addField("validated", message ? "true" : "false"); + + context.addRecord(Record(signatureIdent, signatureVersion, std::move(jsonBuilder))); return std::move(context); } } diff --git a/source/lib/interpreter/detail/vdv/source/VDVUtility.cpp b/source/lib/interpreter/detail/vdv/source/VDVUtility.cpp new file mode 100644 index 00000000..811df0d3 --- /dev/null +++ b/source/lib/interpreter/detail/vdv/source/VDVUtility.cpp @@ -0,0 +1,8 @@ +// SPDX-FileCopyrightText: (C) 2025 user4223 and (other) contributors to ticket-decoder +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "../include/VDVUtility.h" + +namespace interpreter::detail::vdv +{ +} diff --git a/source/lib/interpreter/detail/verifier/include/BotanSignatureVerifier.h b/source/lib/interpreter/detail/verifier/include/BotanSignatureVerifier.h index 2fce0f00..872d2aba 100644 --- a/source/lib/interpreter/detail/verifier/include/BotanSignatureVerifier.h +++ b/source/lib/interpreter/detail/verifier/include/BotanSignatureVerifier.h @@ -5,11 +5,11 @@ #include "Certificate.h" +#include "lib/interpreter/api/include/SignatureVerifier.h" + #include "lib/infrastructure/include/ContextFwd.h" #include "lib/infrastructure/include/Logger.h" -#include "lib/interpreter/api/include/SignatureVerifier.h" - #include #include #include @@ -22,7 +22,7 @@ namespace interpreter::detail::verifier std::map keys; public: - BotanSignatureVerifier(infrastructure::Context &context, std::filesystem::path const &uicSignatureXml); + BotanSignatureVerifier(infrastructure::Context &context, std::filesystem::path const &uicPublicKeyXmlFile); virtual api::SignatureVerifier::Result check( std::string const &ricsCode, std::string const &keyId, diff --git a/source/lib/interpreter/detail/verifier/include/Certificate.h b/source/lib/interpreter/detail/verifier/include/Certificate.h index f5f7e8a4..4d4487dc 100644 --- a/source/lib/interpreter/detail/verifier/include/Certificate.h +++ b/source/lib/interpreter/detail/verifier/include/Certificate.h @@ -11,12 +11,17 @@ namespace interpreter::detail::verifier { - struct Certificate + /* TODO Separate key retrieval (from xml) from verification and hide behind interface, see vdv/include/CertificateProvider.h + */ + class Certificate { + public: struct Internal; + private: std::shared_ptr internal; + public: static std::string getNormalizedCode(std::string const &ricsCode); static std::string getNormalizedId(std::string const &keyId); diff --git a/source/lib/interpreter/detail/verifier/source/BotanSignatureVerifier.cpp b/source/lib/interpreter/detail/verifier/source/BotanSignatureVerifier.cpp index 6548773d..3ee08103 100644 --- a/source/lib/interpreter/detail/verifier/source/BotanSignatureVerifier.cpp +++ b/source/lib/interpreter/detail/verifier/source/BotanSignatureVerifier.cpp @@ -10,17 +10,17 @@ namespace interpreter::detail::verifier { - BotanSignatureVerifier::BotanSignatureVerifier(infrastructure::Context &context, std::filesystem::path const &uicSignatureXml) + BotanSignatureVerifier::BotanSignatureVerifier(infrastructure::Context &context, std::filesystem::path const &uicPublicKeyXmlFile) : logger(CREATE_LOGGER(context.getLoggerFactory())) { - if (!std::filesystem::exists(uicSignatureXml) || !std::filesystem::is_regular_file(uicSignatureXml)) + if (!std::filesystem::exists(uicPublicKeyXmlFile) || !std::filesystem::is_regular_file(uicPublicKeyXmlFile)) { - LOG_WARN(logger) << "UIC signature file not found or not a regular file: " << uicSignatureXml; + LOG_WARN(logger) << "UIC signature file not found or not a regular file: " << uicPublicKeyXmlFile; return; } auto doc = pugi::xml_document{}; - auto const result = doc.load_file(uicSignatureXml.c_str()); + auto const result = doc.load_file(uicPublicKeyXmlFile.c_str()); if (!result) { LOG_WARN(logger) << "Loading UIC signature file failed with: " << result.description(); diff --git a/source/lib/interpreter/detail/verifier/source/Certificate.cpp b/source/lib/interpreter/detail/verifier/source/Certificate.cpp index e72818c0..aeb488c0 100644 --- a/source/lib/interpreter/detail/verifier/source/Certificate.cpp +++ b/source/lib/interpreter/detail/verifier/source/Certificate.cpp @@ -24,9 +24,9 @@ namespace interpreter::detail::verifier static auto const sha224Pattern = std::regex("sha[^\\d]?224[^\\d]?", std::regex::icase); static auto const sha256Pattern = std::regex("sha[^\\d]?(2048|256)[^\\d]?", std::regex::icase); - static auto const sha1 = "EMSA1(SHA-160)"; - static auto const sha224 = "EMSA1(SHA-224)"; - static auto const sha256 = "EMSA1(SHA-256)"; + static auto const sha1 = "SHA-1"; + static auto const sha224 = "SHA-224"; + static auto const sha256 = "SHA-256"; static auto const sha1Der46 = std::make_tuple(sha1, 46, Botan::Signature_Format::DER_SEQUENCE); static auto const sha224Der62 = std::make_tuple(sha224, 62, Botan::Signature_Format::DER_SEQUENCE); @@ -86,9 +86,9 @@ namespace interpreter::detail::verifier auto keyBits = std::vector{}; Botan::BER_Decoder(dataSource) - .start_cons(Botan::ASN1_Tag::SEQUENCE) + .start_sequence() .decode(algorithmIdent) - .decode(keyBits, Botan::ASN1_Tag::BIT_STRING) + .decode(keyBits, Botan::ASN1_Type::BitString) .end_cons(); internal.publicKey = std::make_unique(algorithmIdent, keyBits); @@ -216,13 +216,12 @@ namespace interpreter::detail::verifier bool Certificate::verify(std::span message, std::span signature) const { - auto const config = getConfig(internal->algorithm); - auto const signatureLength = std::get<1>(config); - if (signatureLength > signature.size()) + auto const [ident, length, format] = getConfig(internal->algorithm); + if (length > signature.size()) { - throw std::runtime_error("Signature with length " + std::to_string(signature.size()) + " is shorter than expected, minimal expected: " + std::to_string(signatureLength)); + throw std::runtime_error("Signature with length " + std::to_string(signature.size()) + " is shorter than expected, minimal expected: " + std::to_string(length)); } - auto verifier = Botan::PK_Verifier(getPublicKey(*internal), std::get<0>(config), std::get<2>(config)); + auto verifier = Botan::PK_Verifier(getPublicKey(*internal), ident, format); auto const sig = Certificate::trimTrailingNulls(signature); return verifier.verify_message(message.data(), message.size(), sig.data(), sig.size()); } diff --git a/source/lib/utility/include/Base64.h b/source/lib/utility/include/Base64.h index d7a1d703..ec877b03 100644 --- a/source/lib/utility/include/Base64.h +++ b/source/lib/utility/include/Base64.h @@ -6,6 +6,7 @@ #include #include #include +#include namespace utility::base64 { @@ -14,6 +15,8 @@ namespace utility::base64 std::string encode(std::vector const &in); + std::string encode(std::span in); + std::string encode(std::uint8_t const *const data, size_t size); } diff --git a/source/lib/utility/source/Base64.cpp b/source/lib/utility/source/Base64.cpp index a9b8dfa3..ff4c2d59 100644 --- a/source/lib/utility/source/Base64.cpp +++ b/source/lib/utility/source/Base64.cpp @@ -27,6 +27,11 @@ namespace utility::base64 return encode(in.data(), in.size()); } + std::string encode(std::span in) + { + return encode(in.data(), in.size()); + } + std::string encode(std::uint8_t const *const data, size_t size) { if (size == 0) diff --git a/source/python/interpret_only.py b/source/python/interpret_only.py index 9c71ec77..93151310 100644 --- a/source/python/interpret_only.py +++ b/source/python/interpret_only.py @@ -12,5 +12,5 @@ print("No barcodes found") exit(1) -decoder_facade = DecoderFacade(fail_on_interpreter_error = False, public_key_file = "cert/UIC_PublicKeys.xml") +decoder_facade = DecoderFacade(fail_on_interpreter_error = False, uic_public_key_xml_file = "cert/UIC_PublicKeys.xml", vdv_certificate_ldif_file = "cert/VDV_Certificates.ldif") print(decoder_facade.decode_uic918(b64encode(barcodes[0].bytes))) diff --git a/source/python/run.py b/source/python/run.py index 1e24a69b..a2f9ce49 100644 --- a/source/python/run.py +++ b/source/python/run.py @@ -16,7 +16,7 @@ def get_source_and_details(result: Tuple[str,str]) -> str: return result[0] + ": " + get_details(result[1]) -decoder_facade = DecoderFacade(fail_on_interpreter_error = False, public_key_file = "cert/UIC_PublicKeys.xml") +decoder_facade = DecoderFacade(fail_on_interpreter_error = False, uic_public_key_xml_file = "cert/UIC_PublicKeys.xml", vdv_certificate_ldif_file = "cert/VDV_Certificates.ldif") print("\n### UIC918-9") for result in decoder_facade.decode_files("images/Muster-UIC918-9"): diff --git a/source/python/source/Binding.cpp b/source/python/source/Binding.cpp index ed3ef637..d7d97f66 100644 --- a/source/python/source/Binding.cpp +++ b/source/python/source/Binding.cpp @@ -34,10 +34,11 @@ class DecoderFacadeWrapper std::shared_ptr context; api::DecoderFacade facade; - Instance(std::string publicKeyFile, bool const failOnDecoderError, bool const failOnInterpreterError) + Instance(std::string uicPublicKeyXmlFile, std::string vdvCertificateLdifFile, bool const failOnDecoderError, bool const failOnInterpreterError) : context(getContext()), facade(api::DecoderFacade::create(*context) - .withPublicKeyFile(std::move(publicKeyFile)) + .withUicPublicKeyXmlFile(std::move(uicPublicKeyXmlFile)) + .withVdvCertificateLdifFile(std::move(vdvCertificateLdifFile)) .withFailOnDecoderError(failOnDecoderError) .withFailOnInterpreterError(failOnInterpreterError) .build()) @@ -50,8 +51,8 @@ class DecoderFacadeWrapper api::DecoderFacade &get() { return instance->facade; } public: - DecoderFacadeWrapper(std::string publicKeyFile, bool const failOnDecoderError, bool const failOnInterpreterError) - : instance(std::make_shared(std::move(publicKeyFile), failOnDecoderError, failOnInterpreterError)) {} + DecoderFacadeWrapper(std::string uicPublicKeyXmlFile, std::string vdvCertificateLdifFile, bool const failOnDecoderError, bool const failOnInterpreterError) + : instance(std::make_shared(std::move(uicPublicKeyXmlFile), std::move(vdvCertificateLdifFile), failOnDecoderError, failOnInterpreterError)) {} DecoderFacadeWrapper(DecoderFacadeWrapper const &) = default; DecoderFacadeWrapper &operator=(DecoderFacadeWrapper const &) = default; @@ -85,8 +86,9 @@ BOOST_PYTHON_MODULE(ticket_decoder) boost::python::register_exception_translator(errorTranslator); - boost::python::class_("DecoderFacade", boost::python::init( - (boost::python::arg("public_key_file") = "cert/UIC_PublicKeys.xml", + boost::python::class_("DecoderFacade", boost::python::init( + (boost::python::arg("uic_public_key_xml_file") = "cert/UIC_PublicKeys.xml", + boost::python::arg("vdv_certificate_ldif_file") = "cert/VDV_Certificates.ldif", boost::python::arg("fail_on_decoder_error") = false, boost::python::arg("fail_on_interpreter_error") = true))) .def("decode_uic918", &DecoderFacadeWrapper::decodeUIC918, "Decode base64-encoded raw UIC918 data into structured json", diff --git a/source/test/interpreter/source/BCDDecoderTest.cpp b/source/test/interpreter/source/BCDDecoderTest.cpp new file mode 100644 index 00000000..acab8484 --- /dev/null +++ b/source/test/interpreter/source/BCDDecoderTest.cpp @@ -0,0 +1,73 @@ +// SPDX-FileCopyrightText: (C) 2025 user4223 and (other) contributors to ticket-decoder +// SPDX-License-Identifier: GPL-3.0-or-later + +#include + +#include "lib/interpreter/detail/common/include/BCDDecoder.h" +#include "lib/interpreter/detail/common/include/Context.h" + +namespace interpreter::detail::common +{ + auto const buffer = std::vector{0x20, 0x26, 0x0, 0x0, 0x99, 0x99}; + + TEST(BCDDecoder, decodePackedInteger1) + { + EXPECT_EQ(20, BCDDecoder::decodePackedInteger1(buffer[0])); + EXPECT_EQ(20, BCDDecoder::decodePackedInteger1(buffer[0])); + EXPECT_EQ(26, BCDDecoder::decodePackedInteger1(buffer[1])); + EXPECT_EQ(26, BCDDecoder::decodePackedInteger1(buffer[1])); + } + + TEST(BCDDecoder, decodePackedInteger1Min) + { + EXPECT_EQ(0, BCDDecoder::decodePackedInteger1(buffer[2])); + EXPECT_EQ(0, BCDDecoder::decodePackedInteger1(buffer[3])); + } + + TEST(BCDDecoder, decodePackedInteger1Max) + { + EXPECT_EQ(99, BCDDecoder::decodePackedInteger1(buffer[4])); + EXPECT_EQ(99, BCDDecoder::decodePackedInteger1(buffer[5])); + } + + TEST(BCDDecoder, consumePackedInteger1) + { + auto context = common::Context({0x20, 0x26}); + EXPECT_EQ(20, BCDDecoder::consumePackedInteger1(context)); + EXPECT_FALSE(context.isEmpty()); + EXPECT_EQ(26, BCDDecoder::consumePackedInteger1(context)); + EXPECT_TRUE(context.isEmpty()); + } + + TEST(BCDDecoder, decodePackedInteger2) + { + EXPECT_EQ(2026, BCDDecoder::decodePackedInteger2({buffer.begin(), 2})); + EXPECT_EQ(2026, BCDDecoder::decodePackedInteger2({buffer.begin(), buffer.end()})); + EXPECT_EQ(2600, BCDDecoder::decodePackedInteger2({buffer.begin() + 1, 2})); + EXPECT_EQ(2600, BCDDecoder::decodePackedInteger2({buffer.begin() + 1, buffer.end()})); + } + + TEST(BCDDecoder, decodePackedInteger2Min) + { + EXPECT_EQ(0, BCDDecoder::decodePackedInteger2({buffer.begin() + 2, 2})); + EXPECT_EQ(0, BCDDecoder::decodePackedInteger2({buffer.begin() + 2, buffer.end()})); + } + + TEST(BCDDecoder, decodePackedInteger2Max) + { + EXPECT_EQ(9999, BCDDecoder::decodePackedInteger2({buffer.begin() + 4, 2})); + EXPECT_EQ(9999, BCDDecoder::decodePackedInteger2({buffer.begin() + 4, buffer.end()})); + } + + TEST(BCDDecoder, decodePackedInteger2Invalid) + { + EXPECT_THROW(BCDDecoder::decodePackedInteger2({buffer.begin(), 1}), std::runtime_error); + } + + TEST(BCDDecoder, consumePackedInteger2) + { + auto context = common::Context({0x20, 0x26}); + EXPECT_EQ(2026, BCDDecoder::consumePackedInteger2(context)); + EXPECT_TRUE(context.isEmpty()); + } +} diff --git a/source/test/interpreter/source/DateTimeDecoderTest.cpp b/source/test/interpreter/source/DateTimeDecoderTest.cpp new file mode 100644 index 00000000..19022c09 --- /dev/null +++ b/source/test/interpreter/source/DateTimeDecoderTest.cpp @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: (C) 2022 user4223 and (other) contributors to ticket-decoder +// SPDX-License-Identifier: GPL-3.0-or-later + +#include + +#include "lib/interpreter/detail/common/include/Context.h" +#include "lib/interpreter/detail/common/include/DateTimeDecoder.h" + +namespace interpreter::detail::common +{ + TEST(DateTimeDecoder, consumeDateTimeCompact4) + { + auto context = Context({0x28, 0x39, 0x70, 0x62}); + EXPECT_EQ(DateTimeDecoder::consumeDateTimeCompact4(context), "2010-01-25T14:03:02"); + } + + TEST(DateTimeDecoder, consumeDateTimeCompact4Minimal) + { + auto context = Context({0, 0, 0, 0}); + EXPECT_EQ(DateTimeDecoder::consumeDateTimeCompact4(context), "0000-00-00T00:00:00"); + } + + TEST(DateTimeDecoder, consumeDateTime12) + { + auto context = Context({'2', '7', '1', '0', '2', '0', '2', '0', '1', '3', '4', '5'}); + EXPECT_EQ(DateTimeDecoder::consumeDateTime12(context), "2020-10-27T13:45:00"); + } + + TEST(DateTimeDecoder, consumeDateTime12Minimal) + { + auto context = Context({'0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0', '0'}); + EXPECT_EQ(DateTimeDecoder::consumeDateTime12(context), "0000-00-00T00:00:00"); + } + + TEST(DateTimeDecoder, consumeDate8) + { + auto context = Context({'1', '3', '0', '1', '2', '0', '2', '1'}); + EXPECT_EQ(DateTimeDecoder::consumeDate8(context), "2021-01-13"); + } + + TEST(DateTimeDecoder, consumeDate8Minimal) + { + auto context = Context({'0', '0', '0', '0', '0', '0', '0', '0'}); + EXPECT_EQ(DateTimeDecoder::consumeDate8(context), "0000-00-00"); + } +} diff --git a/source/test/interpreter/source/InterpreterContextTest.cpp b/source/test/interpreter/source/InterpreterContextTest.cpp index aa239693..d079a1e3 100644 --- a/source/test/interpreter/source/InterpreterContextTest.cpp +++ b/source/test/interpreter/source/InterpreterContextTest.cpp @@ -15,16 +15,27 @@ namespace interpreter::detail::common return std::vector(input.begin(), input.end()); } + TEST(InterpreterContext, peekByte) + { + auto context = Context(data, "origin"); + EXPECT_EQ(1, context.peekByte()); + EXPECT_EQ(1, context.peekByte()); + context.ignoreBytes(1); + EXPECT_EQ(2, context.peekByte()); + EXPECT_EQ(2, context.peekByte()); + EXPECT_EQ(1, context.getConsumedSize()); + } + TEST(InterpreterContext, peekBytes) { auto context = Context(data, "origin"); EXPECT_EQ((std::vector{0x1, 0x2, 0x3}), toVector(context.peekBytes(3))); EXPECT_EQ((std::vector{0x1, 0x2, 0x3}), toVector(context.peekBytes(3))); - EXPECT_EQ(context.position, context.begin); - context.position += 1; + EXPECT_EQ(0, context.getConsumedSize()); + context.consumeBytes(1); EXPECT_EQ((std::vector{0x2, 0x3, 0x4}), toVector(context.peekBytes(3))); EXPECT_EQ((std::vector{0x2, 0x3, 0x4, 0x5}), toVector(context.peekBytes(4))); - EXPECT_EQ(context.position, context.begin + 1); + EXPECT_EQ(1, context.getConsumedSize()); } TEST(InterpreterContext, peekExceedingBytes) @@ -32,7 +43,7 @@ namespace interpreter::detail::common auto context = Context(data, "origin"); EXPECT_EQ(data, toVector(context.peekBytes(5))); EXPECT_THROW(context.peekBytes(6), std::runtime_error); - context.position += 1; + context.consumeBytes(1); EXPECT_EQ((std::vector{0x2, 0x3, 0x4, 0x5}), toVector(context.peekBytes(4))); EXPECT_THROW(context.peekBytes(5), std::runtime_error); } @@ -42,11 +53,33 @@ namespace interpreter::detail::common auto context = Context(data, "origin"); EXPECT_EQ((std::vector{0x1, 0x2, 0x3}), toVector(context.peekBytes(0, 3))); EXPECT_EQ((std::vector{0x2, 0x3, 0x4}), toVector(context.peekBytes(1, 3))); - EXPECT_EQ(context.position, context.begin); - context.position += 1; + EXPECT_EQ(0, context.getConsumedSize()); + context.consumeBytes(1); EXPECT_EQ((std::vector{0x4, 0x5}), toVector(context.peekBytes(2, 2))); EXPECT_THROW(toVector(context.peekBytes(2, 3)), std::runtime_error); - EXPECT_EQ(context.position, context.begin + 1); + EXPECT_EQ(1, context.getConsumedSize()); + } + + TEST(InterpreterContext, consumeByte) + { + auto context = Context(data, "origin"); + EXPECT_EQ(1, context.consumeByte()); + EXPECT_EQ(2, context.consumeByte()); + EXPECT_EQ(3, context.consumeByte()); + EXPECT_EQ(4, context.consumeByte()); + EXPECT_EQ(5, context.consumeByte()); + EXPECT_THROW(context.consumeByte(), std::runtime_error); + } + + TEST(InterpreterContext, consumeByteEnd) + { + auto context = Context(data, "origin"); + EXPECT_EQ(5, context.consumeByteEnd()); + EXPECT_EQ(4, context.consumeByteEnd()); + EXPECT_EQ(3, context.consumeByteEnd()); + EXPECT_EQ(2, context.consumeByteEnd()); + EXPECT_EQ(1, context.consumeByteEnd()); + EXPECT_THROW(context.consumeByteEnd(), std::runtime_error); } TEST(InterpreterContext, consumeBytes) @@ -55,10 +88,10 @@ namespace interpreter::detail::common EXPECT_EQ((std::vector{}), toVector(context.consumeBytes(0))); EXPECT_EQ((std::vector{0x1}), toVector(context.consumeBytes(1))); EXPECT_EQ((std::vector{0x2, 0x3}), toVector(context.consumeBytes(2))); - EXPECT_EQ(context.position, context.begin + 3); - context.position += 1; + EXPECT_EQ(3, context.getConsumedSize()); + context.consumeBytes(1); EXPECT_EQ((std::vector{0x5}), toVector(context.consumeBytes(1))); - EXPECT_EQ(context.position, context.begin + 5); + EXPECT_EQ(5, context.getConsumedSize()); } TEST(InterpreterContext, consumeExceedingBytes) @@ -80,16 +113,24 @@ namespace interpreter::detail::common EXPECT_EQ((std::vector{}), toVector(context.consumeMaximalBytes(5))); } + TEST(InterpreterContext, consumeRemainingBytesAppend) + { + auto context = Context({0x1, 0x2, 0x3}); + auto postfix = std::vector{0x4, 0x5}; + context.ignoreBytes(1); + EXPECT_EQ((std::vector{0x2, 0x3, 0x4, 0x5}), context.consumeRemainingBytesAppend(std::span(postfix.begin(), postfix.end()))); + } + TEST(InterpreterContext, ignoreBytes) { auto context = Context(data, "origin"); EXPECT_EQ(0, context.ignoreBytes(0)); EXPECT_EQ(1, context.ignoreBytes(1)); EXPECT_EQ(2, context.ignoreBytes(2)); - EXPECT_EQ(context.position, context.begin + 3); - context.position += 1; + EXPECT_EQ(3, context.getConsumedSize()); + context.consumeBytes(1); EXPECT_EQ(1, context.ignoreBytes(1)); - EXPECT_EQ(context.position, context.begin + 5); + EXPECT_EQ(5, context.getConsumedSize()); } TEST(InterpreterContext, ignoreExceedingBytes) @@ -100,4 +141,42 @@ namespace interpreter::detail::common EXPECT_EQ(0, context.ignoreBytes(0)); EXPECT_THROW(context.ignoreBytes(1), std::runtime_error); } + + TEST(InterpreterContext, ignoreBytesIf) + { + auto context = Context({0x1, 0x23, 0x42, 0x2}); + EXPECT_FALSE(context.ignoreBytesIf({0x23, 0x42})); + EXPECT_TRUE(context.ignoreBytesIf({0x1})); + EXPECT_FALSE(context.ignoreBytesIf({0x1})); + EXPECT_TRUE(context.ignoreBytesIf({0x23, 0x42})); + EXPECT_FALSE(context.ignoreBytesIf({0x23, 0x42})); + EXPECT_TRUE(context.ignoreBytesIf({0x2})); + EXPECT_TRUE(context.isEmpty()); + } + + TEST(InterpreterContext, ignoreRemainingBytes) + { + auto context = Context(data, "origin"); + context.consumeBytes(2); + EXPECT_FALSE(context.isEmpty()); + EXPECT_EQ(3, context.ignoreRemainingBytes()); + EXPECT_TRUE(context.isEmpty()); + } + + TEST(InterpreterContext, ensureEmpty) + { + auto context = Context(data, "origin"); + context.ignoreBytes(4); + EXPECT_THROW(context.ensureEmpty(), std::runtime_error); + context.ignoreBytes(1); + EXPECT_NO_THROW(context.ensureEmpty()); + EXPECT_NO_THROW(Context(std::vector{}).ensureEmpty()); + } + + TEST(InterpreterContext, getAllBase64Encoded) + { + auto context = Context(data, "origin"); + context.consumeByte(); + EXPECT_EQ("AQIDBAU=", context.getAllBase64Encoded()); + } } diff --git a/source/test/interpreter/source/InterpreterUtilityTest.cpp b/source/test/interpreter/source/InterpreterUtilityTest.cpp deleted file mode 100644 index 4a6e9a93..00000000 --- a/source/test/interpreter/source/InterpreterUtilityTest.cpp +++ /dev/null @@ -1,134 +0,0 @@ -// SPDX-FileCopyrightText: (C) 2022 user4223 and (other) contributors to ticket-decoder -// SPDX-License-Identifier: GPL-3.0-or-later - -#include - -#include "lib/interpreter/detail/common/include/InterpreterUtility.h" - -namespace interpreter::detail::common -{ - TEST(getAlphanumeric, readAndStopAtNull) - { - auto const source = std::vector{'R', 'P', 'E', 'X', '4', 'F', '-', '4', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; - auto context = Context(source, ""); - EXPECT_EQ(getAlphanumeric(context, 20), std::string("RPEX4F-4")); - } - - TEST(getAlphanumeric, readAndTrimTrailingSpaces) - { - auto const source = std::vector{'A', 'B', 'C', ' ', '\n', ' ', ' ', ' ', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; - auto context = Context(source, ""); - EXPECT_EQ(getAlphanumeric(context, 20), std::string("ABC")); - } - - TEST(getAlphanumeric, readAll) - { - auto const source = std::vector{'R', 'P', 'E', 'X', '4', 'F', '-', '4'}; - auto context = Context(source, ""); - EXPECT_EQ(getAlphanumeric(context, 8), std::string("RPEX4F-4")); - } - - TEST(getAlphanumeric, readEmpty) - { - auto const source = std::vector{0}; - auto context = Context(source, ""); - EXPECT_EQ(getAlphanumeric(context, 8), std::string("")); - } - - TEST(getNumeric, min8) - { - auto const source = std::vector{0xff, 1, 0xff}; - auto context = Context(source, ""); - context.ignoreBytes(1); - EXPECT_EQ(getNumeric8(context), 1); - } - - TEST(getNumeric, max8) - { - auto const source = std::vector{0xfe, 0xff, 0xfe}; - auto context = Context(source, ""); - context.ignoreBytes(1); - EXPECT_EQ(getNumeric8(context), 255); - } - - TEST(getNumeric, min16) - { - auto const source = std::vector{0xff, 0, 1, 0xff}; - auto context = Context(source, ""); - context.ignoreBytes(1); - EXPECT_EQ(getNumeric16(context), 1); - } - - TEST(getNumeric, max16) - { - auto const source = std::vector{0xfe, 0xff, 0xff, 0xfe}; - auto context = Context(source, ""); - context.ignoreBytes(1); - EXPECT_EQ(getNumeric16(context), 65535); - } - - TEST(getNumeric, min24) - { - auto const source = std::vector{0xff, 0, 0, 1, 0xff}; // big endian 1 - auto context = Context(source, ""); - context.ignoreBytes(1); - EXPECT_EQ(getNumeric24(context), 1); - } - - TEST(getNumeric, max24) - { - auto const source = std::vector{0xfe, 0xff, 0xff, 0xff, 0xfe}; - auto context = Context(source, ""); - context.ignoreBytes(1); - EXPECT_EQ(getNumeric24(context), 16777215); - } - - TEST(getNumeric, min32) - { - auto const source = std::vector{0xff, 0, 0, 0, 1, 0xff}; // big endian 1 - auto context = Context(source, ""); - context.ignoreBytes(1); - EXPECT_EQ(getNumeric32(context), 1); - } - - TEST(getNumeric, max32) - { - auto const source = std::vector{0xfe, 0xff, 0xff, 0xff, 0xff, 0xfe}; - auto context = Context(source, ""); - context.ignoreBytes(1); - EXPECT_EQ(getNumeric32(context), 4294967295); - } - - TEST(getDateTimeCompact, initial) - { - auto const source = std::vector{0x28, 0x39, 0x70, 0x62}; - auto context = Context(source, ""); - EXPECT_EQ(getDateTimeCompact(context), "2010-01-25T14:03:02"); - } - - TEST(getDateTime12, initial) - { - auto const source = std::vector{'2', '7', '1', '0', '2', '0', '2', '0', '1', '3', '4', '5'}; - auto context = Context(source, ""); - EXPECT_EQ(getDateTime12(context), "2020-10-27T13:45:00"); - } - - TEST(getDate8, initial) - { - auto const source = std::vector{'1', '3', '0', '1', '2', '0', '2', '1'}; - auto context = Context(source, ""); - EXPECT_EQ(getDate8(context), "2021-01-13"); - } - - TEST(bytesToString, filled) - { - auto const source = std::vector{0x12, 0x0A, 0xAB, 0x00, 0xFF}; - EXPECT_EQ(bytesToString(source), "0x120AAB00FF"); - } - - TEST(bytesToString, empty) - { - auto const source = std::vector{}; - EXPECT_EQ(bytesToString(source), ""); - } -} diff --git a/source/test/interpreter/source/NumberDecoderTest.cpp b/source/test/interpreter/source/NumberDecoderTest.cpp new file mode 100644 index 00000000..d645339f --- /dev/null +++ b/source/test/interpreter/source/NumberDecoderTest.cpp @@ -0,0 +1,66 @@ +// SPDX-FileCopyrightText: (C) 2022 user4223 and (other) contributors to ticket-decoder +// SPDX-License-Identifier: GPL-3.0-or-later + +#include + +#include "lib/interpreter/detail/common/include/Context.h" +#include "lib/interpreter/detail/common/include/NumberDecoder.h" + +namespace interpreter::detail::common +{ + TEST(getNumeric, min8) + { + auto context = Context({0xff, 1, 0xff}); + context.ignoreBytes(1); + EXPECT_EQ(NumberDecoder::consumeInteger1(context), 1); + } + + TEST(getNumeric, max8) + { + auto context = Context({0xfe, 0xff, 0xfe}); + context.ignoreBytes(1); + EXPECT_EQ(NumberDecoder::consumeInteger1(context), 255); + } + + TEST(getNumeric, min16) + { + auto context = Context({0xff, 0, 1, 0xff}); + context.ignoreBytes(1); + EXPECT_EQ(NumberDecoder::consumeInteger2(context), 1); + } + + TEST(getNumeric, max16) + { + auto context = Context({0xfe, 0xff, 0xff, 0xfe}); + context.ignoreBytes(1); + EXPECT_EQ(NumberDecoder::consumeInteger2(context), 65535); + } + + TEST(getNumeric, min24) + { + auto context = Context({0xff, 0, 0, 1, 0xff}); // big endian 1 + context.ignoreBytes(1); + EXPECT_EQ(NumberDecoder::consumeInteger3(context), 1); + } + + TEST(getNumeric, max24) + { + auto context = Context({0xfe, 0xff, 0xff, 0xff, 0xfe}); + context.ignoreBytes(1); + EXPECT_EQ(NumberDecoder::consumeInteger3(context), 16777215); + } + + TEST(getNumeric, min32) + { + auto context = Context({0xff, 0, 0, 0, 1, 0xff}); // big endian 1 + context.ignoreBytes(1); + EXPECT_EQ(NumberDecoder::consumeInteger4(context), 1); + } + + TEST(getNumeric, max32) + { + auto context = Context({0xfe, 0xff, 0xff, 0xff, 0xff, 0xfe}); + context.ignoreBytes(1); + EXPECT_EQ(NumberDecoder::consumeInteger4(context), 4294967295); + } +} diff --git a/source/test/interpreter/source/StringDecoderTest.cpp b/source/test/interpreter/source/StringDecoderTest.cpp new file mode 100644 index 00000000..6146d0eb --- /dev/null +++ b/source/test/interpreter/source/StringDecoderTest.cpp @@ -0,0 +1,171 @@ +// SPDX-FileCopyrightText: (C) 2026 user4223 and (other) contributors to ticket-decoder +// SPDX-License-Identifier: GPL-3.0-or-later + +#include + +#include "lib/interpreter/detail/common/include/Context.h" +#include "lib/interpreter/detail/common/include/StringDecoder.h" + +namespace interpreter::detail::common +{ + TEST(StringDecoder, consumeUtf8) + { + auto const buffer = std::string("öäü"); + auto context = Context(std::span{(std::uint8_t const *)buffer.data(), 6}); + EXPECT_EQ(StringDecoder::consumeUTF8(context, 6), std::string("öäü")); + + context = Context({0xC3, 0xBC, 0xC3, 0xB6, 0xC3, 0xA4, 0xC3, 0x9F, 0xC3, 0x9C, 0xC3, 0x96, 0xC3, 0x84}); + EXPECT_EQ(StringDecoder::consumeUTF8(context, 6), std::string("üöä")); + EXPECT_EQ(StringDecoder::consumeUTF8(context, 8), std::string("ßÜÖÄ")); + } + + TEST(StringDecoder, consumeUtf8StopAtNull) + { + auto context = Context({'R', 'P', 'E', 'X', '4', 'F', '-', '4', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}); + EXPECT_EQ(StringDecoder::consumeUTF8(context, 20), std::string("RPEX4F-4")); + } + + TEST(StringDecoder, consumeUtf8TrimTrailingSpaces) + { + auto context = Context({'A', 'B', 'C', ' ', '\n', ' ', ' ', ' ', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}); + EXPECT_EQ(StringDecoder::consumeUTF8(context, 20), std::string("ABC")); + } + + TEST(StringDecoder, consumeUtf8All) + { + auto context = Context({'R', 'P', 'E', 'X', '4', 'F', '-', '4'}); + EXPECT_EQ(StringDecoder::consumeUTF8(context, 8), std::string("RPEX4F-4")); + } + + TEST(StringDecoder, consumeUtf8Empty) + { + auto context = Context({0}); + EXPECT_EQ(StringDecoder::consumeUTF8(context, 8), std::string("")); + } + + TEST(StringDecoder, consumeLatin1) + { + auto context = Context({0xf6, 0xe4, 0xfc}); + EXPECT_EQ(StringDecoder::consumeLatin1(context, 6), std::string("öäü")); + + context = Context({0xfc, 0xf6, 0xe4, 0xdf, 0xdc, 0xd6, 0xc4}); + EXPECT_EQ(StringDecoder::consumeLatin1(context, 3), std::string("üöä")); + EXPECT_EQ(StringDecoder::consumeLatin1(context, 4), std::string("ßÜÖÄ")); + } + + TEST(StringDecoder, consumeLatin1StopAtNull) + { + auto context = Context({'R', 'P', 'E', 'X', '4', 'F', '-', '4', 0}); + EXPECT_EQ(StringDecoder::consumeLatin1(context, 20), std::string("RPEX4F-4")); + } + + TEST(StringDecoder, consumeLatin1TrimTrailingSpaces) + { + auto context = Context({'A', 'B', 'C', ' ', '\n', ' ', ' ', ' ', 0}); + EXPECT_EQ(StringDecoder::consumeLatin1(context, 20), std::string("ABC")); + } + + TEST(StringDecoder, consumeLatin1All) + { + auto context = Context({'R', 'P', 'E', 'X', '4', 'F', '-', '4'}); + EXPECT_EQ(StringDecoder::consumeLatin1(context, 8), std::string("RPEX4F-4")); + } + + TEST(StringDecoder, consumeLatin1Empty) + { + auto context = Context({0}); + EXPECT_EQ(StringDecoder::consumeLatin1(context, 8), std::string("")); + } + + TEST(StringDecoder, consumeASCII) + { + auto context = Context({'h', 'e', 'l', 'l', 'o', ' ', 'w', 'o', 'r', 'l', 'd', '!'}); + EXPECT_EQ(StringDecoder::consumeASCII(context, 100), std::string("hello world!")); + } + + TEST(StringDecoder, consumeASCIIInvalid) + { + auto context = Context({'h', 'e', 'l', 'l', 0xf6, ' ', 'w', 'o', 'r', 'l', 'd', '!'}); + EXPECT_THROW(StringDecoder::consumeASCII(context, 100), std::runtime_error); + } + + TEST(StringDecoder, consumeASCIIPrintable) + { + auto context = Context({0x20, '!', 'a', 'A', '0', '9', 'z', 'Z', 0x7E}); + EXPECT_EQ(StringDecoder::consumeASCII(context, 9, true), std::string(" !aA09zZ~")); + } + + TEST(StringDecoder, consumeASCIIEnsurePrintableTrue) + { + auto context = Context({0x00, 0x1F, 0x20, 0x7E, 0x7F, 0xFF}); + EXPECT_THROW(StringDecoder::consumeASCII(context, 1, true), std::runtime_error); + EXPECT_THROW(StringDecoder::consumeASCII(context, 1, true), std::runtime_error); + EXPECT_NO_THROW(StringDecoder::consumeASCII(context, 1, true)); + EXPECT_NO_THROW(StringDecoder::consumeASCII(context, 1, true)); + EXPECT_THROW(StringDecoder::consumeASCII(context, 1, true), std::runtime_error); + EXPECT_THROW(StringDecoder::consumeASCII(context, 1, true), std::runtime_error); + EXPECT_TRUE(context.isEmpty()); + } + + TEST(StringDecoder, consumeASCIIEnsurePrintableFalse) + { + auto context = Context({0x00, 0x1F, 0x20, 0x7E, 0x7F, 0x80, 0xFF}); + EXPECT_NO_THROW(StringDecoder::consumeASCII(context, 1, false)); + EXPECT_NO_THROW(StringDecoder::consumeASCII(context, 1, false)); + EXPECT_NO_THROW(StringDecoder::consumeASCII(context, 1, false)); + EXPECT_NO_THROW(StringDecoder::consumeASCII(context, 1, false)); + EXPECT_NO_THROW(StringDecoder::consumeASCII(context, 1, false)); + EXPECT_THROW(StringDecoder::consumeASCII(context, 1, false), std::runtime_error); + EXPECT_THROW(StringDecoder::consumeASCII(context, 1, false), std::runtime_error); + EXPECT_TRUE(context.isEmpty()); + } + + TEST(StringDecoder, filledVectorToHexString) + { + EXPECT_EQ(StringDecoder::toHexString(std::vector{0x23}), "23"); + EXPECT_EQ(StringDecoder::toHexString(std::vector(4, ' ')), "20202020"); + EXPECT_EQ(StringDecoder::toHexString(std::vector(3)), "000000"); + EXPECT_EQ(StringDecoder::toHexString(std::vector{0x12, 0x0A, 0xAB, 0x00, 0xFF}), "120AAB00FF"); + } + + TEST(StringDecoder, emptyVectorToHexString) + { + EXPECT_EQ(StringDecoder::toHexString(std::vector{}), ""); + } + + TEST(StringDecoder, filledArrayToHexString) + { + EXPECT_EQ(StringDecoder::toHexString(std::array{}), "00"); + EXPECT_EQ(StringDecoder::toHexString(std::array{0x12, 0x0A, 0xAB}), "120AAB"); + EXPECT_EQ(StringDecoder::toHexString(std::array{0x12, 0x0A}), "120A00"); + EXPECT_EQ(StringDecoder::toHexString(std::array{}), "000000"); + EXPECT_EQ(StringDecoder::toHexString(std::array{0x11, 0x22, 0x33, 0x44, 0x55}), "1122334455"); + } + + TEST(StringDecoder, emptyArrayToHexString) + { + EXPECT_EQ(StringDecoder::toHexString(std::array{}), ""); + } + + TEST(StringDecoder, integer4ToHexString) + { + EXPECT_EQ(StringDecoder::toHexString(std::uint32_t()), "00000000"); + EXPECT_EQ(StringDecoder::toHexString(std::uint32_t(1)), "00000001"); + EXPECT_EQ(StringDecoder::toHexString(std::uint32_t(0xffffffff)), "FFFFFFFF"); + EXPECT_EQ(StringDecoder::toHexString(2323u), "00000913"); + } + + TEST(StringDecoder, integer2ToHexString) + { + EXPECT_EQ(StringDecoder::toHexString(std::uint16_t()), "0000"); + EXPECT_EQ(StringDecoder::toHexString(std::uint16_t(1)), "0001"); + EXPECT_EQ(StringDecoder::toHexString(std::uint16_t(0xffff)), "FFFF"); + } + + TEST(StringDecoder, integer1ToHexString) + { + EXPECT_EQ(StringDecoder::toHexString(std::uint8_t()), "00"); + EXPECT_EQ(StringDecoder::toHexString(std::uint8_t(1)), "01"); + EXPECT_EQ(StringDecoder::toHexString(std::uint8_t(0xff)), "FF"); + } +} diff --git a/source/test/interpreter/source/TLVDecoderTest.cpp b/source/test/interpreter/source/TLVDecoderTest.cpp new file mode 100644 index 00000000..c864f370 --- /dev/null +++ b/source/test/interpreter/source/TLVDecoderTest.cpp @@ -0,0 +1,257 @@ +// SPDX-FileCopyrightText: (C) 2025 user4223 and (other) contributors to ticket-decoder +// SPDX-License-Identifier: GPL-3.0-or-later + +#include +#include + +#include "lib/interpreter/detail/common/include/Context.h" +#include "lib/interpreter/detail/common/include/TLVDecoder.h" +#include "lib/interpreter/detail/common/include/BCDDecoder.h" +#include "lib/interpreter/detail/common/include/DateTimeDecoder.h" + +namespace interpreter::detail::common +{ + TEST(TLVTag, empty) + { + auto tag = TLVTag{}; + EXPECT_EQ(0, tag[0]); + EXPECT_EQ(0, tag[1]); + EXPECT_EQ(0, tag[2]); + EXPECT_EQ(0, tag[3]); + } + + TEST(TLVTag, compareEqual) + { + EXPECT_EQ((TLVTag{}), (TLVTag{})); + EXPECT_EQ((TLVTag{0, 0}), (TLVTag{0, 0})); + auto tag = TLVTag{}; + tag.assign(2, 5); + EXPECT_EQ((TLVTag{0, 0, 5}), tag); + } + + TEST(TLVTag, compareNotEqual) + { + EXPECT_NE((TLVTag{}), (TLVTag{1})); + EXPECT_NE((TLVTag{1}), (TLVTag{})); + EXPECT_NE((TLVTag{0}), (TLVTag{0, 0})); + EXPECT_NE((TLVTag{0, 0}), (TLVTag{0, 0, 0})); + EXPECT_NE((TLVTag{1}), (TLVTag{0, 0, 0, 1})); + } + + TEST(TLVTag, constSubscription) + { + auto tag = TLVTag{23, 42, 5, 6}; + EXPECT_EQ(23, tag[0]); + EXPECT_EQ(42, tag[1]); + EXPECT_EQ(5, tag[2]); + EXPECT_EQ(6, tag[3]); + } + + TEST(TLVTag, subscription) + { + auto tag = TLVTag{}; + EXPECT_EQ(0, tag.size()); + tag.assign(0, 23); + EXPECT_EQ(1, tag.size()); + tag.assign(1, 42); + EXPECT_EQ(2, tag.size()); + tag.assign(2, 5); + EXPECT_EQ(3, tag.size()); + tag.assign(3, 6); + EXPECT_EQ((TLVTag{23, 42, 5, 6}), tag); + } + + TEST(TLVTag, size) + { + auto tag = TLVTag{}; + tag.assign(2, 23); + EXPECT_EQ(3, tag.size()); + tag.assign(1, 42); + EXPECT_EQ(3, tag.size()); + tag.assign(0, 5); + EXPECT_EQ(3, tag.size()); + tag.assign(3, 6); + EXPECT_EQ(4, tag.size()); + } + + TEST(TLVTag, toHexString) + { + EXPECT_EQ((TLVTag{}).toHexString(), ""); + EXPECT_EQ((TLVTag{0, 8}).toHexString(), "0008"); + EXPECT_EQ((TLVTag{0, 0, 0, 1}).toHexString(), "00000001"); + EXPECT_EQ((TLVTag{1, 0, 0, 0}).toHexString(), "01000000"); + EXPECT_EQ((TLVTag{0xff, 0, 0, 0}).toHexString(), "FF000000"); + EXPECT_EQ((TLVTag{23, 42, 80, 255}).toHexString(), "172A50FF"); + + auto tag = TLVTag{}; + EXPECT_EQ(tag[0], 0); + EXPECT_EQ(tag.toHexString(), ""); + tag.assign(0, 0); + EXPECT_EQ(tag.toHexString(), "00"); + tag.assign(1, 7); + EXPECT_EQ(tag.toHexString(), "0007"); + } + + TEST(TLVTag, ensureEqual) + { + EXPECT_NO_THROW((TLVTag{}.ensureEqual(TLVTag{}))); + EXPECT_NO_THROW((TLVTag{23}.ensureEqual(TLVTag{23}))); + EXPECT_NO_THROW((TLVTag{0, 1}.ensureEqual(TLVTag{0, 1}))); + EXPECT_NO_THROW((TLVTag{1, 2, 3, 4}.ensureEqual(TLVTag{1, 2, 3, 4}))); + } + + TEST(TLVTag, ensureNotEqual) + { + EXPECT_THROW((TLVTag{}.ensureEqual(TLVTag{0})), std::runtime_error); + EXPECT_THROW((TLVTag{23}.ensureEqual(TLVTag{0, 23})), std::runtime_error); + EXPECT_THROW((TLVTag{23}.ensureEqual(TLVTag{23, 0})), std::runtime_error); + EXPECT_THROW((TLVTag{0, 1}.ensureEqual(TLVTag{1, 0})), std::runtime_error); + EXPECT_THROW((TLVTag{1, 2, 3, 4}.ensureEqual(TLVTag{1, 2, 3})), std::runtime_error); + EXPECT_THROW((TLVTag{1, 2, 3, 4}.ensureEqual(TLVTag{})), std::runtime_error); + } + + TEST(TLVDecoder, consume4ByteTagStatic) + { + auto context = Context(std::vector{0b01111111, 0b10000001, 0b11111111, 0b01010101, 0x23}); + auto const tag = TLVDecoder::consumeTag(context); + EXPECT_EQ(tag, (TLVTag{0x7f, 0x81, 0xff, 0x55})); + EXPECT_EQ(1, context.getRemainingSize()); + } + + TEST(TLVDecoder, consume3ByteTagStatic) + { + auto context = Context(std::vector{0b01111111, 0b10000001, 0b01111111, 0x23}); + auto const tag = TLVDecoder::consumeTag(context); + EXPECT_EQ(tag, (TLVTag{0x7f, 0x81, 0x7f})); + EXPECT_EQ(1, context.getRemainingSize()); + } + + TEST(TLVDecoder, consume2ByteTagStatic) + { + auto context = Context(std::vector{0b01111111, 0b00100001, 0x42}); + auto const tag = TLVDecoder::consumeTag(context); + EXPECT_EQ(tag, (TLVTag{0x7f, 0x21})); + EXPECT_EQ(1, context.getRemainingSize()); + } + + TEST(TLVDecoder, consume1ByteTagStatic) + { + auto context = Context(std::vector{0b10011010, 0x42}); + auto const tag = TLVDecoder::consumeTag(context); + EXPECT_EQ(tag, (TLVTag{0x9a})); + EXPECT_EQ(1, context.getRemainingSize()); + } + + TEST(TLVDecoder, consumeExpectedTagStatic) + { + auto context = Context(std::vector{0b01111111, 0b00100001, 0x42}); + EXPECT_NO_THROW(TLVDecoder::consumeExpectedTag(context, {0x7f, 0x21})); + EXPECT_EQ(1, context.getRemainingSize()); + EXPECT_NO_THROW(TLVDecoder::consumeExpectedTag(context, {0x42})); + EXPECT_EQ(0, context.getRemainingSize()); + } + + TEST(TLVDecoder, consumeNotExpectedTagStatic) + { + auto context = Context(std::vector{0b01111111, 0b00100001, 0x42}); + EXPECT_THROW(TLVDecoder::consumeExpectedTag(context, {0x7e, 0x21}), std::runtime_error); + EXPECT_EQ(1, context.getRemainingSize()); + EXPECT_THROW(TLVDecoder::consumeExpectedTag(context, {0x41}), std::runtime_error); + EXPECT_EQ(0, context.getRemainingSize()); + } + + TEST(TLVDecoder, consume1ByteLength) + { + auto context = Context(std::vector{1, 127, 0x80}); + EXPECT_EQ(1, TLVDecoder::consumeLength(context)); + EXPECT_EQ(127, TLVDecoder::consumeLength(context)); + EXPECT_EQ(0, TLVDecoder::consumeLength(context)); + } + + TEST(TLVDecoder, consume2ByteLength) + { + auto context = Context(std::vector{0x81, 23, 0x81, 0, 0x81, 0xff}); + EXPECT_EQ(23, TLVDecoder::consumeLength(context)); + EXPECT_EQ(0, TLVDecoder::consumeLength(context)); + EXPECT_EQ(255, TLVDecoder::consumeLength(context)); + } + + TEST(TLVDecoder, consume3ByteLength) + { + auto context = Context(std::vector{0x82, 1, 2, 0x82, 0, 0, 0x82, 0xff, 0xff}); + EXPECT_EQ(0x0102, TLVDecoder::consumeLength(context)); + EXPECT_EQ(0, TLVDecoder::consumeLength(context)); + EXPECT_EQ(std::numeric_limits::max(), TLVDecoder::consumeLength(context)); + } + + TEST(TLVDecoder, consume4ByteLength) + { + auto context = Context(std::vector{0x83, 1, 2, 3, 0x83, 0, 0, 0, 0x83, 0xff, 0xff, 0xff}); + EXPECT_EQ(0x010203, TLVDecoder::consumeLength(context)); + EXPECT_EQ(0, TLVDecoder::consumeLength(context)); + EXPECT_EQ(0xffffff, TLVDecoder::consumeLength(context)); + } + + TEST(TLVDecoder, consume5ByteLength) + { + auto context = Context(std::vector{0x84, 1, 2, 3, 4, 0x84, 0, 0, 0, 0, 0x84, 0xff, 0xff, 0xff, 0xff}); + EXPECT_EQ(0x01020304, TLVDecoder::consumeLength(context)); + EXPECT_EQ(0, TLVDecoder::consumeLength(context)); + EXPECT_EQ(0xffffffff, TLVDecoder::consumeLength(context)); + } + + TEST(TLVDecoder, consume6ByteLength) + { + auto context = Context(std::vector{0x85, 1, 2, 3, 4, 5, 0x85, 0, 0, 0, 0, 0, 0x85, 0xff, 0xff, 0xff, 0xff, 0xff}); + EXPECT_EQ(0x0102030405, TLVDecoder::consumeLength(context)); + EXPECT_EQ(0, TLVDecoder::consumeLength(context)); + EXPECT_EQ(0xffffffffff, TLVDecoder::consumeLength(context)); + } + + TEST(TLVDecoder, consume9ByteLength) + { + auto context = Context(std::vector{0x88, 1, 2, 3, 4, 5, 6, 7, 8, 0x88, 0, 0, 0, 0, 0, 0, 0, 0, 0x88, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}); + EXPECT_EQ(0x0102030405060708, TLVDecoder::consumeLength(context)); + EXPECT_EQ(0, TLVDecoder::consumeLength(context)); + EXPECT_EQ(0xffffffffffffffff, TLVDecoder::consumeLength(context)); + } + + TEST(TLVDecoder, consume10ByteLength) + { + auto context = Context(std::vector{0x89, 1, 2, 3, 4, 5, 6, 7, 8, 9}); + EXPECT_THROW(TLVDecoder::consumeLength(context), std::runtime_error); + } + + TEST(TLVDecoder, consumeExpectedElement) + { + auto context = Context(std::vector{0x9a, 0x81, 0x02, 0x23, 0x42}); + auto const value = TLVDecoder::consumeExpectedElement(context, {0x9a}); + EXPECT_EQ(value.size(), 2); + EXPECT_EQ(0x23, value[0]); + EXPECT_EQ(0x42, value[1]); + } + + TEST(TLVDecoder, consumeUnexpectedElement) + { + auto context = Context(std::vector{0x9a, 0x81, 0x02, 0x23, 0x42}); + EXPECT_THROW(TLVDecoder::consumeExpectedElement(context, {0x9b}), std::runtime_error); + } + + TEST(TLVDecoder, consumeSelectedTags) + { + auto a1 = std::uint16_t{0}; + auto a2 = std::string{}; + auto const decoder = TLVDecoder({// clang-format off + {{0xA1}, [&](std::span bytes) { a1 = BCDDecoder::decodePackedInteger2(bytes); }}, + {{0xA2}, [&](std::span bytes) { a2 = DateTimeDecoder::decodeDateTimeCompact4(bytes); }} + }); // clang-format on + auto context = Context({0xA0, 0x00, 0xA1, 0x81, 0x02, 0x42, 0x23, 0xA2, 0x04, 0x44, 0xF2, 0x00, 0x01}); + auto const [matches, ignores] = decoder.consume(context); + EXPECT_EQ(matches, 2); + EXPECT_EQ(ignores, 1); + EXPECT_TRUE(context.isEmpty()); + + EXPECT_EQ(a1, 4223u); + EXPECT_EQ(a2, "2024-07-18T00:00:01"); + } +} diff --git a/source/test/interpreter/source/UicInterpreterTest.cpp b/source/test/interpreter/source/UicInterpreterTest.cpp index a9927eb4..634d794c 100644 --- a/source/test/interpreter/source/UicInterpreterTest.cpp +++ b/source/test/interpreter/source/UicInterpreterTest.cpp @@ -13,7 +13,7 @@ #include #include "lib/interpreter/detail/uic918/include/Uic918Interpreter.h" -#include "lib/interpreter/detail/common/include/InterpreterUtility.h" +#include "lib/interpreter/detail/common/include/NumberDecoder.h" #include "lib/utility/include/Base64.h"