From 49340eaa7b5532f6417f29e2208e44840ff57edc Mon Sep 17 00:00:00 2001 From: dev0 Date: Sun, 25 Jan 2026 22:55:24 +0530 Subject: [PATCH] IACore v1.2 --- .clang-format | 32 ++ .clang-tidy | 78 +++ .clangd | 3 + .github/workflows/ci.yaml | 31 + .gitignore | 54 ++ .vscode/launch.json | 16 + .vscode/settings.json | 9 + .vscode/tasks.json | 19 + CMake/FindDeps.cmake | 143 +++++ CMake/IAProjectConfig.cmake | 29 + CMake/PatchMimalloc.cmake | 36 ++ CMake/Toolchains/arm64-linux-clang.cmake | 21 + CMake/Toolchains/arm64-windows-clang.cmake | 18 + .../Toolchains/wasm32-emscripten-clang.cmake | 7 + CMake/Toolchains/x64-linux-clang.cmake | 6 + CMake/Toolchains/x64-windows.cmake | 14 + CMakeLists.txt | 41 ++ CMakePresets.json | 78 +++ Docs/BUILDING.md | 37 ++ Docs/USING.md | 27 + LICENSE | 177 ++++++ README.md | 182 ++++++ Src/CMakeLists.txt | 2 + Src/IACore/CMakeLists.txt | 97 ++++ Src/IACore/imp/cpp/AsyncOps.cpp | 200 +++++++ Src/IACore/imp/cpp/CLI.cpp | 27 + Src/IACore/imp/cpp/DataOps.cpp | 452 +++++++++++++++ Src/IACore/imp/cpp/FileOps.cpp | 539 ++++++++++++++++++ Src/IACore/imp/cpp/Http/Client.cpp | 150 +++++ Src/IACore/imp/cpp/Http/Common.cpp | 117 ++++ Src/IACore/imp/cpp/Http/Server.cpp | 161 ++++++ Src/IACore/imp/cpp/IACore.cpp | 56 ++ Src/IACore/imp/cpp/IPC.cpp | 451 +++++++++++++++ Src/IACore/imp/cpp/JSON.cpp | 18 + Src/IACore/imp/cpp/Logger.cpp | 86 +++ Src/IACore/imp/cpp/Platform.cpp | 134 +++++ Src/IACore/imp/cpp/ProcessOps.cpp | 309 ++++++++++ Src/IACore/imp/cpp/SIMD.cpp | 18 + Src/IACore/imp/cpp/SocketOps.cpp | 146 +++++ Src/IACore/imp/cpp/StreamReader.cpp | 85 +++ Src/IACore/imp/cpp/StreamWriter.cpp | 162 ++++++ Src/IACore/imp/cpp/StringOps.cpp | 122 ++++ Src/IACore/imp/cpp/Utils.cpp | 112 ++++ Src/IACore/imp/cpp/XML.cpp | 82 +++ Src/IACore/inc/IACore/ADT/RingBuffer.hpp | 264 +++++++++ Src/IACore/inc/IACore/AsyncOps.hpp | 71 +++ Src/IACore/inc/IACore/CLI.hpp | 63 ++ Src/IACore/inc/IACore/DataOps.hpp | 47 ++ Src/IACore/inc/IACore/DynamicLib.hpp | 151 +++++ Src/IACore/inc/IACore/Environment.hpp | 99 ++++ Src/IACore/inc/IACore/FileOps.hpp | 127 +++++ Src/IACore/inc/IACore/Http/Client.hpp | 95 +++ Src/IACore/inc/IACore/Http/Common.hpp | 155 +++++ Src/IACore/inc/IACore/Http/Server.hpp | 151 +++++ Src/IACore/inc/IACore/IACore.hpp | 66 +++ Src/IACore/inc/IACore/IATest.hpp | 293 ++++++++++ Src/IACore/inc/IACore/IPC.hpp | 160 ++++++ Src/IACore/inc/IACore/JSON.hpp | 119 ++++ Src/IACore/inc/IACore/Logger.hpp | 123 ++++ Src/IACore/inc/IACore/PCH.hpp | 135 +++++ Src/IACore/inc/IACore/Platform.hpp | 52 ++ Src/IACore/inc/IACore/ProcessOps.hpp | 69 +++ Src/IACore/inc/IACore/SIMD.hpp | 280 +++++++++ Src/IACore/inc/IACore/SocketOps.hpp | 116 ++++ Src/IACore/inc/IACore/StreamReader.hpp | 106 ++++ Src/IACore/inc/IACore/StreamWriter.hpp | 71 +++ Src/IACore/inc/IACore/StringOps.hpp | 26 + Src/IACore/inc/IACore/Utils.hpp | 107 ++++ Src/IACore/inc/IACore/XML.hpp | 39 ++ Tests/CMakeLists.txt | 4 + Tests/Regression/CMakeLists.txt | 0 Tests/Subjects/CMakeLists.txt | 1 + Tests/Subjects/LongProcess/Main.cpp | 12 + Tests/Unit/AsyncOps.cpp | 160 ++++++ Tests/Unit/CLI.cpp | 103 ++++ Tests/Unit/CMakeLists.txt | 36 ++ Tests/Unit/DataOps.cpp | 108 ++++ Tests/Unit/Environment.cpp | 127 +++++ Tests/Unit/FileOps.cpp | 156 +++++ Tests/Unit/IPC.cpp | 130 +++++ Tests/Unit/JSON.cpp | 164 ++++++ Tests/Unit/Logger.cpp | 118 ++++ Tests/Unit/Main.cpp | 41 ++ Tests/Unit/Platform.cpp | 119 ++++ Tests/Unit/ProcessOps.cpp | 213 +++++++ Tests/Unit/RingBuffer.cpp | 107 ++++ Tests/Unit/SIMD/FloatVec4.cpp | 102 ++++ Tests/Unit/SIMD/IntVec4.cpp | 144 +++++ Tests/Unit/SocketOps.cpp | 107 ++++ Tests/Unit/StreamReader.cpp | 137 +++++ Tests/Unit/StreamWriter.cpp | 116 ++++ Tests/Unit/StringOps.cpp | 111 ++++ Tests/Unit/Utils.cpp | 162 ++++++ Tests/Unit/XML.cpp | 112 ++++ logo.svg | 23 + 95 files changed, 10182 insertions(+) create mode 100644 .clang-format create mode 100644 .clang-tidy create mode 100644 .clangd create mode 100644 .github/workflows/ci.yaml create mode 100644 .gitignore create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json create mode 100644 .vscode/tasks.json create mode 100644 CMake/FindDeps.cmake create mode 100644 CMake/IAProjectConfig.cmake create mode 100644 CMake/PatchMimalloc.cmake create mode 100644 CMake/Toolchains/arm64-linux-clang.cmake create mode 100644 CMake/Toolchains/arm64-windows-clang.cmake create mode 100644 CMake/Toolchains/wasm32-emscripten-clang.cmake create mode 100644 CMake/Toolchains/x64-linux-clang.cmake create mode 100644 CMake/Toolchains/x64-windows.cmake create mode 100644 CMakeLists.txt create mode 100644 CMakePresets.json create mode 100644 Docs/BUILDING.md create mode 100644 Docs/USING.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 Src/CMakeLists.txt create mode 100644 Src/IACore/CMakeLists.txt create mode 100644 Src/IACore/imp/cpp/AsyncOps.cpp create mode 100644 Src/IACore/imp/cpp/CLI.cpp create mode 100644 Src/IACore/imp/cpp/DataOps.cpp create mode 100644 Src/IACore/imp/cpp/FileOps.cpp create mode 100644 Src/IACore/imp/cpp/Http/Client.cpp create mode 100644 Src/IACore/imp/cpp/Http/Common.cpp create mode 100644 Src/IACore/imp/cpp/Http/Server.cpp create mode 100644 Src/IACore/imp/cpp/IACore.cpp create mode 100644 Src/IACore/imp/cpp/IPC.cpp create mode 100644 Src/IACore/imp/cpp/JSON.cpp create mode 100644 Src/IACore/imp/cpp/Logger.cpp create mode 100644 Src/IACore/imp/cpp/Platform.cpp create mode 100644 Src/IACore/imp/cpp/ProcessOps.cpp create mode 100644 Src/IACore/imp/cpp/SIMD.cpp create mode 100644 Src/IACore/imp/cpp/SocketOps.cpp create mode 100644 Src/IACore/imp/cpp/StreamReader.cpp create mode 100644 Src/IACore/imp/cpp/StreamWriter.cpp create mode 100644 Src/IACore/imp/cpp/StringOps.cpp create mode 100644 Src/IACore/imp/cpp/Utils.cpp create mode 100644 Src/IACore/imp/cpp/XML.cpp create mode 100644 Src/IACore/inc/IACore/ADT/RingBuffer.hpp create mode 100644 Src/IACore/inc/IACore/AsyncOps.hpp create mode 100644 Src/IACore/inc/IACore/CLI.hpp create mode 100644 Src/IACore/inc/IACore/DataOps.hpp create mode 100644 Src/IACore/inc/IACore/DynamicLib.hpp create mode 100644 Src/IACore/inc/IACore/Environment.hpp create mode 100644 Src/IACore/inc/IACore/FileOps.hpp create mode 100644 Src/IACore/inc/IACore/Http/Client.hpp create mode 100644 Src/IACore/inc/IACore/Http/Common.hpp create mode 100644 Src/IACore/inc/IACore/Http/Server.hpp create mode 100644 Src/IACore/inc/IACore/IACore.hpp create mode 100644 Src/IACore/inc/IACore/IATest.hpp create mode 100644 Src/IACore/inc/IACore/IPC.hpp create mode 100644 Src/IACore/inc/IACore/JSON.hpp create mode 100644 Src/IACore/inc/IACore/Logger.hpp create mode 100644 Src/IACore/inc/IACore/PCH.hpp create mode 100644 Src/IACore/inc/IACore/Platform.hpp create mode 100644 Src/IACore/inc/IACore/ProcessOps.hpp create mode 100644 Src/IACore/inc/IACore/SIMD.hpp create mode 100644 Src/IACore/inc/IACore/SocketOps.hpp create mode 100644 Src/IACore/inc/IACore/StreamReader.hpp create mode 100644 Src/IACore/inc/IACore/StreamWriter.hpp create mode 100644 Src/IACore/inc/IACore/StringOps.hpp create mode 100644 Src/IACore/inc/IACore/Utils.hpp create mode 100644 Src/IACore/inc/IACore/XML.hpp create mode 100644 Tests/CMakeLists.txt create mode 100644 Tests/Regression/CMakeLists.txt create mode 100644 Tests/Subjects/CMakeLists.txt create mode 100644 Tests/Subjects/LongProcess/Main.cpp create mode 100644 Tests/Unit/AsyncOps.cpp create mode 100644 Tests/Unit/CLI.cpp create mode 100644 Tests/Unit/CMakeLists.txt create mode 100644 Tests/Unit/DataOps.cpp create mode 100644 Tests/Unit/Environment.cpp create mode 100644 Tests/Unit/FileOps.cpp create mode 100644 Tests/Unit/IPC.cpp create mode 100644 Tests/Unit/JSON.cpp create mode 100644 Tests/Unit/Logger.cpp create mode 100644 Tests/Unit/Main.cpp create mode 100644 Tests/Unit/Platform.cpp create mode 100644 Tests/Unit/ProcessOps.cpp create mode 100644 Tests/Unit/RingBuffer.cpp create mode 100644 Tests/Unit/SIMD/FloatVec4.cpp create mode 100644 Tests/Unit/SIMD/IntVec4.cpp create mode 100644 Tests/Unit/SocketOps.cpp create mode 100644 Tests/Unit/StreamReader.cpp create mode 100644 Tests/Unit/StreamWriter.cpp create mode 100644 Tests/Unit/StringOps.cpp create mode 100644 Tests/Unit/Utils.cpp create mode 100644 Tests/Unit/XML.cpp create mode 100644 logo.svg diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..97a556b --- /dev/null +++ b/.clang-format @@ -0,0 +1,32 @@ +--- +Language: Cpp +Standard: c++20 +BasedOnStyle: Microsoft + +IndentWidth: 4 +TabWidth: 4 +UseTab: Never +AccessModifierOffset: -4 + +IndentPPDirectives: AfterHash +IndentRequiresClause: true +BreakBeforeConceptDeclarations: true +SpaceAfterTemplateKeyword: false + +FixNamespaceComments: true +NamespaceIndentation: All +CompactNamespaces: true + +AlignAfterOpenBracket: Align +AlignOperands: Align +AlignTrailingComments: true + +SpaceAfterCStyleCast: true +SpaceAfterLogicalNot: false +SpaceBeforeAssignmentOperators: true +SpaceInEmptyParentheses: false +SpacesInAngles: false + +SeparateDefinitionBlocks: Always +SortIncludes: false +--- \ No newline at end of file diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 0000000..7e48697 --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,78 @@ +Checks: > + -*, + readability-identifier-naming, + readability-const-return-type, + modernize-use-nodiscard, + modernize-use-override, + modernize-use-nullptr, + bugprone-use-after-move + +CheckOptions: + # ---------------------------------------------------------------------------- + # Types (PascalCase) - Matches Rust structs/enums + # ---------------------------------------------------------------------------- + - key: readability-identifier-naming.ClassCase + value: PascalCase + - key: readability-identifier-naming.StructCase + value: PascalCase + - key: readability-identifier-naming.TypedefCase + value: PascalCase + - key: readability-identifier-naming.EnumCase + value: PascalCase + - key: readability-identifier-naming.TemplateParameterCase + value: PascalCase + + # ---------------------------------------------------------------------------- + # Functions & Methods (snake_case) - Matches Rust fn + # ---------------------------------------------------------------------------- + - key: readability-identifier-naming.FunctionCase + value: lower_case + - key: readability-identifier-naming.MethodCase + value: lower_case + - key: readability-identifier-naming.ParameterCase + value: lower_case + + # ---------------------------------------------------------------------------- + # Variables (snake_case) - Matches Rust let + # ---------------------------------------------------------------------------- + - key: readability-identifier-naming.VariableCase + value: lower_case + - key: readability-identifier-naming.LocalVariableCase + value: lower_case + + # ---------------------------------------------------------------------------- + # Members (m_snake_case for private, snake_case for public) + # ---------------------------------------------------------------------------- + # Public struct members (like a Rust struct) -> x, y, width + - key: readability-identifier-naming.PublicMemberCase + value: lower_case + - key: readability-identifier-naming.StructMemberIgnoredRegexp + value: ^_[a-z0-9_]*$ + + # Private/Protected class members -> m_parser, m_count + - key: readability-identifier-naming.PrivateMemberCase + value: lower_case + - key: readability-identifier-naming.PrivateMemberPrefix + value: m_ + - key: readability-identifier-naming.ProtectedMemberCase + value: lower_case + - key: readability-identifier-naming.ProtectedMemberPrefix + value: m_ + + # ---------------------------------------------------------------------------- + # Constants (SCREAMING_SNAKE_CASE) + # ---------------------------------------------------------------------------- + - key: readability-identifier-naming.GlobalConstantCase + value: UPPER_CASE + - key: readability-identifier-naming.ConstexprVariableCase + value: UPPER_CASE + - key: readability-identifier-naming.EnumConstantCase + value: PascalCase + # Note: Rust uses PascalCase for Enum Variants (Option::None). + # Change to UPPER_CASE if you prefer C-style enums. + + # ---------------------------------------------------------------------------- + # Macros (SCREAMING_SNAKE_CASE) + # ---------------------------------------------------------------------------- + - key: readability-identifier-naming.MacroDefinitionCase + value: UPPER_CASE \ No newline at end of file diff --git a/.clangd b/.clangd new file mode 100644 index 0000000..d2ee80f --- /dev/null +++ b/.clangd @@ -0,0 +1,3 @@ +CompileFlags: + Add: [-Wno-missing-field-initializers, -Wno-missing-designated-field-initializers] + \ No newline at end of file diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..3b1a226 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,31 @@ +name: CI + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + build-linux-and-wasm: + runs-on: ubuntu-latest + permissions: + contents: read + packages: read + container: + image: ghcr.io/i-a-s/iabuild-env:latest + strategy: + fail-fast: false + matrix: + target: [x64-linux] + steps: + - uses: actions/checkout@v4 + + - name: Install OpenSSL v3 (System) + run: sudo dnf install -y openssl-devel + + - name: Configure + run: cmake --preset ${{ matrix.target }} + + - name: Build + run: cmake --build --preset ${{ matrix.target }} --config Release diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b23d456 --- /dev/null +++ b/.gitignore @@ -0,0 +1,54 @@ +# ---> C++ +# Prerequisites +*.d + +# Compiled Object files +*.slo +*.lo +*.o +*.obj + +# Precompiled Headers +*.gch +*.pch + +# Compiled Dynamic libraries +*.so +*.dylib +*.dll + +# Fortran module files +*.mod +*.smod + +# Compiled Static libraries +*.lai +*.la +*.a +*.lib + +# Executables +*.exe +*.out +*.app + +# ---> VisualStudioCode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +[Ss]andbox/ + +.cache/ +.local/ +out/ +[Bb]uild +/compile_commands.json diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..77f1190 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,16 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Debug (Linux)", + "type": "lldb", + "request": "launch", + "program": "${command:cmake.launchTargetPath}", + "args": [ + "dummy" + ], + "cwd": "${workspaceFolder}/", + "preLaunchTask": "CMake Build" + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..b28d6ef --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +{ + "editor.formatOnSave": true, + "C_Cpp.codeAnalysis.runAutomatically": "onSave", + "C_Cpp.codeAnalysis.clangTidy.enabled": true, + "C_Cpp.codeAnalysis.clangTidy.useBuildPath": false, + "C_Cpp.codeAnalysis.clangTidy.config": "", + "C_Cpp.clang_format_fallbackStyle": "Microsoft", + "cmake.copyCompileCommands": "${workspaceFolder}/compile_commands.json", +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..3cd9419 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,19 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "CMake Build", + "type": "cmake", + "command": "build", + "group": { + "kind": "build", + "isDefault": true + }, + "presentation": { + "reveal": "silent", + "panel": "shared" + }, + "detail": "Builds the currently selected CMake Preset" + } + ] +} \ No newline at end of file diff --git a/CMake/FindDeps.cmake b/CMake/FindDeps.cmake new file mode 100644 index 0000000..a4d20c7 --- /dev/null +++ b/CMake/FindDeps.cmake @@ -0,0 +1,143 @@ +include(FetchContent) + +find_package(OpenSSL 3.0.0 REQUIRED) + +set(BUILD_SHARED_LIBS OFF CACHE INTERNAL "Force static libs") + +set(HWY_ENABLE_TESTS OFF CACHE BOOL "Disable Highway tests" FORCE) +set(HWY_ENABLE_EXAMPLES OFF CACHE BOOL "Disable Highway examples" FORCE) +set(HWY_ENABLE_CONTRIB OFF CACHE BOOL "Disable Highway contrib" FORCE) +set(HWY_ENABLE_INSTALL OFF CACHE BOOL "Disable Highway install rules" FORCE) + +set(ZLIB_USE_STATIC_LIBS ON CACHE BOOL "" FORCE) +set(ZLIB_COMPAT ON CACHE BOOL "" FORCE) +set(ZLIB_ENABLE_TESTS OFF CACHE BOOL "" FORCE) +set(WITH_GZFILEOP ON CACHE BOOL "" FORCE) + +FetchContent_Declare( + Oxide + GIT_REPOSITORY https://github.com/I-A-S/Oxide + GIT_TAG main + OVERRIDE_FIND_PACKAGE +) + +FetchContent_Declare( + zlib + GIT_REPOSITORY https://github.com/zlib-ng/zlib-ng.git + GIT_TAG 2.1.6 + OVERRIDE_FIND_PACKAGE +) + +set(ZSTD_BUILD_PROGRAMS OFF CACHE BOOL "" FORCE) +set(ZSTD_BUILD_TESTS OFF CACHE BOOL "" FORCE) +set(ZSTD_BUILD_SHARED OFF CACHE BOOL "" FORCE) +set(ZSTD_BUILD_STATIC ON CACHE BOOL "" FORCE) + +FetchContent_Declare( + zstd + GIT_REPOSITORY https://github.com/facebook/zstd.git + GIT_TAG v1.5.5 + SOURCE_SUBDIR build/cmake + OVERRIDE_FIND_PACKAGE +) + +FetchContent_Declare( + httplib + GIT_REPOSITORY https://github.com/yhirose/cpp-httplib.git + GIT_TAG v0.28.0 + SYSTEM + EXCLUDE_FROM_ALL +) + +FetchContent_Declare( + nlohmann_json + GIT_REPOSITORY https://github.com/nlohmann/json.git + GIT_TAG v3.12.0 + SYSTEM + EXCLUDE_FROM_ALL +) + +FetchContent_Declare( + glaze + GIT_REPOSITORY https://github.com/stephenberry/glaze.git + GIT_TAG v5.0.0 + SYSTEM + EXCLUDE_FROM_ALL +) + +FetchContent_Declare( + simdjson + GIT_REPOSITORY https://github.com/simdjson/simdjson.git + GIT_TAG v4.2.2 + SYSTEM + EXCLUDE_FROM_ALL +) + +FetchContent_Declare( + mimalloc + GIT_REPOSITORY https://github.com/microsoft/mimalloc.git + GIT_TAG v3.0.10 + SYSTEM + EXCLUDE_FROM_ALL + PATCH_COMMAND ${CMAKE_COMMAND} + -DSOURCE_DIR= + -P ${CMAKE_CURRENT_SOURCE_DIR}/CMake/PatchMimalloc.cmake +) + +FetchContent_Declare( + unordered_dense + GIT_REPOSITORY https://github.com/martinus/unordered_dense.git + GIT_TAG v4.8.1 + SYSTEM + EXCLUDE_FROM_ALL +) + +FetchContent_Declare( + pugixml + GIT_REPOSITORY https://github.com/zeux/pugixml.git + GIT_TAG v1.15 + SYSTEM + EXCLUDE_FROM_ALL +) + +FetchContent_Declare( + highway + GIT_REPOSITORY https://github.com/google/highway.git + GIT_TAG 1.3.0 + SYSTEM +) + +set(MI_OVERRIDE ON CACHE BOOL "" FORCE) +set(MI_BUILD_STATIC ON CACHE BOOL "" FORCE) +set(MI_BUILD_TESTS OFF CACHE BOOL "" FORCE) +set(MI_BUILD_SHARED OFF CACHE BOOL "" FORCE) +set(MI_DEBUG OFF CACHE BOOL "" FORCE) +set(MI_SHOW_ERRORS OFF CACHE BOOL "" FORCE) + +set(EXPECTED_BUILD_TESTS OFF CACHE BOOL "" FORCE) + +set(HTTPLIB_REQUIRE_OPENSSL ON CACHE BOOL "" FORCE) +set(HTTPLIB_REQUIRE_ZLIB OFF CACHE BOOL "" FORCE) +set(HTTPLIB_NO_EXCEPTIONS ON CACHE BOOL "" FORCE) +set(HTTPLIB_COMPILE OFF CACHE BOOL "" FORCE) +set(HTTPLIB_TEST OFF CACHE BOOL "" FORCE) +set(HTTPLIB_EXAMPLE OFF CACHE BOOL "" FORCE) + +FetchContent_MakeAvailable(zlib zstd) + +target_include_directories(libzstd_static INTERFACE + $ +) + +if(NOT TARGET zstd::libzstd) + add_library(zstd::libzstd ALIAS libzstd_static) +endif() + +FetchContent_MakeAvailable(Oxide httplib pugixml nlohmann_json glaze simdjson unordered_dense mimalloc highway) + +if(NOT TARGET simdjson::simdjson) + add_library(simdjson::simdjson ALIAS simdjson) +endif() + +target_compile_options(hwy PRIVATE -w) +target_compile_options(libzstd_static PRIVATE -w) diff --git a/CMake/IAProjectConfig.cmake b/CMake/IAProjectConfig.cmake new file mode 100644 index 0000000..869134f --- /dev/null +++ b/CMake/IAProjectConfig.cmake @@ -0,0 +1,29 @@ +macro(iacore_setup_project) + + set(CMAKE_CXX_STANDARD 20) + set(CMAKE_CXX_STANDARD_REQUIRED ON) + set(CMAKE_CXX_EXTENSIONS OFF) + set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + + set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") + set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib") + set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib") + + if(MSVC) + add_compile_options(/W4) + if(CMAKE_CXX_COMPILER_ID MATCHES "Clang") + add_compile_options(-Wno-c++98-compat -Wno-c++98-compat-pedantic) + endif() + elseif(CMAKE_CXX_COMPILER_ID MATCHES "Clang|GNU") + add_compile_options(-Wall -Wextra -Wpedantic -Wno-language-extension-token) + endif() + + if(CMAKE_SYSTEM_PROCESSOR MATCHES "amd64|x86_64|AMD64") + set(IACORE_ARCH_X64 TRUE CACHE INTERNAL "") + elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "aarch64|arm64|ARM64") + set(IACORE_ARCH_ARM64 TRUE CACHE INTERNAL "") + elseif(CMAKE_SYSTEM_PROCESSOR MATCHES "wasm32|emscripten") + set(IACORE_ARCH_WASM TRUE CACHE INTERNAL "") + endif() + +endmacro() diff --git a/CMake/PatchMimalloc.cmake b/CMake/PatchMimalloc.cmake new file mode 100644 index 0000000..e877dbe --- /dev/null +++ b/CMake/PatchMimalloc.cmake @@ -0,0 +1,36 @@ +# Get the file path from the command line argument +set(TARGET_FILE "${SOURCE_DIR}/src/prim/windows/prim.c") + +# Read the file +file(READ "${TARGET_FILE}" FILE_CONTENT) + +# Check if the patch is already applied +string(FIND "${FILE_CONTENT}" "struct _TEB* const teb" ALREADY_PATCHED) + +if(ALREADY_PATCHED EQUAL -1) + message(STATUS "Patching mimalloc source: ${TARGET_FILE}") + + # Perform the replacement + string(REPLACE + "_TEB* const teb = NtCurrentTeb()" + "struct _TEB* const teb = NtCurrentTeb()" + FILE_CONTENT + "${FILE_CONTENT}" + ) + + # Write the file back only if changes were made + file(WRITE "${TARGET_FILE}" "${FILE_CONTENT}") +else() + message(STATUS "mimalloc source is already patched. Skipping.") +endif() + +# Patch mimalloc complaing about false positive alignment issues in libc loader +file(READ "${SOURCE_DIR}/CMakeLists.txt" MI_CMAKE_CONTENT) +string(REPLACE + "set(mi_debug_default ON)" + "set(mi_debug_default OFF)" + MI_CMAKE_CONTENT + "${MI_CMAKE_CONTENT}" +) +file(WRITE "${SOURCE_DIR}/CMakeLists.txt" "${MI_CMAKE_CONTENT}") +message(STATUS "Patched mimalloc: Forced MI_DEBUG default to OFF to silence alignment warnings.") \ No newline at end of file diff --git a/CMake/Toolchains/arm64-linux-clang.cmake b/CMake/Toolchains/arm64-linux-clang.cmake new file mode 100644 index 0000000..c9480aa --- /dev/null +++ b/CMake/Toolchains/arm64-linux-clang.cmake @@ -0,0 +1,21 @@ +set(CMAKE_SYSTEM_NAME Linux) +set(CMAKE_SYSTEM_PROCESSOR aarch64) + +set(triple aarch64-linux-gnu) +set(CMAKE_C_COMPILER_TARGET ${triple}) +set(CMAKE_CXX_COMPILER_TARGET ${triple}) + +set(CMAKE_SYSROOT /usr/aarch64-linux-gnu/sys-root) +set(CMAKE_LIBRARY_ARCHITECTURE aarch64-linux-gnu) + +string(APPEND CMAKE_C_FLAGS " -march=armv8-a+simd") +string(APPEND CMAKE_CXX_FLAGS " -march=armv8-a+simd") + +set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) +set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) + +link_directories(${CMAKE_SYSROOT}/usr/lib64) +link_directories(${CMAKE_SYSROOT}/lib64) + diff --git a/CMake/Toolchains/arm64-windows-clang.cmake b/CMake/Toolchains/arm64-windows-clang.cmake new file mode 100644 index 0000000..a4791bd --- /dev/null +++ b/CMake/Toolchains/arm64-windows-clang.cmake @@ -0,0 +1,18 @@ +set(CMAKE_SYSTEM_NAME Windows) +set(CMAKE_SYSTEM_PROCESSOR aarch64) +set(CMAKE_C_COMPILER clang-cl) +set(CMAKE_CXX_COMPILER clang-cl) +set(CMAKE_RC_COMPILER llvm-rc) + +set(CMAKE_LINKER lld-link) + +set(triple arm64-pc-windows-msvc) +set(CMAKE_C_COMPILER_TARGET ${triple}) +set(CMAKE_CXX_COMPILER_TARGET ${triple}) + +set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) +set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) +set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) + +string(APPEND CMAKE_C_FLAGS " /clang:--target=arm64-pc-windows-msvc") +string(APPEND CMAKE_CXX_FLAGS " /clang:--target=arm64-pc-windows-msvc") diff --git a/CMake/Toolchains/wasm32-emscripten-clang.cmake b/CMake/Toolchains/wasm32-emscripten-clang.cmake new file mode 100644 index 0000000..78ff32a --- /dev/null +++ b/CMake/Toolchains/wasm32-emscripten-clang.cmake @@ -0,0 +1,7 @@ +set(CMAKE_SYSTEM_NAME Emscripten) +set(CMAKE_SYSTEM_PROCESSOR wasm32) + +include("$ENV{EMSDK}/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake") + +string(APPEND CMAKE_C_FLAGS " -msimd128 -pthread") +string(APPEND CMAKE_CXX_FLAGS " -msimd128 -pthread") diff --git a/CMake/Toolchains/x64-linux-clang.cmake b/CMake/Toolchains/x64-linux-clang.cmake new file mode 100644 index 0000000..371dbc7 --- /dev/null +++ b/CMake/Toolchains/x64-linux-clang.cmake @@ -0,0 +1,6 @@ +set(CMAKE_SYSTEM_NAME Linux) +set(CMAKE_SYSTEM_PROCESSOR AMD64) + +string(APPEND CMAKE_C_FLAGS " -mavx2 -mfma -mxsave -msse4.2") +string(APPEND CMAKE_CXX_FLAGS " -mavx2 -mfma -mxsave -msse4.2") + diff --git a/CMake/Toolchains/x64-windows.cmake b/CMake/Toolchains/x64-windows.cmake new file mode 100644 index 0000000..cde73b9 --- /dev/null +++ b/CMake/Toolchains/x64-windows.cmake @@ -0,0 +1,14 @@ +set(CMAKE_SYSTEM_NAME Windows) +set(CMAKE_SYSTEM_PROCESSOR AMD64) +set(CMAKE_C_COMPILER clang-cl) +set(CMAKE_CXX_COMPILER clang-cl) +set(CMAKE_RC_COMPILER llvm-rc) + +set(triple x86_64-pc-windows-msvc) +set(CMAKE_C_COMPILER_TARGET ${triple}) +set(CMAKE_CXX_COMPILER_TARGET ${triple}) + +set(CMAKE_LINKER lld-link) + +string(APPEND CMAKE_C_FLAGS " /arch:AVX2 /clang:-mfma") +string(APPEND CMAKE_CXX_FLAGS " /arch:AVX2 /clang:-mfma") diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..d6c5868 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,41 @@ +cmake_minimum_required(VERSION 3.28 FATAL_ERROR) + +project(IACore LANGUAGES C CXX) + +if(CMAKE_CURRENT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR) + set(IACORE_IS_TOP_LEVEL ON) +else() + set(IACORE_IS_TOP_LEVEL OFF) +endif() + +set(IACORE_ROOT "${CMAKE_CURRENT_LIST_DIR}" CACHE INTERNAL "") +set(IACORE_CMAKE_DIR "${IACORE_ROOT}/CMake" CACHE INTERNAL "") +list(APPEND CMAKE_MODULE_PATH "${IACORE_CMAKE_DIR}") + +include(IAProjectConfig) +iacore_setup_project() + +message(STATUS "[IACore] Detected Compiler: ${CMAKE_CXX_COMPILER_ID}") + +if (MSVC AND NOT CMAKE_CXX_COMPILER_ID MATCHES "Clang") + message(FATAL_ERROR + "\n-------------------------------------------------------------\n" + "CRITICAL ERROR: Unsupported Compiler (MSVC/cl.exe)\n" + "IACore requires GCC or Clang. On Windows, use 'Clang-cl' or MinGW.\n" + "-------------------------------------------------------------\n" + ) +endif() + +include(FindDeps) + +option(IACore_BUILD_TESTS "Build unit tests" ${IACORE_IS_TOP_LEVEL}) + +add_subdirectory(Src) + +if(IACore_BUILD_TESTS) + add_subdirectory(Tests) +endif() + +if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/Sandbox") + # add_subdirectory(Sandbox) +endif() diff --git a/CMakePresets.json b/CMakePresets.json new file mode 100644 index 0000000..ab3a685 --- /dev/null +++ b/CMakePresets.json @@ -0,0 +1,78 @@ +{ + "version": 3, + "cmakeMinimumRequired": { + "major": 3, + "minor": 28 + }, + "configurePresets": [ + { + "name": "iacore-base", + "hidden": true, + "generator": "Ninja Multi-Config", + "binaryDir": "${sourceDir}/out/build/${presetName}", + "cacheVariables": { + "CMAKE_C_COMPILER": "clang", + "CMAKE_CXX_COMPILER": "clang++", + "CMAKE_TOOLCHAIN_FILE": "${sourceDir}/CMake/Toolchains/${presetName}-clang.cmake" + } + }, + { + "name": "x64-linux", + "displayName": "IACore Linux x64 (Clang)", + "inherits": "iacore-base", + "cacheVariables": {} + }, + { + "name": "arm64-linux", + "displayName": "IACore Linux ARM64 (Clang Cross)", + "inherits": "iacore-base", + "cacheVariables": {} + }, + { + "name": "x64-windows", + "displayName": "IACore Windows x64 (Clang)", + "inherits": "iacore-base", + "cacheVariables": { + "CMAKE_C_COMPILER": "clang-cl", + "CMAKE_CXX_COMPILER": "clang-cl" + } + }, + { + "name": "arm64-windows", + "displayName": "IACore Windows ARM64 (Clang Cross)", + "inherits": "iacore-base", + "cacheVariables": { + "CMAKE_C_COMPILER": "clang-cl", + "CMAKE_CXX_COMPILER": "clang-cl" + } + }, + { + "name": "wasm", + "displayName": "IACore WebAssembly (Clang)", + "inherits": "iacore-base", + "cacheVariables": {} + } + ], + "buildPresets": [ + { + "name": "x64-linux", + "configurePreset": "x64-linux" + }, + { + "name": "arm64-linux", + "configurePreset": "arm64-linux" + }, + { + "name": "x64-windows", + "configurePreset": "x64-windows" + }, + { + "name": "arm64-windows", + "configurePreset": "arm64-windows" + }, + { + "name": "wasm", + "configurePreset": "wasm" + } + ] +} \ No newline at end of file diff --git a/Docs/BUILDING.md b/Docs/BUILDING.md new file mode 100644 index 0000000..e5aa78e --- /dev/null +++ b/Docs/BUILDING.md @@ -0,0 +1,37 @@ +## ๐Ÿ› ๏ธ Building IACore + +IACore uses **CMake Presets** to manage toolchains and cross-compilation. This ensures that the correct compilers (Clang) and flags (AVX2/SIMD) are used automatically. + +### Prerequisites +* CMake 3.28+ +* Ninja Build System +* Vcpkg (Environment variable `VCPKG_ROOT` must be set) +* Clang / Clang-CL + +### Build Instructions + +**1. Configure** +Select the preset for your target platform. +```bash +# List available presets +cmake --list-presets + +# Configure for your platform (e.g., windows-x64, linux-arm64, wasm) +cmake --preset windows-x64 +``` + +**2. Build** + +```bash +cmake --build --preset windows-x64 +``` + +### Available Presets + +|Preset |Description |Toolchain | +|-------------|------------------------------|------------------------------------| +|windows-x64 |Windows (Clang-CL) |CMake/Toolchains/windows-x64.cmake | +|linux-x64 |Linux (Clang) |CMake/Toolchains/linux-x64.cmake | +|wasm |WebAssembly (Emscripten) |CMake/Toolchains/wasm.cmake | +|windows-arm64|Windows on ARM (Cross-compile)|CMake/Toolchains/windows-arm64.cmake| +|linux-arm64 |Linux on ARM (Cross-compile) |CMake/Toolchains/linux-arm64.cmake | diff --git a/Docs/USING.md b/Docs/USING.md new file mode 100644 index 0000000..e58594f --- /dev/null +++ b/Docs/USING.md @@ -0,0 +1,27 @@ +## ๐Ÿš€ Using IACore in a New Project + +IACore provides a CMake macro `iacore_setup_project()`, which standardizes your build environment. This macro automatically: + +* Enforces C++20 standard. +* Sets warning levels (-Wall -Wextra -Wpedantic for Clang/GCC, /W4 for MSVC/Clang-CL). +* Detects the target architecture (x64, ARM64, WASM) and sets internal cache variables. +* Suppresses C++98 compatibility warnings when using Clang on Windows. + +Example CMakeLists.txt +```cmake +cmake_minimum_required(VERSION 3.28) +project(MyGame) + +# Or you can use FetchContent +add_subdirectory(external/IACore) + +## Apply IACore's standard project configuration +# This applies C++20 and strict warning flags globally to your targets. +iacore_setup_project() + +# Define your target(s) +add_executable(MyGame src/main.cpp) + +# Link IACore +target_link_libraries(MyGame PRIVATE IACore) +``` diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f433b1a --- /dev/null +++ b/LICENSE @@ -0,0 +1,177 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/README.md b/README.md new file mode 100644 index 0000000..bdd02a0 --- /dev/null +++ b/README.md @@ -0,0 +1,182 @@ +# IACore (Independent Architecture Core) + +
+ IACore Logo +
+ + License + C++ Standard + Platform + +

