diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c6c89e3..4c585fa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,6 +25,8 @@ jobs: run: pixi run format-check - name: Run clang-tidy run: pixi run tidy + - name: Validate workflow configuration + run: pixi run workflow-check - name: Test release publication states run: bash test/release_publish_test.sh @@ -82,6 +84,17 @@ jobs: - name: Test shared and static consumers run: pixi run -e cmake-316 cmake-316-test + gcc-10: + name: GCC 10 compatibility + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v7 + with: + persist-credentials: false + - uses: prefix-dev/setup-pixi@v0.10.0 + - name: Build and test with GCC 10 + run: bash test/gcc10_compatibility_test.sh + sanitizers: name: ASan and UBSan runs-on: ubuntu-24.04 diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index d5ac10b..e4b753e 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -28,6 +28,7 @@ jobs: with: languages: c-cpp build-mode: manual + packs: +codeql/cpp-queries:AlertSuppression.ql - name: Configure run: >- pixi run cmake -S . -B build/codeql -G Ninja diff --git a/CHANGELOG.md b/CHANGELOG.md index 20658ae..86d6a10 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project are documented in this file. +## 0.2.1 - 2026-07-25 + +### Fixed + +- Restore GCC 10 and manylinux2014 source compatibility by parsing sensor calibration counts with a C++17 locale-independent implementation. + ## 0.2.0 - 2026-07-24 ### Added diff --git a/CMakeLists.txt b/CMakeLists.txt index fdc5e84..6caf0a1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.16) -project(netft VERSION 0.2.0 LANGUAGES CXX) +project(netft VERSION 0.2.1 LANGUAGES CXX) include(CTest) include(GNUInstallDirs) diff --git a/pixi.lock b/pixi.lock index bf309f2..8f16963 100644 --- a/pixi.lock +++ b/pixi.lock @@ -19,6 +19,7 @@ environments: packages: linux-64: - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/actionlint-1.7.12-h8bc977c_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/binutils-2.46.1-default_h4852527_102.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/binutils_impl_linux-64-2.46.1-bootstrap_h59bd682_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/binutils_linux-64-2.46.1-bootstrap_h4852527_2.conda @@ -42,6 +43,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/gxx_impl_linux-64-14.3.0-h2185e75_19.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/gxx_linux-64-14.3.0-hd240bd5_27.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/icu-73.2-h59595ed_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/jq-1.8.2-h280c20c_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.3-hb9d3cd8_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.21.3-h659f571_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/lcov-1.16-ha770c72_0.tar.bz2 @@ -77,17 +79,21 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.3-py312h8a5da7c_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.6-hdb14827_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ninja-1.13.2-h171cf75_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/oniguruma-6.9.10-hb9d3cd8_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.3-h35e630c_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/perl-5.32.1-7_hd590300_perl5.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.12.3-hab00c5b_0_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.3-py312h8a5da7c_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.3-h853b02a_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/rhash-1.4.3-hd590300_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h4845f30_101.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/xz-5.8.3-ha02ee65_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/xz-gpl-tools-5.8.3-ha02ee65_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/xz-tools-5.8.3-hb03c661_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h280c20c_3.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/zlib-1.2.13-h4ab18f5_6.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.6-ha6fb4c9_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/argcomplete-3.7.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.6.17-hbd8a1cb_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/colorlog-6.11.0-pyh8f84b5b_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/gcovr-8.3-pyhd8ed1ab_0.conda @@ -99,9 +105,13 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-8_cp312.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sysroot_linux-64-2.28-h4ee821c_9.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomlkit-0.15.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2026c-h151e31d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/xmltodict-1.0.4-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/yq-4.1.2-pyhcf101f3_1.conda linux-aarch64: - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/_openmp_mutex-4.5-20_gnu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/actionlint-1.7.12-hddfeb4d_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/binutils-2.46.1-default_hf1166c9_102.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/binutils_impl_linux-aarch64-2.46.1-bootstrap_h7a78c10_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/binutils_linux-aarch64-2.46.1-bootstrap_hf1166c9_2.conda @@ -125,6 +135,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/gxx_impl_linux-aarch64-14.3.0-h0d4f5d4_19.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/gxx_linux-aarch64-14.3.0-h0da240b_27.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/icu-73.2-h787c7f5_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/jq-1.8.2-h80f16a2_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/keyutils-1.6.3-h86ecc28_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/krb5-1.21.3-h50a48e9_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/lcov-1.16-h8af1aa0_0.tar.bz2 @@ -160,17 +171,21 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/markupsafe-3.0.3-py312hd077ced_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ncurses-6.6-hf8d1292_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ninja-1.13.2-hdc560ac_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/oniguruma-6.9.10-h86ecc28_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/openssl-3.6.3-h546c87b_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/perl-5.32.1-7_h31becfc_perl5.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/python-3.12.3-h43d1f9e_0_cpython.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pyyaml-6.0.3-py312ha4530ae_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/readline-8.3-hb682ff5_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/rhash-1.4.3-h31becfc_2.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/tk-8.6.13-h194ca79_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xz-5.8.3-hd704e39_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xz-gpl-tools-5.8.3-hd704e39_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/xz-tools-5.8.3-he30d5cf_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/yaml-0.2.5-h80f16a2_3.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/zlib-1.2.13-h68df207_6.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/zstd-1.5.6-h02f22dd_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/argcomplete-3.7.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.6.17-hbd8a1cb_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/colorlog-6.11.0-pyh8f84b5b_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/gcovr-8.3-pyhd8ed1ab_0.conda @@ -182,13 +197,17 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.12-8_cp312.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sysroot_linux-aarch64-2.28-h585391f_9.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomlkit-0.15.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2026c-h151e31d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/xmltodict-1.0.4-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/yq-4.1.2-pyhcf101f3_1.conda default: channels: - url: https://conda.anaconda.org/conda-forge/ packages: linux-64: - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/actionlint-1.7.12-h8bc977c_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/binutils-2.46.1-default_h4852527_102.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/binutils_impl_linux-64-2.46.1-default_hfdba357_102.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/binutils_linux-64-2.46.1-default_h4852527_102.conda @@ -216,6 +235,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/gxx_impl_linux-64-14.3.0-h2185e75_19.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/gxx_linux-64-14.3.0-hd240bd5_27.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/icu-78.3-h33c6efd_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/jq-1.8.2-h280c20c_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.3-hb9d3cd8_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.22.2-hbde042b_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/lcov-1.16-ha770c72_0.tar.bz2 @@ -253,13 +273,17 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.3-py314h67df5f8_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.6-hdb14827_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/ninja-1.13.2-h171cf75_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/oniguruma-6.9.10-hb9d3cd8_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.3-h35e630c_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/perl-5.32.1-7_hd590300_perl5.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/python-3.14.6-habeac84_100_cp314.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.3-py314h67df5f8_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.3-h853b02a_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/rhash-1.4.6-hb9d3cd8_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_hd70dff1_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h280c20c_3.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.7-hb78ec9c_6.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/argcomplete-3.7.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.6.17-hbd8a1cb_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/colorlog-6.11.0-pyh8f84b5b_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/compiler-rt22_linux-64-22.1.8-hffcefe0_1.conda @@ -273,9 +297,13 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.14-8_cp314.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sysroot_linux-64-2.28-h4ee821c_9.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomlkit-0.15.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2026c-h151e31d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/xmltodict-1.0.4-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/yq-4.1.2-pyhcf101f3_1.conda linux-aarch64: - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/_openmp_mutex-4.5-20_gnu.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/actionlint-1.7.12-hddfeb4d_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/binutils-2.46.1-default_hf1166c9_102.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/binutils_impl_linux-aarch64-2.46.1-default_h5f4c503_102.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/binutils_linux-aarch64-2.46.1-default_hf1166c9_102.conda @@ -303,6 +331,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/gxx_impl_linux-aarch64-14.3.0-h0d4f5d4_19.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/gxx_linux-aarch64-14.3.0-h0da240b_27.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/icu-78.3-hcab7f73_1.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/jq-1.8.2-h80f16a2_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/keyutils-1.6.3-h86ecc28_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/krb5-1.22.2-h2fb54aa_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/lcov-1.16-h8af1aa0_0.tar.bz2 @@ -340,13 +369,17 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/markupsafe-3.0.3-py314hb76de3f_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ncurses-6.6-hf8d1292_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/ninja-1.13.2-hdc560ac_0.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/oniguruma-6.9.10-h86ecc28_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/openssl-3.6.3-h546c87b_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/perl-5.32.1-7_h31becfc_perl5.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/python-3.14.6-hc679e19_100_cp314.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pyyaml-6.0.3-py314h807365f_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/readline-8.3-hb682ff5_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/rhash-1.4.6-h86ecc28_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/tk-8.6.13-noxft_h5cf4473_3.conda + - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/yaml-0.2.5-h80f16a2_3.conda - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/zstd-1.5.7-h85ac4a6_6.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/argcomplete-3.7.0-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.6.17-hbd8a1cb_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/colorlog-6.11.0-pyh8f84b5b_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/compiler-rt22_linux-aarch64-22.1.8-hfefdfc9_1.conda @@ -360,7 +393,10 @@ environments: - conda: https://conda.anaconda.org/conda-forge/noarch/python_abi-3.14-8_cp314.conda - conda: https://conda.anaconda.org/conda-forge/noarch/sysroot_linux-aarch64-2.28-h585391f_9.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tomli-2.4.1-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/tomlkit-0.15.1-pyhcf101f3_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2026c-h151e31d_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/xmltodict-1.0.4-pyhcf101f3_0.conda + - conda: https://conda.anaconda.org/conda-forge/noarch/yq-4.1.2-pyhcf101f3_1.conda packages: - conda: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-20_gnu.conda build_number: 20 @@ -378,6 +414,18 @@ packages: - _openmp_mutex >=4.5 size: 28948 timestamp: 1770939786096 +- conda: https://conda.anaconda.org/conda-forge/linux-64/actionlint-1.7.12-h8bc977c_0.conda + sha256: 1fff5ee0d83045ef90104ca83f52b4c44feb4ab2036fc7903fe9733f50721209 + md5: bab393750db08f50eebaf96f50d18734 + depends: + - libgcc >=14 + - __glibc >=2.17 + - __glibc >=2.17,<3.0.a0 + license: MIT + license_family: MIT + run_exports: {} + size: 2131192 + timestamp: 1774975766537 - conda: https://conda.anaconda.org/conda-forge/linux-64/binutils-2.46.1-default_h4852527_102.conda sha256: 659c367ef49df7741749a2ad240b007d7df51b4f210505a78543deafb001e44c md5: e8452fe381cac5fff20563a07722dfa5 @@ -933,6 +981,19 @@ packages: - icu >=78.3,<79.0a0 size: 12709032 timestamp: 1784588496729 +- conda: https://conda.anaconda.org/conda-forge/linux-64/jq-1.8.2-h280c20c_0.conda + sha256: 74d0a62d64cbe84a916ab437f71c61bf113b29a2b75e80cb5cc440626eef947e + md5: e246309d3884a2b8a4cde89d17f4a1a3 + depends: + - oniguruma 6.9.* + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + - oniguruma >=6.9.10,<6.10.0a0 + license: MIT + license_family: MIT + run_exports: {} + size: 317303 + timestamp: 1782114905891 - conda: https://conda.anaconda.org/conda-forge/linux-64/keyutils-1.6.3-hb9d3cd8_0.conda sha256: 0960d06048a7185d3542d850986d807c6e37ca2e644342dd0c72feefcf26c2a4 md5: b38117a3c920364aff79f870c984b4a3 @@ -1720,6 +1781,19 @@ packages: run_exports: {} size: 186323 timestamp: 1763688260928 +- conda: https://conda.anaconda.org/conda-forge/linux-64/oniguruma-6.9.10-hb9d3cd8_0.conda + sha256: bbff8a60f70d5ebab138b564554f28258472e1e63178614562d4feee29d10da2 + md5: 6ce853cb231f18576d2db5c2d4cb473e + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=13 + license: BSD-2-Clause + license_family: BSD + run_exports: + weak: + - oniguruma >=6.9.10,<6.10.0a0 + size: 248670 + timestamp: 1735727084819 - conda: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.6.3-h35e630c_0.conda sha256: d48f5c22b9897c01e4dff3680f1f57ceb02711ab9c62f74339b080419dfad34b md5: 79dd2074b5cd5c5c6b2930514a11e22d @@ -1811,6 +1885,34 @@ packages: size: 36717183 timestamp: 1781255094700 python_site_packages_path: lib/python3.14/site-packages +- conda: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.3-py312h8a5da7c_1.conda + sha256: cb142bfd92f6e55749365ddc244294fa7b64db6d08c45b018ff1c658907bfcbf + md5: 15878599a87992e44c059731771591cb + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - python >=3.12,<3.13.0a0 + - python_abi 3.12.* *_cp312 + - yaml >=0.2.5,<0.3.0a0 + license: MIT + license_family: MIT + run_exports: {} + size: 198293 + timestamp: 1770223620706 +- conda: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.3-py314h67df5f8_1.conda + sha256: b318fb070c7a1f89980ef124b80a0b5ccf3928143708a85e0053cde0169c699d + md5: 2035f68f96be30dc60a5dfd7452c7941 + depends: + - __glibc >=2.17,<3.0.a0 + - libgcc >=14 + - python >=3.14,<3.15.0a0 + - python_abi 3.14.* *_cp314 + - yaml >=0.2.5,<0.3.0a0 + license: MIT + license_family: MIT + run_exports: {} + size: 202391 + timestamp: 1770223462836 - conda: https://conda.anaconda.org/conda-forge/linux-64/readline-8.3-h853b02a_0.conda sha256: 12ffde5a6f958e285aa22c191ca01bbd3d6e710aa852e00618fa6ddc59149002 md5: d7d95fc8287ea7bf33e0e7116d2b95ec @@ -1923,6 +2025,19 @@ packages: run_exports: {} size: 95955 timestamp: 1775825530484 +- conda: https://conda.anaconda.org/conda-forge/linux-64/yaml-0.2.5-h280c20c_3.conda + sha256: 6d9ea2f731e284e9316d95fa61869fe7bbba33df7929f82693c121022810f4ad + md5: a77f85f77be52ff59391544bfe73390a + depends: + - libgcc >=14 + - __glibc >=2.17,<3.0.a0 + license: MIT + license_family: MIT + run_exports: + weak: + - yaml >=0.2.5,<0.3.0a0 + size: 85189 + timestamp: 1753484064210 - conda: https://conda.anaconda.org/conda-forge/linux-64/zlib-1.2.13-h4ab18f5_6.conda sha256: 534824ea44939f3e59ca8ebb95e3ece6f50f9d2a0e69999fbc692311252ed6ac md5: 559d338a4234c2ad6e676f460a093e67 @@ -1978,6 +2093,16 @@ packages: - _openmp_mutex >=4.5 size: 28926 timestamp: 1770939656741 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/actionlint-1.7.12-hddfeb4d_0.conda + sha256: f05c5b7919974955fbb29dd28d3658ecac758fed89c3fd8223d1db97b139d722 + md5: ebe43da3051320d989a8ffbeb72b7a9c + depends: + - libgcc >=14 + license: MIT + license_family: MIT + run_exports: {} + size: 1905426 + timestamp: 1774975778273 - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/binutils-2.46.1-default_hf1166c9_102.conda sha256: 548d128bc257badd2d9dbdff8173eb693fcbec84ceeb0fa74cbfed141ed42ad9 md5: 7a2f1cdac4c9fd2e87533ff2d2dcb702 @@ -2517,6 +2642,18 @@ packages: - icu >=78.3,<79.0a0 size: 12870753 timestamp: 1784588696185 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/jq-1.8.2-h80f16a2_0.conda + sha256: ff3ba43cd42df6e9f5577076369be778b6a185f88441869f102cac4e097e3423 + md5: 3cbbda9c2023051b122aff36e148d81b + depends: + - oniguruma 6.9.* + - libgcc >=14 + - oniguruma >=6.9.10,<6.10.0a0 + license: MIT + license_family: MIT + run_exports: {} + size: 350428 + timestamp: 1782115224470 - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/keyutils-1.6.3-h86ecc28_0.conda sha256: 5ce830ca274b67de11a7075430a72020c1fb7d486161a82839be15c2b84e9988 md5: e7df0aab10b9cbb73ab2a467ebfaf8c7 @@ -3265,6 +3402,18 @@ packages: run_exports: {} size: 182666 timestamp: 1763688214250 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/oniguruma-6.9.10-h86ecc28_0.conda + sha256: c876e7bde137d7fdf065fb16e5e2969026fafa6420e71f93e4abe43bccbfa0c4 + md5: 35c0c3113a0cfd9381940ba7f01a961e + depends: + - libgcc >=13 + license: BSD-2-Clause + license_family: BSD + run_exports: + weak: + - oniguruma >=6.9.10,<6.10.0a0 + size: 253511 + timestamp: 1735729164909 - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/openssl-3.6.3-h546c87b_0.conda sha256: da4a5df42614166b69c2f6d8602fc1425f7aaa699f77c3bafb5c7fe69b3d9fb7 md5: fa6260b3e6eababf6ca85a7eb3336383 @@ -3354,6 +3503,34 @@ packages: size: 34900936 timestamp: 1781254861576 python_site_packages_path: lib/python3.14/site-packages +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pyyaml-6.0.3-py312ha4530ae_1.conda + sha256: 0ba02720b470150a8c6261a86ea4db01dcf121e16a3e3978a84e965d3fe9c39a + md5: 47018c13dbb26186b577fd8bd1823a44 + depends: + - libgcc >=14 + - python >=3.12,<3.13.0a0 + - python >=3.12,<3.13.0a0 *_cpython + - python_abi 3.12.* *_cp312 + - yaml >=0.2.5,<0.3.0a0 + license: MIT + license_family: MIT + run_exports: {} + size: 192182 + timestamp: 1770223431156 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/pyyaml-6.0.3-py314h807365f_1.conda + sha256: 496b5e65dfdd0aaaaa5de0dcaaf3bceea00fcb4398acf152f89e567c82ec1046 + md5: 9ae2c92975118058bd720e9ba2bb7c58 + depends: + - libgcc >=14 + - python >=3.14,<3.15.0a0 + - python >=3.14,<3.15.0a0 *_cp314 + - python_abi 3.14.* *_cp314 + - yaml >=0.2.5,<0.3.0a0 + license: MIT + license_family: MIT + run_exports: {} + size: 195678 + timestamp: 1770223441816 - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/readline-8.3-hb682ff5_0.conda sha256: fe695f9d215e9a2e3dd0ca7f56435ab4df24f5504b83865e3d295df36e88d216 md5: 3d49cad61f829f4f0e0611547a9cda12 @@ -3460,6 +3637,18 @@ packages: run_exports: {} size: 102970 timestamp: 1775828449899 +- conda: https://conda.anaconda.org/conda-forge/linux-aarch64/yaml-0.2.5-h80f16a2_3.conda + sha256: 66265e943f32ce02396ad214e27cb35f5b0490b3bd4f064446390f9d67fa5d88 + md5: 032d8030e4a24fe1f72c74423a46fb88 + depends: + - libgcc >=14 + license: MIT + license_family: MIT + run_exports: + weak: + - yaml >=0.2.5,<0.3.0a0 + size: 88088 + timestamp: 1753484092643 - conda: https://conda.anaconda.org/conda-forge/linux-aarch64/zlib-1.2.13-h68df207_6.conda sha256: 67d2e05bb76308ad2e6d8bd27d54e5f8d866d7900826a13f22b66ecacce02fed md5: 11012f81be8e7dae8495df7ec17c0cc5 @@ -3499,6 +3688,17 @@ packages: - zstd >=1.5.7,<1.6.0a0 size: 614429 timestamp: 1764777145593 +- conda: https://conda.anaconda.org/conda-forge/noarch/argcomplete-3.7.0-pyhcf101f3_0.conda + sha256: fca7deee4f28060413d483851703c1287351797c1f660fb9ab63f8d175576e3f + md5: 4553b661c03a2f435d8c889eca8f2075 + depends: + - python >=3.10 + - python + license: Apache-2.0 + license_family: APACHE + run_exports: {} + size: 46445 + timestamp: 1782888485558 - conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.6.17-hbd8a1cb_0.conda sha256: f8e3c730fa14ee3f170493779f06522c4acf89169f43db4f039727709b6419cf md5: a9965dd99f683c5f444428f896635716 @@ -3722,6 +3922,17 @@ packages: run_exports: {} size: 21561 timestamp: 1774492402955 +- conda: https://conda.anaconda.org/conda-forge/noarch/tomlkit-0.15.1-pyhcf101f3_0.conda + sha256: 5cf833b4d199688b7652fb322ecc134de9555e9444d9249750de9a153421ad0a + md5: e4942e2de7436ff28be7f5645cf3c8d6 + depends: + - python >=3.10 + - python + license: MIT + license_family: MIT + run_exports: {} + size: 49054 + timestamp: 1784287707171 - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2026c-h151e31d_0.conda sha256: b928c30ddcb0e3f544c6eade8352737e6e610e263276b90232db6a578ef899d8 md5: fcb489df604d100968b737f2cb6076c6 @@ -3729,3 +3940,30 @@ packages: run_exports: {} size: 118849 timestamp: 1784250406640 +- conda: https://conda.anaconda.org/conda-forge/noarch/xmltodict-1.0.4-pyhcf101f3_0.conda + sha256: 7588e77a5d3885145e693d8b98493952f6efac8f3fabb1c218cd0cbd1a739fad + md5: 0f02dbcae61967ced21fea829a8ee927 + depends: + - python >=3.10 + - python + license: MIT + license_family: MIT + run_exports: {} + size: 20673 + timestamp: 1771770296472 +- conda: https://conda.anaconda.org/conda-forge/noarch/yq-4.1.2-pyhcf101f3_1.conda + sha256: 440698e0564c9a17690e0fb4e6b4593fd1763c3f24e6af453b89ffbe4c4c2043 + md5: e870d742f01656105a37ed6524f711fb + depends: + - tomlkit >=0.11.6 + - argcomplete >=1.8.1 + - python >=3.10 + - pyyaml >=5.3.1 + - xmltodict >=0.11.0 + - jq >=1.8 + - python + license: Apache-2.0 + license_family: APACHE + run_exports: {} + size: 33373 + timestamp: 1784296404889 diff --git a/pixi.toml b/pixi.toml index 1063601..a9c9e25 100644 --- a/pixi.toml +++ b/pixi.toml @@ -1,6 +1,6 @@ [workspace] name = "netft-cpp" -version = "0.2.0" +version = "0.2.1" channels = ["conda-forge"] platforms = ["linux-64", "linux-aarch64"] @@ -16,6 +16,8 @@ clang-tools = "*" lcov = "*" gcovr = "*" python = "*" +actionlint = "*" +yq = "*" [tasks] configure = "cmake -S . -B build -G Ninja -DBUILD_TESTING=ON" @@ -30,6 +32,8 @@ format = "git ls-files -z '*.cpp' '*.hpp' | xargs -0 clang-format -i" format-check = "git ls-files -z '*.cpp' '*.hpp' | xargs -0 clang-format --dry-run --Werror" tidy = { cmd = "cmake -S . -B build/tidy -G Ninja -DBUILD_TESTING=OFF -DCMAKE_EXPORT_COMPILE_COMMANDS=ON && clang-tidy -p build/tidy --config-file=.clang-tidy $(git ls-files 'src/*.cpp' 'app/*.cpp')", depends-on = [] } coverage = "cmake -S . -B build/coverage -G Ninja -DCMAKE_BUILD_TYPE=Debug -DBUILD_SHARED_LIBS=ON -DBUILD_TESTING=ON -DCMAKE_CXX_FLAGS='--coverage -O0 -g' -DCMAKE_EXE_LINKER_FLAGS=--coverage -DCMAKE_SHARED_LINKER_FLAGS=--coverage && cmake --build build/coverage && ctest --test-dir build/coverage --output-on-failure && gcovr --root . --filter 'src/' --filter 'app/' --exclude 'test/' --exclude-unreachable-branches --exclude-throw-branches --xml-pretty --output coverage.xml build/coverage" +gcc-10-test = "bash test/gcc10_compatibility_test.sh" +workflow-check = "actionlint && bash test/codeql_workflow_test.sh" [feature.cmake-316.dependencies] cmake = "3.16.*" diff --git a/src/detail/xml_config.cpp b/src/detail/xml_config.cpp index 124c241..3e6bb03 100644 --- a/src/detail/xml_config.cpp +++ b/src/detail/xml_config.cpp @@ -1,10 +1,12 @@ #include "detail/xml_config.hpp" #include -#include +#include #include +#include +#include +#include #include -#include #include #include "netft/discovery.hpp" @@ -64,6 +66,46 @@ struct OpenElement { using RequiredFields = std::array; +class ScopedRoundToNearest { +public: + ScopedRoundToNearest() { + if (std::feholdexcept(&environment_) != 0) { + throw DiscoveryError("failed to hold the floating-point environment"); + } + active_ = true; + + const int rounding_mode = std::fegetround(); + if (rounding_mode == -1) { + restore_environment_or_terminate(); + throw DiscoveryError("failed to inspect the floating-point rounding mode"); + } + if (rounding_mode != FE_TONEAREST && std::fesetround(FE_TONEAREST) != 0) { + restore_environment_or_terminate(); + throw DiscoveryError("failed to select round-to-nearest floating-point parsing"); + } + } + + ~ScopedRoundToNearest() { + if (active_) { + restore_environment_or_terminate(); + } + } + + ScopedRoundToNearest(const ScopedRoundToNearest &) = delete; + ScopedRoundToNearest &operator=(const ScopedRoundToNearest &) = delete; + +private: + void restore_environment_or_terminate() noexcept { + active_ = false; + if (std::fesetenv(&environment_) != 0) { + std::terminate(); + } + } + + std::fenv_t environment_{}; + bool active_{false}; +}; + void append_required_text(const std::vector &elements, RequiredFields &fields, std::string_view text) { if (!elements.empty() && elements.back().required_index >= 0) { @@ -184,11 +226,13 @@ RequiredFields extract_required_fields(std::string_view xml) { } double parse_positive_count(std::string_view value, std::string_view tag) { + const ScopedRoundToNearest rounding_mode; double result{}; - const auto parsed = std::from_chars(value.data(), value.data() + value.size(), result, - std::chars_format::general); - if (parsed.ec != std::errc{} || parsed.ptr != value.data() + value.size() || - !std::isfinite(result) || result <= 0.0) { + std::istringstream input{std::string{value}}; + input.imbue(std::locale::classic()); + input >> std::noskipws >> result; + if (value.empty() || value.front() == '+' || !input || + input.peek() != std::char_traits::eof() || !std::isfinite(result) || result <= 0.0) { throw DiscoveryError("sensor configuration field '" + std::string{tag} + "' must be a finite positive number"); } diff --git a/src/discovery.cpp b/src/discovery.cpp index cd507f2..69eb48e 100644 --- a/src/discovery.cpp +++ b/src/discovery.cpp @@ -17,6 +17,11 @@ namespace { constexpr std::size_t kMaximumResponseBytes = 65'536; +// ATI Net F/T firmware exposes configuration through HTTP only; SECURITY.md defines the trusted +// network boundary for this unauthenticated device protocol. +// codeql[cpp/non-https-url] +constexpr const char *kAtiConfigurationProtocol = "http"; + struct CurlHandleDeleter { void operator()(CURL *handle) const noexcept { curl_easy_cleanup(handle); } }; @@ -134,7 +139,7 @@ SensorConfiguration discover_sensor(const DiscoveryOptions &options) { host = "[" + host + "]"; } const std::string port = std::to_string(options.http_port); - set_url_part(url.get(), CURLUPART_SCHEME, "http"); + set_url_part(url.get(), CURLUPART_SCHEME, kAtiConfigurationProtocol); set_url_part(url.get(), CURLUPART_HOST, host.c_str()); set_url_part(url.get(), CURLUPART_PORT, port.c_str()); set_url_part(url.get(), CURLUPART_PATH, "/netftapi2.xml"); @@ -142,7 +147,7 @@ SensorConfiguration discover_sensor(const DiscoveryOptions &options) { ResponseBuffer response; set_curl_option(handle.get(), CURLOPT_CURLU, url.get()); #if LIBCURL_VERSION_NUM >= 0x075500 - set_curl_option(handle.get(), CURLOPT_PROTOCOLS_STR, "http"); + set_curl_option(handle.get(), CURLOPT_PROTOCOLS_STR, kAtiConfigurationProtocol); #else set_curl_option(handle.get(), CURLOPT_PROTOCOLS, static_cast(CURLPROTO_HTTP)); #endif diff --git a/test/codeql_workflow_test.sh b/test/codeql_workflow_test.sh new file mode 100755 index 0000000..f42f716 --- /dev/null +++ b/test/codeql_workflow_test.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash + +set -euo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +workflow="${repo_root}/.github/workflows/codeql.yml" +expected="+codeql/cpp-queries:AlertSuppression.ql" + +packs="$( + yq -r \ + '.jobs.analyze.steps[] | select(.uses == "github/codeql-action/init@v4") | .with.packs' \ + "${workflow}" +)" + +queries="$( + yq -r \ + '.jobs.analyze.steps[] | select(.uses == "github/codeql-action/init@v4") | .with.queries' \ + "${workflow}" +)" + +if [[ "${packs}" != "${expected}" ]]; then + printf 'CodeQL init packs must append the alert-suppression query\n' >&2 + exit 1 +fi + +if [[ "${queries}" != "null" ]]; then + printf 'CodeQL init queries must remain unset\n' >&2 + exit 1 +fi diff --git a/test/gcc10_compatibility_test.sh b/test/gcc10_compatibility_test.sh new file mode 100755 index 0000000..12326e4 --- /dev/null +++ b/test/gcc10_compatibility_test.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash + +set -euo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +build_dir="${repo_root}/build/gcc-10" + +pixi exec \ + --spec "gxx_linux-64=10.4.*" \ + --spec cmake \ + --spec ninja \ + --spec gtest \ + --spec libcurl \ + bash -c ' + set -euo pipefail + repo_root="$1" + build_dir="$2" + cmake -S "${repo_root}" -B "${build_dir}" -G Ninja \ + -DCMAKE_CXX_COMPILER=x86_64-conda-linux-gnu-c++ \ + -DCMAKE_BUILD_TYPE=Release \ + -DBUILD_SHARED_LIBS=ON \ + -DBUILD_TESTING=ON + cmake --build "${build_dir}" + ctest --test-dir "${build_dir}" --output-on-failure + ' bash "${repo_root}" "${build_dir}" diff --git a/test/test_discovery.cpp b/test/test_discovery.cpp index f8ace75..030fb0b 100644 --- a/test/test_discovery.cpp +++ b/test/test_discovery.cpp @@ -1,14 +1,18 @@ #include #include +#include #include +#include #include #include +#include #include #include #include #include +#include #include #include #include @@ -63,6 +67,56 @@ std::string insert_after_root_open(std::string_view xml, std::string_view markup return result; } +class CommaDecimalPoint final : public std::numpunct { +protected: + char do_decimal_point() const override { return ','; } +}; + +class GlobalLocaleGuard { +public: + explicit GlobalLocaleGuard(const std::locale &replacement) + : original_{std::locale::global(replacement)} {} + + ~GlobalLocaleGuard() { std::locale::global(original_); } + + GlobalLocaleGuard(const GlobalLocaleGuard &) = delete; + GlobalLocaleGuard &operator=(const GlobalLocaleGuard &) = delete; + +private: + std::locale original_; +}; + +class FloatingPointEnvironmentGuard { +public: + FloatingPointEnvironmentGuard() : saved_{}, valid_{std::fegetenv(&saved_) == 0} {} + + ~FloatingPointEnvironmentGuard() { + if (valid_) { + static_cast(std::fesetenv(&saved_)); + } + } + + FloatingPointEnvironmentGuard(const FloatingPointEnvironmentGuard &) = delete; + FloatingPointEnvironmentGuard &operator=(const FloatingPointEnvironmentGuard &) = delete; + +private: + std::fenv_t saved_; + bool valid_; +}; + +#if defined(__linux__) && defined(__GLIBC__) +constexpr int kTrapSetupFailed = 1; +constexpr int kTrapControlUnsupported = 77; + +constexpr int unavailable_trap_control_exit_code() { +#if defined(__aarch64__) + return kTrapControlUnsupported; +#else + return kTrapSetupFailed; +#endif +} +#endif + netft::DiscoveryOptions options_for(const FakeHttpServer &server) { netft::DiscoveryOptions options; options.sensor_host = server.host(); @@ -119,6 +173,16 @@ TEST(XmlConfiguration, ParsesTheRealSensorFixtureExactly) { EXPECT_EQ(result.revision, 1U); } +TEST(XmlConfiguration, ParsesDecimalAndScientificCountsExactly) { + auto xml = replace_value(kValidXml, "cfgcpf", "1234.5"); + xml = replace_value(xml, "cfgcpt", "2.5e6"); + + const auto result = netft::detail::parse_sensor_configuration(xml); + + EXPECT_DOUBLE_EQ(result.calibration.counts_per_force_unit, 1234.5); + EXPECT_DOUBLE_EQ(result.calibration.counts_per_torque_unit, 2'500'000.0); +} + class RequiredXmlField : public ::testing::TestWithParam {}; TEST_P(RequiredXmlField, RejectsMissingFields) { @@ -164,12 +228,129 @@ INSTANTIATE_TEST_SUITE_P( EveryInvalidForm, InvalidXmlCount, ::testing::Values(InvalidCountCase{"cfgcpf", "not-a-number"}, InvalidCountCase{"cfgcpf", "10remaining"}, InvalidCountCase{"cfgcpf", "0"}, - InvalidCountCase{"cfgcpf", "-1"}, InvalidCountCase{"cfgcpf", "NaN"}, - InvalidCountCase{"cfgcpf", "inf"}, InvalidCountCase{"cfgcpt", "not-a-number"}, + InvalidCountCase{"cfgcpf", "-1"}, InvalidCountCase{"cfgcpf", "+1"}, + InvalidCountCase{"cfgcpf", "NaN"}, InvalidCountCase{"cfgcpf", "inf"}, + InvalidCountCase{"cfgcpf", "1e9999"}, InvalidCountCase{"cfgcpf", "1e-9999"}, + InvalidCountCase{"cfgcpt", "not-a-number"}, InvalidCountCase{"cfgcpt", "10remaining"}, InvalidCountCase{"cfgcpt", "0"}, InvalidCountCase{"cfgcpt", "-1"}, InvalidCountCase{"cfgcpt", "NaN"}, InvalidCountCase{"cfgcpt", "infinity"})); +TEST(XmlConfiguration, ParsesCountsIndependentlyOfTheGlobalLocale) { + const GlobalLocaleGuard locale_guard{std::locale{std::locale::classic(), new CommaDecimalPoint}}; + const auto xml = replace_value(kValidXml, "cfgcpf", "1234.5"); + + const auto result = netft::detail::parse_sensor_configuration(xml); + + EXPECT_DOUBLE_EQ(result.calibration.counts_per_force_unit, 1234.5); + EXPECT_THROW( + netft::detail::parse_sensor_configuration(replace_value(kValidXml, "cfgcpf", "1234,5")), + netft::DiscoveryError); +} + +TEST(XmlConfiguration, UsesRoundToNearestAndRestoresTheCallersRoundingMode) { + const FloatingPointEnvironmentGuard restore_environment; + ASSERT_EQ(std::fesetround(FE_UPWARD), 0); + const auto xml = + replace_value(kValidXml, "cfgcpf", "1.00000000000000011102230246251565404236316680908203125"); + + const auto result = netft::detail::parse_sensor_configuration(xml); + + EXPECT_EQ(result.calibration.counts_per_force_unit, 0x1p+0); + EXPECT_EQ(std::fegetround(), FE_UPWARD); +} + +TEST(XmlConfiguration, RestoresTheCallersRoundingModeWhenParsingThrows) { + const FloatingPointEnvironmentGuard restore_environment; + ASSERT_EQ(std::fesetround(FE_UPWARD), 0); + + EXPECT_THROW( + netft::detail::parse_sensor_configuration(replace_value(kValidXml, "cfgcpf", "invalid")), + netft::DiscoveryError); + EXPECT_EQ(std::fegetround(), FE_UPWARD); +} + +struct FloatingPointEnvironmentCase { + int rounding_mode; + const char *value; + bool succeeds; +}; + +class FloatingPointEnvironment : public ::testing::TestWithParam {}; + +TEST_P(FloatingPointEnvironment, PreservesTheCallersModeAndExceptionFlags) { + const FloatingPointEnvironmentGuard restore_environment; + ASSERT_EQ(std::feclearexcept(FE_ALL_EXCEPT), 0); + ASSERT_EQ(std::fesetround(GetParam().rounding_mode), 0); + ASSERT_EQ(std::feraiseexcept(FE_DIVBYZERO), 0); + const int expected_flags = std::fetestexcept(FE_ALL_EXCEPT); + const auto xml = replace_value(kValidXml, "cfgcpf", GetParam().value); + + if (GetParam().succeeds) { + const auto result = netft::detail::parse_sensor_configuration(xml); + EXPECT_EQ(result.calibration.counts_per_force_unit, 0x1p+0); + } else { + EXPECT_THROW(netft::detail::parse_sensor_configuration(xml), netft::DiscoveryError); + } + + EXPECT_EQ(std::fegetround(), GetParam().rounding_mode); + EXPECT_EQ(std::fetestexcept(FE_ALL_EXCEPT), expected_flags); +} + +INSTANTIATE_TEST_SUITE_P( + EveryRoundingModeAndResult, FloatingPointEnvironment, + ::testing::Values( + FloatingPointEnvironmentCase{ + FE_TONEAREST, "1.00000000000000011102230246251565404236316680908203125", true}, + FloatingPointEnvironmentCase{FE_TONEAREST, "1e9999", false}, + FloatingPointEnvironmentCase{FE_TONEAREST, "1e-9999", false}, + FloatingPointEnvironmentCase{FE_TONEAREST, "+1", false}, + FloatingPointEnvironmentCase{ + FE_UPWARD, "1.00000000000000011102230246251565404236316680908203125", true}, + FloatingPointEnvironmentCase{FE_UPWARD, "1e9999", false}, + FloatingPointEnvironmentCase{FE_UPWARD, "1e-9999", false}, + FloatingPointEnvironmentCase{FE_UPWARD, "+1", false})); + +#if defined(__linux__) && defined(__GLIBC__) +TEST(FloatingPointTrapPlatformPolicy, TreatsUnavailableTrapControlAsAnErrorWhereItIsSupported) { +#if defined(__aarch64__) + EXPECT_EQ(unavailable_trap_control_exit_code(), kTrapControlUnsupported); +#else + EXPECT_EQ(unavailable_trap_control_exit_code(), kTrapSetupFailed); +#endif +} + +TEST(XmlConfiguration, MasksCallerOverflowTrapsWhileParsing) { + const pid_t child = fork(); + ASSERT_NE(child, -1); + if (child == 0) { + if (std::feclearexcept(FE_ALL_EXCEPT) != 0) { + _exit(1); + } + if (feenableexcept(FE_OVERFLOW) == -1) { + _exit(unavailable_trap_control_exit_code()); + } + try { + static_cast( + netft::detail::parse_sensor_configuration(replace_value(kValidXml, "cfgcpf", "1e9999"))); + } catch (const netft::DiscoveryError &) { + _exit((fegetexcept() & FE_OVERFLOW) != 0 ? 0 : 2); + } catch (...) { + _exit(3); + } + _exit(4); + } + + int status{}; + ASSERT_EQ(waitpid(child, &status, 0), child); + ASSERT_TRUE(WIFEXITED(status)); + if (WEXITSTATUS(status) == kTrapControlUnsupported) { + GTEST_SKIP() << "floating-point trap control is unavailable on this platform"; + } + EXPECT_EQ(WEXITSTATUS(status), 0); +} +#endif + TEST(XmlConfiguration, TrimsAsciiWhitespaceAroundEveryValue) { auto xml = replace_value(kValidXml, "prodname", " \tEthernet Axia\r\n"); xml = replace_value(xml, "cfgcpf", "\n 1000000 \t");