diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml index d0aee75..7d0697e 100644 --- a/.gitea/workflows/build.yml +++ b/.gitea/workflows/build.yml @@ -1,8 +1,14 @@ name: Build -# Scaffolding CI: proves the CMake toolchain configures and builds on all -# three platforms the design doc names. It does not yet link livekit-ffi -# or produce a real OBS-installable package -- see README.md. +# Every job builds the real plugin: the core library linked against the +# pinned livekit/client-sdk-cpp release, and -- where libobs is available -- +# the OBS adapter module itself. +# +# Linux gets libobs from Ubuntu's libobs-dev. macOS and Windows have no +# equivalent system package, so they run obs-plugintemplate's buildspec +# bootstrap (buildspec.json + cmake/common/buildspec_common.cmake), which +# downloads the pinned obs-deps bundle and obs-studio source and builds just +# `libobs`. That step is the slow one: several minutes on a cold runner. on: push: @@ -19,10 +25,12 @@ jobs: - name: Install build dependencies run: | sudo apt-get update -qq - sudo apt-get install -y -qq cmake ninja-build libobs-dev + sudo apt-get install -y -qq cmake ninja-build libobs-dev libcurl4-openssl-dev - name: Configure - run: cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release + # Linux keeps its distribution libobs; the buildspec bootstrap is for + # the two platforms that have no such package. + run: cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -DSTPLUGIN_BOOTSTRAP_OBS=OFF - name: Build run: cmake --build build @@ -30,6 +38,19 @@ jobs: - name: Test (core library) run: ctest --test-dir build --output-on-failure + - name: Show what was built + run: | + ls -la build/package/bin build/package/data/locale build/package/licenses + ldd build/package/bin/streamer-tools-camera.so | grep -E 'obs|livekit' + nm -D build/package/bin/streamer-tools-camera.so | grep -E ' T obs_module_(load|unload)' + + - name: Upload plugin + continue-on-error: true + uses: actions/upload-artifact@v3 + with: + name: streamer-tools-camera-linux-x64 + path: build/package + macos: name: macOS (macos-latest) runs-on: macos-latest @@ -41,10 +62,8 @@ jobs: run: brew install cmake ninja - name: Configure - # libobs is not expected to be found here yet (no Homebrew - # formula / SDK download wired up in this pass) -- the top-level - # CMakeLists.txt falls back to building only the core library in - # that case. See README.md. + # The buildspec bootstrap runs here: it fetches obs-deps + the pinned + # obs-studio source and builds libobs before this project configures. run: cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release - name: Build @@ -53,6 +72,18 @@ jobs: - name: Test (core library) run: ctest --test-dir build --output-on-failure + - name: Show what was built + run: | + ls -la build/package/bin || true + otool -L build/package/bin/streamer-tools-camera.so || true + + - name: Upload plugin + continue-on-error: true + uses: actions/upload-artifact@v3 + with: + name: streamer-tools-camera-macos + path: build/package + windows: name: Windows (windows-latest) runs-on: windows-latest @@ -61,22 +92,29 @@ jobs: uses: actions/checkout@v4 - name: Install build dependencies - # winvm-builder is a self-hosted act_runner labeled - # "windows-latest" -- it is NOT the GitHub-hosted windows-latest - # image, so none of the tools that image preinstalls (cmake - # included) can be assumed present. Confirmed by a first CI run - # on this repo: "cmake : The term 'cmake' is not recognized...". - # lukka/get-cmake downloads a pinned cmake+ninja binary and adds - # it to PATH for this job, with no admin/choco dependency. + # winvm-builder is a self-hosted act_runner labeled "windows-latest"; + # it is NOT the GitHub-hosted image, so none of that image's + # preinstalled tooling (cmake included) can be assumed present. uses: lukka/get-cmake@latest - name: Configure - # Same story as macOS: no OBS SDK available yet on this runner, - # so this proves the core library + MSVC toolchain only. - run: cmake -S . -B build -DCMAKE_BUILD_TYPE=Release + # The default Visual Studio generator is required, not Ninja: + # cmake/windows/buildspec.cmake keys the dependency slice off + # CMAKE_VS_PLATFORM_NAME, which only a VS generator sets. + run: cmake -S . -B build -A x64 - name: Build run: cmake --build build --config Release - name: Test (core library) run: ctest --test-dir build -C Release --output-on-failure + + - name: Show what was built + run: dir build\package\bin + + - name: Upload plugin + continue-on-error: true + uses: actions/upload-artifact@v3 + with: + name: streamer-tools-camera-windows-x64 + path: build/package diff --git a/.gitignore b/.gitignore index a4fb4fb..2cb0a9b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,6 @@ build/ +build*/ .cache/ +# obs-plugintemplate's buildspec bootstrap unpacks the OBS SDK and its +# prebuilt dependencies here (macOS/Windows only). +.deps/ diff --git a/CMakeLists.txt b/CMakeLists.txt index e138df7..5e86970 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -18,6 +18,37 @@ endif() enable_testing() list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake/common") + +# --- OBS SDK --------------------------------------------------------------- +# Linux gets libobs from the distribution (Ubuntu's libobs-dev ships real +# libobsConfig.cmake), and that path is left exactly as it was. +# +# macOS and Windows have no such package, so they use obs-plugintemplate's +# buildspec bootstrap, trimmed: it downloads the pinned obs-deps bundle and +# the pinned obs-studio source, then builds and installs just `libobs`. See +# buildspec.json for why the OBS pin is deliberately low, and +# cmake/common/buildspec_common.cmake for every change from upstream. +# +# STPLUGIN_BOOTSTRAP_OBS=OFF falls back to plain find_package(libobs), for a +# developer who already has an OBS SDK on their prefix path and does not want +# a from-source libobs build. +option(STPLUGIN_BOOTSTRAP_OBS "Download and build libobs from source (macOS/Windows)" ON) + +include(osconfig) +if(STPLUGIN_BOOTSTRAP_OBS AND (OS_MACOS OR OS_WINDOWS)) + if(OS_MACOS) + # client-sdk-cpp ships single-arch dylibs, so this plugin is built for + # one architecture even though the libobs it links is universal. + if(NOT CMAKE_OSX_ARCHITECTURES) + set(CMAKE_OSX_ARCHITECTURES "${CMAKE_HOST_SYSTEM_PROCESSOR}" CACHE STRING "" FORCE) + endif() + if(NOT CMAKE_OSX_DEPLOYMENT_TARGET) + set(CMAKE_OSX_DEPLOYMENT_TARGET "13.0" CACHE STRING "" FORCE) + endif() + endif() + include(buildspec) +endif() # --- LiveKit C++ client SDK ------------------------------------------------- # Pinned, prebuilt release of livekit/client-sdk-cpp, downloaded and unpacked diff --git a/buildspec.json b/buildspec.json new file mode 100644 index 0000000..af434cb --- /dev/null +++ b/buildspec.json @@ -0,0 +1,54 @@ +{ + "_comment": [ + "Dependency manifest for the macOS/Windows OBS SDK bootstrap, in the shape", + "obsproject/obs-plugintemplate's cmake/common/buildspec_common.cmake reads.", + "Linux does NOT use this: it gets libobs from Ubuntu's libobs-dev package.", + "", + "obs-studio is pinned to 30.0.2 on purpose, and low rather than high:", + "OBS refuses to load a module built against a NEWER libobs than the one", + "running (libobs/obs-module.c's version check) and accepts older ones, so", + "this pin is the minimum OBS version users need. 30.0.2 is also exactly", + "what Ubuntu 24.04's libobs-dev ships, which keeps the three platforms on", + "one floor. 30.0.2 supports the modern CMake layout this bootstrap drives", + "via -DOBS_CMAKE_VERSION=3.0.0.", + "", + "prebuilt is obs-deps 2023-11-03, the version obs-studio 30.0.2's own", + "buildspec.json pins, with its own published hashes. qt6 is deliberately", + "absent: this plugin never links Qt.", + "", + "The obs-studio hashes are of the GitHub source archives for tag 30.0.2,", + "computed on 2026-09-06: .tar.gz (15861643 bytes) for macOS, .zip", + "(18168471 bytes) for Windows." + ], + "dependencies": { + "obs-studio": { + "version": "30.0.2", + "baseUrl": "https://github.com/obsproject/obs-studio/archive/refs/tags", + "label": "OBS sources", + "hashes": { + "macos": "be12c3ad0a85713750d8325e4b1db75086223402d7080d0e3c2833d7c5e83c27", + "windows-x64": "970058c49322cfa9cd6d620abb393fed89743ba7e74bd9dbb6ebe0ea8141d9c7" + } + }, + "prebuilt": { + "version": "2023-11-03", + "baseUrl": "https://github.com/obsproject/obs-deps/releases/download", + "label": "Pre-Built obs-deps", + "hashes": { + "macos": "90c2fc069847ec2768dcc867c1c63b112c615ed845a907dc44acab7a97181974", + "windows-x64": "d0825a6fb65822c993a3059edfba70d72d2e632ef74893588cf12b1f0d329ce6" + } + } + }, + "platformConfig": { + "macos": { + "bundleId": "net.cybercove.streamer-tools-camera" + } + }, + "name": "streamer-tools-camera", + "displayName": "streamer-tools Camera", + "version": "0.1.0", + "author": "CyberCoveLLC", + "website": "https://repo.anhonesthost.net/CyberCoveLLC/obs-streamer-tools-plugin", + "email": "jknapp85@gmail.com" +} diff --git a/cmake/common/buildspec_common.cmake b/cmake/common/buildspec_common.cmake new file mode 100644 index 0000000..72f9cab --- /dev/null +++ b/cmake/common/buildspec_common.cmake @@ -0,0 +1,224 @@ +# Adapted from obsproject/obs-plugintemplate (cmake/common/buildspec_common.cmake, +# master as of 2026-09-06). Deliberate changes from upstream, all recorded here +# so a future re-sync knows what to keep: +# +# 1. _setup_obs_studio builds and installs the `libobs` target, not +# `obs-frontend-api`. This plugin's properties UI is plain +# obs_properties_* and it never touches the frontend API, so building it +# would only drag Qt back in -- which is the whole point of dropping qt6 +# from dependencies_list. +# 2. It passes -DENABLE_UI:BOOL=OFF and -DENABLE_SCRIPTING:BOOL=OFF as well +# as upstream's -DENABLE_FRONTEND:BOOL=OFF. The pinned OBS (30.0.2) +# predates the ENABLE_FRONTEND option and gates its Qt-dependent UI on +# ENABLE_UI instead; without this the sub-build configures the whole +# OBS UI and demands Qt. +# 3. Only the Release configuration is built and installed. Upstream builds +# Debug as well; nothing here consumes a debug libobs, and it doubles the +# slowest step in CI. +# + +include_guard(GLOBAL) + +# _check_deps_version: Checks for obs-deps VERSION file in prefix paths +function(_check_deps_version version) + set(found FALSE) + + foreach(path IN LISTS CMAKE_PREFIX_PATH) + if(EXISTS "${path}/share/obs-deps/VERSION") + if(dependency STREQUAL qt6 AND NOT EXISTS "${path}/lib/cmake/Qt6/Qt6Config.cmake") + set(found FALSE) + continue() + endif() + + file(READ "${path}/share/obs-deps/VERSION" _check_version) + string(REPLACE "\n" "" _check_version "${_check_version}") + string(REPLACE "-" "." _check_version "${_check_version}") + string(REPLACE "-" "." version "${version}") + + if(_check_version VERSION_EQUAL version) + set(found TRUE) + break() + elseif(_check_version VERSION_LESS version) + message( + AUTHOR_WARNING + "Older ${label} version detected in ${path}: \n" + "Found ${_check_version}, require ${version}" + ) + list(REMOVE_ITEM CMAKE_PREFIX_PATH "${path}") + list(APPEND CMAKE_PREFIX_PATH "${path}") + set(CMAKE_PREFIX_PATH ${CMAKE_PREFIX_PATH}) + continue() + else() + message( + AUTHOR_WARNING + "Newer ${label} version detected in ${path}: \n" + "Found ${_check_version}, require ${version}" + ) + set(found TRUE) + break() + endif() + endif() + endforeach() + + return(PROPAGATE found CMAKE_PREFIX_PATH) +endfunction() + +# _setup_obs_studio: Create obs-studio build project, then build libobs and obs-frontend-api +function(_setup_obs_studio) + if(NOT libobs_DIR) + set(_is_fresh --fresh) + endif() + + if(OS_WINDOWS) + set(_cmake_generator "${CMAKE_GENERATOR}") + set(_cmake_arch "-A ${arch},version=${CMAKE_VS_WINDOWS_TARGET_PLATFORM_VERSION}") + set(_cmake_extra "-DCMAKE_SYSTEM_VERSION=${CMAKE_SYSTEM_VERSION}") + elseif(OS_MACOS) + set(_cmake_generator "Xcode") + set(_cmake_arch "-DCMAKE_OSX_ARCHITECTURES:STRING='arm64;x86_64'") + set(_cmake_extra "-DCMAKE_OSX_DEPLOYMENT_TARGET=${CMAKE_OSX_DEPLOYMENT_TARGET}") + endif() + + message(STATUS "Configure ${label} (${arch})") + execute_process( + COMMAND + "${CMAKE_COMMAND}" -S "${dependencies_dir}/${_obs_destination}" -B + "${dependencies_dir}/${_obs_destination}/build_${arch}" -G ${_cmake_generator} "${_cmake_arch}" + -DOBS_CMAKE_VERSION:STRING=3.0.0 -DENABLE_PLUGINS:BOOL=OFF -DENABLE_FRONTEND:BOOL=OFF + -DENABLE_UI:BOOL=OFF -DENABLE_SCRIPTING:BOOL=OFF -DENABLE_BROWSER:BOOL=OFF + -DOBS_VERSION_OVERRIDE:STRING=${_obs_version} "-DCMAKE_PREFIX_PATH='${CMAKE_PREFIX_PATH}'" ${_is_fresh} + ${_cmake_extra} + RESULT_VARIABLE _process_result + COMMAND_ERROR_IS_FATAL ANY + ) + message(STATUS "Configure ${label} (${arch}) - done") + + message(STATUS "Build ${label} (Release - ${arch})") + execute_process( + COMMAND "${CMAKE_COMMAND}" --build build_${arch} --target libobs --config Release --parallel + WORKING_DIRECTORY "${dependencies_dir}/${_obs_destination}" + RESULT_VARIABLE _process_result + COMMAND_ERROR_IS_FATAL ANY + ) + message(STATUS "Build ${label} (Release - ${arch}) - done") + + message(STATUS "Install ${label} (${arch})") + execute_process( + COMMAND + "${CMAKE_COMMAND}" --install build_${arch} --component Development --config Release --prefix "${dependencies_dir}" + WORKING_DIRECTORY "${dependencies_dir}/${_obs_destination}" + RESULT_VARIABLE _process_result + COMMAND_ERROR_IS_FATAL ANY + ) + message(STATUS "Install ${label} (${arch}) - done") +endfunction() + +# _check_dependencies: Fetch and extract pre-built OBS build dependencies +function(_check_dependencies) + file(READ "${CMAKE_CURRENT_SOURCE_DIR}/buildspec.json" buildspec) + + string(JSON dependency_data GET ${buildspec} dependencies) + + foreach(dependency IN LISTS dependencies_list) + string(JSON data GET ${dependency_data} ${dependency}) + string(JSON version GET ${data} version) + string(JSON hash GET ${data} hashes ${platform}) + string(JSON url GET ${data} baseUrl) + string(JSON label GET ${data} label) + string(JSON revision ERROR_VARIABLE error GET ${data} revision ${platform}) + + message(STATUS "Setting up ${label} (${arch})") + + set(file "${${dependency}_filename}") + set(destination "${${dependency}_destination}") + string(REPLACE "VERSION" "${version}" file "${file}") + string(REPLACE "VERSION" "${version}" destination "${destination}") + string(REPLACE "ARCH" "${arch}" file "${file}") + string(REPLACE "ARCH" "${arch}" destination "${destination}") + if(revision) + string(REPLACE "_REVISION" "_v${revision}" file "${file}") + string(REPLACE "-REVISION" "-v${revision}" file "${file}") + else() + string(REPLACE "_REVISION" "" file "${file}") + string(REPLACE "-REVISION" "" file "${file}") + endif() + + if(EXISTS "${dependencies_dir}/.dependency_${dependency}_${arch}.sha256") + file( + READ + "${dependencies_dir}/.dependency_${dependency}_${arch}.sha256" + OBS_DEPENDENCY_${dependency}_${arch}_HASH + ) + endif() + + set(skip FALSE) + if(dependency STREQUAL prebuilt OR dependency STREQUAL qt6) + if(OBS_DEPENDENCY_${dependency}_${arch}_HASH STREQUAL ${hash}) + _check_deps_version(${version}) + + if(found) + set(skip TRUE) + endif() + endif() + endif() + + if(skip) + message(STATUS "Setting up ${label} (${arch}) - skipped") + continue() + endif() + + if(dependency STREQUAL obs-studio) + set(url ${url}/${file}) + else() + set(url ${url}/${version}/${file}) + endif() + + if(NOT EXISTS "${dependencies_dir}/${file}") + message(STATUS "Downloading ${url}") + file(DOWNLOAD "${url}" "${dependencies_dir}/${file}" STATUS download_status EXPECTED_HASH SHA256=${hash}) + + list(GET download_status 0 error_code) + list(GET download_status 1 error_message) + if(error_code GREATER 0) + message(STATUS "Downloading ${url} - Failure") + message(FATAL_ERROR "Unable to download ${url}, failed with error: ${error_message}") + file(REMOVE "${dependencies_dir}/${file}") + else() + message(STATUS "Downloading ${url} - done") + endif() + endif() + + if(NOT OBS_DEPENDENCY_${dependency}_${arch}_HASH STREQUAL ${hash}) + file(REMOVE_RECURSE "${dependencies_dir}/${destination}") + endif() + + if(NOT EXISTS "${dependencies_dir}/${destination}") + file(MAKE_DIRECTORY "${dependencies_dir}/${destination}") + if(dependency STREQUAL obs-studio) + file(ARCHIVE_EXTRACT INPUT "${dependencies_dir}/${file}" DESTINATION "${dependencies_dir}") + else() + file(ARCHIVE_EXTRACT INPUT "${dependencies_dir}/${file}" DESTINATION "${dependencies_dir}/${destination}") + endif() + endif() + + file(WRITE "${dependencies_dir}/.dependency_${dependency}_${arch}.sha256" "${hash}") + + if(dependency STREQUAL prebuilt) + list(APPEND CMAKE_PREFIX_PATH "${dependencies_dir}/${destination}") + elseif(dependency STREQUAL qt6) + list(APPEND CMAKE_PREFIX_PATH "${dependencies_dir}/${destination}") + elseif(dependency STREQUAL obs-studio) + set(_obs_version ${version}) + set(_obs_destination "${destination}") + list(APPEND CMAKE_PREFIX_PATH "${dependencies_dir}") + endif() + + message(STATUS "Setting up ${label} (${arch}) - done") + endforeach() + + list(REMOVE_DUPLICATES CMAKE_PREFIX_PATH) + + set(CMAKE_PREFIX_PATH ${CMAKE_PREFIX_PATH} CACHE PATH "CMake prefix search path" FORCE) + + _setup_obs_studio() +endfunction() diff --git a/cmake/common/osconfig.cmake b/cmake/common/osconfig.cmake new file mode 100644 index 0000000..87d435a --- /dev/null +++ b/cmake/common/osconfig.cmake @@ -0,0 +1,20 @@ +# CMake operating system bootstrap module + +include_guard(GLOBAL) + +if(CMAKE_HOST_SYSTEM_NAME STREQUAL "Windows") + set(CMAKE_C_EXTENSIONS FALSE) + set(CMAKE_CXX_EXTENSIONS FALSE) + list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake/windows") + set(OS_WINDOWS TRUE) +elseif(CMAKE_HOST_SYSTEM_NAME STREQUAL "Darwin") + set(CMAKE_C_EXTENSIONS FALSE) + set(CMAKE_CXX_EXTENSIONS FALSE) + list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake/macos") + set(OS_MACOS TRUE) +elseif(CMAKE_HOST_SYSTEM_NAME MATCHES "Linux|FreeBSD|OpenBSD") + set(CMAKE_CXX_EXTENSIONS FALSE) + list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake/linux") + string(TOUPPER "${CMAKE_HOST_SYSTEM_NAME}" _SYSTEM_NAME_U) + set(OS_${_SYSTEM_NAME_U} TRUE) +endif() diff --git a/cmake/macos/buildspec.cmake b/cmake/macos/buildspec.cmake new file mode 100644 index 0000000..ce142de --- /dev/null +++ b/cmake/macos/buildspec.cmake @@ -0,0 +1,39 @@ +# CMake macOS build dependencies module +# +# Adapted from obsproject/obs-plugintemplate. Only change from upstream: qt6 +# is dropped from dependencies_list. This plugin's properties UI is plain +# obs_properties_*, it never links Qt, and the OBS sub-build is configured +# with ENABLE_UI=OFF -- so downloading a ~100 MB Qt bundle on every CI run +# would buy nothing. + +include_guard(GLOBAL) + +include(buildspec_common) + +# _check_dependencies_macos: Set up macOS slice for _check_dependencies +function(_check_dependencies_macos) + set(arch universal) + set(platform macos) + + file(READ "${CMAKE_CURRENT_SOURCE_DIR}/buildspec.json" buildspec) + + set(dependencies_dir "${CMAKE_CURRENT_SOURCE_DIR}/.deps") + set(prebuilt_filename "macos-deps-VERSION-ARCH_REVISION.tar.xz") + set(prebuilt_destination "obs-deps-VERSION-ARCH") + set(obs-studio_filename "VERSION.tar.gz") + set(obs-studio_destination "obs-studio-VERSION") + set(dependencies_list prebuilt obs-studio) + + _check_dependencies() + + execute_process( + COMMAND "xattr" -r -d com.apple.quarantine "${dependencies_dir}" + RESULT_VARIABLE result + COMMAND_ERROR_IS_FATAL ANY + ) + + list(APPEND CMAKE_FRAMEWORK_PATH "${dependencies_dir}/Frameworks") + set(CMAKE_FRAMEWORK_PATH ${CMAKE_FRAMEWORK_PATH} PARENT_SCOPE) +endfunction() + +_check_dependencies_macos() diff --git a/cmake/windows/buildspec.cmake b/cmake/windows/buildspec.cmake new file mode 100644 index 0000000..c6fe484 --- /dev/null +++ b/cmake/windows/buildspec.cmake @@ -0,0 +1,28 @@ +# CMake Windows build dependencies module +# +# Adapted from obsproject/obs-plugintemplate. Only change from upstream: qt6 +# is dropped from dependencies_list. This plugin's properties UI is plain +# obs_properties_*, it never links Qt, and the OBS sub-build is configured +# with ENABLE_UI=OFF -- so downloading a ~100 MB Qt bundle on every CI run +# would buy nothing. + +include_guard(GLOBAL) + +include(buildspec_common) + +# _check_dependencies_windows: Set up Windows slice for _check_dependencies +function(_check_dependencies_windows) + set(arch ${CMAKE_VS_PLATFORM_NAME}) + set(platform windows-${arch}) + + set(dependencies_dir "${CMAKE_CURRENT_SOURCE_DIR}/.deps") + set(prebuilt_filename "windows-deps-VERSION-ARCH-REVISION.zip") + set(prebuilt_destination "obs-deps-VERSION-ARCH") + set(obs-studio_filename "VERSION.zip") + set(obs-studio_destination "obs-studio-VERSION") + set(dependencies_list prebuilt obs-studio) + + _check_dependencies() +endfunction() + +_check_dependencies_windows() diff --git a/core/tests/test_json.cpp b/core/tests/test_json.cpp index de17412..e1fbf05 100644 --- a/core/tests/test_json.cpp +++ b/core/tests/test_json.cpp @@ -17,6 +17,7 @@ with this program. If not, see */ #include +#include #include "stplugin/json.h" #include "test_util.h" @@ -50,6 +51,24 @@ static void testRealResponses() ST_ASSERT_EQ(error["error"].asString(), std::string("not found")); } +// One backslash, as it appears in the JSON *text* being parsed. +// +// Every JSON input below that contains a backslash is built by concatenation +// rather than written as a literal. Two separate portability problems make +// the obvious spellings unsafe, both observed on the Windows CI runner: +// - MSVC still forms escape sequences and universal-character-names inside +// RAW string literals, which it must not: R"( ... backslash-u-d-8-3-d ... )" +// is a hard compile error ("a universal-character-name specifies an +// invalid character"), and a raw string containing backslash-slash is an +// "illegal escape sequence". +// - A doubled backslash immediately followed by 'u' inside an ordinary +// literal sits on a genuinely murky corner of translation phase 1, where +// compilers have historically disagreed about whether a +// universal-character-name is formed. +// Concatenation sidesteps both: no backslash is ever adjacent to a 'u' in +// the source text at all. +static const std::string kBS = "\\"; + static void testScalarsAndEscapes() { ST_ASSERT(parse("null").isNull()); @@ -59,14 +78,20 @@ static void testScalarsAndEscapes() ST_ASSERT_EQ(parse("-12").asNumber(), -12.0); ST_ASSERT_EQ(parse("1.5e2").asNumber(), 150.0); ST_ASSERT_EQ(parse("\"\"").asString("x"), std::string("")); - ST_ASSERT_EQ(parse(R"("a\"b\\c\/d")").asString(), std::string("a\"b\\c/d")); - ST_ASSERT_EQ(parse(R"("\n\t\r\b\f")").asString(), std::string("\n\t\r\b\f")); - // \u escapes, including a surrogate pair (an emoji in a display name is - // entirely plausible and must not corrupt the dropdown). - ST_ASSERT_EQ(parse(R"("\u0041")").asString(), std::string("A")); - ST_ASSERT_EQ(parse(R"("caf\u00e9")").asString(), std::string("caf\xc3\xa9")); - ST_ASSERT_EQ(parse(R"("\ud83d\ude00")").asString(), std::string("\xf0\x9f\x98\x80")); + // "a\"b\\c\/d" -> a"b\c/d + ST_ASSERT_EQ(parse("\"a" + kBS + "\"b" + kBS + kBS + "c" + kBS + "/d\"").asString(), + std::string("a\"b\\c/d")); + // "\n\t\r\b\f" + ST_ASSERT_EQ(parse("\"" + kBS + "n" + kBS + "t" + kBS + "r" + kBS + "b" + kBS + "f\"").asString(), + std::string("\n\t\r\b\f")); + + // \uXXXX escapes, including a surrogate pair (an emoji in a display name + // is entirely plausible and must not corrupt the dropdown). + ST_ASSERT_EQ(parse("\"" + kBS + "u0041\"").asString(), std::string("A")); + ST_ASSERT_EQ(parse("\"caf" + kBS + "u00e9\"").asString(), std::string("caf\xc3\xa9")); + ST_ASSERT_EQ(parse("\"" + kBS + "ud83d" + kBS + "ude00\"").asString(), + std::string("\xf0\x9f\x98\x80")); // Whitespace everywhere legal. ST_ASSERT_EQ(parse(" {\n \"a\" :\t[ 1 , 2 ]\r\n} ")["a"].size(), std::size_t(2)); @@ -74,7 +99,7 @@ static void testScalarsAndEscapes() static void testMalformedIsRejectedNotCrashed() { - const char *bad[] = { + const std::vector bad = { "", " ", "{", @@ -88,12 +113,12 @@ static void testMalformedIsRejectedNotCrashed() "{a:1}", "{'a':1}", "\"unterminated", - "\"bad\\escape\"", - "\"\\u00\"", - "\"\\uZZZZ\"", - "\"\\ud83d\"", // lone high surrogate - "\"\\ude00\"", // lone low surrogate - "01", // leading zero + "\"bad" + kBS + "escape\"", // not a JSON escape character + "\"" + kBS + "u00\"", // truncated code point + "\"" + kBS + "uZZZZ\"", // non-hex code point + "\"" + kBS + "ud83d\"", // lone high surrogate + "\"" + kBS + "ude00\"", // lone low surrogate + "01", // leading zero "+1", ".5", "1.", @@ -101,13 +126,13 @@ static void testMalformedIsRejectedNotCrashed() "1e+", "tru", "nulll", - "{}garbage", // trailing content + "{}garbage", // trailing content "[1,2] [3]", - "\"raw\ncontrol\"", // literal control char inside a string - "\xff\xfe", // binary garbage, e.g. an HTML error page prefix + "\"raw\ncontrol\"", // literal control char inside a string + "\xff\xfe", // binary garbage "502 Bad Gateway", }; - for (const char *text : bad) { + for (const std::string &text : bad) { const Value v = parse(text); ST_ASSERT(!v.valid()); // Accessors on an invalid value must still be safe and return the