+ A High-Performance Foundation for Modern C++ 20 Applications. +

+
+ +> [!NOTE] +> This branch presents the latest version of IACore (v1.2), which is a major rewrite of the previous API. If you're looking for the previous API instead, please find it in the `_legay_api` branch! + +## ๐Ÿ“– Description + +IACore is a high-performance C++20 foundation library bundling essential systems (IPC, Logging, Networking, Compression, and Async Scheduling) into a single, coherent API. + +IACore strictly follows the coding style and philosophy of [Oxide](https://github.com/I-A-S/Oxide). + +Originally developed as the internal core for IASoft (PVT) LTD., it is now open-source to provide a standardized bedrock for C++ applications where performance matters. + +## โœจ Features + +* **๐Ÿš€ High-Performance IPC:** Shared-Memory Ring Buffers with wait-free SPSC synchronization. +* **๐ŸŒ Networking:** Integrated HTTP/HTTPS client (wrapper around `cpp-httplib` with automatic Zlib/Gzip handling). +* **๐Ÿงต Async Scheduler:** A job system with high/normal priority queues. +* **๐Ÿ’พ File I/O:** Memory-mapped file operations and optimized binary stream readers/writers. +* **๐Ÿ“ฆ Compression:** Unified API for Zlib, Gzip, and Zstd. +* **๐Ÿ“œ Logging:** Thread-safe, colored console and disk logging. +* **โšก Modern C++:** Heavily utilizes modern C++20 concepts, `std::span`, and `Oxide` for error handling. + +## ๐Ÿ’ก Usage Examples +### 1. IPC (Manager & Node) +IACore provides a manager/node architecture using shared memory. + +#### Manager: +```C++ +#include + +// Spawns a child process and connects via Shared Memory +auto nodeID = manager.SpawnNode("MyChildNodeExe"); +manager.WaitTillNodeIsOnline(*nodeID); + +// Send data with zero-copy overhead +String msg = "Hello Node"; +manager.SendPacket(*nodeID, 100, {(PCUINT8)msg.data(), msg.size()}); +``` + +#### Node: +```C++ +#include + +class Node : public IACore::IPC_Node { +public: + void OnSignal(uint8_t signal) override { + // Handle signals + } + + void OnPacket(uint16_t packetID, std::span payload) override { + // Handle packets + } +}; + +int main(int argc, char* argv[]) { + // The connection string is passed as the first argument by the Manager + if (argc < 2) return -1; + + Node node; + // Connect back to the manager via Shared Memory + if (!node.Connect(argv[1])) return -1; + + while(true) { + node.Update(); + } + return 0; +} +``` + +### 2. Async Jobs +```C++ + +#include + +// Initialize worker threads (hardware_concurrency - 2) +IACore::AsyncOps::InitializeScheduler(); + +// Schedule a task +IACore::AsyncOps::Schedule *mySchedule = new IACore::AsyncOps::Schedule(); + +IACore::AsyncOps::ScheduleTask([](auto workerID) { + printf("Running on worker %d\n", workerID); +}, 0, mySchedule); + +// Wait for completion +IACore::AsyncOps::WaitForScheduleCompletion(mySchedule); +``` + +### 3. HTTP Request +```C++ +#include + +IACore::HttpClient client("https://api.example.com"); +auto res = client.JsonGet("/data", {}); + +if (res) { + std::cout << "Data: " << res->value << "\n"; +} else { + std::cerr << "Error: " << res.error() << "\n"; +} +``` + +## ๐Ÿ› ๏ธ Integration + +IACore is built with CMake. You can include it in your project via `FetchContent` or by adding it as a subdirectory. + +**Note:** On Windows, you must have **VCPKG** installed and the `VCPKG_ROOT` environment variable set, for OpenSSL support. + +### CMake Example +```cmake +cmake_minimum_required(VERSION 3.28) +project(MyGame) + +# Or you can use FetchContent +add_subdirectory(external/IACore) + +# Apply IACore's standard project configuration +# This applies C++20 and strict warning flags globally to your targets. +iacore_setup_project() + +# Define your targets +add_executable(MyGame src/main.cpp) + +# Link IACore +target_link_libraries(MyGame PRIVATE IACore) +``` + +## ๐Ÿค Contributing + +We welcome contributions from the community! + +### What we accept immediately: +* **๐Ÿ“š Documentation:** Improvements to comments, the README, or external docs. +* **๐Ÿงช Tests:** New unit tests (in `Tests/`) to improve coverage or reproduce bugs. +* **๐Ÿ’ก Examples:** New usage examples or sample projects. +* **๐Ÿ› Bug Reports:** detailed issues describing reproduction steps are highly valued. + +### Core Library Policy (`Src/` Directory) +Currently, **we are not accepting Pull Requests that modify the core source code (`Src/`)**. + +If you find a critical bug in `Src/`, please open an **Issue** rather than a PR, and the core team will implement a fix ASAP. + +## ๐Ÿ“ฆ Dependencies +IACore manages its own dependencies via CMake's FetchContent. You do not need to install these manually: + +* **JSON:** `glaze` +* **SIMD:** `google-highway` +* **Networking:** `cpp-httplib` +* **Compression:** `zlib-ng` & `zstd` +* **Utilities:** `tl-expected` & `unordered_dense` + +**Note:** Following dependencies are not directly used by IACore, but bundles them (+ helper wrappers) for user convenience: `nlohmann_json`, `simdjson`, `pugixml` + +## โš–๏ธ License + +This project is licensed under the Apache License Version 2.0. + +## ๐Ÿค– Use of Generative AI at IASoft + +While we never let Generative AI to make architecural/design decisions, and we hand-write almost all of the implementations, we do use Generative AI (Google Gemini) for the following and *(ONLY following)* tasks: + +1) **Controlled Repititive Boilerplate Generation:** Each and **every single line of AI generated code** is **manually reviewed** one-by-one for hallucinations and logic errors. Trust, but verify. + +2) **Concept Research and Development:** Design pattern comparisions, cost-benefit analysis and as a second pair of eyes to evalute and critique our design decisions. + +3) **Documentation:** Repititive method doc strings (parameter, return value descriptions) are done mostly using Generative AI. + +4) **Code Review:** Automated logic checking and static analysis on top of deterministic tools. diff --git a/Src/CMakeLists.txt b/Src/CMakeLists.txt new file mode 100644 index 0000000..86f4de2 --- /dev/null +++ b/Src/CMakeLists.txt @@ -0,0 +1,2 @@ + +add_subdirectory(IACore/) diff --git a/Src/IACore/CMakeLists.txt b/Src/IACore/CMakeLists.txt new file mode 100644 index 0000000..677c853 --- /dev/null +++ b/Src/IACore/CMakeLists.txt @@ -0,0 +1,97 @@ +set(SRC_FILES + "imp/cpp/CLI.cpp" + "imp/cpp/IPC.cpp" + "imp/cpp/XML.cpp" + "imp/cpp/SIMD.cpp" + "imp/cpp/JSON.cpp" + "imp/cpp/IACore.cpp" + "imp/cpp/Logger.cpp" + "imp/cpp/Utils.cpp" + "imp/cpp/FileOps.cpp" + "imp/cpp/DataOps.cpp" + "imp/cpp/AsyncOps.cpp" + "imp/cpp/Platform.cpp" + "imp/cpp/SocketOps.cpp" + "imp/cpp/StringOps.cpp" + "imp/cpp/ProcessOps.cpp" + "imp/cpp/StreamReader.cpp" + "imp/cpp/StreamWriter.cpp" + + "imp/cpp/Http/Common.cpp" + "imp/cpp/Http/Client.cpp" + "imp/cpp/Http/Server.cpp" +) + +add_library(IACore STATIC ${SRC_FILES}) + +target_include_directories(IACore PUBLIC inc/) +target_include_directories(IACore PRIVATE imp/hpp/) + +target_link_libraries(IACore PUBLIC + hwy + zlib + Oxide + zstd::libzstd + glaze::glaze + httplib::httplib + pugixml::pugixml + simdjson::simdjson + nlohmann_json::nlohmann_json + unordered_dense::unordered_dense +) + +target_link_libraries(IACore PRIVATE + OpenSSL::SSL + OpenSSL::Crypto +) + +target_link_libraries(IACore PUBLIC mimalloc-static) + +if(WIN32) + if(MSVC) + target_link_options(IACore PUBLIC "/INCLUDE:mi_version") + else() + target_link_options(IACore PUBLIC "") + endif() +endif() + +target_precompile_headers(IACore PUBLIC inc/IACore/PCH.hpp) + +set(NO_EXCEPT_FLAG "$,/EHs-c-,-fno-exceptions>") +target_compile_options(IACore PRIVATE ${NO_EXCEPT_FLAG}) +target_compile_options(IACore INTERFACE + $<$>>:${NO_EXCEPT_FLAG}> +) + +define_property(TARGET PROPERTY USE_EXCEPTIONS + BRIEF_DOCS "If ON, this target is allowed to use C++ exceptions." + FULL_DOCS "Prevents IACore from propagating -fno-exceptions to this target." +) + +target_compile_definitions(IACore PRIVATE + CPPHTTPLIB_OPENSSL_SUPPORT + CPPHTTPLIB_ZLIB_SUPPORT + NOMINMAX +) + +target_compile_definitions(IACore PUBLIC + $<$:__IA_DEBUG=1> + $<$:__IA_DEBUG=0> +) + +if(IACORE_ARCH_X64) + if(MSVC) + target_compile_options(IACore INTERFACE /arch:AVX2) + else() + target_compile_options(IACore INTERFACE -mavx2 -mfma -mpclmul -maes -mbmi -mbmi2 -mf16c) + endif() + target_compile_definitions(IACore INTERFACE HWY_BASELINE_TARGETS=HWY_AVX2) +elseif(IACORE_ARCH_ARM64) + if(NOT MSVC) + target_compile_options(IACore INTERFACE -march=armv8-a+simd) + endif() + target_compile_definitions(IACore INTERFACE HWY_BASELINE_TARGETS=HWY_NEON) +elseif(IACORE_ARCH_WASM) + target_compile_options(IACore INTERFACE -msimd128) + target_compile_definitions(IACore INTERFACE HWY_BASELINE_TARGETS=HWY_WASM) +endif() \ No newline at end of file diff --git a/Src/IACore/imp/cpp/AsyncOps.cpp b/Src/IACore/imp/cpp/AsyncOps.cpp new file mode 100644 index 0000000..8f73ad1 --- /dev/null +++ b/Src/IACore/imp/cpp/AsyncOps.cpp @@ -0,0 +1,200 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2026 IAS (ias@iasoft.dev) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include + +namespace IACore { +Mut AsyncOps::s_queue_mutex; +Mut AsyncOps::s_wake_condition; +Mut> AsyncOps::s_schedule_workers; +Mut> AsyncOps::s_high_priority_queue; +Mut> AsyncOps::s_normal_priority_queue; + +auto AsyncOps::run_task(Mut> task) -> void { + std::jthread(std::move(task)).detach(); +} + +auto AsyncOps::initialize_scheduler(Mut worker_count) -> Result { + if (worker_count == 0) { + Const hw_concurrency = std::thread::hardware_concurrency(); + Mut threads = 2; + if (hw_concurrency > 2) { + threads = hw_concurrency - 2; + } + + if (threads > 255) { + threads = 255; + } + worker_count = static_cast(threads); + } + + for (Mut i = 0; i < worker_count; ++i) { + s_schedule_workers.emplace_back(schedule_worker_loop, + static_cast(i + 1)); + } + + return {}; +} + +auto AsyncOps::terminate_scheduler() -> void { + for (MutRef worker : s_schedule_workers) { + worker.request_stop(); + } + + s_wake_condition.notify_all(); + + for (MutRef worker : s_schedule_workers) { + if (worker.joinable()) { + worker.join(); + } + } + + s_schedule_workers.clear(); +} + +auto AsyncOps::schedule_task(Mut> task, + Const tag, Const schedule, + Const priority) -> void { + ensure(!s_schedule_workers.empty(), + "Scheduler must be initialized before calling schedule_task"); + + schedule->counter.fetch_add(1); + { + Const> lock(s_queue_mutex); + if (priority == Priority::High) { + s_high_priority_queue.emplace_back( + ScheduledTask{tag, schedule, std::move(task)}); + } else { + s_normal_priority_queue.emplace_back( + ScheduledTask{tag, schedule, std::move(task)}); + } + } + s_wake_condition.notify_one(); +} + +auto AsyncOps::cancel_tasks_of_tag(Const tag) -> void { + Const> lock(s_queue_mutex); + + { + MutRef> queue = s_high_priority_queue; + for (Mut::iterator> it = queue.begin(); + it != queue.end(); + /* no incr */) { + if (it->tag == tag) { + if (it->schedule_handle->counter.fetch_sub(1) == 1) { + it->schedule_handle->counter.notify_all(); + } + it = queue.erase(it); + } else { + ++it; + } + } + } + + { + MutRef> queue = s_normal_priority_queue; + for (Mut::iterator> it = queue.begin(); + it != queue.end(); + /* no incr */) { + if (it->tag == tag) { + if (it->schedule_handle->counter.fetch_sub(1) == 1) { + it->schedule_handle->counter.notify_all(); + } + it = queue.erase(it); + } else { + ++it; + } + } + } +} + +auto AsyncOps::wait_for_schedule_completion(Const schedule) + -> void { + ensure(!s_schedule_workers.empty(), "Scheduler must be initialized before " + "calling wait_for_schedule_completion"); + + while (schedule->counter.load() > 0) { + Mut task; + Mut found_task = false; + { + Mut> lock(s_queue_mutex); + if (!s_high_priority_queue.empty()) { + task = std::move(s_high_priority_queue.front()); + s_high_priority_queue.pop_front(); + found_task = true; + } else if (!s_normal_priority_queue.empty()) { + task = std::move(s_normal_priority_queue.front()); + s_normal_priority_queue.pop_front(); + found_task = true; + } + } + + if (found_task) { + task.task(MAIN_THREAD_WORKER_ID); + if (task.schedule_handle->counter.fetch_sub(1) == 1) { + task.schedule_handle->counter.notify_all(); + } + } else { + Const current_val = schedule->counter.load(); + if (current_val > 0) { + schedule->counter.wait(current_val); + } + } + } +} + +auto AsyncOps::get_worker_count() -> WorkerId { + return static_cast(s_schedule_workers.size()); +} + +auto AsyncOps::schedule_worker_loop(Const stop_token, + Const worker_id) -> void { + while (!stop_token.stop_requested()) { + Mut task; + Mut found_task = false; + { + Mut> lock(s_queue_mutex); + + s_wake_condition.wait(lock, [&stop_token] { + return !s_high_priority_queue.empty() || + !s_normal_priority_queue.empty() || stop_token.stop_requested(); + }); + + if (stop_token.stop_requested() && s_high_priority_queue.empty() && + s_normal_priority_queue.empty()) { + return; + } + + if (!s_high_priority_queue.empty()) { + task = std::move(s_high_priority_queue.front()); + s_high_priority_queue.pop_front(); + found_task = true; + } else if (!s_normal_priority_queue.empty()) { + task = std::move(s_normal_priority_queue.front()); + s_normal_priority_queue.pop_front(); + found_task = true; + } + } + + if (found_task) { + task.task(worker_id); + if (task.schedule_handle->counter.fetch_sub(1) == 1) { + task.schedule_handle->counter.notify_all(); + } + } + } +} + +} // namespace IACore \ No newline at end of file diff --git a/Src/IACore/imp/cpp/CLI.cpp b/Src/IACore/imp/cpp/CLI.cpp new file mode 100644 index 0000000..4616c3b --- /dev/null +++ b/Src/IACore/imp/cpp/CLI.cpp @@ -0,0 +1,27 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2026 IAS (ias@iasoft.dev) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include + +namespace IACore { +CLIParser::CLIParser(Const>> args) : m_arg_list(args) { + m_current_arg = m_arg_list.begin(); + + // Skip executable path + if (m_current_arg != m_arg_list.end()) { + m_current_arg++; + } +} +} // namespace IACore \ No newline at end of file diff --git a/Src/IACore/imp/cpp/DataOps.cpp b/Src/IACore/imp/cpp/DataOps.cpp new file mode 100644 index 0000000..9810196 --- /dev/null +++ b/Src/IACore/imp/cpp/DataOps.cpp @@ -0,0 +1,452 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2026 IAS (ias@iasoft.dev) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include + +#include +#include +#include +#include + +#if IA_ARCH_X64 +#include +#endif + +#if IA_ARCH_ARM64 +#include +#endif + +namespace IACore { +template +[[nodiscard]] inline auto read_unaligned(Const *> ptr) -> T { + Mut v; + std::memcpy(&v, ptr, sizeof(T)); + return v; +} + +struct Crc32Tables { + Mut table[8][256] = {}; + + consteval Crc32Tables() { + constexpr Const T = 0x82F63B78; + + for (Mut i = 0; i < 256; i++) { + Mut crc = i; + for (Mut j = 0; j < 8; j++) { + crc = (crc >> 1) ^ ((crc & 1) ? T : 0); + } + table[0][i] = crc; + } + + for (Mut i = 0; i < 256; i++) { + for (Mut slice = 1; slice < 8; slice++) { + Const prev = table[slice - 1][i]; + table[slice][i] = (prev >> 8) ^ table[0][prev & 0xFF]; + } + } + } +}; + +static constexpr Const CRC32_TABLES{}; + +#if IA_ARCH_X64 +inline auto crc32_x64_hw(Ref>> data) -> u32 { + Mut p = data.data(); + + Mut crc = 0xFFFFFFFF; + Mut len = data.size(); + + while (len >= 8) { + Const chunk = read_unaligned(p); + crc = static_cast(_mm_crc32_u64(static_cast(crc), chunk)); + p += 8; + len -= 8; + } + + while (len--) { + crc = _mm_crc32_u8(crc, *p++); + } + + return ~crc; +} +#endif + +#if IA_ARCH_ARM64 +__attribute__((target("+crc"))) inline auto +crc32_arm64_hw(Ref>> data) -> u32 { + Mut p = data.data(); + + Mut crc = 0xFFFFFFFF; + Mut len = data.size(); + + while (len >= 8) { + Const chunk = read_unaligned(p); + crc = __crc32cd(crc, chunk); + p += 8; + len -= 8; + } + + while (len--) { + crc = __crc32cb(crc, *p++); + } + + return ~crc; +} +#endif + +inline auto crc32_software_slice8(Ref>> data) -> u32 { + Mut p = data.data(); + Mut crc = 0xFFFFFFFF; + Mut len = data.size(); + + while (len >= 8) { + Const term1 = crc ^ read_unaligned(p); + Const term2 = read_unaligned(p + 4); + + crc = CRC32_TABLES.table[7][term1 & 0xFF] ^ + CRC32_TABLES.table[6][(term1 >> 8) & 0xFF] ^ + CRC32_TABLES.table[5][(term1 >> 16) & 0xFF] ^ + CRC32_TABLES.table[4][(term1 >> 24)] ^ + CRC32_TABLES.table[3][term2 & 0xFF] ^ + CRC32_TABLES.table[2][(term2 >> 8) & 0xFF] ^ + CRC32_TABLES.table[1][(term2 >> 16) & 0xFF] ^ + CRC32_TABLES.table[0][(term2 >> 24)]; + + p += 8; + len -= 8; + } + + while (len--) { + crc = (crc >> 8) ^ CRC32_TABLES.table[0][(crc ^ *p++) & 0xFF]; + } + + return ~crc; +} + +auto DataOps::crc32(Ref>> data) -> u32 { +#if IA_ARCH_X64 + // IACore mandates AVX2 so no need to check + return crc32_x64_hw(data); +#elif IA_ARCH_ARM64 + if (Platform::GetCapabilities().HardwareCRC32) { + return crc32_arm64_hw(data); + } +#endif + return crc32_software_slice8(data); +} + +constexpr Const XXH_PRIME32_1 = 0x9E3779B1U; +constexpr Const XXH_PRIME32_2 = 0x85EBCA77U; +constexpr Const XXH_PRIME32_3 = 0xC2B2AE3DU; +constexpr Const XXH_PRIME32_4 = 0x27D4EB2FU; +constexpr Const XXH_PRIME32_5 = 0x165667B1U; + +inline auto xxh32_round(Mut seed, Const input) -> u32 { + seed += input * XXH_PRIME32_2; + seed = std::rotl(seed, 13); + seed *= XXH_PRIME32_1; + return seed; +} + +auto DataOps::hash_xxhash(Ref string, Const seed) -> u32 { + return hash_xxhash( + Span>(reinterpret_cast(string.data()), + string.length()), + seed); +} + +auto DataOps::hash_xxhash(Ref>> data, Const seed) -> u32 { + Mut p = data.data(); + Const b_end = p + data.size(); + Mut h32{}; + + if (data.size() >= 16) { + Const limit = b_end - 16; + + Mut v1 = seed + XXH_PRIME32_1 + XXH_PRIME32_2; + Mut v2 = seed + XXH_PRIME32_2; + Mut v3 = seed + 0; + Mut v4 = seed - XXH_PRIME32_1; + + do { + v1 = xxh32_round(v1, read_unaligned(p)); + p += 4; + v2 = xxh32_round(v2, read_unaligned(p)); + p += 4; + v3 = xxh32_round(v3, read_unaligned(p)); + p += 4; + v4 = xxh32_round(v4, read_unaligned(p)); + p += 4; + } while (p <= limit); + + h32 = std::rotl(v1, 1) + std::rotl(v2, 7) + std::rotl(v3, 12) + + std::rotl(v4, 18); + } else { + h32 = seed + XXH_PRIME32_5; + } + + h32 += static_cast(data.size()); + + while (p + 4 <= b_end) { + Const t = read_unaligned(p) * XXH_PRIME32_3; + h32 += t; + h32 = std::rotl(h32, 17) * XXH_PRIME32_4; + p += 4; + } + + while (p < b_end) { + h32 += (*p++) * XXH_PRIME32_5; + h32 = std::rotl(h32, 11) * XXH_PRIME32_1; + } + + h32 ^= h32 >> 15; + h32 *= XXH_PRIME32_2; + h32 ^= h32 >> 13; + h32 *= XXH_PRIME32_3; + h32 ^= h32 >> 16; + + return h32; +} + +constexpr Const FNV1A_32_PRIME = 0x01000193; +constexpr Const FNV1A_32_OFFSET = 0x811c9dc5; + +auto DataOps::hash_fnv1a(Ref string) -> u32 { + Mut hash = FNV1A_32_OFFSET; + for (Const c : string) { + hash ^= static_cast(c); + hash *= FNV1A_32_PRIME; + } + return hash; +} + +auto DataOps::hash_fnv1a(Ref>> data) -> u32 { + Mut hash = FNV1A_32_OFFSET; + Const ptr = data.data(); + + for (Mut i = 0; i < data.size(); ++i) { + hash ^= ptr[i]; + hash *= FNV1A_32_PRIME; + } + return hash; +} + +auto DataOps::detect_compression(Const>> data) + -> CompressionType { + if (data.size() < 2) { + return CompressionType::None; + } + + if (data[0] == 0x1F && data[1] == 0x8B) { + return CompressionType::Gzip; + } + + if (data[0] == 0x78 && + (data[1] == 0x01 || data[1] == 0x9C || data[1] == 0xDA)) { + return CompressionType::Zlib; + } + + return CompressionType::None; +} + +auto DataOps::zlib_inflate(Ref>> data) -> Result> { + Mut zs{}; + zs.zalloc = Z_NULL; + zs.zfree = Z_NULL; + zs.opaque = Z_NULL; + + if (inflateInit2(&zs, 15 + 32) != Z_OK) { + return fail("Failed to initialize zlib inflate"); + } + + zs.next_in = const_cast(data.data()); + zs.avail_in = static_cast(data.size()); + + Mut> out_buffer; + Const guess_size = + data.size() < 1024 ? data.size() * 4 : data.size() * 2; + out_buffer.resize(guess_size); + + zs.next_out = reinterpret_cast(out_buffer.data()); + zs.avail_out = static_cast(out_buffer.size()); + + Mut ret; + do { + if (zs.avail_out == 0) { + Const current_pos = zs.total_out; + Const new_size = out_buffer.size() * 2; + out_buffer.resize(new_size); + + zs.next_out = reinterpret_cast(out_buffer.data() + current_pos); + zs.avail_out = static_cast(new_size - current_pos); + } + + ret = inflate(&zs, Z_NO_FLUSH); + + } while (ret == Z_OK); + + inflateEnd(&zs); + + if (ret != Z_STREAM_END) { + return fail("Failed to inflate: corrupt data or stream error"); + } + + out_buffer.resize(zs.total_out); + + return out_buffer; +} + +auto DataOps::zlib_deflate(Ref>> data) -> Result> { + Mut zs{}; + zs.zalloc = Z_NULL; + zs.zfree = Z_NULL; + zs.opaque = Z_NULL; + + if (deflateInit(&zs, Z_DEFAULT_COMPRESSION) != Z_OK) { + return fail("Failed to initialize zlib deflate"); + } + + zs.next_in = const_cast(data.data()); + zs.avail_in = static_cast(data.size()); + + Mut> out_buffer; + out_buffer.resize(deflateBound(&zs, static_cast(data.size()))); + + zs.next_out = reinterpret_cast(out_buffer.data()); + zs.avail_out = static_cast(out_buffer.size()); + + Const ret = deflate(&zs, Z_FINISH); + + if (ret != Z_STREAM_END) { + deflateEnd(&zs); + return fail("Failed to deflate, ran out of buffer memory"); + } + + out_buffer.resize(zs.total_out); + + deflateEnd(&zs); + return out_buffer; +} + +auto DataOps::zstd_inflate(Ref>> data) -> Result> { + Const content_size = + ZSTD_getFrameContentSize(data.data(), data.size()); + + if (content_size == ZSTD_CONTENTSIZE_ERROR) { + return fail("Failed to inflate: Not valid ZSTD compressed data"); + } + + if (content_size != ZSTD_CONTENTSIZE_UNKNOWN) { + Mut> out_buffer; + out_buffer.resize(static_cast(content_size)); + + Const d_size = ZSTD_decompress(out_buffer.data(), out_buffer.size(), + data.data(), data.size()); + + if (ZSTD_isError(d_size)) { + return fail("Failed to inflate: {}", ZSTD_getErrorName(d_size)); + } + + return out_buffer; + } + + Mut dctx = ZSTD_createDCtx(); + Mut> out_buffer; + out_buffer.resize(data.size() * 2); + + Mut input = {data.data(), data.size(), 0}; + Mut output = {out_buffer.data(), out_buffer.size(), 0}; + + Mut ret; + do { + ret = ZSTD_decompressStream(dctx, &output, &input); + + if (ZSTD_isError(ret)) { + ZSTD_freeDCtx(dctx); + return fail("Failed to inflate: {}", ZSTD_getErrorName(ret)); + } + + if (output.pos == output.size) { + Const new_size = out_buffer.size() * 2; + out_buffer.resize(new_size); + output.dst = out_buffer.data(); + output.size = new_size; + } + + } while (ret != 0); + + out_buffer.resize(output.pos); + ZSTD_freeDCtx(dctx); + + return out_buffer; +} + +auto DataOps::zstd_deflate(Ref>> data) -> Result> { + Const max_dst_size = ZSTD_compressBound(data.size()); + + Mut> out_buffer; + out_buffer.resize(max_dst_size); + + Const compressed_size = ZSTD_compress(out_buffer.data(), max_dst_size, + data.data(), data.size(), 3); + + if (ZSTD_isError(compressed_size)) { + return fail("Failed to deflate: {}", ZSTD_getErrorName(compressed_size)); + } + + out_buffer.resize(compressed_size); + return out_buffer; +} + +auto DataOps::gzip_deflate(Ref>> data) -> Result> { + Mut zs{}; + zs.zalloc = Z_NULL; + zs.zfree = Z_NULL; + zs.opaque = Z_NULL; + + if (deflateInit2(&zs, Z_DEFAULT_COMPRESSION, Z_DEFLATED, 15 + 16, 8, + Z_DEFAULT_STRATEGY) != Z_OK) { + return fail("Failed to initialize gzip deflate"); + } + + zs.next_in = const_cast(data.data()); + zs.avail_in = static_cast(data.size()); + + Mut> out_buffer; + + out_buffer.resize(deflateBound(&zs, static_cast(data.size())) + 1024); + + zs.next_out = reinterpret_cast(out_buffer.data()); + zs.avail_out = static_cast(out_buffer.size()); + + Const ret = deflate(&zs, Z_FINISH); + + if (ret != Z_STREAM_END) { + deflateEnd(&zs); + return fail("Failed to deflate"); + } + + out_buffer.resize(zs.total_out); + + deflateEnd(&zs); + return out_buffer; +} + +auto DataOps::gzip_inflate(Ref>> data) -> Result> { + return zlib_inflate(data); +} + +} // namespace IACore \ No newline at end of file diff --git a/Src/IACore/imp/cpp/FileOps.cpp b/Src/IACore/imp/cpp/FileOps.cpp new file mode 100644 index 0000000..2f587ef --- /dev/null +++ b/Src/IACore/imp/cpp/FileOps.cpp @@ -0,0 +1,539 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2026 IAS (ias@iasoft.dev) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +#include + +#if IA_PLATFORM_UNIX +#include +#include +#include +#include +#endif + +namespace IACore { + +Mut>> + FileOps::s_mapped_files; + +auto FileOps::unmap_file(Const *> mapped_ptr) -> void { + if (!s_mapped_files.contains(mapped_ptr)) { + return; + } + + Mut it = s_mapped_files.find(mapped_ptr); + Const> handles = it->second; + s_mapped_files.erase(it); + +#if IA_PLATFORM_WINDOWS + ::UnmapViewOfFile(std::get<1>(handles)); + ::CloseHandle(static_cast(std::get<2>(handles))); + + Const handle = static_cast(std::get<0>(handles)); + if (handle != INVALID_HANDLE_VALUE) { + ::CloseHandle(handle); + } +#elif IA_PLATFORM_UNIX + ::munmap(std::get<1>(handles), (usize)std::get<2>(handles)); + Const fd = (i32)((u64)std::get<0>(handles)); + if (fd != -1) { + ::close(fd); + } +#endif +} + +auto FileOps::map_shared_memory(Ref name, Const size, + Const is_owner) -> Result { +#if IA_PLATFORM_WINDOWS + Const wchars_num = + MultiByteToWideChar(CP_UTF8, 0, name.c_str(), -1, NULL, 0); + Mut w_name(wchars_num, 0); + MultiByteToWideChar(CP_UTF8, 0, name.c_str(), -1, &w_name[0], wchars_num); + + Mut h_map = NULL; + if (is_owner) { + h_map = CreateFileMappingW(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, + (DWORD)(size >> 32), (DWORD)(size & 0xFFFFFFFF), + w_name.c_str()); + } else { + h_map = OpenFileMappingW(FILE_MAP_ALL_ACCESS, false, w_name.c_str()); + } + + if (h_map == NULL) { + return fail("Failed to {} shared memory '{}'", + is_owner ? "owner" : "consumer", name); + } + + Mut result = + static_cast(MapViewOfFile(h_map, FILE_MAP_ALL_ACCESS, 0, 0, size)); + if (result == NULL) { + CloseHandle(h_map); + return fail("Failed to map view of shared memory '{}'", name); + } + + s_mapped_files[result] = std::make_tuple((void *)INVALID_HANDLE_VALUE, + (void *)result, (void *)h_map); + return result; + +#elif IA_PLATFORM_UNIX + Mut fd = -1; + if (is_owner) { + fd = shm_open(name.c_str(), O_RDWR | O_CREAT | O_TRUNC, 0666); + if (fd != -1) { + if (ftruncate(fd, size) == -1) { + close(fd); + shm_unlink(name.c_str()); + return fail("Failed to truncate shared memory '{}'", name); + } + } + } else { + fd = shm_open(name.c_str(), O_RDWR, 0666); + } + + if (fd == -1) { + return fail("Failed to {} shared memory '{}'", + is_owner ? "owner" : "consumer", name); + } + + Mut addr = + mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); + if (addr == MAP_FAILED) { + close(fd); + return fail("Failed to mmap shared memory '{}'", name); + } + + Mut result = static_cast(addr); + + s_mapped_files[result] = + std::make_tuple((void *)((u64)fd), (void *)addr, (void *)size); + return result; +#endif +} + +auto FileOps::unlink_shared_memory(Ref name) -> void { + if (name.empty()) { + return; + } +#if IA_PLATFORM_UNIX + shm_unlink(name.c_str()); +#endif +} + +auto FileOps::map_file(Ref path, MutRef size) + -> Result { +#if IA_PLATFORM_WINDOWS + Const handle = CreateFileA( + path.string().c_str(), GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL | FILE_FLAG_SEQUENTIAL_SCAN, NULL); + + if (handle == INVALID_HANDLE_VALUE) { + return fail("Failed to open {} for memory mapping", path.string()); + } + + Mut file_size; + if (!GetFileSizeEx(handle, &file_size)) { + CloseHandle(handle); + return fail("Failed to get size of {} for memory mapping", path.string()); + } + size = static_cast(file_size.QuadPart); + if (size == 0) { + CloseHandle(handle); + return fail("Failed to get size of {} for memory mapping", path.string()); + } + + Mut h_map = + CreateFileMappingW(handle, NULL, PAGE_READONLY, 0, 0, NULL); + if (h_map == NULL) { + CloseHandle(handle); + return fail("Failed to memory map {}", path.string()); + } + + Const *result = + static_cast(MapViewOfFile(h_map, FILE_MAP_READ, 0, 0, 0)); + if (result == NULL) { + CloseHandle(handle); + CloseHandle(h_map); + return fail("Failed to memory map {}", path.string()); + } + s_mapped_files[result] = std::make_tuple( + (void *)handle, (void *)const_cast(result), (void *)h_map); + return result; + +#elif IA_PLATFORM_UNIX + Const handle = open(path.string().c_str(), O_RDONLY); + if (handle == -1) { + return fail("Failed to open {} for memory mapping", path.string()); + } + Mut sb; + if (fstat(handle, &sb) == -1) { + close(handle); + return fail("Failed to get stats of {} for memory mapping", path.string()); + } + size = static_cast(sb.st_size); + if (size == 0) { + close(handle); + return fail("Failed to get size of {} for memory mapping", path.string()); + } + Mut addr = mmap(nullptr, size, PROT_READ, MAP_PRIVATE, handle, 0); + if (addr == MAP_FAILED) { + close(handle); + return fail("Failed to memory map {}", path.string()); + } + Const *> result = static_cast(addr); + madvise(addr, size, MADV_SEQUENTIAL); + s_mapped_files[result] = + std::make_tuple((void *)((u64)handle), (void *)addr, (void *)size); + return result; +#endif +} + +auto FileOps::stream_to_file(Ref path, Const overwrite) + -> Result { + if (!overwrite && std::filesystem::exists(path)) { + return fail("File already exists: {}", path.string()); + } + return StreamWriter::create_from_file(path); +} + +auto FileOps::stream_from_file(Ref path) -> Result { + if (!std::filesystem::exists(path)) { + return fail("File does not exist: {}", path.string()); + } + return StreamReader::create_from_file(path); +} + +auto FileOps::read_text_file(Ref path) -> Result { + Mut f = fopen(path.string().c_str(), "r"); + if (!f) { + return fail("Failed to open file: {}", path.string()); + } + Mut result; + fseek(f, 0, SEEK_END); + Const len = ftell(f); + if (len > 0) { + result.resize(static_cast(len)); + fseek(f, 0, SEEK_SET); + fread(result.data(), 1, result.size(), f); + } + fclose(f); + return result; +} + +auto FileOps::read_binary_file(Ref path) -> Result> { + Mut f = fopen(path.string().c_str(), "rb"); + if (!f) { + return fail("Failed to open file: {}", path.string()); + } + Mut> result; + fseek(f, 0, SEEK_END); + Const len = ftell(f); + if (len > 0) { + result.resize(static_cast(len)); + fseek(f, 0, SEEK_SET); + fread(result.data(), 1, result.size(), f); + } + fclose(f); + return result; +} + +auto FileOps::write_text_file(Ref path, Ref contents, + Const overwrite) -> Result { + Const *> mode = overwrite ? "w" : "wx"; + Mut f = fopen(path.string().c_str(), mode); + if (!f) { + if (!overwrite && errno == EEXIST) { + return fail("File already exists: {}", path.string()); + } + return fail("Failed to write to file: {}", path.string()); + } + Const result = fwrite(contents.data(), 1, contents.size(), f); + fclose(f); + return result; +} + +auto FileOps::write_binary_file(Ref path, Const>> contents, + Const overwrite) -> Result { + Const *> mode = overwrite ? "w" : "wx"; + Mut f = fopen(path.string().c_str(), mode); + if (!f) { + if (!overwrite && errno == EEXIST) { + return fail("File already exists: {}", path.string()); + } + return fail("Failed to write to file: {}", path.string()); + } + Const result = fwrite(contents.data(), 1, contents.size(), f); + fclose(f); + return result; +} + +auto FileOps::normalize_executable_path(Ref path) -> Path { + Mut result = path; + +#if IA_PLATFORM_WINDOWS + if (!result.has_extension()) { + result.replace_extension(".exe"); + } +#elif IA_PLATFORM_UNIX + if (result.extension() == ".exe") { + result.replace_extension(""); + } + + if (result.is_relative()) { + Mut path_str = result.string(); + if (!path_str.starts_with("./") && !path_str.starts_with("../")) { + result = "./" + path_str; + } + } +#endif + return result; +} + +auto FileOps::native_open_file(Ref path, Const access, + Const mode, Const permissions) + -> Result { +#if IA_PLATFORM_WINDOWS + Mut dw_access = 0; + Mut dw_share = FILE_SHARE_READ; + Mut dw_disposition = 0; + Mut dw_flags_and_attributes = FILE_ATTRIBUTE_NORMAL; + + switch (access) { + case FileAccess::Read: + dw_access = GENERIC_READ; + break; + case FileAccess::Write: + dw_access = GENERIC_WRITE; + break; + case FileAccess::ReadWrite: + dw_access = GENERIC_READ | GENERIC_WRITE; + break; + } + + switch (mode) { + case FileMode::OpenExisting: + dw_disposition = OPEN_EXISTING; + break; + case FileMode::OpenAlways: + dw_disposition = OPEN_ALWAYS; + break; + case FileMode::CreateNew: + dw_disposition = CREATE_NEW; + break; + case FileMode::CreateAlways: + dw_disposition = CREATE_ALWAYS; + break; + case FileMode::TruncateExisting: + dw_disposition = TRUNCATE_EXISTING; + break; + } + + Mut h_file = + CreateFileA(path.string().c_str(), dw_access, dw_share, NULL, + dw_disposition, dw_flags_and_attributes, NULL); + + if (h_file == INVALID_HANDLE_VALUE) { + return fail("Failed to open file '{}': {}", path.string(), GetLastError()); + } + + return h_file; + +#elif IA_PLATFORM_UNIX + Mut flags = 0; + + switch (access) { + case FileAccess::Read: + flags = O_RDONLY; + break; + case FileAccess::Write: + flags = O_WRONLY; + break; + case FileAccess::ReadWrite: + flags = O_RDWR; + break; + } + + switch (mode) { + case FileMode::OpenExisting: + break; + case FileMode::OpenAlways: + flags |= O_CREAT; + break; + case FileMode::CreateNew: + flags |= O_CREAT | O_EXCL; + break; + case FileMode::CreateAlways: + flags |= O_CREAT | O_TRUNC; + break; + case FileMode::TruncateExisting: + flags |= O_TRUNC; + break; + } + + Mut fd = open(path.string().c_str(), flags, permissions); + + if (fd == -1) { + return fail("Failed to open file '{}': {}", path.string(), errno); + } + + return fd; +#endif +} + +auto FileOps::native_close_file(Const handle) -> void { + if (handle == INVALID_FILE_HANDLE) { + return; + } + +#if IA_PLATFORM_WINDOWS + CloseHandle(handle); +#elif IA_PLATFORM_UNIX + close(handle); +#endif +} + +FileOps::MemoryMappedRegion::~MemoryMappedRegion() { unmap(); } + +FileOps::MemoryMappedRegion::MemoryMappedRegion( + ForwardRef other) noexcept { + *this = std::move(other); +} + +auto FileOps::MemoryMappedRegion::operator=( + ForwardRef other) noexcept + -> MutRef { + if (this != &other) { + unmap(); + m_ptr = other.m_ptr; + m_size = other.m_size; +#if IA_PLATFORM_WINDOWS + m_map_handle = other.m_map_handle; + other.m_map_handle = NULL; +#endif + other.m_ptr = nullptr; + other.m_size = 0; + } + return *this; +} + +auto FileOps::MemoryMappedRegion::map(Const handle, + Const offset, Const size) + -> Result { + unmap(); + + if (handle == INVALID_FILE_HANDLE) { + return fail("Invalid file handle provided to Map"); + } + + if (size == 0) { + return fail("Cannot map region of size 0"); + } + +#if IA_PLATFORM_WINDOWS + Mut file_size; + if (!GetFileSizeEx(handle, &file_size)) { + return fail("Failed to get file size"); + } + + Const end_offset = offset + size; + if (static_cast(file_size.QuadPart) < end_offset) { + Mut new_size; + new_size.QuadPart = static_cast(end_offset); + if (!SetFilePointerEx(handle, new_size, NULL, FILE_BEGIN)) { + return fail("Failed to seek to new end of file"); + } + + if (!SetEndOfFile(handle)) { + return fail("Failed to extend file for mapping"); + } + } + + m_map_handle = CreateFileMappingW(handle, NULL, PAGE_READWRITE, 0, 0, NULL); + if (m_map_handle == NULL) { + return fail("CreateFileMapping failed: {}", GetLastError()); + } + + Const offset_high = static_cast(offset >> 32); + Const offset_low = static_cast(offset & 0xFFFFFFFF); + + m_ptr = static_cast(MapViewOfFile(m_map_handle, FILE_MAP_WRITE, + offset_high, offset_low, size)); + if (m_ptr == NULL) { + CloseHandle(m_map_handle); + m_map_handle = NULL; + return fail("MapViewOfFile failed (Offset: {}, Size: {}): {}", offset, size, + GetLastError()); + } + m_size = size; + +#elif IA_PLATFORM_UNIX + Mut sb; + if (fstat(handle, &sb) == -1) { + return fail("Failed to fstat file"); + } + + Const end_offset = offset + size; + if (static_cast(sb.st_size) < end_offset) { + if (ftruncate(handle, static_cast(end_offset)) == -1) { + return fail("Failed to ftruncate (extend) file"); + } + } + + Mut ptr = mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, + handle, static_cast(offset)); + if (ptr == MAP_FAILED) { + return fail("mmap failed: {}", errno); + } + + m_ptr = static_cast(ptr); + m_size = size; + + madvise(m_ptr, m_size, MADV_SEQUENTIAL); +#endif + + return {}; +} + +auto FileOps::MemoryMappedRegion::unmap() -> void { + if (!m_ptr) { + return; + } + +#if IA_PLATFORM_WINDOWS + UnmapViewOfFile(m_ptr); + if (m_map_handle) { + CloseHandle(m_map_handle); + m_map_handle = NULL; + } +#elif IA_PLATFORM_UNIX + munmap(m_ptr, m_size); +#endif + m_ptr = nullptr; + m_size = 0; +} + +auto FileOps::MemoryMappedRegion::flush() -> void { + if (!m_ptr) { + return; + } + +#if IA_PLATFORM_WINDOWS + FlushViewOfFile(m_ptr, m_size); +#elif IA_PLATFORM_UNIX + msync(m_ptr, m_size, MS_SYNC); +#endif +} + +} // namespace IACore \ No newline at end of file diff --git a/Src/IACore/imp/cpp/Http/Client.cpp b/Src/IACore/imp/cpp/Http/Client.cpp new file mode 100644 index 0000000..32e802a --- /dev/null +++ b/Src/IACore/imp/cpp/Http/Client.cpp @@ -0,0 +1,150 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2026 IAS (ias@iasoft.dev) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include + +namespace IACore { +auto HttpClient::create(Ref host) -> Result> { + return make_box_protected(httplib::Client(host)); +} + +static auto build_headers(Span> headers, + Const *> default_content_type) + -> httplib::Headers { + Mut out; + Mut has_content_type = false; + + for (Ref h : headers) { + out.emplace(h.first, h.second); + + if (h.first == HttpClient::header_type_to_string( + HttpClient::EHeaderType::CONTENT_TYPE)) { + has_content_type = true; + } + } + + if (!has_content_type && default_content_type) { + out.emplace("Content-Type", default_content_type); + } + return out; +} + +HttpClient::HttpClient(ForwardRef client) + : m_client(std::move(client)), + m_last_response_code(EResponseCode::INTERNAL_SERVER_ERROR) { + m_client.enable_server_certificate_verification(true); +} + +HttpClient::~HttpClient() = default; + +auto HttpClient::enable_certificate_verification() -> void { + m_client.enable_server_certificate_verification(true); +} + +auto HttpClient::disable_certificate_verification() -> void { + m_client.enable_server_certificate_verification(false); +} + +auto HttpClient::preprocess_response(Ref response) -> String { + Const>> response_bytes = { + reinterpret_cast *>(response.data()), response.size()}; + Const compression = + DataOps::detect_compression(response_bytes); + + switch (compression) { + case DataOps::CompressionType::Gzip: { + Const>> data = DataOps::gzip_inflate(response_bytes); + if (!data) { + return response; + } + return String(reinterpret_cast *>(data->data()), data->size()); + } + + case DataOps::CompressionType::Zlib: { + Const>> data = DataOps::zlib_inflate(response_bytes); + if (!data) { + return response; + } + return String(reinterpret_cast *>(data->data()), data->size()); + } + + case DataOps::CompressionType::None: + default: + break; + } + return response; +} + +auto HttpClient::raw_get(Ref path, Span> headers, + Const *> default_content_type) + -> Result { + Const http_headers = + build_headers(headers, default_content_type); + + Mut adjusted_path = path; + if (!path.empty() && path[0] != '/') { + adjusted_path = "/" + path; + } + + Const res = + m_client.Get(adjusted_path.c_str(), http_headers); + + if (res) { + m_last_response_code = static_cast(res->status); + if (res->status >= 200 && res->status < 300) { + return preprocess_response(res->body); + } + return fail("HTTP Error {} : {}", res->status, res->body); + } + + return fail("Network Error: {}", httplib::to_string(res.error())); +} + +auto HttpClient::raw_post(Ref path, Span> headers, + Ref body, + Const *> default_content_type) + -> Result { + Mut http_headers = + build_headers(headers, default_content_type); + + Mut content_type = default_content_type; + if (http_headers.count("Content-Type")) { + Const t = http_headers.find("Content-Type"); + content_type = t->second; + http_headers.erase(t); + } + + m_client.set_keep_alive(true); + + Mut adjusted_path = path; + if (!path.empty() && path[0] != '/') { + adjusted_path = "/" + path; + } + + Const res = m_client.Post( + adjusted_path.c_str(), http_headers, body, content_type.c_str()); + + if (res) { + m_last_response_code = static_cast(res->status); + if (res->status >= 200 && res->status < 300) { + return preprocess_response(res->body); + } + return fail("HTTP Error {} : {}", res->status, res->body); + } + + return fail("Network Error: {}", httplib::to_string(res.error())); +} +} // namespace IACore \ No newline at end of file diff --git a/Src/IACore/imp/cpp/Http/Common.cpp b/Src/IACore/imp/cpp/Http/Common.cpp new file mode 100644 index 0000000..b0f1716 --- /dev/null +++ b/Src/IACore/imp/cpp/Http/Common.cpp @@ -0,0 +1,117 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2026 IAS (ias@iasoft.dev) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include + +namespace IACore { +auto HttpCommon::url_encode(Ref value) -> String { + Mut escaped; + escaped.fill('0'); + escaped << std::hex << std::uppercase; + + for (Const c : value) { + if (std::isalnum(static_cast(c)) || c == '-' || c == '_' || + c == '.' || c == '~') + escaped << c; + else + escaped << '%' << std::setw(2) + << static_cast(static_cast(c)); + } + + return escaped.str(); +} + +auto HttpCommon::url_decode(Ref value) -> String { + Mut result; + result.reserve(value.length()); + + for (Mut i = 0; i < value.length(); ++i) { + if (value[i] == '%' && i + 2 < value.length()) { + Const hex_str = value.substr(i + 1, 2); + Const decoded_char = + static_cast(std::strtol(hex_str.c_str(), nullptr, 16)); + result += decoded_char; + i += 2; + } else if (value[i] == '+') + result += ' '; + else + result += value[i]; + } + + return result; +} + +auto HttpCommon::header_type_to_string(Const type) -> String { + switch (type) { + case EHeaderType::ACCEPT: + return "Accept"; + case EHeaderType::ACCEPT_CHARSET: + return "Accept-Charset"; + case EHeaderType::ACCEPT_ENCODING: + return "Accept-Encoding"; + case EHeaderType::ACCEPT_LANGUAGE: + return "Accept-Language"; + case EHeaderType::AUTHORIZATION: + return "Authorization"; + case EHeaderType::CACHE_CONTROL: + return "Cache-Control"; + case EHeaderType::CONNECTION: + return "Connection"; + case EHeaderType::CONTENT_LENGTH: + return "Content-Length"; + case EHeaderType::CONTENT_TYPE: + return "Content-Type"; + case EHeaderType::COOKIE: + return "Cookie"; + case EHeaderType::DATE: + return "Date"; + case EHeaderType::EXPECT: + return "Expect"; + case EHeaderType::HOST: + return "Host"; + case EHeaderType::IF_MATCH: + return "If-Match"; + case EHeaderType::IF_MODIFIED_SINCE: + return "If-Modified-Since"; + case EHeaderType::IF_NONE_MATCH: + return "If-None-Match"; + case EHeaderType::ORIGIN: + return "Origin"; + case EHeaderType::PRAGMA: + return "Pragma"; + case EHeaderType::PROXY_AUTHORIZATION: + return "Proxy-Authorization"; + case EHeaderType::RANGE: + return "Range"; + case EHeaderType::REFERER: + return "Referer"; + case EHeaderType::TE: + return "TE"; + case EHeaderType::UPGRADE: + return "Upgrade"; + case EHeaderType::USER_AGENT: + return "User-Agent"; + case EHeaderType::VIA: + return "Via"; + case EHeaderType::WARNING: + return "Warning"; + } + return ""; +} + +auto HttpCommon::is_success_response_code(Const code) -> bool { + return (i32)code >= 200 && (i32)code < 300; +} +} // namespace IACore \ No newline at end of file diff --git a/Src/IACore/imp/cpp/Http/Server.cpp b/Src/IACore/imp/cpp/Http/Server.cpp new file mode 100644 index 0000000..e1ccf3d --- /dev/null +++ b/Src/IACore/imp/cpp/Http/Server.cpp @@ -0,0 +1,161 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2026 IAS (ias@iasoft.dev) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include + +namespace IACore { + +auto HttpServer::Request::get_header(Ref key) const -> String { + if (Const::const_iterator> it = headers.find(key); + it != headers.end()) { + return it->second; + } + return ""; +} + +auto HttpServer::Request::get_param(Ref key) const -> String { + if (Const::const_iterator> it = params.find(key); + it != params.end()) { + return it->second; + } + return ""; +} + +auto HttpServer::Request::get_path_param(Ref key) const -> String { + if (Const::const_iterator> it = path_params.find(key); + it != path_params.end()) { + return it->second; + } + return ""; +} + +auto HttpServer::Request::has_header(Ref key) const -> bool { + return headers.contains(key); +} + +auto HttpServer::Request::has_param(Ref key) const -> bool { + return params.contains(key); +} + +auto HttpServer::Request::has_path_param(Ref key) const -> bool { + return path_params.contains(key); +} + +void HttpServer::Response::set_content(Ref content, Ref type) { + body = content; + content_type = type; +} + +void HttpServer::Response::set_status(Const status_code) { + code = status_code; +} + +void HttpServer::Response::add_header(Ref key, Ref value) { + headers[key] = value; +} + +struct PublicHttpServer : public HttpServer { + PublicHttpServer() = default; +}; + +HttpServer::HttpServer() = default; + +HttpServer::~HttpServer() { stop(); } + +auto HttpServer::create() -> Result> { + return make_box(); +} + +auto HttpServer::listen(Ref host, Const port) -> Result { + if (!m_server.listen(host.c_str(), static_cast(port))) { + return fail("Failed to start HTTP server on {}:{}", host, port); + } + return {}; +} + +void HttpServer::stop() { + if (m_server.is_running()) { + m_server.stop(); + } +} + +auto HttpServer::is_running() const -> bool { return m_server.is_running(); } + +void HttpServer::register_handler(Ref method, Ref pattern, + Const handler) { + Const wrapper = + [handler](Ref req, MutRef res) { + Mut ia_req; + ia_req.path = req.path; + ia_req.method = req.method; + ia_req.body = req.body; + + for (Ref> item : req.headers) { + ia_req.headers[item.first] = item.second; + } + + for (Ref> item : req.params) { + ia_req.params[item.first] = item.second; + } + + for (Ref> item : req.path_params) { + ia_req.path_params[item.first] = item.second; + } + + Mut ia_res; + handler(ia_req, ia_res); + + res.status = static_cast(ia_res.code); + res.set_content(ia_res.body, ia_res.content_type.c_str()); + + for (Ref> item : ia_res.headers) { + res.set_header(item.first.c_str(), item.second.c_str()); + } + }; + + if (method == "GET") { + m_server.Get(pattern.c_str(), wrapper); + } else if (method == "POST") { + m_server.Post(pattern.c_str(), wrapper); + } else if (method == "PUT") { + m_server.Put(pattern.c_str(), wrapper); + } else if (method == "DELETE") { + m_server.Delete(pattern.c_str(), wrapper); + } else if (method == "OPTIONS") { + m_server.Options(pattern.c_str(), wrapper); + } +} + +void HttpServer::get(Ref pattern, Const handler) { + register_handler("GET", pattern, handler); +} + +void HttpServer::post(Ref pattern, Const handler) { + register_handler("POST", pattern, handler); +} + +void HttpServer::put(Ref pattern, Const handler) { + register_handler("PUT", pattern, handler); +} + +void HttpServer::del(Ref pattern, Const handler) { + register_handler("DELETE", pattern, handler); +} + +void HttpServer::options(Ref pattern, Const handler) { + register_handler("OPTIONS", pattern, handler); +} + +} // namespace IACore \ No newline at end of file diff --git a/Src/IACore/imp/cpp/IACore.cpp b/Src/IACore/imp/cpp/IACore.cpp new file mode 100644 index 0000000..a759c81 --- /dev/null +++ b/Src/IACore/imp/cpp/IACore.cpp @@ -0,0 +1,56 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2026 IAS (ias@iasoft.dev) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include + +#include +#include + +namespace IACore { +Mut g_start_time = {}; + +static Mut g_main_thread_id = {}; +static Mut g_core_init_count = 0; + +auto initialize() -> void { + g_core_init_count++; + if (g_core_init_count > 1) { + return; + } + + g_main_thread_id = std::this_thread::get_id(); + g_start_time = std::chrono::high_resolution_clock::now(); + + Logger::initialize(); + + mi_option_set(mi_option_verbose, 0); +} + +auto terminate() -> void { + g_core_init_count--; + if (g_core_init_count > 0) { + return; + } + + Logger::terminate(); +} + +auto is_initialized() -> bool { return g_core_init_count > 0; } + +auto is_main_thread() -> bool { + return std::this_thread::get_id() == g_main_thread_id; +} +} // namespace IACore \ No newline at end of file diff --git a/Src/IACore/imp/cpp/IPC.cpp b/Src/IACore/imp/cpp/IPC.cpp new file mode 100644 index 0000000..203cdb9 --- /dev/null +++ b/Src/IACore/imp/cpp/IPC.cpp @@ -0,0 +1,451 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2026 IAS (ias@iasoft.dev) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include + +#include +#include +#include +#include + +namespace IACore { +struct IpcConnectionDescriptor { + Mut socket_path; + Mut shared_mem_path; + Mut shared_mem_size; + + [[nodiscard]] auto serialize() const -> String { + return std::format("{}|{}|{}|", socket_path, shared_mem_path, + shared_mem_size); + } + + static auto deserialize(Const data) + -> Option { + enum class ParseState { SocketPath, SharedMemPath, SharedMemSize }; + + Mut result{}; + Mut t = 0; + Mut state = ParseState::SocketPath; + + for (Mut i = 0; i < data.size(); ++i) { + if (data[i] != '|') { + continue; + } + + switch (state) { + case ParseState::SocketPath: + result.socket_path = String(data.substr(t, i - t)); + state = ParseState::SharedMemPath; + break; + + case ParseState::SharedMemPath: + result.shared_mem_path = String(data.substr(t, i - t)); + state = ParseState::SharedMemSize; + break; + + case ParseState::SharedMemSize: { + Const start = data.data() + t; + Const end = data.data() + i; + if (std::from_chars(start, end, result.shared_mem_size).ec != + std::errc{}) { + return std::nullopt; + } + return result; + } + } + t = i + 1; + } + return std::nullopt; + } +}; + +IpcNode::~IpcNode() { + if (m_socket != INVALID_SOCKET) { + SocketOps::close(m_socket); + } +} + +auto IpcNode::connect(Const connection_string) -> Result { + Const> desc_opt = + IpcConnectionDescriptor::deserialize(connection_string); + if (!desc_opt) { + return fail("Failed to parse connection string"); + } + Ref desc = *desc_opt; + m_shm_name = desc.shared_mem_path; + + m_socket = OX_TRY(SocketOps::create_unix_socket()); + OX_TRY_PURE( + SocketOps::connect_unix_socket(m_socket, desc.socket_path.c_str())); + + Mut mapped_ptr = OX_TRY(FileOps::map_shared_memory( + desc.shared_mem_path, desc.shared_mem_size, false)); + m_shared_memory = mapped_ptr; + + Mut layout = + reinterpret_cast(m_shared_memory); + + if (layout->meta.magic != 0x49414950) { + return fail("Invalid shared memory header signature"); + } + + if (layout->meta.version != 1) { + return fail("IPC version mismatch"); + } + + Mut moni_ptr = m_shared_memory + layout->moni_data_offset; + Mut mino_ptr = m_shared_memory + layout->mino_data_offset; + + m_moni = OX_TRY(RingBufferView::create( + &layout->moni_control, + Span(moni_ptr, static_cast(layout->moni_data_size)), false)); + + m_mino = OX_TRY(RingBufferView::create( + &layout->mino_control, + Span(mino_ptr, static_cast(layout->mino_data_size)), false)); + +#if IA_PLATFORM_WINDOWS + Mut mode = 1; + ioctlsocket(m_socket, FIONBIO, &mode); +#else + fcntl(m_socket, F_SETFL, O_NONBLOCK); +#endif + + m_receive_buffer.resize(UINT16_MAX + 1); + + return {}; +} + +void IpcNode::update() { + if (!m_moni.is_valid()) { + return; + } + + Mut header; + + while (m_moni.pop( + header, Span(m_receive_buffer.data(), m_receive_buffer.size()))) { + on_packet(header.id, {m_receive_buffer.data(), header.payload_size}); + } + + Mut signal = 0; + Const res = recv(m_socket, reinterpret_cast(&signal), 1, 0); + if (res == 1) { + on_signal(signal); + } else if (res == 0 || (res < 0 && !SocketOps::is_would_block())) { + SocketOps::close(m_socket); + FileOps::unlink_shared_memory(m_shm_name); + + std::exit(-1); + } +} + +void IpcNode::send_signal(Const signal) { + if (m_socket != INVALID_SOCKET) { + send(m_socket, reinterpret_cast(&signal), sizeof(signal), 0); + } +} + +auto IpcNode::send_packet(Const packet_id, Const>> payload) + -> Result { + if (!m_mino.is_valid()) + return fail("invalid MINO"); + return m_mino.push(packet_id, payload); +} + +void IpcManager::NodeSession::send_signal(Const signal) { + if (data_socket != INVALID_SOCKET) { + send(data_socket, reinterpret_cast(&signal), sizeof(signal), + 0); + } +} + +auto IpcManager::NodeSession::send_packet(Const packet_id, + Const>> payload) + -> Result { + Const> lock(send_mutex); + if (!moni.is_valid()) + return fail("invalid MONI"); + return moni.push(packet_id, payload); +} + +IpcManager::IpcManager() { + ensure(SocketOps::is_initialized(), + "SocketOps must be initialized before using IpcManager"); + + m_receive_buffer.resize(UINT16_MAX + 1); +} + +IpcManager::~IpcManager() { + for (MutRef> session : m_active_sessions) { + ProcessOps::terminate_process(session->node_process); + FileOps::unmap_file(session->mapped_ptr); + FileOps::unlink_shared_memory(session->shared_mem_name); + SocketOps::close(session->data_socket); + } + m_active_sessions.clear(); + + for (MutRef> session : m_pending_sessions) { + ProcessOps::terminate_process(session->node_process); + FileOps::unmap_file(session->mapped_ptr); + FileOps::unlink_shared_memory(session->shared_mem_name); + SocketOps::close(session->listener_socket); + } + m_pending_sessions.clear(); +} + +void IpcManager::update() { + Const now = + std::chrono::system_clock::now(); + + for (Mut i = static_cast(m_pending_sessions.size()) - 1; i >= 0; + --i) { + MutRef> session = + m_pending_sessions[static_cast(i)]; + + if (now - session->creation_time > std::chrono::seconds(5)) { + ProcessOps::terminate_process(session->node_process); + + FileOps::unmap_file(session->mapped_ptr); + FileOps::unlink_shared_memory(session->shared_mem_name); + SocketOps::close(session->listener_socket); + + m_pending_sessions.erase(m_pending_sessions.begin() + i); + continue; + } + +#if IA_PLATFORM_WINDOWS + Mut new_sock = accept(session->listener_socket, nullptr, nullptr); +#else + Mut new_sock = accept(session->listener_socket, nullptr, nullptr); +#endif + + if (new_sock != INVALID_SOCKET) { + session->data_socket = new_sock; + session->is_ready = true; + +#if IA_PLATFORM_WINDOWS + Mut mode = 1; + ioctlsocket(session->data_socket, FIONBIO, &mode); +#else + fcntl(session->data_socket, F_SETFL, O_NONBLOCK); +#endif + + SocketOps::close(session->listener_socket); + session->listener_socket = INVALID_SOCKET; + + Const session_id = session->node_process->id.load(); + Mut session_ptr = session.get(); + m_active_sessions.push_back(std::move(session)); + m_pending_sessions.erase(m_pending_sessions.begin() + i); + m_active_session_map[session_id] = session_ptr; + } + } + + for (Mut i = static_cast(m_active_sessions.size()) - 1; i >= 0; + --i) { + MutRef> node = m_active_sessions[static_cast(i)]; + + Mut node_id = node->node_process->id.load(); + + Mut header; + + while (node->mino.pop( + header, Span(m_receive_buffer.data(), m_receive_buffer.size()))) { + on_packet(node_id, header.id, + {m_receive_buffer.data(), header.payload_size}); + } + + Mut signal = 0; + Const res = + recv(node->data_socket, reinterpret_cast(&signal), 1, 0); + + if (res == 1) { + on_signal(node_id, signal); + } else if (res == 0 || (res < 0 && !SocketOps::is_would_block())) { + ProcessOps::terminate_process(node->node_process); + + FileOps::unmap_file(node->mapped_ptr); + FileOps::unlink_shared_memory(node->shared_mem_name); + SocketOps::close(node->data_socket); + + m_active_sessions.erase(m_active_sessions.begin() + i); + m_active_session_map.erase(node_id); + } + } +} + +auto IpcManager::spawn_node(Ref executable_path, + Const shared_memory_size) + -> Result { + Mut> session = make_box(); + + static Mut> s_id_gen{0}; + Const sid = ++s_id_gen; + + Mut sock_path; +#if IA_PLATFORM_WINDOWS + Mut temp_path; + GetTempPathA(MAX_PATH, temp_path); + sock_path = std::format("{}\\ia_sess_{}.sock", temp_path, sid); +#else + sock_path = std::format("/tmp/ia_sess_{}.sock", sid); +#endif + + session->listener_socket = OX_TRY(SocketOps::create_unix_socket()); + OX_TRY_PURE( + SocketOps::bind_unix_socket(session->listener_socket, sock_path.c_str())); + OX_TRY_PURE(SocketOps::listen(session->listener_socket, 1)); + +#if IA_PLATFORM_WINDOWS + Mut mode = 1; + ioctlsocket(session->listener_socket, FIONBIO, &mode); +#else + fcntl(session->listener_socket, F_SETFL, O_NONBLOCK); +#endif + + Const shm_name = std::format("ia_shm_{}", sid); + session->mapped_ptr = + OX_TRY(FileOps::map_shared_memory(shm_name, shared_memory_size, true)); + + Mut layout = + reinterpret_cast(session->mapped_ptr); + + layout->meta.magic = 0x49414950; + layout->meta.version = 1; + layout->meta.total_size = shared_memory_size; + + Const header_size = IpcSharedMemoryLayout::get_header_size(); + Const usable_bytes = shared_memory_size - header_size; + + Mut half_size = (usable_bytes / 2); + half_size -= (half_size % 64); + + layout->moni_data_offset = header_size; + layout->moni_data_size = half_size; + + layout->mino_data_offset = header_size + half_size; + layout->mino_data_size = half_size; + + session->moni = OX_TRY(RingBufferView::create( + &layout->moni_control, + Span(session->mapped_ptr + layout->moni_data_offset, + static_cast(layout->moni_data_size)), + true)); + + session->mino = OX_TRY(RingBufferView::create( + &layout->mino_control, + Span(session->mapped_ptr + layout->mino_data_offset, + static_cast(layout->mino_data_size)), + true)); + + Mut desc; + desc.socket_path = sock_path; + desc.shared_mem_path = shm_name; + desc.shared_mem_size = shared_memory_size; + + Const args = std::format("\"{}\"", desc.serialize()); + + session->node_process = OX_TRY(ProcessOps::spawn_process_async( + FileOps::normalize_executable_path(executable_path).string(), args, + [sid](Const line) { + if (Env::IS_DEBUG) { + std::cout << std::format("{}[Node:{}:STDOUT|STDERR]: {}{}\n", + console::MAGENTA, sid, line, console::RESET); + } + }, + [sid](Const> result) { + if (Env::IS_DEBUG) { + if (!result) { + std::cout << std::format( + "{}[Node: {}]: Failed to spawn with error '{}'{}\n", + console::RED, sid, result.error(), console::RESET); + } else { + std::cout << std::format("{}[Node: {}]: Exited with code {}{}\n", + console::RED, sid, *result, + console::RESET); + } + } + })); + + std::this_thread::sleep_for(std::chrono::seconds(1)); + if (!session->node_process->is_active()) { + return fail("Failed to spawn the child process \"{}\"", + executable_path.string()); + } + + Const process_id = session->node_process->id.load(); + + session->shared_mem_name = shm_name; + session->creation_time = std::chrono::system_clock::now(); + m_pending_sessions.push_back(std::move(session)); + + return process_id; +} + +auto IpcManager::wait_till_node_is_online(Const node_id) + -> bool { + Mut is_pending = true; + while (is_pending) { + is_pending = false; + for (Const> &session : m_pending_sessions) { + if (session->node_process->id.load() == node_id) { + is_pending = true; + break; + } + } + update(); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + return m_active_session_map.contains(node_id); +} + +void IpcManager::shutdown_node(Const node_id) { + Const::iterator> it_node = + m_active_session_map.find(node_id); + if (it_node == m_active_session_map.end()) { + return; + } + + Mut node = it_node->second; + + ProcessOps::terminate_process(node->node_process); + FileOps::unmap_file(node->mapped_ptr); + FileOps::unlink_shared_memory(node->shared_mem_name); + SocketOps::close(node->data_socket); + + std::erase_if(m_active_sessions, + [&](Ref> s) { return s.get() == node; }); + m_active_session_map.erase(it_node); +} + +void IpcManager::send_signal(Const node, Const signal) { + Const::iterator> it_node = + m_active_session_map.find(node); + if (it_node == m_active_session_map.end()) { + return; + } + it_node->second->send_signal(signal); +} + +auto IpcManager::send_packet(Const node, Const packet_id, + Const>> payload) -> Result { + Const::iterator> it_node = + m_active_session_map.find(node); + if (it_node == m_active_session_map.end()) + return fail("no such node"); + return it_node->second->send_packet(packet_id, payload); +} +} // namespace IACore \ No newline at end of file diff --git a/Src/IACore/imp/cpp/JSON.cpp b/Src/IACore/imp/cpp/JSON.cpp new file mode 100644 index 0000000..7fbc6e8 --- /dev/null +++ b/Src/IACore/imp/cpp/JSON.cpp @@ -0,0 +1,18 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2026 IAS (ias@iasoft.dev) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include + +namespace IACore {} // namespace IACore \ No newline at end of file diff --git a/Src/IACore/imp/cpp/Logger.cpp b/Src/IACore/imp/cpp/Logger.cpp new file mode 100644 index 0000000..f80dbc3 --- /dev/null +++ b/Src/IACore/imp/cpp/Logger.cpp @@ -0,0 +1,86 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2026 IAS (ias@iasoft.dev) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include + +#include +#include +#include + +namespace IACore { +Mut Logger::m_log_level = Logger::LogLevel::Info; +Mut Logger::m_log_file; + +static auto get_seconds_count() -> f64 { + static Const> start_time = + std::chrono::steady_clock::now(); + Const> now = + std::chrono::steady_clock::now(); + Const> duration = now - start_time; + return duration.count(); +} + +auto Logger::initialize() -> void {} + +auto Logger::terminate() -> void { + if (m_log_file.is_open()) { + m_log_file.flush(); + m_log_file.close(); + } +} + +auto Logger::enable_logging_to_disk(Const *> file_path) + -> Result { + if (m_log_file.is_open()) { + m_log_file.flush(); + m_log_file.close(); + } + + m_log_file.open(file_path); + + if (!m_log_file.is_open()) { + return fail("Failed to open log file: {}", file_path); + } + + return {}; +} + +auto Logger::set_log_level(Const log_level) -> void { + m_log_level = log_level; +} + +auto Logger::flush_logs() -> void { + std::cout.flush(); + if (m_log_file.is_open()) { + m_log_file.flush(); + } +} + +auto Logger::log_internal(Const *> prefix, Const *> tag, + ForwardRef msg) -> void { + Const seconds = get_seconds_count(); + Const out_line = + std::format("[{:>8.3f}]: [{}]: {}", seconds, tag, msg); + + std::cout << prefix << out_line << console::RESET << '\n'; + + if (m_log_file.is_open()) { + m_log_file.write(out_line.data(), + static_cast(out_line.size())); + m_log_file.put('\n'); + m_log_file.flush(); + } +} +} // namespace IACore \ No newline at end of file diff --git a/Src/IACore/imp/cpp/Platform.cpp b/Src/IACore/imp/cpp/Platform.cpp new file mode 100644 index 0000000..112221f --- /dev/null +++ b/Src/IACore/imp/cpp/Platform.cpp @@ -0,0 +1,134 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2026 IAS (ias@iasoft.dev) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include + +#if defined(IA_ARCH_X64) +#ifndef _MSC_VER +#include +#endif +#elif defined(IA_ARCH_ARM64) +#if defined(__linux__) || defined(__ANDROID__) +#include +#include +#endif +#endif + +namespace IACore { +Mut Platform::s_capabilities{}; + +#if defined(IA_ARCH_X64) +auto Platform::cpuid(Const function, Const sub_function, + Mut out[4]) -> void { +#ifdef _MSC_VER + __cpuidex(reinterpret_cast(out), static_cast(function), + static_cast(sub_function)); +#else + Mut a = 0; + Mut b = 0; + Mut c = 0; + Mut d = 0; + __cpuid_count(function, sub_function, a, b, c, d); + out[0] = static_cast(a); + out[1] = static_cast(b); + out[2] = static_cast(c); + out[3] = static_cast(d); +#endif +} +#endif + +auto Platform::check_cpu() -> bool { +#if defined(IA_ARCH_X64) + Mut cpu_info[4]; + + cpuid(0, 0, cpu_info); + if (cpu_info[0] < 7) { + return false; + } + + cpuid(1, 0, cpu_info); + Const osxsave = (cpu_info[2] & (1 << 27)) != 0; + Const avx = (cpu_info[2] & (1 << 28)) != 0; + Const fma = (cpu_info[2] & (1 << 12)) != 0; + + if (!osxsave || !avx || !fma) { + return false; + } + + Const xcr_feature_mask = _xgetbv(0); + if ((xcr_feature_mask & 0x6) != 0x6) { + return false; + } + + cpuid(7, 0, cpu_info); + Const avx2 = (cpu_info[1] & (1 << 5)) != 0; + if (!avx2) { + return false; + } + + s_capabilities.hardware_crc32 = true; + +#elif defined(IA_ARCH_ARM64) +#if defined(__linux__) || defined(__ANDROID__) + Const hw_caps = getauxval(AT_HWCAP); + +#ifndef HWCAP_CRC32 +#define HWCAP_CRC32 (1 << 7) +#endif + + s_capabilities.hardware_crc32 = (hw_caps & HWCAP_CRC32) != 0; +#elif defined(IA_PLATFORM_APPLE) + s_capabilities.hardware_crc32 = true; +#else + s_capabilities.hardware_crc32 = false; +#endif +#else + s_capabilities.hardware_crc32 = false; +#endif + return true; +} + +auto Platform::get_architecture_name() -> const char * { +#if defined(IA_ARCH_X64) + return "x86_64"; +#elif defined(IA_ARCH_ARM64) + return "aarch64"; +#elif defined(IA_ARCH_WASM) + return "wasm"; +#else + return "unknown"; +#endif +} + +auto Platform::get_operating_system_name() -> const char * { +#if IA_PLATFORM_WINDOWS + return "Windows"; +#elif defined(IA_PLATFORM_APPLE) +#if defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE + return "iOS"; +#else + return "macOS"; +#endif +#elif defined(__ANDROID__) + return "Android"; +#elif IA_PLATFORM_LINUX + return "Linux"; +#elif IA_PLATFORM_WASM + return "WebAssembly"; +#else + return "Unknown"; +#endif +} +} // namespace IACore \ No newline at end of file diff --git a/Src/IACore/imp/cpp/ProcessOps.cpp b/Src/IACore/imp/cpp/ProcessOps.cpp new file mode 100644 index 0000000..35a219c --- /dev/null +++ b/Src/IACore/imp/cpp/ProcessOps.cpp @@ -0,0 +1,309 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2026 IAS (ias@iasoft.dev) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include + +namespace IACore { +struct LineBuffer { + Mut m_accumulator; + Const)>> m_callback; + + auto append(Const data, Const size) -> void; + auto flush() -> void; +}; + +auto LineBuffer::append(Const data, Const size) -> void { + Mut start = 0; + for (Mut i = 0; i < size; ++i) { + if (data[i] == '\n' || data[i] == '\r') { + if (!m_accumulator.empty()) { + m_accumulator.append(data + start, i - start); + if (!m_accumulator.empty()) { + m_callback(m_accumulator); + } + m_accumulator.clear(); + } else { + if (i > start) { + m_callback(StringView(data + start, i - start)); + } + } + + if (data[i] == '\r' && i + 1 < size && data[i + 1] == '\n') { + i++; + } + start = i + 1; + } + } + if (start < size) { + m_accumulator.append(data + start, size - start); + } +} + +auto LineBuffer::flush() -> void { + if (!m_accumulator.empty()) { + m_callback(m_accumulator); + m_accumulator.clear(); + } +} + +auto ProcessOps::get_current_process_id() -> NativeProcessID { +#if IA_PLATFORM_WINDOWS + return ::GetCurrentProcessId(); +#else + return getpid(); +#endif +} + +auto ProcessOps::spawn_process_sync( + Ref command, Ref args, + Const)>> on_output_line_callback) + -> Result { + Mut> id = 0; + if constexpr (Env::IS_WINDOWS) { + return spawn_process_windows(command, args, on_output_line_callback, id); + } else { + return spawn_process_posix(command, args, on_output_line_callback, id); + } +} + +auto ProcessOps::spawn_process_async( + Ref command, Ref args, + Const)>> on_output_line_callback, + Const>)>> on_finish_callback) + -> Result> { + Mut> handle = make_box(); + handle->is_running = true; + + Mut h_ptr = handle.get(); + + handle->m_thread_handle = std::jthread([h_ptr, cmd = command, arg = args, + cb = on_output_line_callback, + fin = on_finish_callback]() mutable { + Mut> result = fail("Platform not supported"); + + if constexpr (Env::IS_WINDOWS) { + result = spawn_process_windows(cmd, arg, cb, h_ptr->id); + } else { + result = spawn_process_posix(cmd, arg, cb, h_ptr->id); + } + + h_ptr->is_running = false; + + if (fin) { + if (!result) { + fin(fail(std::move(result.error()))); + } else { + fin(*result); + } + } + }); + + return handle; +} + +auto ProcessOps::terminate_process(Ref> handle) -> void { + if (!handle || !handle->is_active()) { + return; + } + + Const pid = handle->id.load(); + if (pid == 0) { + return; + } + +#if IA_PLATFORM_WINDOWS + Mut h_process = OpenProcess(PROCESS_TERMINATE, false, pid); + if (h_process != NULL) { + ::TerminateProcess(h_process, 9); + CloseHandle(h_process); + } +#endif +#if IA_PLATFORM_UNIX + kill(pid, SIGKILL); +#endif +} + +auto ProcessOps::spawn_process_windows( + Ref command, Ref args, + Const)>> on_output_line_callback, + MutRef> id) -> Result { +#if IA_PLATFORM_WINDOWS + Mut sa_attr = {sizeof(SECURITY_ATTRIBUTES), NULL, true}; + Mut h_read = NULL; + Mut h_write = NULL; + + if (!CreatePipe(&h_read, &h_write, &sa_attr, 0)) { + return fail("Failed to create pipe"); + } + + if (!SetHandleInformation(h_read, HANDLE_FLAG_INHERIT, 0)) { + return fail("Failed to secure pipe handles"); + } + + Mut si = {sizeof(STARTUPINFOA)}; + si.dwFlags |= STARTF_USESTDHANDLES; + si.hStdOutput = h_write; + si.hStdError = h_write; + si.hStdInput = NULL; + + Mut pi = {0}; + + Mut command_line = std::format("\"{}\" {}", command, args); + + Const success = CreateProcessA(NULL, command_line.data(), NULL, NULL, + true, 0, NULL, NULL, &si, &pi); + + CloseHandle(h_write); + + if (!success) { + CloseHandle(h_read); + return fail("CreateProcess failed: {}", GetLastError()); + } + + id.store(pi.dwProcessId); + + Mut line_buf{"", on_output_line_callback}; + Mut bytes_read = 0; + Mut> buffer; + + while (ReadFile(h_read, buffer.data(), static_cast(buffer.size()), + &bytes_read, NULL) && + bytes_read != 0) { + line_buf.append(buffer.data(), bytes_read); + } + line_buf.flush(); + + Mut exit_code = 0; + WaitForSingleObject(pi.hProcess, INFINITE); + GetExitCodeProcess(pi.hProcess, &exit_code); + + CloseHandle(pi.hProcess); + CloseHandle(pi.hThread); + CloseHandle(h_read); + id.store(0); + + return static_cast(exit_code); +#else + OX_UNUSED(command); + OX_UNUSED(args); + OX_UNUSED(on_output_line_callback); + OX_UNUSED(id); + return fail("Windows implementation not available."); +#endif +} + +auto ProcessOps::spawn_process_posix( + Ref command, Ref args, + Const)>> on_output_line_callback, + MutRef> id) -> Result { +#if IA_PLATFORM_UNIX + Mut> pipefd; + if (pipe(pipefd.data()) == -1) { + return fail("Failed to create pipe"); + } + + Const pid = fork(); + + if (pid == -1) { + return fail("Failed to fork process"); + } else if (pid == 0) { + close(pipefd[0]); + + dup2(pipefd[1], STDOUT_FILENO); + dup2(pipefd[1], STDERR_FILENO); + close(pipefd[1]); + + Mut> arg_storage; + Mut> argv; + + Mut cmd_str = command; + argv.push_back(cmd_str.data()); + + Mut current_token; + Mut in_quotes = false; + Mut is_escaped = false; + + for (Const c : args) { + if (is_escaped) { + current_token += c; + is_escaped = false; + continue; + } + + if (c == '\\') { + is_escaped = true; + continue; + } + + if (c == '\"') { + in_quotes = !in_quotes; + continue; + } + + if (c == ' ' && !in_quotes) { + if (!current_token.empty()) { + arg_storage.push_back(current_token); + current_token.clear(); + } + } else { + current_token += c; + } + } + + if (!current_token.empty()) { + arg_storage.push_back(current_token); + } + + for (MutRef s : arg_storage) { + argv.push_back(s.data()); + } + argv.push_back(nullptr); + + execvp(argv[0], argv.data()); + _exit(127); + } else { + id.store(pid); + + close(pipefd[1]); + + Mut line_buf{"", on_output_line_callback}; + Mut> buffer; + Mut count; + + while ((count = read(pipefd[0], buffer.data(), buffer.size())) > 0) { + line_buf.append(buffer.data(), static_cast(count)); + } + line_buf.flush(); + close(pipefd[0]); + + Mut status; + waitpid(pid, &status, 0); + + id.store(0); + if (WIFEXITED(status)) { + return WEXITSTATUS(status); + } + return -1; + } +#else + OX_UNUSED(command); + OX_UNUSED(args); + OX_UNUSED(on_output_line_callback); + OX_UNUSED(id); + return fail("Posix implementation not available."); +#endif +} + +} // namespace IACore \ No newline at end of file diff --git a/Src/IACore/imp/cpp/SIMD.cpp b/Src/IACore/imp/cpp/SIMD.cpp new file mode 100644 index 0000000..687d33a --- /dev/null +++ b/Src/IACore/imp/cpp/SIMD.cpp @@ -0,0 +1,18 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2026 IAS (ias@iasoft.dev) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include + +namespace IACore {} // namespace IACore \ No newline at end of file diff --git a/Src/IACore/imp/cpp/SocketOps.cpp b/Src/IACore/imp/cpp/SocketOps.cpp new file mode 100644 index 0000000..9d1a421 --- /dev/null +++ b/Src/IACore/imp/cpp/SocketOps.cpp @@ -0,0 +1,146 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2026 IAS (ias@iasoft.dev) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include + +namespace IACore { +Mut SocketOps::s_init_count = 0; + +auto SocketOps::close(Const sock) -> void { + if (sock == INVALID_SOCKET) { + return; + } +#if IA_PLATFORM_WINDOWS + closesocket(sock); +#else + ::close(sock); +#endif +} + +auto SocketOps::listen(Const sock, Const queue_size) + -> Result { + if (::listen(sock, queue_size) == 0) { + return {}; + } + +#if IA_PLATFORM_WINDOWS + return fail("listen failed: {}", WSAGetLastError()); +#else + return fail("listen failed: {}", errno); +#endif +} + +auto SocketOps::create_unix_socket() -> Result { + Const sock = socket(AF_UNIX, SOCK_STREAM, 0); + if (sock == INVALID_SOCKET) { +#if IA_PLATFORM_WINDOWS + return fail("socket(AF_UNIX) failed: {}", WSAGetLastError()); +#else + return fail("socket(AF_UNIX) failed: {}", errno); +#endif + } + return sock; +} + +auto SocketOps::bind_unix_socket(Const sock, + Const path) -> Result { + if (sock == INVALID_SOCKET) { + return fail("Invalid socket handle"); + } + + unlink_file(path); + + Mut addr{}; + addr.sun_family = AF_UNIX; + + Const max_len = sizeof(addr.sun_path) - 1; +#if IA_PLATFORM_WINDOWS + strncpy_s(addr.sun_path, sizeof(addr.sun_path), path, max_len); +#else + std::strncpy(addr.sun_path, path, max_len); +#endif + + if (::bind(sock, reinterpret_cast(&addr), sizeof(addr)) == + -1) { +#if IA_PLATFORM_WINDOWS + return fail("bind failed: {}", WSAGetLastError()); +#else + return fail("bind failed: {}", errno); +#endif + } + + return {}; +} + +auto SocketOps::connect_unix_socket(Const sock, + Const path) -> Result { + if (sock == INVALID_SOCKET) { + return fail("Invalid socket handle"); + } + + Mut addr{}; + addr.sun_family = AF_UNIX; + + Const max_len = sizeof(addr.sun_path) - 1; +#if IA_PLATFORM_WINDOWS + strncpy_s(addr.sun_path, sizeof(addr.sun_path), path, max_len); +#else + std::strncpy(addr.sun_path, path, max_len); +#endif + + if (::connect(sock, reinterpret_cast(&addr), + sizeof(addr)) == -1) { +#if IA_PLATFORM_WINDOWS + return fail("connect failed: {}", WSAGetLastError()); +#else + return fail("connect failed: {}", errno); +#endif + } + + return {}; +} + +auto SocketOps::is_port_available(Const port, Const type) -> bool { + Const sock = socket(AF_INET, type, 0); + if (sock == INVALID_SOCKET) { + return false; + } + + Mut addr{}; + addr.sin_family = AF_INET; + addr.sin_port = htons(port); + addr.sin_addr.s_addr = htonl(INADDR_ANY); + + Mut is_free = false; + if (::bind(sock, reinterpret_cast(&addr), sizeof(addr)) == + 0) { + is_free = true; + } + + close(sock); + + return is_free; +} + +auto SocketOps::is_would_block() -> bool { +#if IA_PLATFORM_WINDOWS + return WSAGetLastError() == WSAEWOULDBLOCK; +#else + return errno == EWOULDBLOCK || errno == EAGAIN; +#endif +} + +} // namespace IACore \ No newline at end of file diff --git a/Src/IACore/imp/cpp/StreamReader.cpp b/Src/IACore/imp/cpp/StreamReader.cpp new file mode 100644 index 0000000..8dceb19 --- /dev/null +++ b/Src/IACore/imp/cpp/StreamReader.cpp @@ -0,0 +1,85 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2026 IAS (ias@iasoft.dev) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include + +namespace IACore { +auto StreamReader::create_from_file(Ref path) -> Result { + Mut size = 0; + + Const ptr = OX_TRY(FileOps::map_file(path, size)); + + Mut reader(Span(ptr, size)); + reader.m_storage_type = StorageType::OwningMmap; + + return reader; +} + +StreamReader::StreamReader(ForwardRef> data) + : m_owning_vector(std::move(data)), + m_storage_type(StorageType::OwningVector) { + m_data = m_owning_vector.data(); + m_data_size = m_owning_vector.size(); +} + +StreamReader::StreamReader(Const> data) + : m_data(data.data()), m_data_size(data.size()), + m_storage_type(StorageType::NonOwning) {} + +StreamReader::StreamReader(ForwardRef other) + : m_data(other.m_data), m_cursor(other.m_cursor), + m_data_size(other.m_data_size), + m_owning_vector(std::move(other.m_owning_vector)), + m_storage_type(other.m_storage_type) { + other.m_storage_type = StorageType::NonOwning; + other.m_data = {}; + other.m_data_size = 0; + + if (m_storage_type == StorageType::OwningVector) { + m_data = m_owning_vector.data(); + } +} + +auto StreamReader::operator=(ForwardRef other) + -> MutRef { + if (this != &other) { + if (m_storage_type == StorageType::OwningMmap) { + FileOps::unmap_file(m_data); + } + + m_data = other.m_data; + m_cursor = other.m_cursor; + m_data_size = other.m_data_size; + m_owning_vector = std::move(other.m_owning_vector); + m_storage_type = other.m_storage_type; + + if (m_storage_type == StorageType::OwningVector) { + m_data = m_owning_vector.data(); + } + + other.m_storage_type = StorageType::NonOwning; + other.m_data = {}; + other.m_data_size = 0; + } + return *this; +} + +StreamReader::~StreamReader() { + if (m_storage_type == StorageType::OwningMmap) { + FileOps::unmap_file(m_data); + } +} +} // namespace IACore \ No newline at end of file diff --git a/Src/IACore/imp/cpp/StreamWriter.cpp b/Src/IACore/imp/cpp/StreamWriter.cpp new file mode 100644 index 0000000..7008f5d --- /dev/null +++ b/Src/IACore/imp/cpp/StreamWriter.cpp @@ -0,0 +1,162 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2026 IAS (ias@iasoft.dev) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include + +namespace IACore { + +auto StreamWriter::create_from_file(Ref path) -> Result { + Mut f = std::fopen(path.string().c_str(), "wb"); + if (!f) { + return fail("Failed to open file for writing: {}", path.string()); + } + std::fclose(f); + + Mut writer; + writer.m_file_path = path; + writer.m_storage_type = StorageType::OwningFile; + + return writer; +} + +StreamWriter::StreamWriter() : m_storage_type(StorageType::OwningVector) { + m_capacity = 256; + m_owning_vector.resize(m_capacity); + m_buffer = m_owning_vector.data(); +} + +StreamWriter::StreamWriter(Const> data) + : m_buffer(data.data()), m_cursor(0), m_capacity(data.size()), + m_storage_type(StorageType::NonOwning) {} + +StreamWriter::StreamWriter(ForwardRef other) + : m_buffer(other.m_buffer), m_cursor(other.m_cursor), + m_capacity(other.m_capacity), m_file_path(other.m_file_path), + m_owning_vector(std::move(other.m_owning_vector)), + m_storage_type(other.m_storage_type) { + other.m_capacity = {}; + other.m_buffer = {}; + other.m_storage_type = StorageType::NonOwning; + + if (m_storage_type == StorageType::OwningVector) + m_buffer = m_owning_vector.data(); +} + +auto StreamWriter::operator=(ForwardRef other) + -> MutRef { + if (this != &other) { + if (m_storage_type == StorageType::OwningFile) { + if (Const> res = flush_to_disk(); !res) { + std::fprintf(stderr, "[IACore] Data loss in StreamWriter move: %s\n", + res.error().c_str()); + } + } + + m_buffer = other.m_buffer; + m_cursor = other.m_cursor; + m_capacity = other.m_capacity; + m_file_path = std::move(other.m_file_path); + m_owning_vector = std::move(other.m_owning_vector); + m_storage_type = other.m_storage_type; + + if (m_storage_type == StorageType::OwningVector) + m_buffer = m_owning_vector.data(); + + other.m_capacity = 0; + other.m_cursor = 0; + other.m_buffer = nullptr; + other.m_storage_type = StorageType::NonOwning; + } + return *this; +} + +StreamWriter::~StreamWriter() { + if (m_storage_type == StorageType::OwningFile) { + if (Const> res = flush_to_disk(); !res) { + std::fprintf(stderr, "[IACore] LOST DATA in ~StreamWriter: %s\n", + res.error().c_str()); + } + } +} + +auto StreamWriter::flush() -> Result { + Mut> res = flush_to_disk(); + if (res.has_value()) { + m_storage_type = StorageType::OwningVector; + } + return res; +} + +auto StreamWriter::flush_to_disk() -> Result { + if (m_storage_type != StorageType::OwningFile || m_file_path.empty()) { + return {}; + } + + Mut f = std::fopen(m_file_path.string().c_str(), "wb"); + if (!f) { + return fail("Failed to open file for writing: {}", m_file_path.string()); + } + + Const written = std::fwrite(m_buffer, 1, m_cursor, f); + std::fclose(f); + + if (written != m_cursor) { + return fail("Incomplete write: {} of {} bytes written", written, m_cursor); + } + return {}; +} + +auto StreamWriter::write(Const byte, Const count) -> Result { + if (m_cursor + count > m_capacity) { + if (m_storage_type == StorageType::NonOwning) { + return fail("StreamWriter buffer overflow (NonOwning)"); + } + + Const required = m_cursor + count; + Const double_cap = m_capacity * 2; + Const new_capacity = (double_cap > required) ? double_cap : required; + + m_owning_vector.resize(new_capacity); + m_capacity = m_owning_vector.size(); + m_buffer = m_owning_vector.data(); + } + + std::memset(m_buffer + m_cursor, byte, count); + m_cursor += count; + return {}; +} + +auto StreamWriter::write(Const buffer, Const size) + -> Result { + if (m_cursor + size > m_capacity) { + if (m_storage_type == StorageType::NonOwning) { + return fail("StreamWriter buffer overflow (NonOwning)"); + } + + Const required = m_cursor + size; + Const double_cap = m_capacity * 2; + Const new_capacity = (double_cap > required) ? double_cap : required; + + m_owning_vector.resize(new_capacity); + m_capacity = m_owning_vector.size(); + m_buffer = m_owning_vector.data(); + } + + std::memcpy(m_buffer + m_cursor, buffer, size); + m_cursor += size; + return {}; +} + +} // namespace IACore \ No newline at end of file diff --git a/Src/IACore/imp/cpp/StringOps.cpp b/Src/IACore/imp/cpp/StringOps.cpp new file mode 100644 index 0000000..5e42375 --- /dev/null +++ b/Src/IACore/imp/cpp/StringOps.cpp @@ -0,0 +1,122 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2026 IAS (ias@iasoft.dev) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include + +namespace IACore { + +static Const BASE64_CHAR_TABLE = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + +static auto is_base64(Const c) -> bool { + return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || + (c >= '0' && c <= '9') || (c == '+') || (c == '/'); +} + +static auto get_base64_index(Const c) -> u8 { + if (c >= 'A' && c <= 'Z') + return c - 'A'; + if (c >= 'a' && c <= 'z') + return c - 'a' + 26; + if (c >= '0' && c <= '9') + return c - '0' + 52; + if (c == '+') + return 62; + if (c == '/') + return 63; + return 0; +} + +auto StringOps::encode_base64(Const>> data) -> String { + Mut result; + result.reserve(((data.size() + 2) / 3) * 4); + + for (Mut i = 0; i < data.size(); i += 3) { + Const b0 = data[i]; + Const b1 = (i + 1 < data.size()) ? data[i + 1] : 0; + Const b2 = (i + 2 < data.size()) ? data[i + 2] : 0; + + Const triple = (b0 << 16) | (b1 << 8) | b2; + + result += BASE64_CHAR_TABLE[(triple >> 18) & 0x3F]; + result += BASE64_CHAR_TABLE[(triple >> 12) & 0x3F]; + + if (i + 1 < data.size()) { + result += BASE64_CHAR_TABLE[(triple >> 6) & 0x3F]; + } else { + result += '='; + } + + if (i + 2 < data.size()) { + result += BASE64_CHAR_TABLE[triple & 0x3F]; + } else { + result += '='; + } + } + return result; +} + +auto StringOps::decode_base64(Ref data) -> Vec { + Mut> result; + result.reserve(data.size() * 3 / 4); + + Mut i = 0; + Mut> tmp_buf = {}; + + for (Const c_char : data) { + Const c = static_cast(c_char); + if (c == '=') { + break; + } + if (!is_base64(c)) { + break; + } + + tmp_buf[i++] = c; + if (i == 4) { + Const n0 = get_base64_index(tmp_buf[0]); + Const n1 = get_base64_index(tmp_buf[1]); + Const n2 = get_base64_index(tmp_buf[2]); + Const n3 = get_base64_index(tmp_buf[3]); + + result.push_back((n0 << 2) | ((n1 & 0x30) >> 4)); + result.push_back(((n1 & 0x0F) << 4) | ((n2 & 0x3C) >> 2)); + result.push_back(((n2 & 0x03) << 6) | n3); + + i = 0; + } + } + + if (i > 0) { + for (Mut j = i; j < 4; ++j) { + tmp_buf[j] = 'A'; + } + + Const n0 = get_base64_index(tmp_buf[0]); + Const n1 = get_base64_index(tmp_buf[1]); + Const n2 = get_base64_index(tmp_buf[2]); + + if (i > 1) { + result.push_back((n0 << 2) | ((n1 & 0x30) >> 4)); + } + if (i > 2) { + result.push_back(((n1 & 0x0F) << 4) | ((n2 & 0x3C) >> 2)); + } + } + + return result; +} + +} // namespace IACore \ No newline at end of file diff --git a/Src/IACore/imp/cpp/Utils.cpp b/Src/IACore/imp/cpp/Utils.cpp new file mode 100644 index 0000000..9e24cdf --- /dev/null +++ b/Src/IACore/imp/cpp/Utils.cpp @@ -0,0 +1,112 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2026 IAS (ias@iasoft.dev) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +#include + +namespace IACore { +namespace { +auto from_hex_char(Const c) -> i32 { + if (c >= '0' && c <= '9') { + return c - '0'; + } + if (c >= 'A' && c <= 'F') { + return c - 'A' + 10; + } + if (c >= 'a' && c <= 'f') { + return c - 'a' + 10; + } + return -1; +} +} // namespace + +extern Mut g_start_time; + +auto Utils::get_unix_time() -> u64 { + Const now = + std::chrono::system_clock::now(); + return std::chrono::duration_cast( + now.time_since_epoch()) + .count(); +} + +auto Utils::get_ticks_count() -> u64 { + Const duration = + std::chrono::high_resolution_clock::now() - g_start_time; + return std::chrono::duration_cast(duration) + .count(); +} + +auto Utils::get_seconds_count() -> f64 { + Const duration = + std::chrono::high_resolution_clock::now() - g_start_time; + return static_cast( + std::chrono::duration_cast(duration).count()); +} + +auto Utils::get_random() -> f32 { + return static_cast(std::rand()) / static_cast(RAND_MAX); +} + +auto Utils::get_random(Const max) -> u64 { + return static_cast(static_cast(max) * get_random()); +} + +auto Utils::get_random(Const min, Const max) -> i64 { + return min + static_cast(static_cast(max - min) * get_random()); +} + +auto Utils::sleep(Const milliseconds) -> void { + std::this_thread::sleep_for(std::chrono::milliseconds(milliseconds)); +} + +auto Utils::binary_to_hex_string(Const>> data) -> String { + static constexpr Const lut = "0123456789ABCDEF"; + Mut res = String(); + res.reserve(data.size() * 2); + + for (Const b : data) { + res.push_back(lut[(b >> 4) & 0x0F]); + res.push_back(lut[b & 0x0F]); + } + return res; +} + +auto Utils::hex_string_to_binary(Const hex) -> Result> { + if (hex.size() % 2 != 0) { + return fail("Hex string must have even length"); + } + + Mut> out = Vec(); + out.reserve(hex.size() / 2); + + for (Mut i = 0; i < hex.size(); i += 2) { + Const high = hex[i]; + Const low = hex[i + 1]; + + Const h = from_hex_char(high); + Const l = from_hex_char(low); + + if (h == -1 || l == -1) { + return fail("Invalid hex character found"); + } + + out.push_back(static_cast((h << 4) | l)); + } + + return out; +} +} // namespace IACore \ No newline at end of file diff --git a/Src/IACore/imp/cpp/XML.cpp b/Src/IACore/imp/cpp/XML.cpp new file mode 100644 index 0000000..0831d68 --- /dev/null +++ b/Src/IACore/imp/cpp/XML.cpp @@ -0,0 +1,82 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2026 IAS (ias@iasoft.dev) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include + +namespace IACore { + +auto XML::parse_from_string(Ref data) -> Result { + Mut doc; + Const parse_result = doc.load_string(data.c_str()); + if (!parse_result) { + return fail("Failed to parse XML {}", parse_result.description()); + } + return std::move(doc); +} + +auto XML::parse_from_file(Ref path) -> Result { + Mut doc; + Const parse_result = + doc.load_file(path.string().c_str()); + if (!parse_result) { + return fail("Failed to parse XML {}", parse_result.description()); + } + return std::move(doc); +} + +auto XML::serialize_to_string(Ref node, Const escape) -> String { + Mut oss; + node.print(oss); + return escape ? escape_xml_string(oss.str()) : oss.str(); +} + +auto XML::serialize_to_string(Ref doc, Const escape) -> String { + Mut oss; + doc.save(oss); + return escape ? escape_xml_string(oss.str()) : oss.str(); +} + +auto XML::escape_xml_string(Ref xml) -> String { + Mut buffer; + buffer.reserve(xml.size() + (xml.size() / 10)); + + for (Const c : xml) { + switch (c) { + case '&': + buffer.append("&"); + break; + case '\"': + buffer.append("""); + break; + case '\'': + buffer.append("'"); + break; + case '<': + buffer.append("<"); + break; + case '>': + buffer.append(">"); + break; + default: + buffer.push_back(c); + break; + } + } + + return buffer; +} + +} // namespace IACore \ No newline at end of file diff --git a/Src/IACore/inc/IACore/ADT/RingBuffer.hpp b/Src/IACore/inc/IACore/ADT/RingBuffer.hpp new file mode 100644 index 0000000..2839c73 --- /dev/null +++ b/Src/IACore/inc/IACore/ADT/RingBuffer.hpp @@ -0,0 +1,264 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2026 IAS (ias@iasoft.dev) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include + +namespace IACore { +class RingBufferView { +public: + static constexpr Const PACKET_ID_SKIP = 0; + + struct ControlBlock { + struct alignas(64) { + Mut> write_offset{0}; + } producer; + + struct alignas(64) { + Mut> read_offset{0}; + Mut capacity{0}; + } consumer; + }; + + static_assert(offsetof(ControlBlock, consumer) == 64, + "False sharing detected in ControlBlock"); + + struct PacketHeader { + PacketHeader() : id(0), payload_size(0) {} + + PacketHeader(Const id) : id(id), payload_size(0) {} + + PacketHeader(Const id, Const payload_size) + : id(id), payload_size(payload_size) {} + + Mut id{}; + Mut payload_size{}; + }; + +public: + static auto default_instance() -> RingBufferView; + + static auto create(Ref> buffer, Const is_owner) + -> Result; + static auto create(Const control_block, Ref> buffer, + Const is_owner) -> Result; + + // Returns: + // - nullopt if empty + // - bytes_read if success + // - Error if buffer too small + auto pop(MutRef out_header, Ref> out_buffer) + -> Result>; + + auto push(Const packet_id, Ref> data) -> Result; + + auto get_control_block() -> ControlBlock *; + + [[nodiscard]] auto is_valid() const -> bool; + +protected: + RingBufferView(Ref> buffer, Const is_owner); + RingBufferView(Const control_block, Ref> buffer, + Const is_owner); + +private: + Mut m_data_ptr{}; + Mut m_capacity{}; + Mut m_control_block{}; + +private: + auto write_wrapped(Const offset, Const data, + Const size) -> void; + auto read_wrapped(Const offset, Const out_data, Const size) + -> void; +}; + +inline auto RingBufferView::default_instance() -> RingBufferView { + return RingBufferView(nullptr, {}, false); +} + +inline auto RingBufferView::create(Ref> buffer, Const is_owner) + -> Result { + if (buffer.size() <= sizeof(ControlBlock)) { + return fail("Buffer too small for ControlBlock"); + } + + if (!is_owner) { + Const cb = reinterpret_cast(buffer.data()); + Const capacity = + static_cast(buffer.size()) - sizeof(ControlBlock); + if (cb->consumer.capacity != capacity) { + return fail("Capacity mismatch"); + } + } + + return RingBufferView(buffer, is_owner); +} + +inline auto RingBufferView::create(Const control_block, + Ref> buffer, Const is_owner) + -> Result { + if (control_block == nullptr) { + return fail("ControlBlock is null"); + } + if (buffer.empty()) { + return fail("Buffer is empty"); + } + + return RingBufferView(control_block, buffer, is_owner); +} + +inline RingBufferView::RingBufferView(Ref> buffer, + Const is_owner) { + m_control_block = reinterpret_cast(buffer.data()); + m_data_ptr = buffer.data() + sizeof(ControlBlock); + + m_capacity = static_cast(buffer.size()) - sizeof(ControlBlock); + + if (is_owner) { + m_control_block->consumer.capacity = m_capacity; + m_control_block->producer.write_offset.store(0, std::memory_order_release); + m_control_block->consumer.read_offset.store(0, std::memory_order_release); + } +} + +inline RingBufferView::RingBufferView(Const control_block, + Ref> buffer, + Const is_owner) { + m_control_block = control_block; + m_data_ptr = buffer.data(); + m_capacity = static_cast(buffer.size()); + + if (is_owner) { + m_control_block->consumer.capacity = m_capacity; + m_control_block->producer.write_offset.store(0, std::memory_order_release); + m_control_block->consumer.read_offset.store(0, std::memory_order_release); + } +} + +inline auto RingBufferView::pop(MutRef out_header, + Ref> out_buffer) + -> Result> { + Const write = + m_control_block->producer.write_offset.load(std::memory_order_acquire); + Const read = + m_control_block->consumer.read_offset.load(std::memory_order_relaxed); + Const cap = m_capacity; + + if (read == write) { + return std::nullopt; + } + + read_wrapped(read, &out_header, sizeof(PacketHeader)); + + if (out_header.payload_size > out_buffer.size()) { + return fail("Buffer too small: needed {}, provided {}", + out_header.payload_size, out_buffer.size()); + } + + if (out_header.payload_size > 0) { + Const data_read_offset = (read + sizeof(PacketHeader)) % cap; + read_wrapped(data_read_offset, out_buffer.data(), out_header.payload_size); + } + + Const new_read_offset = + (read + sizeof(PacketHeader) + out_header.payload_size) % cap; + m_control_block->consumer.read_offset.store(new_read_offset, + std::memory_order_release); + + return std::make_optional(static_cast(out_header.payload_size)); +} + +inline auto RingBufferView::push(Const packet_id, Ref> data) + -> Result { + if (data.size() > std::numeric_limits::max()) { + return fail("Data size exceeds u16 limit"); + } + + Const total_size = sizeof(PacketHeader) + static_cast(data.size()); + + Const read = + m_control_block->consumer.read_offset.load(std::memory_order_acquire); + Const write = + m_control_block->producer.write_offset.load(std::memory_order_relaxed); + Const cap = m_capacity; + + Const free_space = + (read <= write) ? (m_capacity - write) + read : (read - write); + + // Leave 1 byte empty (prevent ambiguities) + if (free_space <= total_size) { + return fail("RingBuffer full"); + } + + Const header{packet_id, static_cast(data.size())}; + write_wrapped(write, &header, sizeof(PacketHeader)); + + Const data_write_offset = (write + sizeof(PacketHeader)) % cap; + + if (!data.empty()) { + write_wrapped(data_write_offset, data.data(), + static_cast(data.size())); + } + + Const new_write_offset = (data_write_offset + data.size()) % cap; + m_control_block->producer.write_offset.store(new_write_offset, + std::memory_order_release); + + return {}; +} + +inline auto RingBufferView::get_control_block() -> ControlBlock * { + return m_control_block; +} + +inline auto RingBufferView::write_wrapped(Const offset, + Const data, + Const size) -> void { + if (offset + size <= m_capacity) { + std::memcpy(m_data_ptr + offset, data, size); + } else { + Const first_chunk = m_capacity - offset; + Const second_chunk = size - first_chunk; + + Const src = static_cast(data); + + std::memcpy(m_data_ptr + offset, src, first_chunk); + std::memcpy(m_data_ptr, src + first_chunk, second_chunk); + } +} + +inline auto RingBufferView::read_wrapped(Const offset, + Const out_data, + Const size) -> void { + if (offset + size <= m_capacity) { + std::memcpy(out_data, m_data_ptr + offset, size); + } else { + Const first_chunk = m_capacity - offset; + Const second_chunk = size - first_chunk; + + Const dst = static_cast(out_data); + + std::memcpy(dst, m_data_ptr + offset, first_chunk); + std::memcpy(dst + first_chunk, m_data_ptr, second_chunk); + } +} + +[[nodiscard]] inline auto RingBufferView::is_valid() const -> bool { + return m_control_block && m_data_ptr && m_capacity; +} + +} // namespace IACore \ No newline at end of file diff --git a/Src/IACore/inc/IACore/AsyncOps.hpp b/Src/IACore/inc/IACore/AsyncOps.hpp new file mode 100644 index 0000000..1f15203 --- /dev/null +++ b/Src/IACore/inc/IACore/AsyncOps.hpp @@ -0,0 +1,71 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2026 IAS (ias@iasoft.dev) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include +#include +#include +#include + +namespace IACore { +class AsyncOps { +public: + using TaskTag = u64; + using WorkerId = u16; + + static constexpr Const MAIN_THREAD_WORKER_ID = 0; + + enum class Priority : u8 { High, Normal }; + + struct Schedule { + Mut> counter{0}; + }; + +public: + static auto initialize_scheduler(Const worker_count = 0) -> Result; + static auto terminate_scheduler() -> void; + + static auto schedule_task(Mut)>> task, + Const tag, Mut schedule, + Const priority = Priority::Normal) + -> void; + + static auto cancel_tasks_of_tag(Const tag) -> void; + + static auto wait_for_schedule_completion(Mut schedule) -> void; + + static auto run_task(Mut> task) -> void; + + IA_NODISCARD static auto get_worker_count() -> WorkerId; + +private: + struct ScheduledTask { + Mut tag{}; + Mut schedule_handle{}; + Mut)>> task{}; + }; + + static auto schedule_worker_loop(Mut stop_token, + Const worker_id) -> void; + +private: + static Mut s_queue_mutex; + static Mut s_wake_condition; + static Mut> s_schedule_workers; + static Mut> s_high_priority_queue; + static Mut> s_normal_priority_queue; +}; +} // namespace IACore \ No newline at end of file diff --git a/Src/IACore/inc/IACore/CLI.hpp b/Src/IACore/inc/IACore/CLI.hpp new file mode 100644 index 0000000..1a95192 --- /dev/null +++ b/Src/IACore/inc/IACore/CLI.hpp @@ -0,0 +1,63 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2026 IAS (ias@iasoft.dev) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include + +namespace IACore { +class CLIParser { + /* + * PLEASE READ + * + * CLIParser is still very much in it's baby stages. + * Subject to heavy and frequent changes, use with + * caution! + */ + +public: + CLIParser(Const>> args); + ~CLIParser() = default; + +public: + IA_NODISCARD auto remaining() const -> bool { + return m_current_arg < m_arg_list.end(); + } + + IA_NODISCARD auto peek() const -> StringView { + if (!remaining()) + return ""; + return *m_current_arg; + } + + auto next() -> StringView { + if (!remaining()) + return ""; + return *m_current_arg++; + } + + auto consume(Ref expected) -> bool { + if (peek() == expected) { + next(); + return true; + } + return false; + } + +private: + Const>> m_arg_list; + Mut>::const_iterator> m_current_arg; +}; +} // namespace IACore \ No newline at end of file diff --git a/Src/IACore/inc/IACore/DataOps.hpp b/Src/IACore/inc/IACore/DataOps.hpp new file mode 100644 index 0000000..135540f --- /dev/null +++ b/Src/IACore/inc/IACore/DataOps.hpp @@ -0,0 +1,47 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2026 IAS (ias@iasoft.dev) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include + +namespace IACore { +class DataOps { +public: + enum class CompressionType { None, Gzip, Zlib }; + +public: + static auto hash_fnv1a(Ref string) -> u32; + static auto hash_fnv1a(Ref>> data) -> u32; + + static auto hash_xxhash(Ref string, Const seed = 0) -> u32; + static auto hash_xxhash(Ref>> data, Const seed = 0) + -> u32; + + static auto crc32(Ref>> data) -> u32; + + static auto detect_compression(Const>> data) + -> CompressionType; + + static auto gzip_inflate(Ref>> data) -> Result>; + static auto gzip_deflate(Ref>> data) -> Result>; + + static auto zlib_inflate(Ref>> data) -> Result>; + static auto zlib_deflate(Ref>> data) -> Result>; + + static auto zstd_inflate(Ref>> data) -> Result>; + static auto zstd_deflate(Ref>> data) -> Result>; +}; +} // namespace IACore \ No newline at end of file diff --git a/Src/IACore/inc/IACore/DynamicLib.hpp b/Src/IACore/inc/IACore/DynamicLib.hpp new file mode 100644 index 0000000..12d4756 --- /dev/null +++ b/Src/IACore/inc/IACore/DynamicLib.hpp @@ -0,0 +1,151 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2026 IAS (ias@iasoft.dev) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include + +#if !IA_PLATFORM_WINDOWS +#include +#endif + +namespace IACore { + +class DynamicLib { +public: + IA_NODISCARD static auto load(Ref search_path, Ref name) + -> Result { + namespace fs = std::filesystem; + Mut full_path = fs::path(search_path) / name; + + if (!full_path.has_extension()) { +#if IA_PLATFORM_WINDOWS + full_path += ".dll"; +#elif IA_PLATFORM_APPLE + full_path += ".dylib"; +#else + full_path += ".so"; +#endif + } + + Mut lib; + +#if IA_PLATFORM_WINDOWS + Const h = LoadLibraryA(full_path.string().c_str()); + if (!h) { + return fail(get_windows_error()); + } + lib.m_handle = static_cast(h); +#else + Mut h = dlopen(full_path.c_str(), RTLD_LAZY | RTLD_LOCAL); + if (!h) { + Const err = dlerror(); + return fail(err ? err : "Unknown dlopen error"); + } + lib.m_handle = h; +#endif + + return lib; + } + + DynamicLib() = default; + + DynamicLib(ForwardRef other) noexcept : m_handle(other.m_handle) { + other.m_handle = nullptr; + } + + auto operator=(ForwardRef other) noexcept -> MutRef { + if (this != &other) { + unload(); + m_handle = other.m_handle; + other.m_handle = nullptr; + } + return *this; + } + + DynamicLib(Ref) = delete; + auto operator=(Ref) -> MutRef = delete; + + ~DynamicLib() { unload(); } + + IA_NODISCARD auto get_symbol(Ref name) const -> Result { + if (!m_handle) { + return fail("Library not loaded"); + } + + Mut sym = nullptr; + +#if IA_PLATFORM_WINDOWS + sym = static_cast( + GetProcAddress(static_cast(m_handle), name.c_str())); + if (!sym) { + return fail(get_windows_error()); + } +#else + dlerror(); // Clear prev errors + sym = dlsym(m_handle, name.c_str()); + if (Const err = dlerror()) { + return fail(err); + } +#endif + + return sym; + } + + template + IA_NODISCARD auto get_function(Ref name) const -> Result { + Mut sym = nullptr; + sym = OX_TRY(get_symbol(name)); + return reinterpret_cast(sym); + } + + void unload() { + if (m_handle) { +#if IA_PLATFORM_WINDOWS + FreeLibrary(static_cast(m_handle)); +#else + dlclose(m_handle); +#endif + m_handle = nullptr; + } + } + + IA_NODISCARD auto is_loaded() const -> bool { return m_handle != nullptr; } + +private: + Mut m_handle = nullptr; + +#if IA_PLATFORM_WINDOWS + static auto get_windows_error() -> String { + Const error_id = ::GetLastError(); + if (error_id == 0) { + return String(); + } + + Mut message_buffer = nullptr; + Const size = FormatMessageA( + FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | + FORMAT_MESSAGE_IGNORE_INSERTS, + nullptr, error_id, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + reinterpret_cast(&message_buffer), 0, nullptr); + + Const message(message_buffer, size); + LocalFree(message_buffer); + return "Win32 Error: " + message; + } +#endif +}; + +} // namespace IACore \ No newline at end of file diff --git a/Src/IACore/inc/IACore/Environment.hpp b/Src/IACore/inc/IACore/Environment.hpp new file mode 100644 index 0000000..8b44ea9 --- /dev/null +++ b/Src/IACore/inc/IACore/Environment.hpp @@ -0,0 +1,99 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2026 IAS (ias@iasoft.dev) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include +#include + +namespace IACore { + +class Environment { +public: + static auto find(Ref name) -> Option { +#if IA_PLATFORM_WINDOWS + Const buffer_size = + static_cast(GetEnvironmentVariableA(name.c_str(), nullptr, 0)); + + if (buffer_size == 0) { + return std::nullopt; + } + + Mut result; + result.resize(buffer_size); + + Const actual_size = static_cast( + GetEnvironmentVariableA(name.c_str(), result.data(), buffer_size)); + + if (actual_size == 0 || actual_size > buffer_size) { + return std::nullopt; + } + + result.resize(actual_size); + return result; + +#else + Const val = std::getenv(name.c_str()); + if (val == nullptr) { + return std::nullopt; + } + return String(val); +#endif + } + + static auto get(Ref name, Ref default_value = "") -> String { + return find(name).value_or(default_value); + } + + static auto set(Ref name, Ref value) -> Result { + if (name.empty()) { + return fail("Environment variable name cannot be empty"); + } + +#if IA_PLATFORM_WINDOWS + if (SetEnvironmentVariableA(name.c_str(), value.c_str()) == 0) { + return fail("Failed to set environment variable: {}", name); + } +#else + if (setenv(name.c_str(), value.c_str(), 1) != 0) { + return fail("Failed to set environment variable: {}", name); + } +#endif + return {}; + } + + static auto unset(Ref name) -> Result { + if (name.empty()) { + return fail("Environment variable name cannot be empty"); + } + +#if IA_PLATFORM_WINDOWS + if (SetEnvironmentVariableA(name.c_str(), nullptr) == 0) { + return fail("Failed to unset environment variable: {}", name); + } +#else + if (unsetenv(name.c_str()) != 0) { + return fail("Failed to unset environment variable: {}", name); + } +#endif + return {}; + } + + static auto exists(Ref name) -> bool { + return find(name).has_value(); + } +}; + +} // namespace IACore \ No newline at end of file diff --git a/Src/IACore/inc/IACore/FileOps.hpp b/Src/IACore/inc/IACore/FileOps.hpp new file mode 100644 index 0000000..b6eec4e --- /dev/null +++ b/Src/IACore/inc/IACore/FileOps.hpp @@ -0,0 +1,127 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2026 IAS (ias@iasoft.dev) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include +#include +#include +#include + +#if IA_PLATFORM_WINDOWS +using NativeFileHandle = HANDLE; +static constexpr ox::Const INVALID_FILE_HANDLE = + INVALID_HANDLE_VALUE; +#else +using NativeFileHandle = int; +static constexpr ox::Const INVALID_FILE_HANDLE = -1; +#endif + +namespace IACore { + +class FileOps { +public: + class MemoryMappedRegion; + + enum class FileAccess : u8 { + Read, // Read-only + Write, // Write-only + ReadWrite // Read and Write + }; + + enum class FileMode : u8 { + OpenExisting, // Fails if file doesn't exist + OpenAlways, // Opens if exists, creates if not + CreateNew, // Fails if file exists + CreateAlways, // Overwrites existing + TruncateExisting // Opens existing and clears it + }; + + static auto native_open_file(Ref path, Const access, + Const mode, + Const permissions = 0644) + -> Result; + + static auto native_close_file(Const handle) -> void; + +public: + static auto normalize_executable_path(Ref path) -> Path; + +public: + static auto unmap_file(Const mapped_ptr) -> void; + + static auto map_file(Ref path, MutRef size) + -> Result; + + // @param `is_owner` true to allocate/truncate. false to just open. + static auto map_shared_memory(Ref name, Const size, + Const is_owner) -> Result; + + static auto unlink_shared_memory(Ref name) -> void; + + static auto stream_from_file(Ref path) -> Result; + + static auto stream_to_file(Ref path, Const overwrite = false) + -> Result; + + static auto read_text_file(Ref path) -> Result; + + static auto read_binary_file(Ref path) -> Result>; + + static auto write_text_file(Ref path, Ref contents, + Const overwrite = false) -> Result; + + static auto write_binary_file(Ref path, Const> contents, + Const overwrite = false) -> Result; + +private: + static Mut>> + s_mapped_files; +}; + +class FileOps::MemoryMappedRegion { +public: + MemoryMappedRegion() = default; + ~MemoryMappedRegion(); + + MemoryMappedRegion(Ref) = delete; + auto operator=(Ref) -> MemoryMappedRegion & = delete; + + MemoryMappedRegion(ForwardRef other) noexcept; + auto operator=(ForwardRef other) noexcept + -> MemoryMappedRegion &; + + auto map(Const handle, Const offset, Const size) + -> Result; + + auto unmap() -> void; + auto flush() -> void; + + [[nodiscard]] auto get_ptr() const -> u8 * { return m_ptr; } + + [[nodiscard]] auto get_size() const -> usize { return m_size; } + + [[nodiscard]] auto is_valid() const -> bool { return m_ptr != nullptr; } + +private: + Mut m_ptr = nullptr; + Mut m_size = 0; + +#if IA_PLATFORM_WINDOWS + Mut m_map_handle = NULL; +#endif +}; + +} // namespace IACore \ No newline at end of file diff --git a/Src/IACore/inc/IACore/Http/Client.hpp b/Src/IACore/inc/IACore/Http/Client.hpp new file mode 100644 index 0000000..c48fd6f --- /dev/null +++ b/Src/IACore/inc/IACore/Http/Client.hpp @@ -0,0 +1,95 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2026 IAS (ias@iasoft.dev) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include +#include + +namespace IACore { +class HttpClient : public HttpCommon { +public: + static auto create(Ref host) -> Result>; + + ~HttpClient(); + + HttpClient(ForwardRef) = default; + HttpClient(Ref) = delete; + auto operator=(ForwardRef) -> MutRef = default; + auto operator=(Ref) -> MutRef = delete; + +public: + auto raw_get(Ref path, Span> headers, + Const *default_content_type = + "application/x-www-form-urlencoded") -> Result; + + auto raw_post(Ref path, Span> headers, Ref body, + Const *default_content_type = + "application/x-www-form-urlencoded") -> Result; + + template + auto json_get(Ref path, Span> headers) + -> Result; + + template + auto json_post(Ref path, Span> headers, + Ref body) -> Result; + + // Certificate verification is enabled by default + auto enable_certificate_verification() -> void; + auto disable_certificate_verification() -> void; + +public: + auto last_response_code() -> EResponseCode { return m_last_response_code; } + +private: + Mut m_client; + Mut m_last_response_code; + +private: + auto preprocess_response(Ref response) -> String; + +protected: + explicit HttpClient(ForwardRef client); +}; + +template +auto HttpClient::json_get(Ref path, Span> headers) + -> Result { + Const raw_response = + OX_TRY(raw_get(path, headers, "application/json")); + + if (last_response_code() != EResponseCode::OK) { + return fail("Server responded with code {}", + static_cast(last_response_code())); + } + return Json::parse_to_struct(raw_response); +} + +template +auto HttpClient::json_post(Ref path, Span> headers, + Ref body) -> Result { + Const encoded_body = OX_TRY(Json::encode_struct(body)); + + Const raw_response = + OX_TRY(raw_post(path, headers, encoded_body, "application/json")); + + if (last_response_code() != EResponseCode::OK) { + return fail("Server responded with code {}", + static_cast(last_response_code())); + } + return Json::parse_to_struct(raw_response); +} +} // namespace IACore \ No newline at end of file diff --git a/Src/IACore/inc/IACore/Http/Common.hpp b/Src/IACore/inc/IACore/Http/Common.hpp new file mode 100644 index 0000000..96216e9 --- /dev/null +++ b/Src/IACore/inc/IACore/Http/Common.hpp @@ -0,0 +1,155 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2026 IAS (ias@iasoft.dev) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include + +#include + +namespace IACore { +class HttpCommon { +public: + enum class EHeaderType { + ACCEPT, + ACCEPT_CHARSET, + ACCEPT_ENCODING, + ACCEPT_LANGUAGE, + AUTHORIZATION, + CACHE_CONTROL, + CONNECTION, + CONTENT_LENGTH, + CONTENT_TYPE, + COOKIE, + DATE, + EXPECT, + HOST, + IF_MATCH, + IF_MODIFIED_SINCE, + IF_NONE_MATCH, + ORIGIN, + PRAGMA, + PROXY_AUTHORIZATION, + RANGE, + REFERER, + TE, + UPGRADE, + USER_AGENT, + VIA, + WARNING + }; + + enum class EResponseCode : i32 { + // 1xx Informational + CONTINUE = 100, + SWITCHING_PROTOCOLS = 101, + PROCESSING = 102, + EARLY_HINTS = 103, + + // 2xx Success + OK = 200, + CREATED = 201, + ACCEPTED = 202, + NON_AUTHORITATIVE_INFORMATION = 203, + NO_CONTENT = 204, + RESET_CONTENT = 205, + PARTIAL_CONTENT = 206, + MULTI_STATUS = 207, + ALREADY_REPORTED = 208, + IM_USED = 226, + + // 3xx Redirection + MULTIPLE_CHOICES = 300, + MOVED_PERMANENTLY = 301, + FOUND = 302, + SEE_OTHER = 303, + NOT_MODIFIED = 304, + USE_PROXY = 305, + TEMPORARY_REDIRECT = 307, + PERMANENT_REDIRECT = 308, + + // 4xx Client Error + BAD_REQUEST = 400, + UNAUTHORIZED = 401, + PAYMENT_REQUIRED = 402, + FORBIDDEN = 403, + NOT_FOUND = 404, + METHOD_NOT_ALLOWED = 405, + NOT_ACCEPTABLE = 406, + PROXY_AUTHENTICATION_REQUIRED = 407, + REQUEST_TIMEOUT = 408, + CONFLICT = 409, + GONE = 410, + LENGTH_REQUIRED = 411, + PRECONDITION_FAILED = 412, + PAYLOAD_TOO_LARGE = 413, + URI_TOO_LONG = 414, + UNSUPPORTED_MEDIA_TYPE = 415, + RANGE_NOT_SATISFIABLE = 416, + EXPECTATION_FAILED = 417, + IM_A_TEAPOT = 418, + MISDIRECTED_REQUEST = 421, + UNPROCESSABLE_ENTITY = 422, + LOCKED = 423, + FAILED_DEPENDENCY = 424, + TOO_EARLY = 425, + UPGRADE_REQUIRED = 426, + PRECONDITION_REQUIRED = 428, + TOO_MANY_REQUESTS = 429, + REQUEST_HEADER_FIELDS_TOO_LARGE = 431, + UNAVAILABLE_FOR_LEGAL_REASONS = 451, + + // 5xx Server Error + INTERNAL_SERVER_ERROR = 500, + NOT_IMPLEMENTED = 501, + BAD_GATEWAY = 502, + SERVICE_UNAVAILABLE = 503, + GATEWAY_TIMEOUT = 504, + HTTP_VERSION_NOT_SUPPORTED = 505, + VARIANT_ALSO_NEGOTIATES = 506, + INSUFFICIENT_STORAGE = 507, + LOOP_DETECTED = 508, + NOT_EXTENDED = 510, + NETWORK_AUTHENTICATION_REQUIRED = 511 + }; + + using Header = Pair; + + static auto url_encode(Ref value) -> String; + static auto url_decode(Ref value) -> String; + + static auto header_type_to_string(Const type) -> String; + + static inline auto create_header(Const key, Ref value) + -> Header; + static inline auto create_header(Ref key, Ref value) + -> Header; + + static auto is_success_response_code(Const code) -> bool; + +protected: + HttpCommon() = default; +}; + +auto HttpCommon::create_header(Const key, Ref value) + -> HttpCommon::Header { + return Header{header_type_to_string(key), value}; +} + +auto HttpCommon::create_header(Ref key, Ref value) + -> HttpCommon::Header { + return Header{key, value}; +} +} // namespace IACore \ No newline at end of file diff --git a/Src/IACore/inc/IACore/Http/Server.hpp b/Src/IACore/inc/IACore/Http/Server.hpp new file mode 100644 index 0000000..80ed89f --- /dev/null +++ b/Src/IACore/inc/IACore/Http/Server.hpp @@ -0,0 +1,151 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2026 IAS (ias@iasoft.dev) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include +#include +#include + +namespace IACore { +class HttpServer : public HttpCommon { +public: + struct Request { + Mut path; + Mut method; + Mut body; + Mut> headers; + Mut> params; // Query params + Mut> path_params; // Path params (like /object/:id) + + [[nodiscard]] auto get_header(Ref key) const -> String; + [[nodiscard]] auto get_param(Ref key) const -> String; + [[nodiscard]] auto get_path_param(Ref key) const -> String; + + [[nodiscard]] auto has_header(Ref key) const -> bool; + [[nodiscard]] auto has_param(Ref key) const -> bool; + [[nodiscard]] auto has_path_param(Ref key) const -> bool; + }; + + struct Response { + Mut code = EResponseCode::OK; + Mut body; + Mut> headers; + Mut content_type = "text/plain"; + + void set_content(Ref content, Ref type); + void set_status(Const status_code); + void add_header(Ref key, Ref value); + }; + + using Handler = std::function, MutRef)>; + +public: + static auto create() -> Result>; + + ~HttpServer(); + + HttpServer(HttpServer &&) = delete; + HttpServer(const HttpServer &) = delete; + auto operator=(HttpServer &&) -> HttpServer & = delete; + auto operator=(const HttpServer &) -> HttpServer & = delete; + + auto listen(Ref host, Const port) -> Result; + void stop(); + auto is_running() const -> bool; + + void get(Ref pattern, Const handler); + void post(Ref pattern, Const handler); + void put(Ref pattern, Const handler); + void del(Ref pattern, Const handler); + void options(Ref pattern, Const handler); + + template + void + json_get(Ref pattern, + Const(Ref)>> handler); + + template + void json_post( + Ref pattern, + Const(Ref)>> handler); + +protected: + HttpServer(); + +private: + Mut m_server; + + void register_handler(Ref method, Ref pattern, + Const handler); +}; + +template +void HttpServer::json_get( + Ref pattern, + Const(Ref)>> handler) { + get(pattern, [handler](Ref req, MutRef res) { + Const> result = handler(req); + if (!result) { + res.set_status(EResponseCode::INTERNAL_SERVER_ERROR); + res.set_content(result.error(), "text/plain"); + return; + } + + Const> json_res = Json::encode_struct(*result); + if (!json_res) { + res.set_status(EResponseCode::INTERNAL_SERVER_ERROR); + res.set_content("Failed to encode JSON response", "text/plain"); + return; + } + + res.set_status(EResponseCode::OK); + res.set_content(*json_res, "application/json"); + }); +} + +template +void HttpServer::json_post( + Ref pattern, + Const(Ref)>> handler) { + post(pattern, [handler](Ref req, MutRef res) { + Const> payload = + Json::parse_to_struct(req.body); + if (!payload) { + res.set_status(EResponseCode::BAD_REQUEST); + res.set_content("Invalid JSON Payload", "text/plain"); + return; + } + + Const> result = handler(*payload); + if (!result) { + res.set_status(EResponseCode::INTERNAL_SERVER_ERROR); + res.set_content(result.error(), "text/plain"); + return; + } + + Const> json_res = Json::encode_struct(*result); + if (!json_res) { + res.set_status(EResponseCode::INTERNAL_SERVER_ERROR); + res.set_content("Failed to encode JSON response", "text/plain"); + return; + } + + res.set_status(EResponseCode::OK); + res.set_content(*json_res, "application/json"); + }); +} + +} // namespace IACore \ No newline at end of file diff --git a/Src/IACore/inc/IACore/IACore.hpp b/Src/IACore/inc/IACore/IACore.hpp new file mode 100644 index 0000000..5a1a44f --- /dev/null +++ b/Src/IACore/inc/IACore/IACore.hpp @@ -0,0 +1,66 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2026 IAS (ias@iasoft.dev) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include +#include + +#define IACORE_MAIN() \ + auto _app_entry(IACore::Ref> args) \ + -> IACore::Result; \ + auto main(Const argc, Mut argv[]) -> int { \ + IACore::Mut exit_code = 0; \ + IACore::initialize(); \ + IACore::Mut> args; \ + args.reserve(static_cast(argc)); \ + for (IACore::Mut i = 0; i < argc; ++i) { \ + args.push_back(argv[i]); \ + } \ + IACore::Const> result = _app_entry(args); \ + if (!result) { \ + IACore::Logger::error("Application exited with an error: '{}'.", \ + result.error()); \ + exit_code = -20; \ + } else { \ + exit_code = *result; \ + if (exit_code == 0) { \ + IACore::Logger::info("Application exited successfully."); \ + } else { \ + IACore::Logger::error("Application exited with error code: {}.", \ + exit_code); \ + } \ + } \ + IACore::terminate(); \ + return exit_code; \ + } \ + auto _app_entry(IACore::Ref> args) \ + -> IACore::Result + +namespace IACore { +// Must be called from main thread +// Safe to call multiple times but, given every initialize call is paired with a +// corresponding terminate call +auto initialize() -> void; + +// Must be called from same thread as initialize +// Safe to call multiple times but, given every initialize call is paired with a +// corresponding terminate call +auto terminate() -> void; + +auto is_initialized() -> bool; + +auto is_main_thread() -> bool; +} // namespace IACore \ No newline at end of file diff --git a/Src/IACore/inc/IACore/IATest.hpp b/Src/IACore/inc/IACore/IATest.hpp new file mode 100644 index 0000000..a6173ff --- /dev/null +++ b/Src/IACore/inc/IACore/IATest.hpp @@ -0,0 +1,293 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2026 IAS (ias@iasoft.dev) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include + +// ----------------------------------------------------------------------------- +// Macros +// ----------------------------------------------------------------------------- + +#define __iat_micro_test(call) \ + if (!(call)) \ + return false + +#define IAT_CHECK(v) __iat_micro_test(_test((v), #v)) +#define IAT_CHECK_NOT(v) __iat_micro_test(_test_not((v), "NOT " #v)) +#define IAT_CHECK_EQ(lhs, rhs) \ + __iat_micro_test(_test_eq((lhs), (rhs), #lhs " == " #rhs)) +#define IAT_CHECK_NEQ(lhs, rhs) \ + __iat_micro_test(_test_neq((lhs), (rhs), #lhs " != " #rhs)) + +#define IAT_CHECK_APPROX(lhs, rhs) \ + __iat_micro_test(_test_approx((lhs), (rhs), #lhs " ~= " #rhs)) + +#define IAT_UNIT(func) _test_unit([this]() { return this->func(); }, #func) +#define IAT_NAMED_UNIT(n, func) _test_unit([this]() { return this->func(); }, n) + +#define IAT_BLOCK(name) class name : public IACore::Test::Block + +#define IAT_BEGIN_BLOCK(_group, _name) \ + class _group##_##_name : public IACore::Test::Block { \ + public: \ + [[nodiscard]] auto get_name() const -> const char * override { \ + return #_group "::" #_name; \ + } \ + \ + private: + +#define IAT_END_BLOCK() \ + } \ + ; + +#define IAT_BEGIN_TEST_LIST() \ +public: \ + auto declare_tests() -> void override { +#define IAT_ADD_TEST(name) IAT_UNIT(name) +#define IAT_END_TEST_LIST() \ + } \ + \ +private: + +namespace IACore::Test { +// ------------------------------------------------------------------------- +// String Conversion Helpers +// ------------------------------------------------------------------------- +template auto to_string(Ref value) -> String { + if constexpr (std::is_arithmetic_v) { + return std::to_string(value); + } else if constexpr (std::is_same_v || + std::is_same_v || + std::is_same_v) { + return String("\"") + String(value) + "\""; + } else { + return "{Object}"; + } +} + +template auto to_string(T *value) -> String { + if (value == nullptr) { + return "nullptr"; + } + return std::format("ptr({})", static_cast(value)); +} + +// ------------------------------------------------------------------------- +// Types +// ------------------------------------------------------------------------- +using TestFunctor = std::function; + +struct TestUnit { + Mut name; + Mut functor; +}; + +class Block { +public: + virtual ~Block() = default; + [[nodiscard]] virtual auto get_name() const -> const char * = 0; + virtual auto declare_tests() -> void = 0; + + auto units() -> MutRef> { return m_units; } + +protected: + template + auto _test_eq(Ref lhs, Ref rhs, Const description) + -> bool { + if (lhs != rhs) { + print_fail(description, to_string(lhs), to_string(rhs)); + return false; + } + return true; + } + + template + auto _test_neq(Ref lhs, Ref rhs, Const description) + -> bool { + if (lhs == rhs) { + print_fail(description, to_string(lhs), "NOT " + to_string(rhs)); + return false; + } + return true; + } + + template + auto _test_approx(Const lhs, Const rhs, Const description) + -> bool { + static_assert(std::is_floating_point_v, + "Approx only works for floats/doubles"); + Const diff = std::abs(lhs - rhs); + if (diff > static_cast(0.0001)) { + print_fail(description, to_string(lhs), to_string(rhs)); + return false; + } + return true; + } + + auto _test(Const value, Const description) -> bool { + if (!value) { + std::cout << console::BLUE << " " << description << "... " + << console::RED << "FAILED" << console::RESET << "\n"; + return false; + } + return true; + } + + auto _test_not(Const value, Const description) -> bool { + if (value) { + std::cout << console::BLUE << " " << description << "... " + << console::RED << "FAILED" << console::RESET << "\n"; + return false; + } + return true; + } + + auto _test_unit(Mut functor, Const name) -> void { + m_units.push_back({name, std::move(functor)}); + } + +private: + auto print_fail(Const desc, Ref v1, Ref v2) + -> void { + std::cout << console::BLUE << " " << desc << "... " << console::RED + << "FAILED" << console::RESET << "\n"; + std::cout << console::RED << " Expected: " << v2 << console::RESET + << "\n"; + std::cout << console::RED << " Actual: " << v1 << console::RESET + << "\n"; + } + + Mut> m_units; +}; + +template +concept ValidBlockClass = std::derived_from; + +// ------------------------------------------------------------------------- +// Runner +// ------------------------------------------------------------------------- +template class Runner { +public: + Runner() = default; + + ~Runner() { summarize(); } + + template + requires ValidBlockClass + auto test_block() -> void; + +private: + auto summarize() -> void; + + Mut m_test_count{0}; + Mut m_fail_count{0}; + Mut m_block_count{0}; +}; + +template +template + requires ValidBlockClass +auto Runner::test_block() -> void { + m_block_count++; + Mut b; + b.declare_tests(); + + std::cout << console::MAGENTA << "Testing [" << b.get_name() << "]..." + << console::RESET << "\n"; + + for (MutRef v : b.units()) { + m_test_count++; + if constexpr (IsVerbose) { + std::cout << console::YELLOW << " Testing " << v.name << "...\n" + << console::RESET; + } + + Const result = v.functor(); + + if (!result) { + m_fail_count++; + if constexpr (StopOnFail) { + summarize(); + std::exit(-1); + } + } + } + std::cout << "\n"; +} + +template +auto Runner::summarize() -> void { + std::cout << console::GREEN + << "\n-----------------------------------\n\t " + "SUMMARY\n-----------------------------------\n"; + + if (m_fail_count == 0) { + std::cout << "\n\tALL TESTS PASSED!\n\n"; + } else { + Const success_rate = + (100.0 * static_cast(m_test_count - m_fail_count) / + static_cast(m_test_count)); + std::cout << console::RED << m_fail_count << " OF " << m_test_count + << " TESTS FAILED\n" + << console::YELLOW + << std::format("Success Rate: {:.2f}%\n", success_rate); + } + + std::cout << console::MAGENTA << "Ran " << m_test_count << " test(s) across " + << m_block_count << " block(s)\n" + << console::GREEN << "-----------------------------------" + << console::RESET << "\n"; +} + +using DefaultRunner = Runner; + +// ------------------------------------------------------------------------- +// Registry +// ------------------------------------------------------------------------- +class TestRegistry { +public: + using TestEntry = std::function)>; + + static auto get_entries() -> MutRef> { + static Mut> entries; + return entries; + } + + static auto run_all() -> i32 { + Mut r; + MutRef> entries = get_entries(); + std::cout << console::CYAN << "[IATest] Discovered " << entries.size() + << " Test Blocks\n\n" + << console::RESET; + + for (MutRef entry : entries) { + entry(r); + } + + return 0; + } +}; + +template struct AutoRegister { + AutoRegister() { + TestRegistry::get_entries().push_back( + [](MutRef r) { r.test_block(); }); + } +}; +} // namespace IACore::Test + +#define IAT_REGISTER_ENTRY(Group, Name) \ + static IACore::Test::AutoRegister _iat_reg_##Group##_##Name; \ No newline at end of file diff --git a/Src/IACore/inc/IACore/IPC.hpp b/Src/IACore/inc/IACore/IPC.hpp new file mode 100644 index 0000000..a0cc4b8 --- /dev/null +++ b/Src/IACore/inc/IACore/IPC.hpp @@ -0,0 +1,160 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2026 IAS (ias@iasoft.dev) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include +#include +#include + +namespace IACore { +using IpcPacketHeader = RingBufferView::PacketHeader; + +struct alignas(64) IpcSharedMemoryLayout { + // ========================================================= + // METADATA & HANDSHAKE + // ========================================================= + struct Header { + Mut magic; // 0x49414950 ("IAIP") + Mut version; // 1 + Mut total_size; // Total size of SHM block + }; + + Mut
meta; + + // Pad to ensure MONI starts on a fresh cache line (64 bytes) + Const> _pad0; + + // ========================================================= + // RING BUFFER CONTROL BLOCKS + // ========================================================= + + // RingBufferView ControlBlock is already 64-byte aligned internally. + Mut moni_control; + Mut mino_control; + + // ========================================================= + // DATA BUFFER OFFSETS + // ========================================================= + + Mut moni_data_offset; + Mut moni_data_size; + + Mut mino_data_offset; + Mut mino_data_size; + + // Pad to ensure the actual Data Buffer starts on a fresh cache line + Const> _pad1; + + static constexpr auto get_header_size() -> usize { + return sizeof(IpcSharedMemoryLayout); + } +}; + +// Check padding logic is gucci +static_assert(sizeof(IpcSharedMemoryLayout) % 64 == 0, + "IPC Layout is not cache-line aligned!"); + +class IpcNode { +public: + virtual ~IpcNode(); + + // When Manager spawns a node, `connection_string` is passed + // as the first command line argument + auto connect(Const connection_string) -> Result; + + auto update() -> void; + + auto send_signal(Const signal) -> void; + auto send_packet(Const packet_id, Const>> payload) + -> Result; + +protected: + virtual auto on_signal(Const signal) -> void = 0; + virtual auto on_packet(Const packet_id, Const>> payload) + -> void = 0; + +private: + Mut m_shm_name; + Mut m_shared_memory{}; + Mut> m_receive_buffer; + Mut m_socket{INVALID_SOCKET}; + + Mut m_moni; // Manager Out, Node In + Mut m_mino; // Manager In, Node Out +}; + +class IpcManager { + struct NodeSession { + Mut creation_time{}; + Mut> node_process; + + Mut send_mutex; + + Mut shared_mem_name; + Mut mapped_ptr{}; + + Mut listener_socket{INVALID_SOCKET}; + Mut data_socket{INVALID_SOCKET}; + + Mut moni = + RingBufferView::default_instance(); // Manager Out, Node In + Mut mino = + RingBufferView::default_instance(); // Manager In, Node Out + + Mut is_ready{false}; + + auto send_signal(Const signal) -> void; + auto send_packet(Const packet_id, Const>> payload) + -> Result; + }; + +public: + static constexpr Const DEFAULT_NODE_SHARED_MEMORY_SIZE = 4 * 1024 * 1024; + +public: + virtual ~IpcManager(); + + auto update() -> void; + + auto + spawn_node(Ref executable_path, + Const shared_memory_size = DEFAULT_NODE_SHARED_MEMORY_SIZE) + -> Result; + + auto wait_till_node_is_online(Const node) -> bool; + + auto shutdown_node(Const node) -> void; + + auto send_signal(Const node, Const signal) -> void; + auto send_packet(Const node, Const packet_id, + Const>> payload) -> Result; + +protected: + virtual auto on_signal(Const node, Const signal) + -> void = 0; + virtual auto on_packet(Const node, Const packet_id, + Const>> payload) -> void = 0; + +private: + Mut> m_receive_buffer; + Mut>> m_active_sessions; + Mut>> m_pending_sessions; + Mut> m_active_session_map; + +protected: + IpcManager(); +}; +} // namespace IACore \ No newline at end of file diff --git a/Src/IACore/inc/IACore/JSON.hpp b/Src/IACore/inc/IACore/JSON.hpp new file mode 100644 index 0000000..16f12ce --- /dev/null +++ b/Src/IACore/inc/IACore/JSON.hpp @@ -0,0 +1,119 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2026 IAS (ias@iasoft.dev) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include + +#include +#include +#include + +namespace IACore { +class JsonDocument { +public: + JsonDocument(ForwardRef) noexcept = default; + auto operator=(ForwardRef) noexcept + -> MutRef = default; + + JsonDocument(Ref) = delete; + auto operator=(Ref) -> MutRef = delete; + + [[nodiscard]] + auto root() const noexcept -> simdjson::dom::element { + return m_root; + } + +private: + friend class Json; + + JsonDocument(Mut> p, Mut r) + : m_parser(std::move(p)), m_root(r) {} + + Mut> m_parser; + Mut m_root; +}; + +class Json { +private: + static constexpr Const GLAZE_OPTS = + glz::opts{.error_on_unknown_keys = false}; + +public: + static auto parse(Ref json_str) -> Result; + static auto encode(Ref data) -> String; + + static auto parse_read_only(Ref json_str) -> Result; + + template + static auto parse_to_struct(Ref json_str) -> Result; + + template + static auto encode_struct(Ref data) -> Result; +}; + +inline auto Json::parse(Ref json_str) -> Result { + Const res = + nlohmann::json::parse(json_str, nullptr, false, true); + + if (res.is_discarded()) { + return fail("Failed to parse JSON (Invalid Syntax)"); + } + return res; +} + +inline auto Json::parse_read_only(Ref json_str) + -> Result { + Mut> parser = make_box(); + + Mut root; + + Const error = parser->parse(json_str).get(root); + + if (error) { + return fail("JSON Error: {}", simdjson::error_message(error)); + } + + return JsonDocument(std::move(parser), root); +} + +inline auto Json::encode(Ref data) -> String { + return data.dump(); +} + +template +inline auto Json::parse_to_struct(Ref json_str) -> Result { + Mut result{}; + + Const err = glz::read(result, json_str); + + if (err) { + return fail("JSON Struct Parse Error: {}", + glz::format_error(err, json_str)); + } + return result; +} + +template +inline auto Json::encode_struct(Ref data) -> Result { + Mut result; + Const err = glz::write_json(data, result); + + if (err) { + return fail("JSON Struct Encode Error"); + } + return result; +} +} // namespace IACore \ No newline at end of file diff --git a/Src/IACore/inc/IACore/Logger.hpp b/Src/IACore/inc/IACore/Logger.hpp new file mode 100644 index 0000000..e801e81 --- /dev/null +++ b/Src/IACore/inc/IACore/Logger.hpp @@ -0,0 +1,123 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2026 IAS (ias@iasoft.dev) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include + +#define IA_LOG_SET_FILE(path) IACore::Logger::enable_logging_to_disk(path) +#define IA_LOG_SET_LEVEL(level) \ + IACore::Logger::set_log_level(IACore::Logger::LogLevel::level) + +#define IA_LOG_TRACE(...) IACore::Logger::trace(__VA_ARGS__) +#define IA_LOG_DEBUG(...) IACore::Logger::debug(__VA_ARGS__) +#define IA_LOG_INFO(...) IACore::Logger::info(__VA_ARGS__) +#define IA_LOG_WARN(...) IACore::Logger::warn(__VA_ARGS__) +#define IA_LOG_ERROR(...) IACore::Logger::error(__VA_ARGS__) + +namespace IACore { +class Logger { +public: + enum class LogLevel { Trace, Debug, Info, Warn, Error }; + +public: + static auto enable_logging_to_disk(Const file_path) + -> Result; + static auto set_log_level(Const log_level) -> void; + + template + static auto trace(Const> fmt, + ForwardRef... args) -> void { + log_trace(std::vformat(fmt.get(), std::make_format_args(args...))); + } + + template + static auto debug(Const> fmt, + ForwardRef... args) -> void { + log_debug(std::vformat(fmt.get(), std::make_format_args(args...))); + } + + template + static auto info(Const> fmt, + ForwardRef... args) -> void { + log_info(std::vformat(fmt.get(), std::make_format_args(args...))); + } + + template + static auto warn(Const> fmt, + ForwardRef... args) -> void { + log_warn(std::vformat(fmt.get(), std::make_format_args(args...))); + } + + template + static auto error(Const> fmt, + ForwardRef... args) -> void { + log_error(std::vformat(fmt.get(), std::make_format_args(args...))); + } + + static auto flush_logs() -> void; + +private: +#if IA_DISABLE_LOGGING > 0 + static auto log_trace(ForwardRef msg) -> void { IA_UNUSED(msg); } + + static auto log_debug(ForwardRef msg) -> void { IA_UNUSED(msg); } + + static auto log_info(ForwardRef msg) -> void { IA_UNUSED(msg); } + + static auto log_warn(ForwardRef msg) -> void { IA_UNUSED(msg); } + + static auto log_error(ForwardRef msg) -> void { IA_UNUSED(msg); } +#else + static auto log_trace(ForwardRef msg) -> void { + if (m_log_level <= LogLevel::Trace) + log_internal(console::RESET, "TRACE", std::move(msg)); + } + + static auto log_debug(ForwardRef msg) -> void { + if (m_log_level <= LogLevel::Debug) + log_internal(console::CYAN, "DEBUG", std::move(msg)); + } + + static auto log_info(ForwardRef msg) -> void { + if (m_log_level <= LogLevel::Info) + log_internal(console::GREEN, "INFO", std::move(msg)); + } + + static auto log_warn(ForwardRef msg) -> void { + if (m_log_level <= LogLevel::Warn) + log_internal(console::YELLOW, "WARN", std::move(msg)); + } + + static auto log_error(ForwardRef msg) -> void { + if (m_log_level <= LogLevel::Error) + log_internal(console::RED, "ERROR", std::move(msg)); + } +#endif + + static auto log_internal(Const prefix, Const tag, + ForwardRef msg) -> void; + +private: + static Mut m_log_level; + static Mut m_log_file; + + static auto initialize() -> void; + static auto terminate() -> void; + + friend void initialize(); + friend void terminate(); +}; +} // namespace IACore \ No newline at end of file diff --git a/Src/IACore/inc/IACore/PCH.hpp b/Src/IACore/inc/IACore/PCH.hpp new file mode 100644 index 0000000..2af0af4 --- /dev/null +++ b/Src/IACore/inc/IACore/PCH.hpp @@ -0,0 +1,135 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2026 IAS (ias@iasoft.dev) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#if defined(__x86_64__) || defined(_M_X64) || defined(_M_AMD64) +#define IA_ARCH_X64 1 +#elif defined(__aarch64__) || defined(_M_ARM64) +#define IA_ARCH_ARM64 1 +#elif defined(__wasm__) || defined(__wasm32__) || defined(__wasm64__) +#define IA_ARCH_WASM 1 +#else +#error "IACore: Unsupported Architecture." +#endif + +#if defined(WIN32) || defined(_WIN32) || defined(__WIN32__) || defined(__NT__) +#define IA_PLATFORM_WINDOWS 1 +#elif __APPLE__ +#include +#define IA_PLATFORM_APPLE 1 +#define IA_PLATFORM_UNIX 1 +#elif __linux__ +#define IA_PLATFORM_LINUX 1 +#define IA_PLATFORM_UNIX 1 +#elif __wasm__ +#define IA_PLATFORM_WASM 1 +#else +#error "IACore: Unsupported Platform." +#endif + +#if IA_PLATFORM_WINDOWS +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#elif IA_PLATFORM_UNIX +#include +#include +#include +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +namespace IACore { + +using namespace Oxide; + +// ============================================================================= +// Build Environment & Constants +// ============================================================================= +namespace Env { + +using namespace Oxide::Env; + +#if IA_PLATFORM_WINDOWS +constexpr Const IS_WINDOWS = true; +constexpr Const IS_UNIX = false; +#else +constexpr Const IS_WINDOWS = false; +constexpr Const IS_UNIX = true; +#endif + +constexpr Const MAX_PATH_LEN = 4096; + +} // namespace Env + +// ============================================================================= +// Data Structures & Aliases +// ============================================================================= +template +using HashMap = ankerl::unordered_dense::map; +template using HashSet = ankerl::unordered_dense::set; + +using Path = std::filesystem::path; + +// ============================================================================= +// Versioning +// ============================================================================= +struct Version { + u32 major = 0; + u32 minor = 0; + u32 patch = 0; + + [[nodiscard]] constexpr auto to_u64() const -> u64 { + return (static_cast(major) << 40) | (static_cast(minor) << 16) | + (static_cast(patch)); + } +}; + +// ============================================================================= +// Console Colors +// ============================================================================= +namespace console { +constexpr Const RESET = "\033[0m"; +constexpr Const RED = "\033[31m"; +constexpr Const GREEN = "\033[32m"; +constexpr Const YELLOW = "\033[33m"; +constexpr Const BLUE = "\033[34m"; +constexpr Const MAGENTA = "\033[35m"; +constexpr Const CYAN = "\033[36m"; +} // namespace console + +} // namespace IACore + +#define IA_NODISCARD [[nodiscard]] +#define IA_UNUSED(v) (void)(v) diff --git a/Src/IACore/inc/IACore/Platform.hpp b/Src/IACore/inc/IACore/Platform.hpp new file mode 100644 index 0000000..b7e452c --- /dev/null +++ b/Src/IACore/inc/IACore/Platform.hpp @@ -0,0 +1,52 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2026 IAS (ias@iasoft.dev) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include + +#if IA_ARCH_X64 +#ifdef _MSC_VER +#include +#else +#include +#endif +#elif IA_ARCH_ARM64 +#include +#endif + +namespace IACore { +class Platform { +public: + struct Capabilities { + Mut hardware_crc32 = false; + }; + + static auto check_cpu() -> bool; + +#if IA_ARCH_X64 + static auto cpuid(Const function, Const sub_function, + Mut out) -> void; +#endif + + static auto get_architecture_name() -> const char *; + static auto get_operating_system_name() -> const char *; + + static auto get_capabilities() -> Ref { return s_capabilities; } + +private: + static Mut s_capabilities; +}; +} // namespace IACore \ No newline at end of file diff --git a/Src/IACore/inc/IACore/ProcessOps.hpp b/Src/IACore/inc/IACore/ProcessOps.hpp new file mode 100644 index 0000000..b21d9e6 --- /dev/null +++ b/Src/IACore/inc/IACore/ProcessOps.hpp @@ -0,0 +1,69 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2026 IAS (ias@iasoft.dev) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include + +#if IA_PLATFORM_WINDOWS +using NativeProcessID = DWORD; +#elif IA_PLATFORM_UNIX +using NativeProcessID = pid_t; +#else +#error "This platform does not support IACore ProcessOps" +#endif + +namespace IACore { +struct ProcessHandle { + Mut> id{0}; + Mut> is_running{false}; + + [[nodiscard]] auto is_active() const -> bool { return is_running && id != 0; } + +private: + Mut m_thread_handle; + + friend class ProcessOps; +}; + +class ProcessOps { +public: + static auto get_current_process_id() -> NativeProcessID; + + static auto spawn_process_sync( + Ref command, Ref args, + Const)>> on_output_line_callback) + -> Result; + + static auto spawn_process_async( + Ref command, Ref args, + Const)>> on_output_line_callback, + Const>)>> on_finish_callback) + -> Result>; + + static auto terminate_process(Ref> handle) -> void; + +private: + static auto spawn_process_windows( + Ref command, Ref args, + Const)>> on_output_line_callback, + MutRef> id) -> Result; + + static auto spawn_process_posix( + Ref command, Ref args, + Const)>> on_output_line_callback, + MutRef> id) -> Result; +}; +} // namespace IACore \ No newline at end of file diff --git a/Src/IACore/inc/IACore/SIMD.hpp b/Src/IACore/inc/IACore/SIMD.hpp new file mode 100644 index 0000000..c944c55 --- /dev/null +++ b/Src/IACore/inc/IACore/SIMD.hpp @@ -0,0 +1,280 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2026 IAS (ias@iasoft.dev) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include + +#if defined(__clang__) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wunused-parameter" +#pragma GCC diagnostic ignored "-Wgnu-zero-variadic-macro-arguments" +#endif + +#include + +#if defined(__clang__) +#pragma GCC diagnostic pop +#endif + +namespace IACore { +namespace hn = hwy::HWY_NAMESPACE; + +#if HWY_TARGET == HWY_SCALAR +#pragma message( \ + "Warning: Configuration mismatch. IACore is being compiled for SCALAR SIMD (Slow)") +#endif + +class alignas(16) IntVec4 { +public: + IntVec4() = default; + + inline explicit IntVec4(Const s); + inline explicit IntVec4(Const values); + inline explicit IntVec4(Const a, Const b, Const c, + Const d); + + inline auto operator+(Ref other) const -> IntVec4; + inline auto operator-(Ref other) const -> IntVec4; + inline auto operator*(Ref other) const -> IntVec4; + + inline auto operator&(Ref other) const -> IntVec4; + inline auto operator|(Ref other) const -> IntVec4; + inline auto operator^(Ref other) const -> IntVec4; + inline auto operator~() const -> IntVec4; + + inline auto operator<<(Const amount) const -> IntVec4; + inline auto operator>>(Const amount) const -> IntVec4; + + [[nodiscard]] inline auto sat_add(Ref other) const -> IntVec4; + [[nodiscard]] inline auto sat_sub(Ref other) const -> IntVec4; + + [[nodiscard]] inline auto clamp(Const min, Const max) const + -> IntVec4; + + [[nodiscard]] inline auto mult_add(Ref multiplier, + Ref addend) const -> IntVec4; + + inline auto store(Mut values) -> void; + static inline auto load(Const values) -> IntVec4; + +private: + using Tag = hn::FixedTag; + + Mut> m_data; + + inline explicit IntVec4(Const> v) : m_data(v) {} +}; + +class alignas(16) FloatVec4 { +public: + FloatVec4() = default; + + inline explicit FloatVec4(Const s); + inline explicit FloatVec4(Const values); + inline explicit FloatVec4(Const a, Const b, Const c, + Const d); + + inline auto operator+(Ref other) const -> FloatVec4; + inline auto operator-(Ref other) const -> FloatVec4; + inline auto operator*(Ref other) const -> FloatVec4; + inline auto operator/(Ref other) const -> FloatVec4; + + [[nodiscard]] inline auto clamp(Const min, Const max) const + -> FloatVec4; + + [[nodiscard]] inline auto abs() const -> FloatVec4; + [[nodiscard]] inline auto sqrt() const -> FloatVec4; + [[nodiscard]] inline auto rsqrt() const -> FloatVec4; + [[nodiscard]] inline auto normalize() const -> FloatVec4; + + [[nodiscard]] inline auto dot(Ref other) const -> f32; + + [[nodiscard]] inline auto mult_add(Ref multiplier, + Ref addend) const -> FloatVec4; + + inline auto store(Mut values) -> void; + static inline auto load(Const values) -> FloatVec4; + +private: + using Tag = hn::FixedTag; + + Mut> m_data; + + inline explicit FloatVec4(Const> v) : m_data(v) {} +}; +} // namespace IACore + +namespace IACore { +IntVec4::IntVec4(Const s) { + Const d; + m_data = hn::Set(d, s); +} + +IntVec4::IntVec4(Const values) { + Const data; + m_data = hn::Load(data, values); +} + +IntVec4::IntVec4(Const a, Const b, Const c, Const d) { + Const data; + alignas(16) Mut> values = {a, b, c, d}; + m_data = hn::Load(data, values.data()); +} + +auto IntVec4::operator+(Ref other) const -> IntVec4 { + return IntVec4(hn::Add(m_data, other.m_data)); +} + +auto IntVec4::operator-(Ref other) const -> IntVec4 { + return IntVec4(hn::Sub(m_data, other.m_data)); +} + +auto IntVec4::operator*(Ref other) const -> IntVec4 { + return IntVec4(hn::Mul(m_data, other.m_data)); +} + +auto IntVec4::operator&(Ref other) const -> IntVec4 { + return IntVec4(hn::And(m_data, other.m_data)); +} + +auto IntVec4::operator|(Ref other) const -> IntVec4 { + return IntVec4(hn::Or(m_data, other.m_data)); +} + +auto IntVec4::operator^(Ref other) const -> IntVec4 { + return IntVec4(hn::Xor(m_data, other.m_data)); +} + +auto IntVec4::operator~() const -> IntVec4 { return IntVec4(hn::Not(m_data)); } + +auto IntVec4::operator<<(Const amount) const -> IntVec4 { + return IntVec4(hn::ShiftLeftSame(m_data, amount)); +} + +auto IntVec4::operator>>(Const amount) const -> IntVec4 { + return IntVec4(hn::ShiftRightSame(m_data, amount)); +} + +auto IntVec4::mult_add(Ref multiplier, Ref addend) const + -> IntVec4 { + return IntVec4(hn::MulAdd(m_data, multiplier.m_data, addend.m_data)); +} + +auto IntVec4::sat_add(Ref other) const -> IntVec4 { + return IntVec4(hn::SaturatedAdd(m_data, other.m_data)); +} + +auto IntVec4::sat_sub(Ref other) const -> IntVec4 { + return IntVec4(hn::SaturatedSub(m_data, other.m_data)); +} + +auto IntVec4::clamp(Const min, Const max) const -> IntVec4 { + Const d; + Const> v_min = hn::Set(d, min); + Const> v_max = hn::Set(d, max); + return IntVec4(hn::Min(hn::Max(m_data, v_min), v_max)); +} + +auto IntVec4::store(Mut values) -> void { + Const d; + hn::Store(m_data, d, values); +} + +auto IntVec4::load(Const values) -> IntVec4 { + Const d; + return IntVec4(hn::Load(d, values)); +} +} // namespace IACore + +namespace IACore { +FloatVec4::FloatVec4(Const s) { + Const d; + m_data = hn::Set(d, s); +} + +FloatVec4::FloatVec4(Const values) { + Const d; + m_data = hn::Load(d, values); +} + +FloatVec4::FloatVec4(Const a, Const b, Const c, Const d) { + Const data; + alignas(16) Mut> temp = {a, b, c, d}; + m_data = hn::Load(data, temp.data()); +} + +auto FloatVec4::operator+(Ref other) const -> FloatVec4 { + return FloatVec4(hn::Add(m_data, other.m_data)); +} + +auto FloatVec4::operator-(Ref other) const -> FloatVec4 { + return FloatVec4(hn::Sub(m_data, other.m_data)); +} + +auto FloatVec4::operator*(Ref other) const -> FloatVec4 { + return FloatVec4(hn::Mul(m_data, other.m_data)); +} + +auto FloatVec4::operator/(Ref other) const -> FloatVec4 { + return FloatVec4(hn::Div(m_data, other.m_data)); +} + +auto FloatVec4::mult_add(Ref multiplier, Ref addend) const + -> FloatVec4 { + return FloatVec4(hn::MulAdd(m_data, multiplier.m_data, addend.m_data)); +} + +auto FloatVec4::clamp(Const min, Const max) const -> FloatVec4 { + Const d; + Const> v_min = hn::Set(d, min); + Const> v_max = hn::Set(d, max); + return FloatVec4(hn::Min(hn::Max(m_data, v_min), v_max)); +} + +auto FloatVec4::sqrt() const -> FloatVec4 { + return FloatVec4(hn::Sqrt(m_data)); +} + +auto FloatVec4::rsqrt() const -> FloatVec4 { + return FloatVec4(hn::ApproximateReciprocalSqrt(m_data)); +} + +auto FloatVec4::abs() const -> FloatVec4 { return FloatVec4(hn::Abs(m_data)); } + +auto FloatVec4::dot(Ref other) const -> f32 { + Const d; + Const> v_mul = hn::Mul(m_data, other.m_data); + return hn::ReduceSum(d, v_mul); +} + +auto FloatVec4::normalize() const -> FloatVec4 { + Const d; + Const> v_mul = hn::Mul(m_data, m_data); + Const> v_len_sq = hn::SumOfLanes(d, v_mul); + Const> v_inv_len = hn::ApproximateReciprocalSqrt(v_len_sq); + return FloatVec4(hn::Mul(m_data, v_inv_len)); +} + +auto FloatVec4::store(Mut values) -> void { + Const d; + hn::Store(m_data, d, values); +} + +auto FloatVec4::load(Const values) -> FloatVec4 { + Const d; + return FloatVec4(hn::Load(d, values)); +} +} // namespace IACore \ No newline at end of file diff --git a/Src/IACore/inc/IACore/SocketOps.hpp b/Src/IACore/inc/IACore/SocketOps.hpp new file mode 100644 index 0000000..440570f --- /dev/null +++ b/Src/IACore/inc/IACore/SocketOps.hpp @@ -0,0 +1,116 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2026 IAS (ias@iasoft.dev) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include + +#if IA_PLATFORM_WINDOWS +#include +#include +#include +#pragma comment(lib, "ws2_32.lib") +#elif IA_PLATFORM_UNIX +#include +#include +#include +#include +#ifndef INVALID_SOCKET +#define INVALID_SOCKET -1 +#endif +#else +#error "IACore SocketOps is not supported on this platform." +#endif + +namespace IACore { +#if IA_PLATFORM_WINDOWS +using SocketHandle = SOCKET; +#elif IA_PLATFORM_UNIX +using SocketHandle = i32; +#endif + +class SocketOps { +public: + // SocketOps correctly handles multiple calls to initialize and terminate. + // Make sure every initialize call is paired with a corresponding terminate + // call. + static auto initialize() -> Result { + s_init_count++; + if (s_init_count > 1) { + return {}; + } +#if IA_PLATFORM_WINDOWS + Mut wsa_data; + Const res = WSAStartup(MAKEWORD(2, 2), &wsa_data); + if (res != 0) { + s_init_count--; + return fail("WSAStartup failed with error: {}", res); + } +#endif + return {}; + } + + // SocketOps correctly handles multiple calls to initialize and terminate. + // Make sure every initialize call is paired with a corresponding terminate + // call. + static auto terminate() -> void { + s_init_count--; + if (s_init_count > 0) { + return; + } +#if IA_PLATFORM_WINDOWS + WSACleanup(); +#endif + } + + static auto is_initialized() -> bool { return s_init_count > 0; } + + static auto is_port_available_tcp(Const port) -> bool { + return is_port_available(port, SOCK_STREAM); + } + + static auto is_port_available_udp(Const port) -> bool { + return is_port_available(port, SOCK_DGRAM); + } + + static auto is_would_block() -> bool; + + static auto close(Const sock) -> void; + + static auto listen(Const sock, Const queue_size = 5) + -> Result; + + static auto create_unix_socket() -> Result; + + static auto bind_unix_socket(Const sock, + Const path) -> Result; + static auto connect_unix_socket(Const sock, + Const path) -> Result; + + static auto unlink_file(Const path) -> void { +#if IA_PLATFORM_WINDOWS + DeleteFileA(path); +#elif IA_PLATFORM_UNIX + unlink(path); +#endif + } + +private: + static auto is_port_available(Const port, Const type) -> bool; + +private: + static Mut s_init_count; +}; +} // namespace IACore \ No newline at end of file diff --git a/Src/IACore/inc/IACore/StreamReader.hpp b/Src/IACore/inc/IACore/StreamReader.hpp new file mode 100644 index 0000000..83543d6 --- /dev/null +++ b/Src/IACore/inc/IACore/StreamReader.hpp @@ -0,0 +1,106 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2026 IAS (ias@iasoft.dev) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include +#include +#include + +namespace IACore { +class StreamReader { +public: + enum class StorageType { + NonOwning, + OwningMmap, + OwningVector, + }; + + static auto create_from_file(Ref path) -> Result; + + explicit StreamReader(ForwardRef> data); + explicit StreamReader(Const> data); + ~StreamReader(); + + StreamReader(ForwardRef other); + auto operator=(ForwardRef other) -> MutRef; + + StreamReader(Ref) = delete; + auto operator=(Ref) -> MutRef = delete; + + auto read(Mut buffer, Const size) -> Result; + + template + [[nodiscard("Check for EOF")]] + auto read() -> Result; + + auto skip(Const amount) -> void { + m_cursor = std::min(m_cursor + amount, m_data_size); + } + + auto seek(Const pos) -> void { + m_cursor = (pos > m_data_size) ? m_data_size : pos; + } + + [[nodiscard]] auto cursor() const -> usize { return m_cursor; } + + [[nodiscard]] auto size() const -> usize { return m_data_size; } + + [[nodiscard]] auto remaining() const -> usize { + return m_data_size - m_cursor; + } + + [[nodiscard]] auto is_eof() const -> bool { return m_cursor >= m_data_size; } + +private: + Mut m_data = nullptr; + Mut m_cursor = 0; + Mut m_data_size = 0; + Mut> m_owning_vector; + Mut m_storage_type = StorageType::NonOwning; +}; + +inline auto StreamReader::read(Mut buffer, Const size) + -> Result { + if (m_cursor + size > m_data_size) [[unlikely]] { + return fail("Unexpected EOF while reading"); + } + + std::memcpy(buffer, &m_data[m_cursor], size); + m_cursor += size; + + return {}; +} + +template +[[nodiscard("Check for EOF")]] +inline auto StreamReader::read() -> Result { + static_assert(std::is_trivially_copyable_v, + "T must be trivially copyable to read via memcpy"); + + constexpr Const SIZE = sizeof(T); + + if (m_cursor + SIZE > m_data_size) [[unlikely]] { + return fail("Unexpected EOF while reading"); + } + + Mut value; + std::memcpy(&value, &m_data[m_cursor], SIZE); + m_cursor += SIZE; + + return value; +} + +} // namespace IACore \ No newline at end of file diff --git a/Src/IACore/inc/IACore/StreamWriter.hpp b/Src/IACore/inc/IACore/StreamWriter.hpp new file mode 100644 index 0000000..1c71d43 --- /dev/null +++ b/Src/IACore/inc/IACore/StreamWriter.hpp @@ -0,0 +1,71 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2026 IAS (ias@iasoft.dev) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include + +namespace IACore { + +class StreamWriter { +public: + enum class StorageType { + NonOwning, + OwningFile, + OwningVector, + }; + + static auto create_from_file(Ref path) -> Result; + + StreamWriter(); + explicit StreamWriter(Const> data); + + StreamWriter(ForwardRef other); + auto operator=(ForwardRef other) -> MutRef; + + StreamWriter(Ref) = delete; + auto operator=(Ref) -> MutRef = delete; + + ~StreamWriter(); + + auto write(Const byte, Const count) -> Result; + auto write(Const buffer, Const size) -> Result; + + template auto write(Ref value) -> Result; + + [[nodiscard]] auto data() const -> const u8 * { return m_buffer; } + + [[nodiscard]] auto cursor() const -> usize { return m_cursor; } + + auto flush() -> Result; + +private: + Mut m_buffer = nullptr; + Mut m_cursor = 0; + Mut m_capacity = 0; + Mut m_file_path; + Mut> m_owning_vector; + Mut m_storage_type = StorageType::OwningVector; + +private: + auto flush_to_disk() -> Result; +}; + +template +inline auto StreamWriter::write(Ref value) -> Result { + return write(&value, sizeof(T)); +} + +} // namespace IACore \ No newline at end of file diff --git a/Src/IACore/inc/IACore/StringOps.hpp b/Src/IACore/inc/IACore/StringOps.hpp new file mode 100644 index 0000000..be6094c --- /dev/null +++ b/Src/IACore/inc/IACore/StringOps.hpp @@ -0,0 +1,26 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2026 IAS (ias@iasoft.dev) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include + +namespace IACore { +class StringOps { +public: + static auto encode_base64(Const>> data) -> String; + static auto decode_base64(Ref data) -> Vec; +}; +} // namespace IACore \ No newline at end of file diff --git a/Src/IACore/inc/IACore/Utils.hpp b/Src/IACore/inc/IACore/Utils.hpp new file mode 100644 index 0000000..36e1431 --- /dev/null +++ b/Src/IACore/inc/IACore/Utils.hpp @@ -0,0 +1,107 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2026 IAS (ias@iasoft.dev) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include + +#include + +namespace IACore { +class Utils { +public: + static auto get_unix_time() -> u64; + + static auto get_ticks_count() -> u64; + + static auto get_seconds_count() -> f64; + + static auto get_random() -> f32; + static auto get_random(Const max) -> u64; + static auto get_random(Const min, Const max) -> i64; + + static auto sleep(Const milliseconds) -> void; + + static auto binary_to_hex_string(Const>> data) -> String; + + static auto hex_string_to_binary(Const hex) -> Result>; + + template + inline static auto sort(ForwardRef range) -> void { + std::ranges::sort(std::forward(range)); + } + + template + inline static auto binary_search_left(ForwardRef range, Ref value) + -> auto { + return std::ranges::lower_bound(std::forward(range), value); + } + + template + inline static auto binary_search_right(ForwardRef range, Ref value) + -> auto { + return std::ranges::upper_bound(std::forward(range), value); + } + + template + inline static auto hash_combine(MutRef seed, Ref v) -> void { + Mut h = 0; + + if constexpr (std::is_constructible_v) { + Const sv(v); + Const> hasher; + h = hasher(sv); + } else { + Const> hasher; + h = hasher(v); + } + + seed ^= h + 0x9e3779b97f4a7c15 + (seed << 6) + (seed >> 2); + } + + template + inline static auto compute_hash(Ref... args) -> u64 { + Mut seed = 0; + (hash_combine(seed, args), ...); + return seed; + } + + template + inline static auto compute_hash_flat(Ref obj, Const... members) + -> u64 { + Mut seed = 0; + (hash_combine(seed, obj.*members), ...); + return seed; + } +}; +} // namespace IACore + +// ----------------------------------------------------------------------------- +// MACRO: IA_MAKE_HASHABLE +// +// Injects the specialization for ankerl::unordered_dense::hash. +// +// Usage: +// struct Vector3 { float x, y, z; }; +// IA_MAKE_HASHABLE(Vector3, &Vector3::x, &Vector3::y, &Vector3::z) +// ----------------------------------------------------------------------------- +#define IA_MAKE_HASHABLE(Type, ...) \ + template <> struct ankerl::unordered_dense::hash { \ + using is_avalanching = void; \ + IA_NODISCARD \ + auto operator()(IACore::Ref v) const noexcept -> IACore::u64 { \ + return IACore::Utils::compute_hash_flat(v, __VA_ARGS__); \ + } \ + }; \ No newline at end of file diff --git a/Src/IACore/inc/IACore/XML.hpp b/Src/IACore/inc/IACore/XML.hpp new file mode 100644 index 0000000..09602f6 --- /dev/null +++ b/Src/IACore/inc/IACore/XML.hpp @@ -0,0 +1,39 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2026 IAS (ias@iasoft.dev) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include + +#include + +namespace IACore { +class XML { +public: + using Node = pugi::xml_node; + using Document = pugi::xml_document; + +public: + static auto parse_from_string(Ref data) -> Result; + static auto parse_from_file(Ref path) -> Result; + + static auto serialize_to_string(Ref node, Const escape = false) + -> String; + static auto serialize_to_string(Ref doc, Const escape = false) + -> String; + + static auto escape_xml_string(Ref xml) -> String; +}; +} // namespace IACore \ No newline at end of file diff --git a/Tests/CMakeLists.txt b/Tests/CMakeLists.txt new file mode 100644 index 0000000..a4bccdc --- /dev/null +++ b/Tests/CMakeLists.txt @@ -0,0 +1,4 @@ + +add_subdirectory(Subjects/) +add_subdirectory(Unit/) +add_subdirectory(Regression/) diff --git a/Tests/Regression/CMakeLists.txt b/Tests/Regression/CMakeLists.txt new file mode 100644 index 0000000..e69de29 diff --git a/Tests/Subjects/CMakeLists.txt b/Tests/Subjects/CMakeLists.txt new file mode 100644 index 0000000..6248438 --- /dev/null +++ b/Tests/Subjects/CMakeLists.txt @@ -0,0 +1 @@ +add_executable(LongProcess LongProcess/Main.cpp) diff --git a/Tests/Subjects/LongProcess/Main.cpp b/Tests/Subjects/LongProcess/Main.cpp new file mode 100644 index 0000000..803803b --- /dev/null +++ b/Tests/Subjects/LongProcess/Main.cpp @@ -0,0 +1,12 @@ +#include +#include + +int main(int, char **) +{ + std::cout << "Started!\n"; + std::cout.flush(); + std::this_thread::sleep_for(std::chrono::seconds(5)); + std::cout << "Ended!\n"; + std::cout.flush(); + return 100; +} \ No newline at end of file diff --git a/Tests/Unit/AsyncOps.cpp b/Tests/Unit/AsyncOps.cpp new file mode 100644 index 0000000..66c1b56 --- /dev/null +++ b/Tests/Unit/AsyncOps.cpp @@ -0,0 +1,160 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2026 IAS (ias@iasoft.dev) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +#include +#include + +using namespace IACore; + +struct SchedulerGuard { + SchedulerGuard(u8 worker_count = 2) { + + (void)AsyncOps::initialize_scheduler(worker_count); + } + + ~SchedulerGuard() { AsyncOps::terminate_scheduler(); } +}; + +IAT_BEGIN_BLOCK(Core, AsyncOps) + +auto test_initialization() -> bool { + + AsyncOps::terminate_scheduler(); + + const auto res = AsyncOps::initialize_scheduler(4); + IAT_CHECK(res.has_value()); + + IAT_CHECK_EQ(AsyncOps::get_worker_count(), static_cast(4)); + + AsyncOps::terminate_scheduler(); + + const auto res2 = AsyncOps::initialize_scheduler(1); + IAT_CHECK(res2.has_value()); + IAT_CHECK_EQ(AsyncOps::get_worker_count(), static_cast(1)); + + AsyncOps::terminate_scheduler(); + return true; +} + +auto test_basic_execution() -> bool { + SchedulerGuard guard(2); + + AsyncOps::Schedule schedule; + std::atomic run_count{0}; + + AsyncOps::schedule_task([&](AsyncOps::WorkerId) { run_count++; }, 0, + &schedule); + + AsyncOps::wait_for_schedule_completion(&schedule); + + IAT_CHECK_EQ(run_count.load(), 1); + + return true; +} + +auto test_concurrency() -> bool { + SchedulerGuard guard(4); + + AsyncOps::Schedule schedule; + std::atomic run_count{0}; + const i32 total_tasks = 100; + + for (i32 i = 0; i < total_tasks; ++i) { + AsyncOps::schedule_task( + [&](AsyncOps::WorkerId) { + std::this_thread::sleep_for(std::chrono::microseconds(10)); + run_count++; + }, + 0, &schedule); + } + + AsyncOps::wait_for_schedule_completion(&schedule); + + IAT_CHECK_EQ(run_count.load(), total_tasks); + + return true; +} + +auto test_priorities() -> bool { + SchedulerGuard guard(2); + AsyncOps::Schedule schedule; + std::atomic high_priority_ran{0}; + std::atomic normal_priority_ran{0}; + + AsyncOps::schedule_task([&](AsyncOps::WorkerId) { high_priority_ran++; }, 0, + &schedule, AsyncOps::Priority::High); + + AsyncOps::schedule_task([&](AsyncOps::WorkerId) { normal_priority_ran++; }, 0, + &schedule, AsyncOps::Priority::Normal); + + AsyncOps::wait_for_schedule_completion(&schedule); + + IAT_CHECK_EQ(high_priority_ran.load(), 1); + IAT_CHECK_EQ(normal_priority_ran.load(), 1); + + return true; +} + +auto test_run_task_fire_and_forget() -> bool { + SchedulerGuard guard(2); + + std::atomic executed{false}; + + AsyncOps::run_task([&]() { executed = true; }); + + for (int i = 0; i < 100; ++i) { + if (executed.load()) + break; + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + + IAT_CHECK(executed.load()); + + return true; +} + +auto test_cancellation_safety() -> bool { + SchedulerGuard guard(2); + + AsyncOps::cancel_tasks_of_tag(999); + + AsyncOps::Schedule schedule; + std::atomic counter{0}; + + AsyncOps::schedule_task([&](AsyncOps::WorkerId) { counter++; }, 10, + &schedule); + + AsyncOps::wait_for_schedule_completion(&schedule); + IAT_CHECK_EQ(counter.load(), 1); + + AsyncOps::cancel_tasks_of_tag(10); + + return true; +} + +IAT_BEGIN_TEST_LIST() +IAT_ADD_TEST(test_initialization); +IAT_ADD_TEST(test_basic_execution); +IAT_ADD_TEST(test_concurrency); +IAT_ADD_TEST(test_priorities); +IAT_ADD_TEST(test_run_task_fire_and_forget); +IAT_ADD_TEST(test_cancellation_safety); +IAT_END_TEST_LIST() + +IAT_END_BLOCK() + +IAT_REGISTER_ENTRY(Core, AsyncOps) \ No newline at end of file diff --git a/Tests/Unit/CLI.cpp b/Tests/Unit/CLI.cpp new file mode 100644 index 0000000..998f508 --- /dev/null +++ b/Tests/Unit/CLI.cpp @@ -0,0 +1,103 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2026 IAS (ias@iasoft.dev) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include + +using namespace IACore; + +IAT_BEGIN_BLOCK(Core, CLI) + +auto test_basic_traversal() -> bool { + const Vec args = {"ignored", "one", "two", "three"}; + CLIParser parser(args); + + IAT_CHECK(parser.remaining()); + + IAT_CHECK_EQ(String(parser.next()), "one"); + IAT_CHECK(parser.remaining()); + + IAT_CHECK_EQ(String(parser.next()), "two"); + IAT_CHECK(parser.remaining()); + + IAT_CHECK_EQ(String(parser.next()), "three"); + + IAT_CHECK_NOT(parser.remaining()); + + IAT_CHECK_EQ(String(parser.next()), ""); + + return true; +} + +auto test_peek() -> bool { + const Vec args = {"ignored", "peek_val", "next_val"}; + CLIParser parser(args); + + IAT_CHECK_EQ(String(parser.peek()), "peek_val"); + IAT_CHECK(parser.remaining()); + + IAT_CHECK_EQ(String(parser.next()), "peek_val"); + + IAT_CHECK_EQ(String(parser.peek()), "next_val"); + IAT_CHECK_EQ(String(parser.next()), "next_val"); + + IAT_CHECK_NOT(parser.remaining()); + + return true; +} + +auto test_consume() -> bool { + const Vec args = {"ignored", "-v", "--output", "file.txt"}; + CLIParser parser(args); + + IAT_CHECK_NOT(parser.consume("-x")); + + IAT_CHECK_EQ(String(parser.peek()), "-v"); + + IAT_CHECK(parser.consume("-v")); + + IAT_CHECK_EQ(String(parser.peek()), "--output"); + + IAT_CHECK(parser.consume("--output")); + + IAT_CHECK_EQ(String(parser.next()), "file.txt"); + + IAT_CHECK_NOT(parser.remaining()); + + return true; +} + +auto test_empty() -> bool { + const Vec args = {}; + CLIParser parser(args); + + IAT_CHECK_NOT(parser.remaining()); + IAT_CHECK_EQ(String(parser.peek()), ""); + IAT_CHECK_EQ(String(parser.next()), ""); + IAT_CHECK_NOT(parser.consume("-help")); + + return true; +} + +IAT_BEGIN_TEST_LIST() +IAT_ADD_TEST(test_basic_traversal); +IAT_ADD_TEST(test_peek); +IAT_ADD_TEST(test_consume); +IAT_ADD_TEST(test_empty); +IAT_END_TEST_LIST() + +IAT_END_BLOCK() + +IAT_REGISTER_ENTRY(Core, CLI) \ No newline at end of file diff --git a/Tests/Unit/CMakeLists.txt b/Tests/Unit/CMakeLists.txt new file mode 100644 index 0000000..1727f18 --- /dev/null +++ b/Tests/Unit/CMakeLists.txt @@ -0,0 +1,36 @@ +set(TEST_SOURCES + AsyncOps.cpp + CLI.cpp + DataOps.cpp + Environment.cpp + FileOps.cpp + IPC.cpp + JSON.cpp + Logger.cpp + Main.cpp + Platform.cpp + ProcessOps.cpp + RingBuffer.cpp + SocketOps.cpp + StreamReader.cpp + StreamWriter.cpp + StringOps.cpp + Utils.cpp + XML.cpp + + SIMD/IntVec4.cpp + SIMD/FloatVec4.cpp +) + +add_executable(IACore_Test_Suite ${TEST_SOURCES}) + +target_compile_options(IACore_Test_Suite PRIVATE -fexceptions) +set_target_properties(IACore_Test_Suite PROPERTIES USE_EXCEPTIONS ON) + +target_link_libraries(IACore_Test_Suite PRIVATE IACore) + +add_custom_command(TARGET IACore_Test_Suite POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + $ + $/LongProcess${CMAKE_EXECUTABLE_SUFFIX} +) \ No newline at end of file diff --git a/Tests/Unit/DataOps.cpp b/Tests/Unit/DataOps.cpp new file mode 100644 index 0000000..8236b7c --- /dev/null +++ b/Tests/Unit/DataOps.cpp @@ -0,0 +1,108 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2026 IAS (ias@iasoft.dev) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include + +using namespace IACore; + +IAT_BEGIN_BLOCK(Core, DataOps) + +auto test_crc32() -> bool { + { + const String s = "123456789"; + const Span span(reinterpret_cast(s.data()), s.size()); + const u32 result = DataOps::crc32(span); + + IAT_CHECK_EQ(result, 0xE3069283); + } + + { + const u32 result = DataOps::crc32({}); + IAT_CHECK_EQ(result, 0U); + } + + { + Vec buffer(33); + for (usize i = 1; i < 33; ++i) { + buffer[i] = static_cast(i); + } + + Vec ref_data(32); + for (usize i = 0; i < 32; ++i) { + ref_data[i] = static_cast(i + 1); + } + + const u32 hash_ref = + DataOps::crc32(Span(ref_data.data(), ref_data.size())); + + const u32 hash_unaligned = + DataOps::crc32(Span(buffer.data() + 1, 32)); + + IAT_CHECK_EQ(hash_ref, hash_unaligned); + } + + return true; +} + +auto test_hash_xxhash() -> bool { + { + const String s = "123456789"; + const u32 result = DataOps::hash_xxhash(s); + IAT_CHECK_EQ(result, 0x937bad67); + } + + { + const String s = "The quick brown fox jumps over the lazy dog"; + const u32 result = DataOps::hash_xxhash(s); + IAT_CHECK_EQ(result, 0xE85EA4DE); + } + + { + const String s = "Test"; + const u32 r1 = DataOps::hash_xxhash(s); + const u32 r2 = DataOps::hash_xxhash( + Span(reinterpret_cast(s.data()), s.size())); + IAT_CHECK_EQ(r1, r2); + } + + return true; +} + +auto test_hash_fnv1a() -> bool { + { + const String s = "123456789"; + const u32 result = DataOps::hash_fnv1a( + Span(reinterpret_cast(s.data()), s.size())); + IAT_CHECK_EQ(result, 0xbb86b11c); + } + + { + const u32 result = DataOps::hash_fnv1a(Span{}); + IAT_CHECK_EQ(result, 0x811C9DC5); + } + + return true; +} + +IAT_BEGIN_TEST_LIST() +IAT_ADD_TEST(test_crc32); +IAT_ADD_TEST(test_hash_fnv1a); +IAT_ADD_TEST(test_hash_xxhash); +IAT_END_TEST_LIST() + +IAT_END_BLOCK() + +IAT_REGISTER_ENTRY(Core, DataOps) \ No newline at end of file diff --git a/Tests/Unit/Environment.cpp b/Tests/Unit/Environment.cpp new file mode 100644 index 0000000..9c5dc5b --- /dev/null +++ b/Tests/Unit/Environment.cpp @@ -0,0 +1,127 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2026 IAS (ias@iasoft.dev) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include + +using namespace IACore; + +static constexpr const char *TEST_KEY = "IA_TEST_ENV_VAR_12345"; +static constexpr const char *TEST_VAL = "Hello World"; + +IAT_BEGIN_BLOCK(Core, Environment) + +auto test_basic_cycle() -> bool { + + (void)Environment::unset(TEST_KEY); + IAT_CHECK_NOT(Environment::exists(TEST_KEY)); + + const auto set_res = Environment::set(TEST_KEY, TEST_VAL); + IAT_CHECK(set_res.has_value()); + IAT_CHECK(Environment::exists(TEST_KEY)); + + const auto opt = Environment::find(TEST_KEY); + IAT_CHECK(opt.has_value()); + IAT_CHECK_EQ(*opt, String(TEST_VAL)); + + const String val = Environment::get(TEST_KEY); + IAT_CHECK_EQ(val, String(TEST_VAL)); + + (void)Environment::unset(TEST_KEY); + return true; +} + +auto test_overwrite() -> bool { + (void)Environment::set(TEST_KEY, "ValueA"); + IAT_CHECK_EQ(Environment::get(TEST_KEY), String("ValueA")); + + (void)Environment::set(TEST_KEY, "ValueB"); + IAT_CHECK_EQ(Environment::get(TEST_KEY), String("ValueB")); + + (void)Environment::unset(TEST_KEY); + return true; +} + +auto test_unset() -> bool { + (void)Environment::set(TEST_KEY, "To Be Deleted"); + IAT_CHECK(Environment::exists(TEST_KEY)); + + const auto unset_res = Environment::unset(TEST_KEY); + IAT_CHECK(unset_res.has_value()); + + IAT_CHECK_NOT(Environment::exists(TEST_KEY)); + + const auto opt = Environment::find(TEST_KEY); + IAT_CHECK_NOT(opt.has_value()); + + return true; +} + +auto test_defaults() -> bool { + const char *ghost_key = "IA_THIS_KEY_DOES_NOT_EXIST"; + + (void)Environment::unset(ghost_key); + + const String empty = Environment::get(ghost_key); + IAT_CHECK(empty.empty()); + + const String fallback = Environment::get(ghost_key, "MyDefault"); + IAT_CHECK_EQ(fallback, String("MyDefault")); + + return true; +} + +auto test_empty_value() -> bool { + (void)Environment::set(TEST_KEY, ""); + +#if IA_PLATFORM_WINDOWS + +#else + + IAT_CHECK(Environment::exists(TEST_KEY)); + const auto opt = Environment::find(TEST_KEY); + IAT_CHECK(opt.has_value()); + IAT_CHECK(opt->empty()); +#endif + + (void)Environment::unset(TEST_KEY); + IAT_CHECK_NOT(Environment::exists(TEST_KEY)); + + return true; +} + +auto test_bad_input() -> bool { + + const auto res = Environment::set("", "Value"); + IAT_CHECK_NOT(res.has_value()); + + const auto res_unset = Environment::unset(""); + IAT_CHECK_NOT(res_unset.has_value()); + + return true; +} + +IAT_BEGIN_TEST_LIST() +IAT_ADD_TEST(test_basic_cycle); +IAT_ADD_TEST(test_overwrite); +IAT_ADD_TEST(test_unset); +IAT_ADD_TEST(test_defaults); +IAT_ADD_TEST(test_empty_value); +IAT_ADD_TEST(test_bad_input); +IAT_END_TEST_LIST() + +IAT_END_BLOCK() + +IAT_REGISTER_ENTRY(Core, Environment) \ No newline at end of file diff --git a/Tests/Unit/FileOps.cpp b/Tests/Unit/FileOps.cpp new file mode 100644 index 0000000..1ff3ae5 --- /dev/null +++ b/Tests/Unit/FileOps.cpp @@ -0,0 +1,156 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2026 IAS (ias@iasoft.dev) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include + +using namespace IACore; + +IAT_BEGIN_BLOCK(Core, FileOps) + +void cleanup_file(const Path &path) { + std::error_code ec; + if (std::filesystem::exists(path, ec)) { + std::filesystem::remove(path, ec); + } +} + +auto test_text_io() -> bool { + const Path path = "iatest_fileops_text.txt"; + const String content = "Hello IACore FileOps!\nLine 2"; + + const auto write_res = FileOps::write_text_file(path, content, true); + IAT_CHECK(write_res.has_value()); + IAT_CHECK_EQ(*write_res, content.size()); + + const auto read_res = FileOps::read_text_file(path); + IAT_CHECK(read_res.has_value()); + IAT_CHECK_EQ(*read_res, content); + + cleanup_file(path); + return true; +} + +auto test_binary_io() -> bool { + const Path path = "iatest_fileops_bin.bin"; + const Vec content = {0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0xFF}; + + const auto write_res = FileOps::write_binary_file(path, content, true); + IAT_CHECK(write_res.has_value()); + IAT_CHECK_EQ(*write_res, content.size()); + + const auto read_res = FileOps::read_binary_file(path); + IAT_CHECK(read_res.has_value()); + IAT_CHECK_EQ(read_res->size(), content.size()); + + for (usize i = 0; i < content.size(); ++i) { + IAT_CHECK_EQ((*read_res)[i], content[i]); + } + + cleanup_file(path); + return true; +} + +auto test_file_mapping() -> bool { + const Path path = "iatest_fileops_map.txt"; + const String content = "MappedContent"; + + (void)FileOps::write_text_file(path, content, true); + + usize size = 0; + const auto map_res = FileOps::map_file(path, size); + IAT_CHECK(map_res.has_value()); + IAT_CHECK_EQ(size, content.size()); + + const u8 *ptr = *map_res; + IAT_CHECK(ptr != nullptr); + + String read_back(reinterpret_cast(ptr), size); + IAT_CHECK_EQ(read_back, content); + + FileOps::unmap_file(ptr); + + cleanup_file(path); + return true; +} + +auto test_shared_memory() -> bool { + const String shm_name = "iatest_shm_block"; + const usize shm_size = 4096; + + auto owner_res = FileOps::map_shared_memory(shm_name, shm_size, true); + IAT_CHECK(owner_res.has_value()); + u8 *owner_ptr = *owner_res; + + std::memset(owner_ptr, 0, shm_size); + const String msg = "Shared Memory Message"; + std::memcpy(owner_ptr, msg.data(), msg.size()); + + auto client_res = FileOps::map_shared_memory(shm_name, shm_size, false); + IAT_CHECK(client_res.has_value()); + u8 *client_ptr = *client_res; + + String read_msg(reinterpret_cast(client_ptr), msg.size()); + IAT_CHECK_EQ(read_msg, msg); + + FileOps::unmap_file(owner_ptr); + FileOps::unmap_file(client_ptr); + FileOps::unlink_shared_memory(shm_name); + + return true; +} + +auto test_stream_integration() -> bool { + const Path path = "iatest_fileops_stream.bin"; + cleanup_file(path); + + { + auto writer_res = FileOps::stream_to_file(path, true); + IAT_CHECK(writer_res.has_value()); + auto &writer = *writer_res; + + (void)writer.write(0x12345678); + (void)writer.write(0xFF); + } + + { + auto reader_res = FileOps::stream_from_file(path); + IAT_CHECK(reader_res.has_value()); + auto &reader = *reader_res; + + auto val_u32 = reader.read(); + IAT_CHECK(val_u32.has_value()); + IAT_CHECK_EQ(*val_u32, 0x12345678u); + + auto val_u8 = reader.read(); + IAT_CHECK(val_u8.has_value()); + IAT_CHECK_EQ(*val_u8, 0xFF); + } + + cleanup_file(path); + return true; +} + +IAT_BEGIN_TEST_LIST() +IAT_ADD_TEST(test_text_io); +IAT_ADD_TEST(test_binary_io); +IAT_ADD_TEST(test_file_mapping); +IAT_ADD_TEST(test_shared_memory); +IAT_ADD_TEST(test_stream_integration); +IAT_END_TEST_LIST() + +IAT_END_BLOCK() + +IAT_REGISTER_ENTRY(Core, FileOps) \ No newline at end of file diff --git a/Tests/Unit/IPC.cpp b/Tests/Unit/IPC.cpp new file mode 100644 index 0000000..1cad067 --- /dev/null +++ b/Tests/Unit/IPC.cpp @@ -0,0 +1,130 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2026 IAS (ias@iasoft.dev) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +#include + +using namespace IACore; + +IAT_BEGIN_BLOCK(Core, IPC) + +auto test_layout_constraints() -> bool { + + IAT_CHECK_EQ(alignof(IpcSharedMemoryLayout), static_cast(64)); + + IAT_CHECK_EQ(offsetof(IpcSharedMemoryLayout, meta), static_cast(0)); + + IAT_CHECK_EQ(offsetof(IpcSharedMemoryLayout, moni_control), + static_cast(64)); + + IAT_CHECK_EQ(offsetof(IpcSharedMemoryLayout, mino_control), + static_cast(192)); + + IAT_CHECK_EQ(offsetof(IpcSharedMemoryLayout, moni_data_offset), + static_cast(320)); + + IAT_CHECK_EQ(sizeof(IpcSharedMemoryLayout) % 64, static_cast(0)); + + return true; +} + +auto test_manual_shm_ringbuffer() -> bool { + const String shm_name = "IA_TEST_IPC_LAYOUT_CHECK"; + const usize shm_size = 16 * 1024; + + FileOps::unlink_shared_memory(shm_name); + + auto map_res = FileOps::map_shared_memory(shm_name, shm_size, true); + IAT_CHECK(map_res.has_value()); + + u8 *base_ptr = *map_res; + auto *layout = reinterpret_cast(base_ptr); + + layout->meta.magic = 0xDEADBEEF; + layout->meta.version = 1; + layout->meta.total_size = shm_size; + + const usize header_size = IpcSharedMemoryLayout::get_header_size(); + const usize data_available = shm_size - header_size; + const usize half_data = data_available / 2; + + layout->moni_data_offset = header_size; + layout->moni_data_size = half_data; + + layout->mino_data_offset = header_size + half_data; + layout->mino_data_size = half_data; + + Span moni_data_span(base_ptr + layout->moni_data_offset, + static_cast(layout->moni_data_size)); + auto moni_res = + RingBufferView::create(&layout->moni_control, moni_data_span, true); + IAT_CHECK(moni_res.has_value()); + auto moni = std::move(*moni_res); + + Span mino_data_span(base_ptr + layout->mino_data_offset, + static_cast(layout->mino_data_size)); + auto mino_res = + RingBufferView::create(&layout->mino_control, mino_data_span, true); + IAT_CHECK(mino_res.has_value()); + auto _ = std::move(*mino_res); + + String msg = "IPC_TEST_MESSAGE"; + IAT_CHECK( + moni.push(100, Span(reinterpret_cast(msg.data()), + msg.size())) + .has_value()); + + auto moni_reader_res = + RingBufferView::create(&layout->moni_control, moni_data_span, false); + IAT_CHECK(moni_reader_res.has_value()); + auto moni_reader = std::move(*moni_reader_res); + + RingBufferView::PacketHeader header; + u8 buffer[128]; + auto pop_res = moni_reader.pop(header, Span(buffer, 128)); + IAT_CHECK(pop_res.has_value()); + IAT_CHECK(pop_res->has_value()); + IAT_CHECK_EQ(header.id, static_cast(100)); + + String received((char *)buffer, *pop_res.value()); + IAT_CHECK_EQ(received, msg); + + FileOps::unmap_file(base_ptr); + FileOps::unlink_shared_memory(shm_name); + + return true; +} + +class TestManager : public IpcManager { +public: + void on_signal(NativeProcessID, u8) override {} + void on_packet(NativeProcessID, u16, Span) override {} +}; + +auto test_manager_instantiation() -> bool { + TestManager mgr; + return true; +} + +IAT_BEGIN_TEST_LIST() +IAT_ADD_TEST(test_layout_constraints); +IAT_ADD_TEST(test_manual_shm_ringbuffer); +IAT_ADD_TEST(test_manager_instantiation); +IAT_END_TEST_LIST() + +IAT_END_BLOCK() + +IAT_REGISTER_ENTRY(Core, IPC) \ No newline at end of file diff --git a/Tests/Unit/JSON.cpp b/Tests/Unit/JSON.cpp new file mode 100644 index 0000000..3c1cd9b --- /dev/null +++ b/Tests/Unit/JSON.cpp @@ -0,0 +1,164 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2026 IAS (ias@iasoft.dev) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include + +using namespace IACore; + +struct UserProfile { + String username; + u32 id; + bool is_active; + Vec roles; + + bool operator==(const UserProfile &other) const { + return username == other.username && id == other.id && + is_active == other.is_active && roles == other.roles; + } +}; + +IAT_BEGIN_BLOCK(Core, JSON) + +auto test_dynamic_parse() -> bool { + const String json_text = R"({ + "string": "Hello World", + "int": 42, + "float": 3.14159, + "bool": true, + "array": [10, 20, 30], + "object": { "key": "value" } + })"; + + auto res = Json::parse(json_text); + IAT_CHECK(res.has_value()); + + const auto &j = *res; + + IAT_CHECK(j["string"].is_string()); + IAT_CHECK_EQ(j["string"].get(), String("Hello World")); + + IAT_CHECK(j["int"].is_number_integer()); + IAT_CHECK_EQ(j["int"].get(), 42); + + IAT_CHECK(j["float"].is_number_float()); + IAT_CHECK_APPROX(j["float"].get(), 3.14159f); + + IAT_CHECK(j["bool"].is_boolean()); + IAT_CHECK_EQ(j["bool"].get(), true); + + IAT_CHECK(j["array"].is_array()); + IAT_CHECK_EQ(j["array"].size(), 3u); + IAT_CHECK_EQ(j["array"][0].get(), 10); + + IAT_CHECK(j["object"].is_object()); + IAT_CHECK_EQ(j["object"]["key"].get(), String("value")); + + return true; +} + +auto test_dynamic_encode() -> bool { + nlohmann::json j; + j["name"] = "IACore"; + j["version"] = 2; + + const String encoded = Json::encode(j); + + IAT_CHECK(encoded.find("IACore") != String::npos); + IAT_CHECK(encoded.find("version") != String::npos); + IAT_CHECK(encoded.find("2") != String::npos); + + return true; +} + +auto test_parse_invalid() -> bool { + const String bad_json = "{ key: value }"; + auto res = Json::parse(bad_json); + IAT_CHECK_NOT(res.has_value()); + return true; +} + +auto test_struct_round_trip() -> bool { + UserProfile original{.username = "test_user", + .id = 12345, + .is_active = true, + .roles = {"admin", "editor"}}; + + auto encode_res = Json::encode_struct(original); + IAT_CHECK(encode_res.has_value()); + String json_str = *encode_res; + + IAT_CHECK(json_str.find("test_user") != String::npos); + IAT_CHECK(json_str.find("roles") != String::npos); + + auto decode_res = Json::parse_to_struct(json_str); + IAT_CHECK(decode_res.has_value()); + + UserProfile decoded = *decode_res; + IAT_CHECK(decoded == original); + + return true; +} + +auto test_struct_parse_error() -> bool { + const String malformed = "{ broken_json: "; + auto res = Json::parse_to_struct(malformed); + IAT_CHECK_NOT(res.has_value()); + return true; +} + +auto test_read_only() -> bool { + const String json_text = R"({ + "id": 999, + "name": "Simd", + "scores": [1.1, 2.2] + })"; + + auto res = Json::parse_read_only(json_text); + IAT_CHECK(res.has_value()); + + auto &doc = *res; + simdjson::dom::element root = doc.root(); + + u64 id = 0; + auto err_id = root["id"].get(id); + IAT_CHECK(!err_id); + IAT_CHECK_EQ(id, 999ULL); + + std::string_view name; + auto err_name = root["name"].get(name); + IAT_CHECK(!err_name); + IAT_CHECK_EQ(String(name), String("Simd")); + + simdjson::dom::array scores; + auto err_arr = root["scores"].get(scores); + IAT_CHECK(!err_arr); + IAT_CHECK_EQ(scores.size(), 2u); + + return true; +} + +IAT_BEGIN_TEST_LIST() +IAT_ADD_TEST(test_dynamic_parse); +IAT_ADD_TEST(test_dynamic_encode); +IAT_ADD_TEST(test_parse_invalid); +IAT_ADD_TEST(test_struct_round_trip); +IAT_ADD_TEST(test_struct_parse_error); +IAT_ADD_TEST(test_read_only); +IAT_END_TEST_LIST() + +IAT_END_BLOCK() + +IAT_REGISTER_ENTRY(Core, JSON) \ No newline at end of file diff --git a/Tests/Unit/Logger.cpp b/Tests/Unit/Logger.cpp new file mode 100644 index 0000000..c1ca899 --- /dev/null +++ b/Tests/Unit/Logger.cpp @@ -0,0 +1,118 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2026 IAS (ias@iasoft.dev) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +#include + +using namespace IACore; + +IAT_BEGIN_BLOCK(Core, Logger) + +static constexpr const char *LOG_FILE = "iacore_test_log.txt"; + +auto test_file_logging() -> bool { + + const auto res = Logger::enable_logging_to_disk(LOG_FILE); + IAT_CHECK(res.has_value()); + + Logger::set_log_level(Logger::LogLevel::Trace); + + const String msg_info = "Test_Info_Msg_123"; + const String msg_err = "Test_Error_Msg_456"; + const String msg_warn = "Test_Warn_Msg_789"; + + Logger::info("{}", msg_info); + Logger::error("{}", msg_err); + Logger::warn("{}", msg_warn); + + Logger::flush_logs(); + + auto read_res = FileOps::read_text_file(LOG_FILE); + if (!read_res) { + std::cout << console::YELLOW << " Warning: Could not read log file (" + << read_res.error() << "). Skipping verification.\n" + << console::RESET; + return true; + } + + const String content = *read_res; + + IAT_CHECK(content.find(msg_info) != String::npos); + IAT_CHECK(content.find(msg_err) != String::npos); + IAT_CHECK(content.find(msg_warn) != String::npos); + + IAT_CHECK(content.find("INFO") != String::npos); + IAT_CHECK(content.find("ERROR") != String::npos); + IAT_CHECK(content.find("WARN") != String::npos); + + return true; +} + +auto test_log_levels() -> bool { + + Logger::set_log_level(Logger::LogLevel::Warn); + + const String unique_info = "Hidden_Info_Msg"; + const String unique_warn = "Visible_Warn_Msg"; + + Logger::info("{}", unique_info); + Logger::warn("{}", unique_warn); + + Logger::flush_logs(); + + auto read_res = FileOps::read_text_file(LOG_FILE); + if (!read_res) { + return true; + } + + const String content = *read_res; + + IAT_CHECK(content.find(unique_info) == String::npos); + + IAT_CHECK(content.find(unique_warn) != String::npos); + + return true; +} + +auto test_formatting() -> bool { + Logger::set_log_level(Logger::LogLevel::Info); + + const String name = "IACore"; + const i32 version = 99; + + Logger::info("System {} online v{}", name, version); + Logger::flush_logs(); + + auto read_res = FileOps::read_text_file(LOG_FILE); + if (!read_res) { + return true; + } + + const String content = *read_res; + IAT_CHECK(content.find("System IACore online v99") != String::npos); + + return true; +} + +IAT_BEGIN_TEST_LIST() +IAT_ADD_TEST(test_file_logging); +IAT_ADD_TEST(test_log_levels); +IAT_ADD_TEST(test_formatting); +IAT_END_TEST_LIST() + +IAT_END_BLOCK() + +IAT_REGISTER_ENTRY(Core, Logger) \ No newline at end of file diff --git a/Tests/Unit/Main.cpp b/Tests/Unit/Main.cpp new file mode 100644 index 0000000..38eb0b9 --- /dev/null +++ b/Tests/Unit/Main.cpp @@ -0,0 +1,41 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2026 IAS (ias@iasoft.dev) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include + +#include + +using namespace IACore; + +IACORE_MAIN() { + (void)args; + + OX_TRY_PURE(SocketOps::initialize()); + + std::cout + << console::GREEN + << "\n===============================================================\n"; + std::cout << " IACore (Independent Architecture Core) - Unit Test Suite\n"; + std::cout + << "===============================================================\n" + << console::RESET << "\n"; + + Const result = Test::TestRegistry::run_all(); + + SocketOps::terminate(); + + return result; +} \ No newline at end of file diff --git a/Tests/Unit/Platform.cpp b/Tests/Unit/Platform.cpp new file mode 100644 index 0000000..0379a0f --- /dev/null +++ b/Tests/Unit/Platform.cpp @@ -0,0 +1,119 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2026 IAS (ias@iasoft.dev) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +#include + +using namespace IACore; + +IAT_BEGIN_BLOCK(Core, Platform) + +auto test_os_name() -> bool { + const char *os_name = Platform::get_operating_system_name(); + IAT_CHECK(os_name != nullptr); + + const String os(os_name); + IAT_CHECK(!os.empty()); + +#if IA_PLATFORM_WINDOWS + IAT_CHECK_EQ(os, String("Windows")); +#elif IA_PLATFORM_LINUX + IAT_CHECK_EQ(os, String("Linux")); +#elif IA_PLATFORM_APPLE + IAT_CHECK_EQ(os, String("MacOS")); +#elif IA_PLATFORM_WASM + IAT_CHECK_EQ(os, String("WebAssembly")); +#endif + + return true; +} + +auto test_arch_name() -> bool { + const char *arch_name = Platform::get_architecture_name(); + IAT_CHECK(arch_name != nullptr); + + const String arch(arch_name); + IAT_CHECK(!arch.empty()); + +#if IA_ARCH_X64 + IAT_CHECK_EQ(arch, String("x86_64")); +#elif IA_ARCH_ARM64 + IAT_CHECK_EQ(arch, String("ARM64")); +#elif IA_ARCH_WASM + IAT_CHECK_EQ(arch, String("WASM")); +#endif + + return true; +} + +auto test_capabilities() -> bool { + + const bool check_result = Platform::check_cpu(); + IAT_CHECK(check_result); + + const auto &caps = Platform::get_capabilities(); + + volatile bool has_crc = caps.hardware_crc32; + (void)has_crc; + + return true; +} + +#if IA_ARCH_X64 +auto test_cpuid() -> bool { + i32 regs[4] = {0}; + + Platform::cpuid(0, 0, regs); + + IAT_CHECK(regs[0] >= 0); + + char vendor[13]; + std::memset(vendor, 0, 13); + + std::memcpy(vendor, ®s[1], 4); + std::memcpy(vendor + 4, ®s[3], 4); + std::memcpy(vendor + 8, ®s[2], 4); + vendor[12] = '\0'; + + const String vendor_str(vendor); + IAT_CHECK(!vendor_str.empty()); + + bool is_known = + (vendor_str == "GenuineIntel" || vendor_str == "AuthenticAMD" || + vendor_str == "KVMKVMKVM" || vendor_str == "Microsoft Hv" || + vendor_str == "VBoxVBoxVBox"); + + if (!is_known) { + + std::cout << " [Info] Unknown CPU Vendor: " << vendor_str << "\n"; + } + + return true; +} +#endif + +IAT_BEGIN_TEST_LIST() +IAT_ADD_TEST(test_os_name); +IAT_ADD_TEST(test_arch_name); +IAT_ADD_TEST(test_capabilities); +#if IA_ARCH_X64 +IAT_ADD_TEST(test_cpuid); +#endif +IAT_END_TEST_LIST() + +IAT_END_BLOCK() + +IAT_REGISTER_ENTRY(Core, Platform) \ No newline at end of file diff --git a/Tests/Unit/ProcessOps.cpp b/Tests/Unit/ProcessOps.cpp new file mode 100644 index 0000000..d3206a7 --- /dev/null +++ b/Tests/Unit/ProcessOps.cpp @@ -0,0 +1,213 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2026 IAS (ias@iasoft.dev) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include + +using namespace IACore; + +#if IA_PLATFORM_WINDOWS +#define CMD_ECHO_EXE "cmd.exe" +#define CMD_ARG_PREFIX "/c echo" +#define NULL_DEVICE "NUL" +#else +#define CMD_ECHO_EXE "/bin/echo" +#define CMD_ARG_PREFIX "" +#define NULL_DEVICE "/dev/null" +#endif + +IAT_BEGIN_BLOCK(Core, ProcessOps) + +auto test_basic_run() -> bool { + + String captured; + + const auto result = + ProcessOps::spawn_process_sync(CMD_ECHO_EXE, CMD_ARG_PREFIX " HelloIA", + [&](StringView line) { captured = line; }); + + IAT_CHECK(result.has_value()); + IAT_CHECK_EQ(*result, 0); + + IAT_CHECK(captured.find("HelloIA") != String::npos); + + return true; +} + +auto test_arguments() -> bool { + Vec lines; + + String args = String(CMD_ARG_PREFIX) + " one two"; + if (!args.empty() && args[0] == ' ') { + args.erase(0, 1); + } + + const auto result = + ProcessOps::spawn_process_sync(CMD_ECHO_EXE, args, [&](StringView line) { + lines.push_back(String(line)); + }); + + IAT_CHECK_EQ(*result, 0); + IAT_CHECK(lines.size() > 0); + + IAT_CHECK(lines[0].find("one two") != String::npos); + + return true; +} + +auto test_exit_codes() -> bool { + + String cmd; + String arg; + +#if IA_PLATFORM_WINDOWS + cmd = "cmd.exe"; + arg = "/c exit 42"; +#else + cmd = "/bin/sh"; + arg = "-c \"exit 42\""; +#endif + + const auto result = + ProcessOps::spawn_process_sync(cmd, arg, [](StringView) {}); + + IAT_CHECK(result.has_value()); + IAT_CHECK_EQ(*result, 42); + + return true; +} + +auto test_missing_exe() -> bool { + + const auto result = + ProcessOps::spawn_process_sync("sdflkjghsdflkjg", "", [](StringView) {}); + +#if IA_PLATFORM_WINDOWS + IAT_CHECK_NOT(result.has_value()); +#else + + IAT_CHECK(result.has_value()); + IAT_CHECK_EQ(*result, 127); +#endif + + return true; +} + +auto test_large_output() -> bool { + + String massive_string; + massive_string.reserve(5000); + for (i32 i = 0; i < 500; ++i) { + massive_string += "1234567890"; + } + + String cmd; + String arg; + +#if IA_PLATFORM_WINDOWS + cmd = "cmd.exe"; + + arg = "/c echo " + massive_string; +#else + cmd = "/bin/echo"; + arg = massive_string; +#endif + + String captured; + const auto result = ProcessOps::spawn_process_sync( + cmd, arg, [&](StringView line) { captured += line; }); + + IAT_CHECK(result.has_value()); + IAT_CHECK_EQ(*result, 0); + + IAT_CHECK_EQ(captured.length(), massive_string.length()); + + return true; +} + +auto test_multi_line() -> bool { + + String cmd; + String arg; +#if IA_PLATFORM_WINDOWS + cmd = "cmd.exe"; + arg = "/c \"echo LineA && echo LineB\""; +#else + cmd = "/bin/sh"; + arg = "-c \"echo LineA; echo LineB\""; +#endif + + i32 line_count = 0; + bool found_a = false; + bool found_b = false; + + const auto res = + ProcessOps::spawn_process_sync(cmd, arg, [&](StringView line) { + line_count++; + if (line.find("LineA") != String::npos) { + found_a = true; + } + if (line.find("LineB") != String::npos) { + found_b = true; + } + }); + IAT_CHECK(res.has_value()); + + IAT_CHECK(found_a); + IAT_CHECK(found_b); + + IAT_CHECK(line_count >= 2); + + return true; +} + +auto test_complex_arguments() -> bool { + + const String complex_args = + "-DDEFINED_MSG=\\\"Hello World\\\" -v path/to/file"; + + const String cmd = CMD_ECHO_EXE; + + String final_args; +#if IA_PLATFORM_WINDOWS + final_args = "/c echo " + complex_args; +#else + final_args = complex_args; +#endif + + String captured; + const auto result = ProcessOps::spawn_process_sync( + cmd, final_args, [&](StringView line) { captured += line; }); + + IAT_CHECK(result.has_value()); + IAT_CHECK_EQ(*result, 0); + + IAT_CHECK(captured.find("Hello World") != String::npos); + return true; +} + +IAT_BEGIN_TEST_LIST() +IAT_ADD_TEST(test_basic_run); +IAT_ADD_TEST(test_arguments); +IAT_ADD_TEST(test_exit_codes); +IAT_ADD_TEST(test_missing_exe); +IAT_ADD_TEST(test_large_output); +IAT_ADD_TEST(test_multi_line); +IAT_ADD_TEST(test_complex_arguments); +IAT_END_TEST_LIST() + +IAT_END_BLOCK() + +IAT_REGISTER_ENTRY(Core, ProcessOps) \ No newline at end of file diff --git a/Tests/Unit/RingBuffer.cpp b/Tests/Unit/RingBuffer.cpp new file mode 100644 index 0000000..698d52c --- /dev/null +++ b/Tests/Unit/RingBuffer.cpp @@ -0,0 +1,107 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2026 IAS (ias@iasoft.dev) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include + +using namespace IACore; + +IAT_BEGIN_BLOCK(Core, RingBuffer) + +auto test_push_pop() -> bool { + + Vec memory(sizeof(RingBufferView::ControlBlock) + 1024); + + auto producer_res = RingBufferView::create(Span(memory), true); + IAT_CHECK(producer_res.has_value()); + auto producer = std::move(*producer_res); + + auto consumer_res = RingBufferView::create(Span(memory), false); + IAT_CHECK(consumer_res.has_value()); + auto consumer = std::move(*consumer_res); + + String msg = "Hello RingBuffer"; + const auto push_res = producer.push( + 1, Span(reinterpret_cast(msg.data()), msg.size())); + IAT_CHECK(push_res.has_value()); + + RingBufferView::PacketHeader header; + u8 read_buf[128]; + + const auto pop_res = consumer.pop(header, Span(read_buf, 128)); + IAT_CHECK(pop_res.has_value()); + + const auto bytes_read_opt = *pop_res; + IAT_CHECK(bytes_read_opt.has_value()); + + const usize bytes_read = *bytes_read_opt; + + IAT_CHECK_EQ(header.id, static_cast(1)); + IAT_CHECK_EQ(bytes_read, static_cast(msg.size())); + + String read_msg(reinterpret_cast(read_buf), bytes_read); + IAT_CHECK_EQ(read_msg, msg); + + return true; +} + +auto test_wrap_around() -> bool { + + Vec memory(sizeof(RingBufferView::ControlBlock) + 100); + + auto rb_res = RingBufferView::create(Span(memory), true); + IAT_CHECK(rb_res.has_value()); + auto rb = std::move(*rb_res); + + Vec junk(80, 0xFF); + const auto push1 = rb.push(1, junk); + IAT_CHECK(push1.has_value()); + + RingBufferView::PacketHeader header; + u8 out_buf[100]; + const auto pop1 = rb.pop(header, out_buf); + IAT_CHECK(pop1.has_value()); + IAT_CHECK(pop1->has_value()); + + Vec wrap_data(40, 0xAA); + const auto push2 = rb.push(2, wrap_data); + IAT_CHECK(push2.has_value()); + + const auto pop2 = rb.pop(header, out_buf); + IAT_CHECK(pop2.has_value()); + IAT_CHECK(pop2->has_value()); + + const usize pop_size = *pop2.value(); + IAT_CHECK_EQ(pop_size, static_cast(40)); + + bool match = true; + for (usize i = 0; i < 40; i++) { + if (out_buf[i] != 0xAA) { + match = false; + } + } + IAT_CHECK(match); + + return true; +} + +IAT_BEGIN_TEST_LIST() +IAT_ADD_TEST(test_push_pop); +IAT_ADD_TEST(test_wrap_around); +IAT_END_TEST_LIST() + +IAT_END_BLOCK() + +IAT_REGISTER_ENTRY(Core, RingBuffer) \ No newline at end of file diff --git a/Tests/Unit/SIMD/FloatVec4.cpp b/Tests/Unit/SIMD/FloatVec4.cpp new file mode 100644 index 0000000..57c7631 --- /dev/null +++ b/Tests/Unit/SIMD/FloatVec4.cpp @@ -0,0 +1,102 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2026 IAS (ias@iasoft.dev) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include + +using namespace IACore; + +IAT_BEGIN_BLOCK(Core, FloatVec4) + +auto test_float_arithmetic() -> bool { + FloatVec4 v1(10.0f, 20.0f, 30.0f, 40.0f); + FloatVec4 v2(2.0f, 4.0f, 5.0f, 8.0f); + + alignas(16) f32 res[4]; + + (v1 / v2).store(res); + IAT_CHECK_APPROX(res[0], 5.0f); + IAT_CHECK_APPROX(res[3], 5.0f); + + (v1 * v2).store(res); + IAT_CHECK_APPROX(res[0], 20.0f); + + (v1 + v2).store(res); + IAT_CHECK_APPROX(res[0], 12.0f); + + return true; +} + +auto test_math_helpers() -> bool { + alignas(16) f32 res[4]; + + FloatVec4 v_sq(4.0f, 9.0f, 16.0f, 25.0f); + v_sq.sqrt().store(res); + IAT_CHECK_APPROX(res[0], 2.0f); + IAT_CHECK_APPROX(res[3], 5.0f); + + FloatVec4 v_neg(-1.0f, -5.0f, 10.0f, -0.0f); + v_neg.abs().store(res); + IAT_CHECK_APPROX(res[0], 1.0f); + IAT_CHECK_APPROX(res[2], 10.0f); + + FloatVec4 v_clamp(-100.0f, 0.0f, 50.0f, 200.0f); + v_clamp.clamp(0.0f, 100.0f).store(res); + IAT_CHECK_APPROX(res[0], 0.0f); + IAT_CHECK_APPROX(res[2], 50.0f); + IAT_CHECK_APPROX(res[3], 100.0f); + + return true; +} + +auto test_approx_math() -> bool { + alignas(16) f32 res[4]; + FloatVec4 v(16.0f, 25.0f, 100.0f, 1.0f); + + v.rsqrt().store(res); + + IAT_CHECK_APPROX(res[0], 0.25f); + IAT_CHECK_APPROX(res[2], 0.1f); + + return true; +} + +auto test_linear_algebra() -> bool { + FloatVec4 v1(1.0f, 2.0f, 3.0f, 4.0f); + FloatVec4 v2(1.0f, 0.0f, 1.0f, 0.0f); + + f32 dot = v1.dot(v2); + IAT_CHECK_APPROX(dot, 4.0f); + + FloatVec4 v_norm(10.0f, 0.0f, 0.0f, 0.0f); + alignas(16) f32 res[4]; + + v_norm.normalize().store(res); + IAT_CHECK_APPROX(res[0], 1.0f); + IAT_CHECK_APPROX(res[1], 0.0f); + + return true; +} + +IAT_BEGIN_TEST_LIST() +IAT_ADD_TEST(test_float_arithmetic); +IAT_ADD_TEST(test_math_helpers); +IAT_ADD_TEST(test_approx_math); +IAT_ADD_TEST(test_linear_algebra); +IAT_END_TEST_LIST() + +IAT_END_BLOCK() + +IAT_REGISTER_ENTRY(Core, FloatVec4) \ No newline at end of file diff --git a/Tests/Unit/SIMD/IntVec4.cpp b/Tests/Unit/SIMD/IntVec4.cpp new file mode 100644 index 0000000..36c6f0e --- /dev/null +++ b/Tests/Unit/SIMD/IntVec4.cpp @@ -0,0 +1,144 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2026 IAS (ias@iasoft.dev) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include + +using namespace IACore; + +IAT_BEGIN_BLOCK(Core, IntVec4) + +auto test_constructors() -> bool { + IntVec4 v_broadcast(10); + alignas(16) u32 store_buf[4]; + v_broadcast.store(store_buf); + + IAT_CHECK_EQ(store_buf[0], 10U); + IAT_CHECK_EQ(store_buf[3], 10U); + + IntVec4 v_comp(1, 2, 3, 4); + v_comp.store(store_buf); + IAT_CHECK_EQ(store_buf[0], 1U); + IAT_CHECK_EQ(store_buf[3], 4U); + + alignas(16) u32 src_buf[4] = {100, 200, 300, 400}; + IntVec4 v_load = IntVec4::load(src_buf); + v_load.store(store_buf); + IAT_CHECK_EQ(store_buf[1], 200U); + + return true; +} + +auto test_arithmetic() -> bool { + const IntVec4 v1(10, 20, 30, 40); + const IntVec4 v2(1, 2, 3, 4); + + IntVec4 v_add = v1 + v2; + alignas(16) u32 res[4]; + v_add.store(res); + IAT_CHECK_EQ(res[0], 11U); + IAT_CHECK_EQ(res[3], 44U); + + IntVec4 v_sub = v1 - v2; + v_sub.store(res); + IAT_CHECK_EQ(res[0], 9U); + + IntVec4 v_mul = v1 * v2; + v_mul.store(res); + IAT_CHECK_EQ(res[0], 10U); + IAT_CHECK_EQ(res[2], 90U); + IAT_CHECK_EQ(res[3], 160U); + + return true; +} + +auto test_bitwise() -> bool { + const IntVec4 v_all_ones(0xFFFFFFFF); + const IntVec4 v_zero((u32)0); + const IntVec4 v_pattern(0xAAAAAAAA); + + alignas(16) u32 res[4]; + + (v_all_ones & v_pattern).store(res); + IAT_CHECK_EQ(res[0], 0xAAAAAAAAU); + + (v_zero | v_pattern).store(res); + IAT_CHECK_EQ(res[0], 0xAAAAAAAAU); + + (v_all_ones ^ v_pattern).store(res); + IAT_CHECK_EQ(res[0], 0x55555555U); + + (~v_pattern).store(res); + IAT_CHECK_EQ(res[0], 0x55555555U); + + const IntVec4 v_shift(1); + (v_shift << 1).store(res); + IAT_CHECK_EQ(res[0], 2U); + + const IntVec4 v_shift_right(4); + (v_shift_right >> 1).store(res); + IAT_CHECK_EQ(res[0], 2U); + + return true; +} + +auto test_saturation() -> bool { + const u32 max = 0xFFFFFFFF; + const IntVec4 v_high(max - 10); + const IntVec4 v_add(20); + + alignas(16) u32 res[4]; + + v_high.sat_add(v_add).store(res); + IAT_CHECK_EQ(res[0], max); + + const IntVec4 v_low(10); + const IntVec4 v_sub(20); + v_low.sat_sub(v_sub).store(res); + IAT_CHECK_EQ(res[0], 0U); + + return true; +} + +auto test_advanced_ops() -> bool { + const IntVec4 v(0, 50, 100, 150); + alignas(16) u32 res[4]; + + v.clamp(40, 110).store(res); + IAT_CHECK_EQ(res[0], 40U); + IAT_CHECK_EQ(res[1], 50U); + IAT_CHECK_EQ(res[2], 100U); + IAT_CHECK_EQ(res[3], 110U); + + const IntVec4 a(2); + const IntVec4 b(10); + const IntVec4 c(5); + a.mult_add(b, c).store(res); + IAT_CHECK_EQ(res[0], 25U); + + return true; +} + +IAT_BEGIN_TEST_LIST() +IAT_ADD_TEST(test_constructors); +IAT_ADD_TEST(test_arithmetic); +IAT_ADD_TEST(test_bitwise); +IAT_ADD_TEST(test_saturation); +IAT_ADD_TEST(test_advanced_ops); +IAT_END_TEST_LIST() + +IAT_END_BLOCK() + +IAT_REGISTER_ENTRY(Core, IntVec4) \ No newline at end of file diff --git a/Tests/Unit/SocketOps.cpp b/Tests/Unit/SocketOps.cpp new file mode 100644 index 0000000..0701b9c --- /dev/null +++ b/Tests/Unit/SocketOps.cpp @@ -0,0 +1,107 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2026 IAS (ias@iasoft.dev) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include + +using namespace IACore; + +IAT_BEGIN_BLOCK(Core, SocketOps) + +auto test_initialization() -> bool { + IAT_CHECK(SocketOps::is_initialized()); + + const auto res = SocketOps::initialize(); + IAT_CHECK(res.has_value()); + + SocketOps::terminate(); + + IAT_CHECK(SocketOps::is_initialized()); + + return true; +} + +auto test_port_availability() -> bool { + + const u16 port = 54321; + + (void)SocketOps::is_port_available_tcp(port); + (void)SocketOps::is_port_available_udp(port); + + return true; +} + +auto test_unix_socket_lifecycle() -> bool { + const String socket_path = "iatest_ipc.sock"; + + SocketOps::unlink_file(socket_path.c_str()); + + auto server_res = SocketOps::create_unix_socket(); + IAT_CHECK(server_res.has_value()); + SocketHandle server = *server_res; + + auto bind_res = SocketOps::bind_unix_socket(server, socket_path.c_str()); + if (!bind_res) { + + SocketOps::close(server); + return false; + } + + auto listen_res = SocketOps::listen(server); + IAT_CHECK(listen_res.has_value()); + + auto client_res = SocketOps::create_unix_socket(); + IAT_CHECK(client_res.has_value()); + SocketHandle client = *client_res; + + auto connect_res = + SocketOps::connect_unix_socket(client, socket_path.c_str()); + IAT_CHECK(connect_res.has_value()); + + SocketOps::close(client); + SocketOps::close(server); + SocketOps::unlink_file(socket_path.c_str()); + + return true; +} + +auto test_unix_socket_errors() -> bool { + const String socket_path = "iatest_missing.sock"; + + SocketOps::unlink_file(socket_path.c_str()); + + auto client_res = SocketOps::create_unix_socket(); + IAT_CHECK(client_res.has_value()); + SocketHandle client = *client_res; + + auto connect_res = + SocketOps::connect_unix_socket(client, socket_path.c_str()); + IAT_CHECK_NOT(connect_res.has_value()); + + SocketOps::close(client); + + return true; +} + +IAT_BEGIN_TEST_LIST() +IAT_ADD_TEST(test_initialization); +IAT_ADD_TEST(test_port_availability); +IAT_ADD_TEST(test_unix_socket_lifecycle); +IAT_ADD_TEST(test_unix_socket_errors); +IAT_END_TEST_LIST() + +IAT_END_BLOCK() + +IAT_REGISTER_ENTRY(Core, SocketOps) \ No newline at end of file diff --git a/Tests/Unit/StreamReader.cpp b/Tests/Unit/StreamReader.cpp new file mode 100644 index 0000000..4062a98 --- /dev/null +++ b/Tests/Unit/StreamReader.cpp @@ -0,0 +1,137 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2026 IAS (ias@iasoft.dev) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include + +using namespace IACore; + +IAT_BEGIN_BLOCK(Core, StreamReader) + +auto test_read_uint8() -> bool { + u8 data[] = {0xAA, 0xBB, 0xCC}; + StreamReader reader(data); + + auto val1 = reader.read(); + IAT_CHECK(val1.has_value()); + IAT_CHECK_EQ(*val1, 0xAA); + IAT_CHECK_EQ(reader.cursor(), static_cast(1)); + + auto val2 = reader.read(); + IAT_CHECK(val2.has_value()); + IAT_CHECK_EQ(*val2, 0xBB); + + return true; +} + +auto test_read_multi_byte() -> bool { + + u8 data[] = {0x01, 0x02, 0x03, 0x04}; + StreamReader reader(data); + + auto val = reader.read(); + IAT_CHECK(val.has_value()); + + IAT_CHECK_EQ(*val, static_cast(0x04030201)); + + IAT_CHECK_EQ(reader.cursor(), static_cast(4)); + IAT_CHECK(reader.is_eof()); + + return true; +} + +auto test_read_float() -> bool { + const f32 pi = 3.14159f; + + u8 data[4]; + std::memcpy(data, &pi, 4); + + StreamReader reader(data); + auto val = reader.read(); + + IAT_CHECK(val.has_value()); + IAT_CHECK_APPROX(*val, pi); + + return true; +} + +auto test_read_buffer() -> bool { + u8 src[] = {1, 2, 3, 4, 5}; + u8 dst[3] = {0}; + StreamReader reader(src); + + const auto res = reader.read(dst, 3); + IAT_CHECK(res.has_value()); + + IAT_CHECK_EQ(dst[0], 1); + IAT_CHECK_EQ(dst[1], 2); + IAT_CHECK_EQ(dst[2], 3); + + IAT_CHECK_EQ(reader.cursor(), static_cast(3)); + + return true; +} + +auto test_navigation() -> bool { + u8 data[10] = {0}; + StreamReader reader(data); + + IAT_CHECK_EQ(reader.remaining(), static_cast(10)); + + reader.skip(5); + IAT_CHECK_EQ(reader.cursor(), static_cast(5)); + IAT_CHECK_EQ(reader.remaining(), static_cast(5)); + + reader.skip(100); + IAT_CHECK_EQ(reader.cursor(), static_cast(10)); + IAT_CHECK(reader.is_eof()); + + reader.seek(2); + IAT_CHECK_EQ(reader.cursor(), static_cast(2)); + IAT_CHECK_EQ(reader.remaining(), static_cast(8)); + IAT_CHECK_NOT(reader.is_eof()); + + return true; +} + +auto test_boundary_checks() -> bool { + u8 data[] = {0x00, 0x00}; + StreamReader reader(data); + + (void)reader.read(); + IAT_CHECK(reader.is_eof()); + + auto val = reader.read(); + IAT_CHECK_NOT(val.has_value()); + + u8 buf[1]; + auto batch = reader.read(buf, 1); + IAT_CHECK_NOT(batch.has_value()); + + return true; +} + +IAT_BEGIN_TEST_LIST() +IAT_ADD_TEST(test_read_uint8); +IAT_ADD_TEST(test_read_multi_byte); +IAT_ADD_TEST(test_read_float); +IAT_ADD_TEST(test_read_buffer); +IAT_ADD_TEST(test_navigation); +IAT_ADD_TEST(test_boundary_checks); +IAT_END_TEST_LIST() + +IAT_END_BLOCK() + +IAT_REGISTER_ENTRY(Core, StreamReader) \ No newline at end of file diff --git a/Tests/Unit/StreamWriter.cpp b/Tests/Unit/StreamWriter.cpp new file mode 100644 index 0000000..75547db --- /dev/null +++ b/Tests/Unit/StreamWriter.cpp @@ -0,0 +1,116 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2026 IAS (ias@iasoft.dev) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +#include + +using namespace IACore; + +IAT_BEGIN_BLOCK(Core, StreamWriter) + +auto test_memory_writer() -> bool { + StreamWriter writer; + + IAT_CHECK(writer.write(static_cast(0xAA), 1).has_value()); + + const u32 val = 0x12345678; + IAT_CHECK(writer.write(val).has_value()); + + IAT_CHECK_EQ(writer.cursor(), static_cast(1 + 4)); + + const u8 *ptr = writer.data(); + IAT_CHECK_EQ(ptr[0], 0xAA); + IAT_CHECK_EQ(ptr[1], 0x78); + IAT_CHECK_EQ(ptr[4], 0x12); + + return true; +} + +auto test_fixed_buffer() -> bool { + u8 buffer[4] = {0}; + StreamWriter writer(Span(buffer, 4)); + + IAT_CHECK(writer.write(static_cast(0xFF), 2).has_value()); + IAT_CHECK_EQ(writer.cursor(), static_cast(2)); + + IAT_CHECK(writer.write(static_cast(0xEE), 2).has_value()); + IAT_CHECK_EQ(writer.cursor(), static_cast(4)); + + const auto res = writer.write(static_cast(0x00), 1); + IAT_CHECK_NOT(res.has_value()); + + IAT_CHECK_EQ(buffer[0], 0xFF); + IAT_CHECK_EQ(buffer[1], 0xFF); + IAT_CHECK_EQ(buffer[2], 0xEE); + IAT_CHECK_EQ(buffer[3], 0xEE); + + return true; +} + +auto test_file_writer() -> bool { + const Path path = "test_stream_writer.bin"; + + if (std::filesystem::exists(path)) { + std::filesystem::remove(path); + } + + { + auto res = StreamWriter::create_from_file(path); + IAT_CHECK(res.has_value()); + StreamWriter writer = std::move(*res); + + const String hello = "Hello World"; + IAT_CHECK(writer.write(hello.data(), hello.size()).has_value()); + + IAT_CHECK(writer.flush().has_value()); + } + + auto read_res = FileOps::read_binary_file(path); + IAT_CHECK(read_res.has_value()); + + const String read_str(reinterpret_cast(read_res->data()), + read_res->size()); + IAT_CHECK_EQ(read_str, String("Hello World")); + + std::filesystem::remove(path); + + return true; +} + +auto test_primitives() -> bool { + StreamWriter writer; + + const f32 f = 1.5f; + const u64 big = 0xDEADBEEFCAFEBABE; + + IAT_CHECK(writer.write(f).has_value()); + IAT_CHECK(writer.write(big).has_value()); + + IAT_CHECK_EQ(writer.cursor(), sizeof(f32) + sizeof(u64)); + + return true; +} + +IAT_BEGIN_TEST_LIST() +IAT_ADD_TEST(test_memory_writer); +IAT_ADD_TEST(test_fixed_buffer); +IAT_ADD_TEST(test_file_writer); +IAT_ADD_TEST(test_primitives); +IAT_END_TEST_LIST() + +IAT_END_BLOCK() + +IAT_REGISTER_ENTRY(Core, StreamWriter) \ No newline at end of file diff --git a/Tests/Unit/StringOps.cpp b/Tests/Unit/StringOps.cpp new file mode 100644 index 0000000..5bb222b --- /dev/null +++ b/Tests/Unit/StringOps.cpp @@ -0,0 +1,111 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2026 IAS (ias@iasoft.dev) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include + +using namespace IACore; + +IAT_BEGIN_BLOCK(Core, StringOps) + +auto test_base64_encode() -> bool { + + { + const String s = "Hello World"; + const Span data(reinterpret_cast(s.data()), s.size()); + const String encoded = StringOps::encode_base64(data); + IAT_CHECK_EQ(encoded, String("SGVsbG8gV29ybGQ=")); + } + + { + const String s = "M"; + const Span data(reinterpret_cast(s.data()), s.size()); + const String encoded = StringOps::encode_base64(data); + IAT_CHECK_EQ(encoded, String("TQ==")); + } + + { + const String s = "Ma"; + const Span data(reinterpret_cast(s.data()), s.size()); + const String encoded = StringOps::encode_base64(data); + IAT_CHECK_EQ(encoded, String("TWE=")); + } + + { + const String s = "Man"; + const Span data(reinterpret_cast(s.data()), s.size()); + const String encoded = StringOps::encode_base64(data); + IAT_CHECK_EQ(encoded, String("TWFu")); + } + + { + const String encoded = StringOps::encode_base64({}); + IAT_CHECK(encoded.empty()); + } + + return true; +} + +auto test_base64_decode() -> bool { + + { + const String encoded = "SGVsbG8gV29ybGQ="; + const Vec decoded = StringOps::decode_base64(encoded); + const String result(reinterpret_cast(decoded.data()), + decoded.size()); + IAT_CHECK_EQ(result, String("Hello World")); + } + + { + const Vec decoded = StringOps::decode_base64(""); + IAT_CHECK(decoded.empty()); + } + + return true; +} + +auto test_base64_round_trip() -> bool { + Vec original; + original.reserve(256); + for (usize i = 0; i < 256; ++i) { + original.push_back(static_cast(i)); + } + + const String encoded = StringOps::encode_base64(original); + const Vec decoded = StringOps::decode_base64(encoded); + + IAT_CHECK_EQ(original.size(), decoded.size()); + + bool match = true; + for (usize i = 0; i < original.size(); ++i) { + if (original[i] != decoded[i]) { + match = false; + break; + } + } + IAT_CHECK(match); + + return true; +} + +IAT_BEGIN_TEST_LIST() +IAT_ADD_TEST(test_base64_encode); +IAT_ADD_TEST(test_base64_decode); +IAT_ADD_TEST(test_base64_round_trip); +IAT_END_TEST_LIST() + +IAT_END_BLOCK() + +IAT_REGISTER_ENTRY(Core, StringOps) \ No newline at end of file diff --git a/Tests/Unit/Utils.cpp b/Tests/Unit/Utils.cpp new file mode 100644 index 0000000..7c7be36 --- /dev/null +++ b/Tests/Unit/Utils.cpp @@ -0,0 +1,162 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2026 IAS (ias@iasoft.dev) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include + +using namespace IACore; + +struct TestVec3 { + f32 x, y, z; + + bool operator==(const TestVec3 &other) const { + return x == other.x && y == other.y && z == other.z; + } +}; + +IA_MAKE_HASHABLE(TestVec3, &TestVec3::x, &TestVec3::y, &TestVec3::z); + +IAT_BEGIN_BLOCK(Core, Utils) + +auto test_hex_conversion() -> bool { + + u8 bin[] = {0xDE, 0xAD, 0xBE, 0xEF, 0x00, 0xFF}; + String hex = Utils::binary_to_hex_string(bin); + + IAT_CHECK_EQ(hex, String("DEADBEEF00FF")); + + auto res_upper = Utils::hex_string_to_binary("DEADBEEF00FF"); + IAT_CHECK(res_upper.has_value()); + IAT_CHECK_EQ(res_upper->size(), static_cast(6)); + IAT_CHECK_EQ((*res_upper)[0], 0xDE); + IAT_CHECK_EQ((*res_upper)[5], 0xFF); + + auto res_lower = Utils::hex_string_to_binary("deadbeef00ff"); + IAT_CHECK(res_lower.has_value()); + IAT_CHECK_EQ((*res_lower)[0], 0xDE); + + Vec original = {1, 2, 3, 4, 5}; + String s = Utils::binary_to_hex_string(original); + auto back = Utils::hex_string_to_binary(s); + IAT_CHECK(back.has_value()); + IAT_CHECK_EQ(original.size(), back->size()); + IAT_CHECK_EQ(original[2], (*back)[2]); + + return true; +} + +auto test_hex_errors() -> bool { + + auto odd = Utils::hex_string_to_binary("ABC"); + IAT_CHECK_NOT(odd.has_value()); + + auto invalid = Utils::hex_string_to_binary("ZZTOP"); + IAT_CHECK_NOT(invalid.has_value()); + + auto empty = Utils::hex_string_to_binary(""); + IAT_CHECK(empty.has_value()); + IAT_CHECK_EQ(empty->size(), static_cast(0)); + + return true; +} + +auto test_sort() -> bool { + Vec nums = {5, 1, 4, 2, 3}; + + Utils::sort(nums); + + IAT_CHECK_EQ(nums[0], 1); + IAT_CHECK_EQ(nums[1], 2); + IAT_CHECK_EQ(nums[2], 3); + IAT_CHECK_EQ(nums[3], 4); + IAT_CHECK_EQ(nums[4], 5); + + return true; +} + +auto test_binary_search() -> bool { + + Vec nums = {10, 20, 20, 20, 30}; + + auto it_left = Utils::binary_search_left(nums, 20); + IAT_CHECK(it_left != nums.end()); + IAT_CHECK_EQ(*it_left, 20); + IAT_CHECK_EQ(std::distance(nums.begin(), it_left), 1); + + auto it_right = Utils::binary_search_right(nums, 20); + IAT_CHECK(it_right != nums.end()); + IAT_CHECK_EQ(*it_right, 30); + IAT_CHECK_EQ(std::distance(nums.begin(), it_right), 4); + + auto it_fail = Utils::binary_search_left(nums, 99); + IAT_CHECK(it_fail == nums.end()); + + return true; +} + +auto test_hash_basics() -> bool { + u64 h1 = Utils::compute_hash(10, 20.5f, "Hello"); + u64 h2 = Utils::compute_hash(10, 20.5f, "Hello"); + u64 h3 = Utils::compute_hash(10, 20.5f, "World"); + + IAT_CHECK_EQ(h1, h2); + + IAT_CHECK_NEQ(h1, h3); + + u64 order_a = Utils::compute_hash(1, 2); + u64 order_b = Utils::compute_hash(2, 1); + IAT_CHECK_NEQ(order_a, order_b); + + return true; +} + +auto test_hash_macro() -> bool { + TestVec3 v1{1.0f, 2.0f, 3.0f}; + TestVec3 v2{1.0f, 2.0f, 3.0f}; + TestVec3 v3{1.0f, 2.0f, 4.0f}; + + ankerl::unordered_dense::hash hasher; + + u64 h1 = hasher(v1); + u64 h2 = hasher(v2); + u64 h3 = hasher(v3); + + IAT_CHECK_EQ(h1, h2); + IAT_CHECK_NEQ(h1, h3); + + u64 h_manual = 0; + Utils::hash_combine(h_manual, v1); + + u64 h_wrapper = Utils::compute_hash(v1); + + IAT_CHECK_EQ(h_manual, h_wrapper); + + IAT_CHECK_NEQ(h1, h_wrapper); + + return true; +} + +IAT_BEGIN_TEST_LIST() +IAT_ADD_TEST(test_hex_conversion); +IAT_ADD_TEST(test_hex_errors); +IAT_ADD_TEST(test_sort); +IAT_ADD_TEST(test_binary_search); +IAT_ADD_TEST(test_hash_basics); +IAT_ADD_TEST(test_hash_macro); +IAT_END_TEST_LIST() + +IAT_END_BLOCK() + +IAT_REGISTER_ENTRY(Core, Utils) \ No newline at end of file diff --git a/Tests/Unit/XML.cpp b/Tests/Unit/XML.cpp new file mode 100644 index 0000000..d5e2f2c --- /dev/null +++ b/Tests/Unit/XML.cpp @@ -0,0 +1,112 @@ +// IACore-OSS; The Core Library for All IA Open Source Projects +// Copyright (C) 2026 IAS (ias@iasoft.dev) +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +#include + +using namespace IACore; + +IAT_BEGIN_BLOCK(Core, XML) + +auto test_parse_string() -> bool { + const String xml_content = R"( + + Value1 + Value2 + + )"; + + auto res = XML::parse_from_string(xml_content); + IAT_CHECK(res.has_value()); + + auto &doc = *res; + auto root = doc.child("root"); + IAT_CHECK(root); + + auto item1 = root.find_child_by_attribute("item", "id", "1"); + IAT_CHECK(item1); + IAT_CHECK_EQ(String(item1.child_value()), String("Value1")); + + auto item2 = root.find_child_by_attribute("item", "id", "2"); + IAT_CHECK(item2); + IAT_CHECK_EQ(String(item2.child_value()), String("Value2")); + + return true; +} + +auto test_parse_error() -> bool { + const String invalid_xml = ""; + auto res = XML::parse_from_string(invalid_xml); + IAT_CHECK_NOT(res.has_value()); + return true; +} + +auto test_serialize() -> bool { + const String xml_content = "Text"; + auto res = XML::parse_from_string(xml_content); + IAT_CHECK(res.has_value()); + + String output = XML::serialize_to_string(*res); + + IAT_CHECK(output.find("") != String::npos); + IAT_CHECK(output.find("Text") != String::npos); + + return true; +} + +auto test_escape() -> bool { + const String raw = "< & > \" '"; + const String escaped = XML::escape_xml_string(raw); + + IAT_CHECK(escaped.find("<") != String::npos); + IAT_CHECK(escaped.find("&") != String::npos); + IAT_CHECK(escaped.find(">") != String::npos); + IAT_CHECK(escaped.find(""") != String::npos); + IAT_CHECK(escaped.find("'") != String::npos); + + return true; +} + +auto test_file_io() -> bool { + const Path path = "test_temp_xml_doc.xml"; + const String content = "1.0"; + + auto write_res = FileOps::write_text_file(path, content, true); + IAT_CHECK(write_res.has_value()); + + auto parse_res = XML::parse_from_file(path); + IAT_CHECK(parse_res.has_value()); + + auto &doc = *parse_res; + IAT_CHECK_EQ(String(doc.child("config").child("ver").child_value()), + String("1.0")); + + std::filesystem::remove(path); + + return true; +} + +IAT_BEGIN_TEST_LIST() +IAT_ADD_TEST(test_parse_string); +IAT_ADD_TEST(test_parse_error); +IAT_ADD_TEST(test_serialize); +IAT_ADD_TEST(test_escape); +IAT_ADD_TEST(test_file_io); +IAT_END_TEST_LIST() + +IAT_END_BLOCK() + +IAT_REGISTER_ENTRY(Core, XML) \ No newline at end of file diff --git a/logo.svg b/logo.svg new file mode 100644 index 0000000..9642a5e --- /dev/null +++ b/logo.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + IACore + + INDEPENDENT ARCHITECTURE + + \ No newline at end of file