From 22b7de424f245bb7e177eb0f8d86f2b28c8dbc41 Mon Sep 17 00:00:00 2001 From: Justin Kim Date: Thu, 30 Apr 2026 13:28:17 +0900 Subject: [PATCH 1/7] ci: lint + cross-platform build/test + sanitizers + ARM64 monitor Adapts the wirelog .github/workflows layout to libksuid. Four files, two pairs of "PR (hard gate) vs main (non-blocking monitor)". .github/workflows/lint-pr.yml hard-gated, called by ci-pr .github/workflows/lint-main.yml non-blocking, called by ci-main .github/workflows/ci-pr.yml on pull_request -> main .github/workflows/ci-main.yml on push -> main Lint phase enforces what the pre-commit hook (hooks/pre-commit.hook) already runs locally: - gst-indent: GNU indent under tools/gst-indent must produce no working-tree diff. Cheap, runs first, fails fast. - clang-tidy 18: any warning/error on libksuid/*.{c,h} blocks the PR. Driven from compile_commands.json that meson generates. Build/test matrix on PR: ubuntu-latest x { gcc, clang } macos-latest x clang windows-latest x msvc (cl.exe via vcvars64) The Linux GCC lane additionally runs DESTDIR meson install and asserts that LICENSE / LICENSE.MIT / NOTICE / libksuid.pc / libksuid/ksuid.h all land in the staging tree -- catching the kind of silent install-data drop the Critic flagged on the original meson.build. Sanitizers (ASan + UBSan) run as Phase 3 after the build matrix clears, on Linux GCC/Clang and macOS Clang. TSan is intentionally omitted: libksuid is built around _Thread_local + stdatomic with no pthread, and TSan does not instrument C11 cleanly. The ci-main.yml monitor adds an ubuntu-24.04-arm lane so the NEON SIMD path actually gets exercised under CI -- the x86_64 lanes only build the SSE2 kernel. It also emits a footprint table to the step summary (stripped sizes of libksuid.so.* / libksuid.a / ksuid-gen) so any size regression shows up against the README's 18 KB claim. All ci-main jobs run with continue-on-error: true so a flaky runner never blocks a merge that already landed. --- .github/workflows/ci-main.yml | 206 ++++++++++++++++++++++++++++++++ .github/workflows/ci-pr.yml | 172 ++++++++++++++++++++++++++ .github/workflows/lint-main.yml | 107 +++++++++++++++++ .github/workflows/lint-pr.yml | 130 ++++++++++++++++++++ 4 files changed, 615 insertions(+) create mode 100644 .github/workflows/ci-main.yml create mode 100644 .github/workflows/ci-pr.yml create mode 100644 .github/workflows/lint-main.yml create mode 100644 .github/workflows/lint-pr.yml diff --git a/.github/workflows/ci-main.yml b/.github/workflows/ci-main.yml new file mode 100644 index 0000000..d94e605 --- /dev/null +++ b/.github/workflows/ci-main.yml @@ -0,0 +1,206 @@ +name: CI Main + +# Non-blocking comprehensive monitoring on main pushes. Same matrix +# as ci-pr.yml plus an ARM64 lane that exercises the NEON SIMD path +# (since x86_64 hosts only build the SSE2 kernel). Every job runs to +# completion; failures show up in the step summary but never block. +# +# Phases: +# Phase 1 (Lint): lint-main.yml — gst-indent + clang-tidy +# Phase 2 (Build/Test): Linux GCC + Clang, macOS, Windows MSVC, ARM64 GCC +# Phase 3 (Sanitizers): Linux + macOS ASan + UBSan +# Phase 4 (Footprint): size table for the stripped library + +on: + push: + branches: [main] + +permissions: + contents: read + +jobs: + # ========================================================================== + # Phase 1: Lint monitoring + # ========================================================================== + lint: + uses: ./.github/workflows/lint-main.yml + + # ========================================================================== + # Phase 2: Build & Test (parallel, non-blocking) + # Adds ARM64 (ubuntu-24.04-arm) so the NEON SIMD path actually + # gets exercised under CI; x86_64 lanes only build the SSE2 kernel. + # ========================================================================== + build: + name: Build / ${{ matrix.os_display || matrix.os }} / ${{ matrix.compiler }} + runs-on: ${{ matrix.os }} + continue-on-error: true + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + compiler: gcc + cc: gcc + - os: ubuntu-latest + compiler: clang + cc: clang + - os: macos-latest + compiler: clang + cc: clang + - os: windows-latest + compiler: msvc + cc: cl + # GitHub-hosted ARM64 runner exercises the NEON SIMD path. + - os: ubuntu-24.04-arm + os_display: ubuntu-24.04-arm + compiler: gcc + cc: gcc + + steps: + - uses: actions/checkout@v5 + + - name: Install dependencies (Linux + ARM64) + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y meson ninja-build + if [ "${{ matrix.compiler }}" = "clang" ]; then + sudo apt-get install -y clang + fi + # ARM64 sanity: confirm /proc/cpuinfo advertises the SIMD ISA + if [ "${{ matrix.os }}" = "ubuntu-24.04-arm" ]; then + grep -q 'asimd\|neon' /proc/cpuinfo \ + && echo "ARM64 NEON / asimd advertised" \ + || echo "::warning::ARM64 NEON not detected" + fi + + - name: Install dependencies (macOS) + if: runner.os == 'macOS' + run: brew install meson ninja + + - name: Install dependencies (Windows) + if: runner.os == 'Windows' + run: pip install meson ninja + + - name: Configure (Linux / macOS) + if: runner.os != 'Windows' + run: meson setup builddir + env: + CC: ${{ matrix.cc }} + + - name: Configure (Windows / MSVC) + if: runner.os == 'Windows' + shell: cmd + run: | + call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat" + meson setup builddir + + - name: Build (Linux / macOS) + if: runner.os != 'Windows' + run: meson compile -C builddir + + - name: Build (Windows / MSVC) + if: runner.os == 'Windows' + shell: cmd + run: | + call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat" + meson compile -C builddir + + - name: Test (Linux / macOS) + if: runner.os != 'Windows' + run: meson test -C builddir --print-errorlogs + + - name: Test (Windows / MSVC) + if: runner.os == 'Windows' + shell: cmd + run: | + call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat" + meson test -C builddir --print-errorlogs + + - name: Footprint + if: runner.os == 'Linux' && matrix.compiler == 'gcc' + continue-on-error: true + run: | + rm -rf build-rel + meson setup build-rel --buildtype=release + meson compile -C build-rel + strip --strip-unneeded build-rel/libksuid.so.* 2>/dev/null || true + { + echo '## Footprint (stripped release build) — ${{ matrix.os }}' + echo '' + echo '| Artifact | Bytes |' + echo '|---|---:|' + for f in build-rel/libksuid.so.* build-rel/libksuid.a build-rel/ksuid-gen; do + [ -e "$f" ] && printf '| %s | %s |\n' "$(basename "$f")" "$(stat -c '%s' "$f")" + done + } >> "$GITHUB_STEP_SUMMARY" + + - name: Publish results + if: always() + shell: bash + run: | + echo "## Build Results: ${{ matrix.os_display || matrix.os }} / ${{ matrix.compiler }}" >> "$GITHUB_STEP_SUMMARY" + echo '' >> "$GITHUB_STEP_SUMMARY" + echo "Status: **${{ job.status }}** (non-blocking)" >> "$GITHUB_STEP_SUMMARY" + + # ========================================================================== + # Phase 3: Sanitizers (ASan + UBSan, non-blocking) + # ========================================================================== + sanitizers: + name: Sanitizers / ${{ matrix.os }} / ${{ matrix.compiler }} + runs-on: ${{ matrix.os }} + continue-on-error: true + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + compiler: gcc + cc: gcc + - os: ubuntu-latest + compiler: clang + cc: clang + - os: macos-latest + compiler: clang + cc: clang + + steps: + - uses: actions/checkout@v5 + + - name: Install dependencies (Linux) + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y meson ninja-build + if [ "${{ matrix.compiler }}" = "clang" ]; then + sudo apt-get install -y clang + fi + + - name: Install dependencies (macOS) + if: runner.os == 'macOS' + run: brew install meson ninja + + - name: Configure with sanitizers + run: > + meson setup builddir-san + -Db_sanitize=address,undefined + -Db_lundef=false + --buildtype=debug + env: + CC: ${{ matrix.cc }} + + - name: Build + run: meson compile -C builddir-san + + - name: Test + run: meson test -C builddir-san --print-errorlogs + env: + ASAN_OPTIONS: abort_on_error=1:halt_on_error=1 + UBSAN_OPTIONS: abort_on_error=1:halt_on_error=1:print_stacktrace=1 + + - name: Publish results + if: always() + run: | + echo "## Sanitizer Results: ${{ matrix.os }} / ${{ matrix.compiler }}" >> "$GITHUB_STEP_SUMMARY" + echo '' >> "$GITHUB_STEP_SUMMARY" + echo "Status: **${{ job.status }}** (non-blocking)" >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/ci-pr.yml b/.github/workflows/ci-pr.yml new file mode 100644 index 0000000..42eb57a --- /dev/null +++ b/.github/workflows/ci-pr.yml @@ -0,0 +1,172 @@ +name: CI PR + +# PR gate: 3-phase validation. Each phase blocks subsequent phases on +# failure so a busted formatter doesn't pay for a 30-minute build run. +# +# Phase 1 (Lint): lint-pr.yml — gst-indent -> clang-tidy +# Phase 2 (Build/Test): Linux GCC + Clang, macOS Clang, Windows MSVC +# Phase 3 (Sanitizers): Linux + macOS ASan + UBSan +# +# Within Phase 2 fail-fast is OFF so we get the full matrix's results +# even if one platform breaks first. Phase 3 only runs after Phase 2 +# matrix has cleared. + +on: + pull_request: + branches: [main] + +permissions: + contents: read + +jobs: + # ========================================================================== + # Phase 1: Lint (hard gates, sequential) + # ========================================================================== + lint: + uses: ./.github/workflows/lint-pr.yml + + # ========================================================================== + # Phase 2: Build & Test matrix + # Linux GCC, Linux Clang, macOS Clang, Windows MSVC + # ========================================================================== + build: + name: Build / ${{ matrix.os }} / ${{ matrix.compiler }} + needs: [lint] + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + compiler: gcc + cc: gcc + - os: ubuntu-latest + compiler: clang + cc: clang + - os: macos-latest + compiler: clang + cc: clang + - os: windows-latest + compiler: msvc + cc: cl + + steps: + - uses: actions/checkout@v5 + + - name: Install dependencies (Linux) + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y meson ninja-build + if [ "${{ matrix.compiler }}" = "clang" ]; then + sudo apt-get install -y clang + fi + + - name: Install dependencies (macOS) + if: runner.os == 'macOS' + run: brew install meson ninja + + - name: Install dependencies (Windows) + if: runner.os == 'Windows' + run: pip install meson ninja + + - name: Configure (Linux / macOS) + if: runner.os != 'Windows' + run: meson setup builddir + env: + CC: ${{ matrix.cc }} + + - name: Configure (Windows / MSVC) + if: runner.os == 'Windows' + shell: cmd + run: | + call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat" + meson setup builddir + + - name: Build (Linux / macOS) + if: runner.os != 'Windows' + run: meson compile -C builddir + + - name: Build (Windows / MSVC) + if: runner.os == 'Windows' + shell: cmd + run: | + call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat" + meson compile -C builddir + + - name: Test (Linux / macOS) + if: runner.os != 'Windows' + run: meson test -C builddir --print-errorlogs + + - name: Test (Windows / MSVC) + if: runner.os == 'Windows' + shell: cmd + run: | + call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat" + meson test -C builddir --print-errorlogs + + - name: Verify install lays down license artifacts + if: runner.os == 'Linux' + run: | + DESTDIR="$PWD/staging" meson install -C builddir + test -f staging/usr/local/include/libksuid/ksuid.h + test -f staging/usr/local/share/doc/libksuid/LICENSE + test -f staging/usr/local/share/doc/libksuid/LICENSE.MIT + test -f staging/usr/local/share/doc/libksuid/NOTICE + test -f staging/usr/local/lib/pkgconfig/libksuid.pc + + # ========================================================================== + # Phase 3: Sanitizers (ASan + UBSan) + # Depends on Phase 2 build matrix passing. + # ========================================================================== + sanitizers: + name: Sanitizers / ${{ matrix.os }} / ${{ matrix.compiler }} + needs: [build] + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + compiler: gcc + cc: gcc + - os: ubuntu-latest + compiler: clang + cc: clang + - os: macos-latest + compiler: clang + cc: clang + + steps: + - uses: actions/checkout@v5 + + - name: Install dependencies (Linux) + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y meson ninja-build + if [ "${{ matrix.compiler }}" = "clang" ]; then + sudo apt-get install -y clang + fi + + - name: Install dependencies (macOS) + if: runner.os == 'macOS' + run: brew install meson ninja + + - name: Configure with sanitizers + run: > + meson setup builddir-san + -Db_sanitize=address,undefined + -Db_lundef=false + --buildtype=debug + env: + CC: ${{ matrix.cc }} + + - name: Build + run: meson compile -C builddir-san + + - name: Test + run: meson test -C builddir-san --print-errorlogs + env: + ASAN_OPTIONS: abort_on_error=1:halt_on_error=1 + UBSAN_OPTIONS: abort_on_error=1:halt_on_error=1:print_stacktrace=1 diff --git a/.github/workflows/lint-main.yml b/.github/workflows/lint-main.yml new file mode 100644 index 0000000..a4a04c0 --- /dev/null +++ b/.github/workflows/lint-main.yml @@ -0,0 +1,107 @@ +name: Lint Main + +# Non-blocking lint monitoring for main-branch pushes. +# +# Same checks as lint-pr.yml but runs both jobs in parallel and uses +# continue-on-error: true so findings surface in the step summary +# without ever blocking a merge that has already landed. ci-main.yml +# imports this workflow as Phase 1. + +on: + workflow_call: + +permissions: + contents: read + +jobs: + # ========================================================================== + # Check 1: gst-indent (fast) + # Non-blocking: deviations from tools/gst-indent surface in the summary. + # ========================================================================== + gst-indent-check: + name: gst-indent code style (monitor) + runs-on: ubuntu-latest + continue-on-error: true + steps: + - uses: actions/checkout@v5 + + - name: Install GNU indent + run: | + sudo apt-get update + sudo apt-get install -y indent + indent --version + + - name: Run gst-indent + run: | + find libksuid examples tests \ + \( -name '*.c' -o -name '*.h' \) -print0 \ + | xargs -0 -r ./tools/gst-indent + find libksuid examples tests -name '*~' -delete + git diff > format-results.txt || true + + - name: Publish results + if: always() + run: | + echo '## gst-indent Results (main monitor)' >> "$GITHUB_STEP_SUMMARY" + echo '' >> "$GITHUB_STEP_SUMMARY" + if [ -s format-results.txt ]; then + count=$(wc -l < format-results.txt | tr -d ' ') + echo "**${count} diff line(s) — non-blocking**" >> "$GITHUB_STEP_SUMMARY" + echo '' >> "$GITHUB_STEP_SUMMARY" + echo '```diff' >> "$GITHUB_STEP_SUMMARY" + head -200 format-results.txt >> "$GITHUB_STEP_SUMMARY" + echo '```' >> "$GITHUB_STEP_SUMMARY" + else + echo 'No formatting issues found.' >> "$GITHUB_STEP_SUMMARY" + fi + + # ========================================================================== + # Check 2: clang-tidy 18 (slow, up to 30 min) + # Non-blocking: findings reported as artifacts + summary. + # ========================================================================== + clang-tidy-check: + name: clang-tidy 18 (monitor) + runs-on: ubuntu-latest + continue-on-error: true + timeout-minutes: 30 + steps: + - uses: actions/checkout@v5 + + - name: Install LLVM 18 + meson + run: | + wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key \ + | sudo tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc + echo "deb https://apt.llvm.org/$(lsb_release -cs)/ llvm-toolchain-$(lsb_release -cs)-18 main" \ + | sudo tee /etc/apt/sources.list.d/llvm-18.list + sudo apt-get update + sudo apt-get install -y clang-tidy-18 clang-tools-18 \ + meson ninja-build + + - name: Configure build (for compile_commands.json) + run: meson setup builddir + + - name: Run clang-tidy + run: | + run-clang-tidy-18 -p builddir \ + "^.*/libksuid/.*\.(c|h)$" \ + 2>&1 | tee tidy-results.txt || true + + - name: Publish results + if: always() + run: | + echo '## clang-tidy Results (main monitor)' >> "$GITHUB_STEP_SUMMARY" + echo '' >> "$GITHUB_STEP_SUMMARY" + if [ -s tidy-results.txt ]; then + echo '```' >> "$GITHUB_STEP_SUMMARY" + tail -50 tidy-results.txt >> "$GITHUB_STEP_SUMMARY" + echo '```' >> "$GITHUB_STEP_SUMMARY" + else + echo 'No issues found.' >> "$GITHUB_STEP_SUMMARY" + fi + + - name: Upload tidy results + if: always() + uses: actions/upload-artifact@v4 + with: + name: tidy-results-main + path: tidy-results.txt diff --git a/.github/workflows/lint-pr.yml b/.github/workflows/lint-pr.yml new file mode 100644 index 0000000..502529b --- /dev/null +++ b/.github/workflows/lint-pr.yml @@ -0,0 +1,130 @@ +name: Lint PR + +# PR-specific lint workflow with hard-gate semantics. Mirrors the +# pre-commit hook (hooks/pre-commit.hook) so anything that lands on +# main is byte-identical to the gst-indent output. +# +# Sequenced so the cheap formatter check fails fast before the slower +# clang-tidy pass spins up: +# gst-indent -> hard gate (formatting must match tools/gst-indent) +# clang-tidy -> hard gate (no warnings/errors on libksuid sources) +# +# This workflow is imported by ci-pr.yml as Phase 1. + +on: + workflow_call: + +permissions: + contents: read + +jobs: + # ========================================================================== + # Phase 1a: gst-indent code-style check (fast, ~seconds) + # Runs the same formatter the pre-commit hook does, then asserts the + # working tree is unchanged. Any deviation blocks the PR. + # ========================================================================== + gst-indent-check: + name: gst-indent code style + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + + - name: Install GNU indent + run: | + sudo apt-get update + sudo apt-get install -y indent + indent --version + + - name: Run gst-indent (hard gate) + run: | + set -o pipefail + # Format every C source/header in-place using the same + # wrapper the pre-commit hook validates against. + find libksuid examples tests \ + \( -name '*.c' -o -name '*.h' \) -print0 \ + | xargs -0 -r ./tools/gst-indent + # GNU indent leaves ~ backup files; clean them up so the + # diff check below only sees real reformatting deltas. + find libksuid examples tests -name '*~' -delete + if ! git diff --exit-code; then + echo "::error::gst-indent introduced changes. Run \ + ./tools/gst-indent on the affected files locally and \ + commit the result." + exit 1 + fi + + - name: Publish results + if: always() + run: | + echo '## gst-indent Results' >> "$GITHUB_STEP_SUMMARY" + echo '' >> "$GITHUB_STEP_SUMMARY" + if git diff --quiet; then + echo 'No formatting issues found.' >> "$GITHUB_STEP_SUMMARY" + else + echo '**Formatting violations found — PR blocked**' >> "$GITHUB_STEP_SUMMARY" + echo '' >> "$GITHUB_STEP_SUMMARY" + echo '```diff' >> "$GITHUB_STEP_SUMMARY" + git diff | head -200 >> "$GITHUB_STEP_SUMMARY" + echo '```' >> "$GITHUB_STEP_SUMMARY" + fi + + # ========================================================================== + # Phase 1b: clang-tidy 18 (slow, up to 30 min) + # Runs only after gst-indent passes. Any warning/error fails the PR. + # ========================================================================== + clang-tidy-check: + name: clang-tidy 18 + needs: [gst-indent-check] + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v5 + + - name: Install LLVM 18 + run: | + wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key \ + | sudo tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc + echo "deb https://apt.llvm.org/$(lsb_release -cs)/ llvm-toolchain-$(lsb_release -cs)-18 main" \ + | sudo tee /etc/apt/sources.list.d/llvm-18.list + sudo apt-get update + sudo apt-get install -y clang-tidy-18 clang-tools-18 \ + meson ninja-build + + - name: Verify tool versions + run: | + clang-tidy-18 --version + meson --version + + - name: Configure build (for compile_commands.json) + run: meson setup builddir + + - name: Run clang-tidy (hard gate) + run: | + set -o pipefail + run-clang-tidy-18 -p builddir \ + "^.*/libksuid/.*\.(c|h)$" \ + 2>&1 | tee tidy-results.txt + if grep -qE '^.*\.(c|h):[0-9]+:[0-9]+: (warning|error):' tidy-results.txt; then + echo "::error::clang-tidy reported issues." + exit 1 + fi + + - name: Publish results + if: always() + run: | + echo '## clang-tidy Results' >> "$GITHUB_STEP_SUMMARY" + echo '' >> "$GITHUB_STEP_SUMMARY" + if [ -s tidy-results.txt ]; then + echo '```' >> "$GITHUB_STEP_SUMMARY" + tail -50 tidy-results.txt >> "$GITHUB_STEP_SUMMARY" + echo '```' >> "$GITHUB_STEP_SUMMARY" + else + echo 'No issues found.' >> "$GITHUB_STEP_SUMMARY" + fi + + - name: Upload tidy results + if: always() + uses: actions/upload-artifact@v4 + with: + name: tidy-results-pr + path: tidy-results.txt From d4e77e6885703b618ad9a18b9cc076e439431296 Mon Sep 17 00:00:00 2001 From: Justin Kim Date: Thu, 30 Apr 2026 13:33:38 +0900 Subject: [PATCH 2/7] ci: bump clang-tidy from LLVM 18 to LLVM 22 Wirelog's workflow templates pinned LLVM 18 because that was the current stable when wirelog adopted them. As of 2026-04-30 the apt. llvm.org noble repo publishes packages for LLVM 17, 18, 19, 20, 21, and 22, and LLVM 22's Release index is dated 2026-04-03 -- a four- week-old stable. Two-year-old static-analysis output is not what we want to gate PRs on. Updates both lint-pr.yml (hard gate) and lint-main.yml (monitor): - apt source line: llvm-toolchain-noble-18 -> -22 - packages: clang-tidy-18 / clang-tools-18 -> -22 - tool invocation: run-clang-tidy-18 -> run-clang-tidy-22 - human-facing names + comments: "LLVM 18" / "clang-tidy 18" -> "LLVM 22" / "clang-tidy 22" No behaviour change beyond the version bump itself; the regex that restricts findings to libksuid/*.{c,h} and the workflow_call plumbing into ci-pr.yml / ci-main.yml are untouched. If LLVM 22 introduces new default checks that flag legitimate-but-known patterns we will add targeted // NOLINTNEXTLINE comments alongside the fix, rather than disable checks globally. --- .github/workflows/lint-main.yml | 12 ++++++------ .github/workflows/lint-pr.yml | 14 +++++++------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/lint-main.yml b/.github/workflows/lint-main.yml index a4a04c0..38cafdb 100644 --- a/.github/workflows/lint-main.yml +++ b/.github/workflows/lint-main.yml @@ -56,25 +56,25 @@ jobs: fi # ========================================================================== - # Check 2: clang-tidy 18 (slow, up to 30 min) + # Check 2: clang-tidy 22 (slow, up to 30 min) # Non-blocking: findings reported as artifacts + summary. # ========================================================================== clang-tidy-check: - name: clang-tidy 18 (monitor) + name: clang-tidy 22 (monitor) runs-on: ubuntu-latest continue-on-error: true timeout-minutes: 30 steps: - uses: actions/checkout@v5 - - name: Install LLVM 18 + meson + - name: Install LLVM 22 + meson run: | wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key \ | sudo tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc - echo "deb https://apt.llvm.org/$(lsb_release -cs)/ llvm-toolchain-$(lsb_release -cs)-18 main" \ + echo "deb https://apt.llvm.org/$(lsb_release -cs)/ llvm-toolchain-$(lsb_release -cs)-22 main" \ | sudo tee /etc/apt/sources.list.d/llvm-18.list sudo apt-get update - sudo apt-get install -y clang-tidy-18 clang-tools-18 \ + sudo apt-get install -y clang-tidy-22 clang-tools-22 \ meson ninja-build - name: Configure build (for compile_commands.json) @@ -82,7 +82,7 @@ jobs: - name: Run clang-tidy run: | - run-clang-tidy-18 -p builddir \ + run-clang-tidy-22 -p builddir \ "^.*/libksuid/.*\.(c|h)$" \ 2>&1 | tee tidy-results.txt || true diff --git a/.github/workflows/lint-pr.yml b/.github/workflows/lint-pr.yml index 502529b..e4cabec 100644 --- a/.github/workflows/lint-pr.yml +++ b/.github/workflows/lint-pr.yml @@ -69,30 +69,30 @@ jobs: fi # ========================================================================== - # Phase 1b: clang-tidy 18 (slow, up to 30 min) + # Phase 1b: clang-tidy 22 (slow, up to 30 min) # Runs only after gst-indent passes. Any warning/error fails the PR. # ========================================================================== clang-tidy-check: - name: clang-tidy 18 + name: clang-tidy 22 needs: [gst-indent-check] runs-on: ubuntu-latest timeout-minutes: 30 steps: - uses: actions/checkout@v5 - - name: Install LLVM 18 + - name: Install LLVM 22 run: | wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key \ | sudo tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc - echo "deb https://apt.llvm.org/$(lsb_release -cs)/ llvm-toolchain-$(lsb_release -cs)-18 main" \ + echo "deb https://apt.llvm.org/$(lsb_release -cs)/ llvm-toolchain-$(lsb_release -cs)-22 main" \ | sudo tee /etc/apt/sources.list.d/llvm-18.list sudo apt-get update - sudo apt-get install -y clang-tidy-18 clang-tools-18 \ + sudo apt-get install -y clang-tidy-22 clang-tools-22 \ meson ninja-build - name: Verify tool versions run: | - clang-tidy-18 --version + clang-tidy-22 --version meson --version - name: Configure build (for compile_commands.json) @@ -101,7 +101,7 @@ jobs: - name: Run clang-tidy (hard gate) run: | set -o pipefail - run-clang-tidy-18 -p builddir \ + run-clang-tidy-22 -p builddir \ "^.*/libksuid/.*\.(c|h)$" \ 2>&1 | tee tidy-results.txt if grep -qE '^.*\.(c|h):[0-9]+:[0-9]+: (warning|error):' tidy-results.txt; then From 079ea2e6f687391336d796b1357ca647a6b50264 Mon Sep 17 00:00:00 2001 From: Justin Kim Date: Thu, 30 Apr 2026 13:35:15 +0900 Subject: [PATCH 3/7] fix(ci): tolerate GNU indent --version exiting 64 GNU indent 2.2.13 exits with 64 (sysexits EX_USAGE) when invoked with --version, even though it prints the version banner first ("GNU indent 2.2.13"). This is a long-standing quirk of the upstream code that ships unchanged in every Linux distro I can test -- including the indent_2.2.13-4build1_amd64.deb that ubuntu-latest installs. GitHub Actions runs every step under `bash -e {0}`, so the non-zero exit terminated the "Install GNU indent" step before the "Run gst-indent (hard gate)" step ever started. Result: every PR checks failed at lint phase 1a with no useful diagnostic, and the workflow looked broken when the formatter itself was working fine. Fix: append `|| true` to the diagnostic version probe in both lint-pr.yml and lint-main.yml. The version banner still lands in the step log; we just no longer take an avoidable failure on a purely informational invocation. --- .github/workflows/lint-main.yml | 6 +++++- .github/workflows/lint-pr.yml | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/lint-main.yml b/.github/workflows/lint-main.yml index 38cafdb..cfe73a9 100644 --- a/.github/workflows/lint-main.yml +++ b/.github/workflows/lint-main.yml @@ -29,7 +29,11 @@ jobs: run: | sudo apt-get update sudo apt-get install -y indent - indent --version + # GNU indent's --version exits 64 ("usage error") on every + # build I have tested -- including the apt package on noble. + # Tolerate the bogus exit code so the step still records the + # version banner without taking down bash -e. + indent --version || true - name: Run gst-indent run: | diff --git a/.github/workflows/lint-pr.yml b/.github/workflows/lint-pr.yml index e4cabec..4df28a2 100644 --- a/.github/workflows/lint-pr.yml +++ b/.github/workflows/lint-pr.yml @@ -33,7 +33,11 @@ jobs: run: | sudo apt-get update sudo apt-get install -y indent - indent --version + # GNU indent's --version exits 64 ("usage error") on every + # build I have tested -- including the apt package on noble. + # Tolerate the bogus exit code so the step still records the + # version banner without taking down bash -e. + indent --version || true - name: Run gst-indent (hard gate) run: | From eb2458577aa8debf0f009c5576d98da6700a0291 Mon Sep 17 00:00:00 2001 From: Justin Kim Date: Thu, 30 Apr 2026 13:37:22 +0900 Subject: [PATCH 4/7] fix(ci): drop File.full_path() call for older meson on Ubuntu noble meson 1.11.x on the local toolchain accepts File.full_path(), but the meson the GitHub-hosted ubuntu-latest installs from apt is older and rejects it: tests/meson.build:32:36: ERROR: Unknown method "full_path" in object <[FileHolder] holds [File]: > of type FileHolder. This took down the build matrix at the meson setup step on every PR matrix lane. Fix: pass the File and the ksuid_gen Executable directly into test() args without calling .full_path(). meson resolves File objects and build targets to absolute paths when they appear in args -- the explicit conversion was unnecessary and breaks under older meson. Verified locally that test_cli still passes after the change. --- tests/meson.build | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/meson.build b/tests/meson.build index 69f3122..42ed4ab 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -24,12 +24,16 @@ foreach t : ['test_smoke', 'test_parts', 'test_base62', 'test_parse_format', endforeach # Integration test for the ksuid-gen CLI; only registered when the -# CLI is built (default on). +# CLI is built (default on). meson resolves File objects and build +# targets to absolute paths automatically when they appear in test() +# args, so we pass them as-is rather than calling .full_path() (which +# is not available on File objects in the meson shipped with Ubuntu +# noble). if get_option('cli') sh = find_program('sh', required : true) test('test_cli', sh, - args : [files('test_cli.sh')[0].full_path(), ksuid_gen.full_path()], + args : [files('test_cli.sh')[0], ksuid_gen], timeout : 30, ) endif From 414ccb121566e337ca3c206e5c81267ca33eef29 Mon Sep 17 00:00:00 2001 From: Justin Kim Date: Thu, 30 Apr 2026 13:44:43 +0900 Subject: [PATCH 5/7] fix(ci): add .clang-tidy config + fix the 3 real findings clang-tidy 22 (vs the 18 we previously pinned) ships with no implicit default check selection -- without an explicit Checks list or a .clang-tidy file the tool emits No checks enabled. Unable to run clang-tidy. and exits 1, which took down the lint phase on every PR matrix lane. This commit lands a project-root .clang-tidy with a moderate, well- known-low-noise selection (clang-analyzer + bugprone + cert + performance + portability) and the small set of exclusions a C codebase always wants: -bugprone-easily-swappable-parameters Notorious false-positive on otherwise-fine APIs. -cert-err33-c, -cert-msc24-c, -cert-msc33-c Flag every fprintf / strtok / asctime call without offering actionable advice on a CSPRNG-backed library. -clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling Recommends C11 Annex K _s functions (fprintf_s, memcpy_s, ...) that are not implemented on Linux glibc, MUSL, macOS, or Windows MSVC without an opt-in flag. 409 of the 412 warnings clang-tidy 22 produced for libksuid were this single check; almost zero were actionable. After excluding the noise three real findings remained, all fixed in this commit: libksuid/base62_sse2.c:30,56 -- cast-align on _mm_loadu_si128 and _mm_storeu_si128. These are the SSE2 *unaligned* load/store intrinsics by spec. Suppressed with NOLINTNEXTLINE + one-line rationale. examples/ksuid-gen.c:150 -- bugprone-switch-missing-default- case on the FMT_* dispatch. Added an explicit default that documents why no other format value ever reaches print_one. Verified locally with clang-tidy 22.1.3: 0 findings. The lint hard gate's grep regex now finds nothing on the local run, so CI should clear the same way. --- .clang-tidy | 31 +++++++++++++++++++++++++++++++ examples/ksuid-gen.c | 5 +++++ libksuid/base62_sse2.c | 6 ++++++ 3 files changed, 42 insertions(+) create mode 100644 .clang-tidy diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 0000000..47f5f0a --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,31 @@ +# clang-tidy 22+ requires an explicit check selection; without this +# file the tool emits "No checks enabled" and exits 1. +# +# We start with the high-signal, low-noise families (analyzer + +# bugprone) and grow the list as the codebase tolerates it. New +# checks should be added in a separate commit so the diff that +# enables a check is the same diff that fixes its findings. +--- +Checks: > + clang-analyzer-*, + bugprone-*, + cert-*, + performance-*, + portability-*, + -bugprone-easily-swappable-parameters, + -cert-err33-c, + -cert-msc24-c, + -cert-msc33-c, + -clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling + +# Apply the same checks to headers that are part of libksuid. +HeaderFilterRegex: '.*/libksuid/.*\.h$' + +# Don't reformat alongside fixes -- keep clang-tidy and gst-indent +# concerns separate. +FormatStyle: none + +# Pin warnings as warnings; the CI step greps for any "warning:" line +# and fails the gate, so promoting them to errors here would just +# confuse the reporting. +WarningsAsErrors: '' diff --git a/examples/ksuid-gen.c b/examples/ksuid-gen.c index 669b983..1c03f43 100644 --- a/examples/ksuid-gen.c +++ b/examples/ksuid-gen.c @@ -166,6 +166,11 @@ print_one (int format, const ksuid_t *id, int verbose) case FMT_RAW: print_raw (id); break; + default: + /* parse_format() validates the input; no other value reaches + * print_one. Branch exists to satisfy + * bugprone-switch-missing-default-case. */ + break; } } diff --git a/libksuid/base62_sse2.c b/libksuid/base62_sse2.c index e12a659..9476be1 100644 --- a/libksuid/base62_sse2.c +++ b/libksuid/base62_sse2.c @@ -27,6 +27,10 @@ int ksuid_base62_translate16_sse2 (uint8_t out[16], const uint8_t in[16]) { + /* _mm_loadu_si128 is the SSE2 unaligned-load intrinsic; the + * (__m128i *) cast is a documented part of the API and does not + * actually require 16-byte alignment. */ + /* NOLINTNEXTLINE(clang-diagnostic-cast-align) */ __m128i v = _mm_loadu_si128 ((const __m128i *) in); /* digit: v in '0'..'9' */ @@ -53,6 +57,8 @@ ksuid_base62_translate16_sse2 (uint8_t out[16], const uint8_t in[16]) __m128i invalid_fill = _mm_andnot_si128 (any_mask, _mm_set1_epi8 ((char) 0xff)); __m128i result = _mm_or_si128 (values, invalid_fill); + /* Mirror cast-alignment exemption: _mm_storeu_si128 is unaligned. */ + /* NOLINTNEXTLINE(clang-diagnostic-cast-align) */ _mm_storeu_si128 ((__m128i *) out, result); /* movemask collects the high bits; for an all-0xff mask we expect From a81428c9d0f4db9dea2963a497418cfc967d009e Mon Sep 17 00:00:00 2001 From: Justin Kim Date: Thu, 30 Apr 2026 13:50:37 +0900 Subject: [PATCH 6/7] fix(ci): cross-platform build matrix (Linux multiarch + macOS + MSVC) Lint phase cleared on the previous push; the build matrix exposed four distinct platform-specific failures, all addressed here: [1] Ubuntu GCC + Ubuntu Clang -- DESTDIR install verification 13/13 tests passed but the workflow's hard-coded test paths (staging/usr/local/lib/pkgconfig/libksuid.pc) miss Debian's multiarch layout where everything under ${libdir} expands to lib/x86_64-linux-gnu/. Switch to a `find ... -print | grep -q .` check that succeeds against either flat or multiarch trees. [2] macOS Clang -- fatal: 'threads.h' file not found Apple Clang's libc has never shipped C11 through at least Xcode 16. The threaded-isolation test for the per-thread CSPRNG was the only consumer; the rest of the library uses _Thread_local + directly. Detect in tests/meson.build via cc.has_header() and only register test_rand_tls when the header is present. Falling back to pthread just for one test is not worth the dependency. [3] Windows MSVC -- C atomic support is not enabled vcruntime_c11_stdatomic.h(12) emits #error "C atomic support is not enabled" without /experimental:c11atomics. Microsoft still gates C11 atomics behind that flag in MSVC 19.44 (current). Add it to common_args under cc.get_argument_syntax() == 'msvc', alongside split-out POSIX feature macros and visibility flags that only mean anything on the gcc-style command line. The warning probes (-Wshadow et al) are similarly skipped on MSVC where they would just emit D9002 noise. [4] Windows MSVC -- clock_gettime undefined This was supposed to land in commit 45be111 alongside the KSUID_GETPID() macro, but the Edit that replaced clock_gettime(CLOCK_REALTIME, ...) with timespec_get(TIME_UTC) silently failed at the time and the file kept the original call. Re-apply the same fix: - rand_tls.c ksuid_now_seconds returns -1 on clock failure (fail-closed; forces reseed via the should_reseed predicate). - seed_pid widened from `long` to int64_t per the same commit's rationale. - struct field comments updated to mention TIME_UTC instead of CLOCK_REALTIME. All 13 tests still pass on Linux GCC after the meson.build restructuring; clang-tidy 22 still reports zero findings. --- .github/workflows/ci-pr.yml | 9 ++++++++- libksuid/rand_tls.c | 13 ++++++++---- meson.build | 40 +++++++++++++++++++++++++------------ tests/meson.build | 33 +++++++++++++++++++----------- 4 files changed, 65 insertions(+), 30 deletions(-) diff --git a/.github/workflows/ci-pr.yml b/.github/workflows/ci-pr.yml index 42eb57a..70eebad 100644 --- a/.github/workflows/ci-pr.yml +++ b/.github/workflows/ci-pr.yml @@ -109,11 +109,18 @@ jobs: if: runner.os == 'Linux' run: | DESTDIR="$PWD/staging" meson install -C builddir + # Architecture-independent paths are stable. test -f staging/usr/local/include/libksuid/ksuid.h test -f staging/usr/local/share/doc/libksuid/LICENSE test -f staging/usr/local/share/doc/libksuid/LICENSE.MIT test -f staging/usr/local/share/doc/libksuid/NOTICE - test -f staging/usr/local/lib/pkgconfig/libksuid.pc + # libksuid.pc / libksuid.a / libksuid.so live under + # ${libdir}, which on Debian-derived distros expands to + # lib// (lib/x86_64-linux-gnu, etc.). Search + # rather than hard-code the path. + find staging -name 'libksuid.pc' -print | grep -q . + find staging -name 'libksuid.a' -print | grep -q . + find staging -name 'libksuid.so' -print | grep -q . # ========================================================================== # Phase 3: Sanitizers (ASan + UBSan) diff --git a/libksuid/rand_tls.c b/libksuid/rand_tls.c index aa80de0..1320e09 100644 --- a/libksuid/rand_tls.c +++ b/libksuid/rand_tls.c @@ -48,19 +48,24 @@ typedef struct uint8_t buf[64]; size_t buf_pos; /* bytes already consumed from |buf| */ uint64_t bytes_emitted; /* since last seed */ - int64_t seed_time; /* CLOCK_REALTIME seconds at last seed */ - long seed_pid; /* getpid() at last seed */ + int64_t seed_time; /* TIME_UTC seconds at last seed */ + int64_t seed_pid; /* getpid()/_getpid() at last seed */ bool seeded; } ksuid_tls_rng_t; static _Thread_local ksuid_tls_rng_t ksuid_tls_rng_; +/* Returns wall-clock seconds (TIME_UTC), or -1 on clock failure. The + * sentinel makes the should-reseed predicate fall through to the + * "now < seed_time" branch which forces a reseed -- the conservative + * choice when the clock is unreadable. timespec_get is C11 standard + * and available on glibc 2.16+, MSVC 2015+, and macOS 10.15+. */ static int64_t ksuid_now_seconds (void) { struct timespec ts; - if (clock_gettime (CLOCK_REALTIME, &ts) != 0) - return 0; + if (timespec_get (&ts, TIME_UTC) != TIME_UTC) + return -1; return (int64_t) ts.tv_sec; } diff --git a/meson.build b/meson.build index eb90b50..13102ab 100644 --- a/meson.build +++ b/meson.build @@ -13,13 +13,25 @@ project('libksuid', 'c', cc = meson.get_compiler('c') pkg = import('pkgconfig') -# Hardening / portability flags applied to all our TUs. -common_args = [ - '-D_DEFAULT_SOURCE', - '-D_POSIX_C_SOURCE=200809L', - '-fvisibility=hidden', - '-DKSUID_BUILDING=1', -] +# Hardening / portability flags applied to all our TUs. Compiler- +# specific defaults split between gcc/clang and MSVC: visibility +# control + POSIX feature macros only mean anything on the GCC-style +# command line, and MSVC needs an explicit opt-in to C11 atomics +# until they leave experimental status. +common_args = ['-DKSUID_BUILDING=1'] +if cc.get_argument_syntax() == 'msvc' + # MSVC's is gated behind /experimental:c11atomics + # until C11 atomics ship as fully supported (Microsoft Learn: + # /experimental:c11atomics). Without it the header emits + # #error: "C atomic support is not enabled". + common_args += ['/experimental:c11atomics'] +else + common_args += [ + '-D_DEFAULT_SOURCE', + '-D_POSIX_C_SOURCE=200809L', + '-fvisibility=hidden', + ] +endif # OS entropy source detection. Per-host: at most one of these is # preferred at runtime; the others either are not present (Windows has @@ -42,12 +54,14 @@ if host_machine.system() == 'windows' extra_link_deps += bcrypt_dep common_args += '-DKSUID_HAVE_BCRYPT=1' endif -foreach a : ['-Wshadow', '-Wstrict-prototypes', '-Wpointer-arith', - '-Wcast-align', '-Wconversion', '-Wmissing-prototypes'] - if cc.has_argument(a) - common_args += a - endif -endforeach +if cc.get_argument_syntax() != 'msvc' + foreach a : ['-Wshadow', '-Wstrict-prototypes', '-Wpointer-arith', + '-Wcast-align', '-Wconversion', '-Wmissing-prototypes'] + if cc.has_argument(a) + common_args += a + endif + endforeach +endif # libsoup-style layout: the project root is on the include path so # every #include uses the form, which works diff --git a/tests/meson.build b/tests/meson.build index 42ed4ab..b79f830 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -2,27 +2,36 @@ test_inc = include_directories('.', '..') test_link = ksuid_lib.get_static_lib() -threads_dep = dependency('threads') +# C11 is not available on Apple Clang's libc shipped with +# macOS through at least Xcode 16 (April 2026); the rest of the +# library is C11-clean but the threaded RNG test relies on it. Skip +# the test rather than carry a pthread dependency just for one test. +have_threads_h = cc.has_header('threads.h') +threads_dep = dependency('threads', required : false) -# Tests that need C11 link against the threads dep; others -# don't need to. -threaded_tests = ['test_rand_tls'] +base_tests = ['test_smoke', 'test_parts', 'test_base62', 'test_parse_format', + 'test_sort', 'test_next_prev', 'test_sequence', 'test_rand_os', + 'test_chacha20', 'test_new', 'test_simd_parity'] -foreach t : ['test_smoke', 'test_parts', 'test_base62', 'test_parse_format', - 'test_sort', 'test_next_prev', 'test_sequence', 'test_rand_os', - 'test_chacha20', 'test_rand_tls', 'test_new', 'test_simd_parity'] - deps = [] - if threaded_tests.contains(t) - deps += threads_dep - endif +foreach t : base_tests exe = executable(t, t + '.c', include_directories : test_inc, link_with : test_link, - dependencies : deps, ) test(t, exe, timeout : 30) endforeach +if have_threads_h and threads_dep.found() + exe = executable('test_rand_tls', 'test_rand_tls.c', + include_directories : test_inc, + link_with : test_link, + dependencies : threads_dep, + ) + test('test_rand_tls', exe, timeout : 30) +else + message('Skipping test_rand_tls -- not available on this host') +endif + # Integration test for the ksuid-gen CLI; only registered when the # CLI is built (default on). meson resolves File objects and build # targets to absolute paths automatically when they appear in test() From cb53c7f469e3c6f1e94c023d0c145a4106fb1c87 Mon Sep 17 00:00:00 2001 From: Justin Kim Date: Thu, 30 Apr 2026 13:54:21 +0900 Subject: [PATCH 7/7] fix(ci): drop unistd.h / getopt for Windows MSVC compatibility Last build-matrix holdout. The ksuid-gen CLI included for POSIX getopt(3); MSVC has neither the header nor the function, so examples/ksuid-gen.c(25): fatal error C1083: Cannot open include file: 'unistd.h': No such file or directory took down the windows-latest / msvc lane while every other matrix slot now passes (Ubuntu GCC, Ubuntu Clang, macOS Clang). Replace the getopt loop with a hand-rolled option parser. The CLI exposes only four flags (-n N, -f FMT, -v, -h) with values supplied as separate tokens -- the same shape upstream Go ksuid uses and the exact set tests/test_cli.sh exercises -- so a six-case while loop is shorter than any portable getopt shim. The header drops -unistd.h- entirely, so MSVC, every libc on Linux, and Apple Clang all build the same translation unit. The new parser intentionally does NOT support combined short options (-vh) or attached values (-n4). Both are unidiomatic for this CLI and not required by the integration test; if a future flag wants either spelling, that is the time to vendor a real getopt implementation. Verified locally: 13/13 tests still pass, including test_cli's full coverage of -f inspect / -f raw / -f payload / -v / parse- mode / size-error / unknown-format paths. --- examples/ksuid-gen.c | 71 ++++++++++++++++++++++++++++---------------- 1 file changed, 46 insertions(+), 25 deletions(-) diff --git a/examples/ksuid-gen.c b/examples/ksuid-gen.c index 1c03f43..8cc3704 100644 --- a/examples/ksuid-gen.c +++ b/examples/ksuid-gen.c @@ -22,7 +22,6 @@ #include #include #include -#include enum { @@ -196,41 +195,63 @@ main (int argc, char **argv) int format = FMT_STRING; int verbose = 0; - int opt; - while ((opt = getopt (argc, argv, "n:f:vh")) != -1) { - switch (opt) { - case 'n': - { + /* Hand-rolled option parser. POSIX getopt(3) lives in + * which is not available on Windows MSVC, and the four flags here + * are simple enough that pulling in a getopt shim would be more + * code than parsing them inline. The accepted spellings are: + * -n N (count, positive integer; -n N as separate tokens) + * -f FMT (format name) + * -v (verbose; switch) + * -h (help; switch) + * Combined short options (-vh) and attached values (-n4) are not + * supported -- the existing tests/test_cli.sh exercises only the + * separate-token spelling, matching upstream Go ksuid's CLI. */ + int idx = 1; + while (idx < argc) { + const char *a = argv[idx]; + if (a[0] != '-' || a[1] == '\0' || a[2] != '\0') { + /* Not a recognised option; treat as a positional KSUID. */ + break; + } + char flag = a[1]; + if (flag == 'v') { + verbose = 1; + ++idx; + } else if (flag == 'h') { + usage (stdout, argv[0]); + return 0; + } else if (flag == 'n' || flag == 'f') { + if (idx + 1 >= argc) { + fprintf (stderr, "missing argument for -%c\n", flag); + usage (stderr, argv[0]); + return 1; + } + const char *val = argv[idx + 1]; + if (flag == 'n') { char *end; errno = 0; - long v = strtol (optarg, &end, 10); + long v = strtol (val, &end, 10); if (errno != 0 || *end != '\0' || v <= 0) { - fprintf (stderr, "invalid -n value: %s\n", optarg); + fprintf (stderr, "invalid -n value: %s\n", val); return 1; } count = v; - break; - } - case 'f': - format = parse_format (optarg); + } else { + format = parse_format (val); if (format < 0) { - fprintf (stderr, "unknown format: %s\n", optarg); + fprintf (stderr, "unknown format: %s\n", val); return 1; } - break; - case 'v': - verbose = 1; - break; - case 'h': - usage (stdout, argv[0]); - return 0; - default: - usage (stderr, argv[0]); - return 1; + } + idx += 2; + } else { + fprintf (stderr, "unknown option: %s\n", a); + usage (stderr, argv[0]); + return 1; } } - if (optind == argc) { + if (idx == argc) { /* Generation mode. */ for (long i = 0; i < count; ++i) { ksuid_t id; @@ -243,7 +264,7 @@ main (int argc, char **argv) } } else { /* Parse mode. */ - for (int i = optind; i < argc; ++i) { + for (int i = idx; i < argc; ++i) { ksuid_t id; size_t len = strlen (argv[i]); ksuid_err_t e = ksuid_parse (&id, argv[i], len);