Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
238 changes: 238 additions & 0 deletions pixi.lock

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion pixi.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[workspace]
name = "netft-cpp"
version = "0.2.0"
version = "0.2.1"
channels = ["conda-forge"]
platforms = ["linux-64", "linux-aarch64"]

Expand All @@ -16,6 +16,8 @@ clang-tools = "*"
lcov = "*"
gcovr = "*"
python = "*"
actionlint = "*"
yq = "*"

[tasks]
configure = "cmake -S . -B build -G Ninja -DBUILD_TESTING=ON"
Expand All @@ -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.*"
Expand Down
56 changes: 50 additions & 6 deletions src/detail/xml_config.cpp
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
#include "detail/xml_config.hpp"

#include <array>
#include <charconv>
#include <cfenv>
#include <cmath>
#include <exception>
#include <locale>
#include <sstream>
#include <string>
#include <system_error>
#include <vector>

#include "netft/discovery.hpp"
Expand Down Expand Up @@ -64,6 +66,46 @@ struct OpenElement {

using RequiredFields = std::array<std::string, kRequiredTags.size()>;

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<OpenElement> &elements, RequiredFields &fields,
std::string_view text) {
if (!elements.empty() && elements.back().required_index >= 0) {
Expand Down Expand Up @@ -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<char>::eof() || !std::isfinite(result) || result <= 0.0) {
throw DiscoveryError("sensor configuration field '" + std::string{tag} +
"' must be a finite positive number");
}
Expand Down
9 changes: 7 additions & 2 deletions src/discovery.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@

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";

Check failure

Code scanning / CodeQL

Failure to use HTTPS URLs High

This URL may be constructed with the HTTP protocol.
Comment thread
han-xudong marked this conversation as resolved.
Dismissed

struct CurlHandleDeleter {
void operator()(CURL *handle) const noexcept { curl_easy_cleanup(handle); }
};
Expand Down Expand Up @@ -134,15 +139,15 @@
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");

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<long>(CURLPROTO_HTTP));
#endif
Expand Down
29 changes: 29 additions & 0 deletions test/codeql_workflow_test.sh
Original file line number Diff line number Diff line change
@@ -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
25 changes: 25 additions & 0 deletions test/gcc10_compatibility_test.sh
Original file line number Diff line number Diff line change
@@ -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}"
Loading
Loading