diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..ee7a845 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,5 @@ +# Sponsor links + +patreon: werwolv +custom: https://werwolv.net/donate +github: WerWolv diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..399361a --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,51 @@ +name: "Unit Tests" + +on: + push: + branches: [ '*' ] + pull_request: + branches: [ '*' ] + +jobs: + tests: + name: 🧪 Unit Tests + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + steps: + - name: 🧰 Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 0 + submodules: recursive + + - name: ⬇️ Install dependencies + run: | + sudo apt update + sudo apt install -y \ + build-essential \ + gcc-11 \ + g++-11 \ + lld \ + ${PKGCONF:-} \ + cmake \ + make \ + + - name: 🛠️ Build + run: | + mkdir -p build + cd build + CC=gcc-11 CXX=g++-11 cmake \ + -DCMAKE_C_FLAGS="-fuse-ld=lld" \ + -DCMAKE_CXX_FLAGS="-fuse-ld=lld" \ + -DLIBPL_ENABLE_TESTS=OFF \ + .. + make -j4 + + - name: 🧪 Perform Unit Tests + run: | + cd build + ctest \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6eaa32f --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ + +tests/cmake-build-debug/ + +.idea/ diff --git a/.gitmodules b/.gitmodules index 9bd8d8c..32f2be6 100644 --- a/.gitmodules +++ b/.gitmodules @@ -2,3 +2,6 @@ path = yara/official_rules url = https://github.com/Yara-Rules/rules branch = master +[submodule "tests/lib/pl"] + path = tests/lib/pl + url = https://github.com/WerWolv/PatternLanguage diff --git a/README.md b/README.md index 17da7ee..b3a988b 100644 --- a/README.md +++ b/README.md @@ -83,4 +83,7 @@ Hex patterns, include patterns and magic files for the use with the ImHex Hex Ed ## Contributing -If you want to contribute a file to the database, please make a PR which adds it to the right folder and adds a new entry to the table in this readme. Thanks a lot :) +If you want to contribute a file to the database, please make a PR which adds it to the right folder and adds a new entry to the table in this readme. +To take advantage of the automatic pattern testing, please consider adding a test file named `.hexpat.` to the `/tests/patterns/test_data` directory. Try to keep this file as small as possible so the repository doesn't become excessively large + +Thanks a lot :) \ No newline at end of file diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..7e88faa --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,11 @@ +cmake_minimum_required(VERSION 3.16) + +project(tests) + +set(CMAKE_CXX_STANDARD 20) + +add_subdirectory(lib/pl) + +enable_testing() + +add_subdirectory(patterns) \ No newline at end of file diff --git a/tests/lib/pl b/tests/lib/pl new file mode 160000 index 0000000..d399836 --- /dev/null +++ b/tests/lib/pl @@ -0,0 +1 @@ +Subproject commit d399836b693feaab3b323acd4374c1371164c0eb diff --git a/tests/patterns/CMakeLists.txt b/tests/patterns/CMakeLists.txt new file mode 100644 index 0000000..0db7c6b --- /dev/null +++ b/tests/patterns/CMakeLists.txt @@ -0,0 +1,38 @@ +cmake_minimum_required(VERSION 3.16) + +project(patterns_tests) + +set(TOP_LEVEL "${CMAKE_CURRENT_SOURCE_DIR}/../..") + +file(GLOB PATTERNS + "${TOP_LEVEL}/patterns/*.hexpat" +) + +set(PATTERN_INCLUDES "${TOP_LEVEL}/includes/std") + +add_executable(patterns_tests + source/main.cpp +) + +target_include_directories(patterns_tests PRIVATE include) +target_link_libraries(patterns_tests libpl) + +set_target_properties(patterns_tests PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}) + +foreach (pattern IN LISTS PATTERNS) + get_filename_component(PATTERN_NAME ${pattern} NAME) + + file(GLOB TEST_FILES "${CMAKE_CURRENT_SOURCE_DIR}/test_data/${PATTERN_NAME}.*") + + message(STATUS ${CMAKE_CURRENT_SOURCE_DIR}/test_data/${PATTERN_NAME}.*) + if (TEST_FILES) + list(GET TEST_FILES 0 TEST_FILE) + else () + set(TEST_FILE "") + endif () + + set(TEST_NAME "Patterns/${PATTERN_NAME}") + + add_test(NAME ${TEST_NAME} COMMAND patterns_tests "${PATTERN_NAME}" "${pattern}" "${PATTERN_INCLUDES}" "${TEST_FILE}" WORKING_DIRECTORY ${CMAKE_BINARY_DIR}) + set_tests_properties(${TEST_NAME} PROPERTIES SKIP_RETURN_CODE 77) +endforeach () \ No newline at end of file diff --git a/tests/patterns/source/main.cpp b/tests/patterns/source/main.cpp new file mode 100644 index 0000000..af72e3a --- /dev/null +++ b/tests/patterns/source/main.cpp @@ -0,0 +1,75 @@ +#include +#include + +#include +#include + +#define EXIT_SKIP 77 + +int main(int argc, char **argv) { + // If only 4 arguments have been provided, then no test file was provided. Skip the test in that case + if (argc == 4) + return EXIT_SKIP; + + // Any number of arguments other than 5 are invalid + if (argc != 5) + return EXIT_FAILURE; + + // Extract command line arguments + const std::string patternName = argv[1]; + const std::fs::path patternFilePath = argv[2]; + const std::fs::path includePath = argv[3]; + const std::fs::path testFilePath = argv[4]; + + if (testFilePath.empty()) + return EXIT_SKIP; + + fmt::print("Running test {} on test file {}\n", patternName, testFilePath.stem().string()); + + // Open pattern file + pl::fs::File patternFile(patternFilePath, pl::fs::File::Mode::Read); + if (!patternFile.isValid()) + return EXIT_FAILURE; + + // Open test file + pl::fs::File testFile(testFilePath, pl::fs::File::Mode::Read); + if (!testFile.isValid()) + return EXIT_FAILURE; + + // Setup Pattern Language Runtime + pl::PatternLanguage runtime; + { + constexpr auto DummyPragmaHandler = [](const auto&, const auto&){ return true; }; + + runtime.setDataSource([&](pl::u64 address, pl::u8 *data, size_t size) { + testFile.seek(address); + testFile.readBuffer(data, size); + }, 0x00, testFile.getSize()); + runtime.setDangerousFunctionCallHandler([]{ return true; }); + runtime.setIncludePaths({ includePath }); + runtime.addPragma("MIME", DummyPragmaHandler); + } + + // Execute pattern + if (!runtime.executeString(patternFile.readString())) { + fmt::print("Error during execution!\n"); + + if (const auto &hardError = runtime.getError(); hardError.has_value()) + fmt::print("Hard error: {}\n\n", hardError.value().what()); + + for (const auto &[level, message] : runtime.getConsoleLog()) { + switch (level) { + case pl::LogConsole::Level::Debug: fmt::print(" [DEBUG] "); break; + case pl::LogConsole::Level::Info: fmt::print(" [INFO] "); break; + case pl::LogConsole::Level::Warning: fmt::print(" [WARN] "); break; + case pl::LogConsole::Level::Error: fmt::print(" [ERROR] "); break; + } + + fmt::print("{}\n", message); + } + + return EXIT_FAILURE; + } + + return EXIT_SUCCESS; +} \ No newline at end of file