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/.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..70eebad --- /dev/null +++ b/.github/workflows/ci-pr.yml @@ -0,0 +1,179 @@ +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 + # 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 + # 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) + # 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..cfe73a9 --- /dev/null +++ b/.github/workflows/lint-main.yml @@ -0,0 +1,111 @@ +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 + # 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: | + 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 22 (slow, up to 30 min) + # Non-blocking: findings reported as artifacts + summary. + # ========================================================================== + clang-tidy-check: + name: clang-tidy 22 (monitor) + runs-on: ubuntu-latest + continue-on-error: true + timeout-minutes: 30 + steps: + - uses: actions/checkout@v5 + + - 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)-22 main" \ + | sudo tee /etc/apt/sources.list.d/llvm-18.list + sudo apt-get update + sudo apt-get install -y clang-tidy-22 clang-tools-22 \ + meson ninja-build + + - name: Configure build (for compile_commands.json) + run: meson setup builddir + + - name: Run clang-tidy + run: | + run-clang-tidy-22 -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..4df28a2 --- /dev/null +++ b/.github/workflows/lint-pr.yml @@ -0,0 +1,134 @@ +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 + # 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: | + 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 22 (slow, up to 30 min) + # Runs only after gst-indent passes. Any warning/error fails the PR. + # ========================================================================== + clang-tidy-check: + name: clang-tidy 22 + needs: [gst-indent-check] + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v5 + + - 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)-22 main" \ + | sudo tee /etc/apt/sources.list.d/llvm-18.list + sudo apt-get update + sudo apt-get install -y clang-tidy-22 clang-tools-22 \ + meson ninja-build + + - name: Verify tool versions + run: | + clang-tidy-22 --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-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 + 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 diff --git a/examples/ksuid-gen.c b/examples/ksuid-gen.c index 669b983..8cc3704 100644 --- a/examples/ksuid-gen.c +++ b/examples/ksuid-gen.c @@ -22,7 +22,6 @@ #include #include #include -#include enum { @@ -166,6 +165,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; } } @@ -191,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; @@ -238,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); 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 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 69f3122..b79f830 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -2,34 +2,47 @@ 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). +# 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