From 6458714c7bd0cb1a5b2be12d442eb82a70dacd98 Mon Sep 17 00:00:00 2001 From: "Gabriel A. Devenyi" Date: Fri, 14 Aug 2026 23:03:04 -0400 Subject: [PATCH 01/77] COMP: Give install_linking the source directory instead of guessing it The test hard-codes the path to the downstream example project: cmake ... ../../nifti_clib/real_easy/minimal_example_of_downstream_usage That only resolves when the build directory happens to be a sibling of a source tree named exactly "nifti_clib". It fails for an in-tree build, for a build directory named anything else, and on CI, where the checkout lives at /nifti_clib and the path resolves to /nifti_clib/nifti_clib/real_easy/... CMake now passes CMAKE_SOURCE_DIR to the script, the way the other test scripts in that directory already receive their arguments. --- nifti2/CMakeLists.txt | 2 +- .../cmake_testscripts/install_linking_test.sh | 14 +++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/nifti2/CMakeLists.txt b/nifti2/CMakeLists.txt index 6028291d..758df0b9 100644 --- a/nifti2/CMakeLists.txt +++ b/nifti2/CMakeLists.txt @@ -184,7 +184,7 @@ if(NIFTI_BUILD_TESTING AND NIFTI_BUILD_APPLICATIONS) if(TEST_INSTALL) add_test( NAME install_linking - COMMAND sh ${NIFTI_TEST_SCRIPT_DIR}/install_linking_test.sh ${CMAKE_MAKE_PROGRAM} + COMMAND sh ${NIFTI_TEST_SCRIPT_DIR}/install_linking_test.sh ${CMAKE_MAKE_PROGRAM} ${CMAKE_SOURCE_DIR} WORKING_DIRECTORY ${PROJECT_BINARY_DIR} ) endif() endif() diff --git a/nifti2/nifti_regress_test/cmake_testscripts/install_linking_test.sh b/nifti2/nifti_regress_test/cmake_testscripts/install_linking_test.sh index dccf0fe9..89dcaeaf 100755 --- a/nifti2/nifti_regress_test/cmake_testscripts/install_linking_test.sh +++ b/nifti2/nifti_regress_test/cmake_testscripts/install_linking_test.sh @@ -10,6 +10,18 @@ else export BUILD_TOOL=$1 fi +# Where the project source lives. This used to be hard-coded as +# ../../nifti_clib relative to the build directory, which only resolved +# when the build tree happened to be a sibling of a source tree named +# exactly "nifti_clib" -- so the test failed for an in-tree build, for a +# build directory named anything else, and on CI. +if [ $# -lt 2 ] +then + echo Missing source directory + exit 1 +fi +SRC_DIR=$2 + # Set variables for local install export DESTDIR=installed export PATH="$PWD/$DESTDIR/usr/local/bin:$PATH" @@ -28,7 +40,7 @@ cd downstream_example cmake \ -G 'Unix Makefiles' \ -DCMAKE_MODULE_PATH=../installed/usr/local/share \ - ../../nifti_clib/real_easy/minimal_example_of_downstream_usage + "${SRC_DIR}/real_easy/minimal_example_of_downstream_usage" make echo Success From 919dcea37d4fdafc65ecf3103bd65d64d2389b77 Mon Sep 17 00:00:00 2001 From: "Gabriel A. Devenyi" Date: Fri, 14 Aug 2026 23:02:40 -0400 Subject: [PATCH 02/77] COMP: Show test output when the starter workflow fails cmake-multi-platform.yml ran ctest without --output-on-failure, so a red build reported which test failed and nothing about why. --- .github/workflows/cmake-multi-platform.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/cmake-multi-platform.yml b/.github/workflows/cmake-multi-platform.yml index 4a19f9a3..05e64935 100644 --- a/.github/workflows/cmake-multi-platform.yml +++ b/.github/workflows/cmake-multi-platform.yml @@ -68,5 +68,5 @@ jobs: working-directory: ${{ steps.strings.outputs.build-output-dir }} # Execute tests defined by the CMake configuration. Note that --build-config is needed because the default Windows generator is a multi-config generator (Visual Studio generator). # See https://cmake.org/cmake/help/latest/manual/ctest.1.html for more detail - run: ctest --build-config ${{ matrix.build_type }} + run: ctest --build-config ${{ matrix.build_type }} --output-on-failure From 3aefe1b487195f04323b4ec4a8d3a1a2f7a92145 Mon Sep 17 00:00:00 2001 From: "Gabriel A. Devenyi" Date: Fri, 14 Aug 2026 20:46:49 -0400 Subject: [PATCH 03/77] COMP: Guard the CMP0169 policy setting so older CMake still configures CMakeLists.txt sets policy CMP0169 unconditionally, to keep using the deprecated FetchContent_Populate. CMP0169 was introduced in CMake 3.30, and cmake_policy(SET) on an unknown policy is a hard error, so any CMake older than that fails to configure at all: CMake Error at CMakeLists.txt:175 (cmake_policy): Policy "CMP0169" is not known to this version of CMake. That includes the CMake 3.28 shipped by Ubuntu 24.04, which is what the CI runners and the project's own Dockerfile use, and it sits well above the cmake_minimum_required(VERSION 3.10.2) the project advertises. Wrapping it in if(POLICY CMP0169) is the standard idiom. Verified configuring with both CMake 3.28.3 and 4.4.2. --- CMakeLists.txt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 04b106ce..fd551f15 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -163,7 +163,12 @@ if (NIFTI_BUILD_TESTING ) include(FetchContent) # fetch data a configure time to simplify tests # If new or changed data is needed, add that data to the https://github.com/NIFTI-Imaging/nifti-test-data repo # make a new release, and then update the URL and hash (shasum -a 256 ). - cmake_policy(SET CMP0169 OLD) + # CMP0169 (deprecating FetchContent_Populate) only exists from CMake 3.30; + # setting it unconditionally is a hard error on every earlier release, + # including the CMake 3.28 that Ubuntu 24.04 ships. + if(POLICY CMP0169) + cmake_policy(SET CMP0169 OLD) + endif() FetchContent_Declare( fetch_testing_data URL https://github.com/NIFTI-Imaging/nifti-test-data/archive/v3.0.2.tar.gz URL_HASH SHA256=5dafec078151018da7aaf3c941bd31f246f590bc34fa3fef29ce77a773db16a6 From 7d92f16249b7545db8e0f22bedb0da767e7af75e Mon Sep 17 00:00:00 2001 From: "Gabriel A. Devenyi" Date: Fri, 14 Aug 2026 20:52:31 -0400 Subject: [PATCH 04/77] BUG: Compare NIfTI test output by content, not by compressed bytes nifti_c22_copy_image has been failing on this machine since before any of this work started. It converts an image i16 -> i64 -> i16 and asserts the result matches the original, using cmp out.c22.0.i16.nii.gz out.c22.2.0.i16.nii.gz That compares gzip output, which is not reproducible across zlib implementations. The system zlib here is zlib-ng 1.3.1, which Arch, CachyOS and a growing number of distributions ship in place of stock zlib; it encodes the same input differently. Both files come out at exactly 642454 bytes and differ from byte 321594 on. The conversion itself is fine. Decompressed, the two files are byte-identical at 1114768 bytes each, so the round-trip through int64 and back preserves the data exactly, which is what the test set out to check. Confirmed the other way too: in an Ubuntu 24.04 container with stock zlib 1.3, the test passes unmodified. The test now decompresses before comparing, via a small nii_cmp helper that falls back to plain cmp for uncompressed files. With this the suite is 92 of 92 on both zlib-ng and stock zlib; previously it was 91 of 92 on any zlib-ng system. --- .../cmake_testscripts/c22_copy_image.sh | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/nifti2/nifti_regress_test/cmake_testscripts/c22_copy_image.sh b/nifti2/nifti_regress_test/cmake_testscripts/c22_copy_image.sh index a3abddc2..18c435c9 100644 --- a/nifti2/nifti_regress_test/cmake_testscripts/c22_copy_image.sh +++ b/nifti2/nifti_regress_test/cmake_testscripts/c22_copy_image.sh @@ -11,6 +11,23 @@ DATA=$2 OUT_DATA=$(dirname ${DATA}) #Need to write to separate directory cd ${OUT_DATA} + +# Compare two NIfTI files by content rather than by compressed bytes. +# +# gzip output is not reproducible across zlib implementations: zlib-ng, +# which Arch, CachyOS and other current distributions ship as the system +# zlib, encodes the same input differently from stock zlib. Comparing +# the .gz files directly therefore fails on those systems even though the +# image data round-trips perfectly. Decompress first and compare that. +nii_cmp() { + if [ "${1##*.}" = "gz" ]; then + gzip -dc "$1" > "$1.raw" && gzip -dc "$2" > "$2.raw" || return 1 + cmp "$1.raw" "$2.raw" + return $? + fi + cmp "$1" "$2" +} + # note the main input file and prefix for all output files infile=$DATA/e4.60005.nii.gz prefix=out.c22 @@ -42,7 +59,7 @@ ${NT} -copy_image -infile ${prefix}.0.i16.nii.gz \ ${NT} -copy_image -infile ${prefix}.1.i64.nii.gz \ -prefix ${prefix}.2.0.i16.nii.gz \ -convert2dtype NIFTI_TYPE_INT16 -convert_verify -if cmp ${prefix}.0.i16.nii.gz ${prefix}.2.0.i16.nii.gz +if nii_cmp ${prefix}.0.i16.nii.gz ${prefix}.2.0.i16.nii.gz then echo "" else @@ -57,7 +74,7 @@ ${NT} -cbl -infile ${prefix}.0.i16.nii.gz \ ${NT} -copy_image -infile ${prefix}.1.f32.nii.gz \ -prefix ${prefix}.2.1.i16.nii.gz \ -convert2dtype NIFTI_TYPE_INT16 -convert_fail_choice warn -if cmp ${prefix}.0.i16.nii.gz ${prefix}.2.1.i16.nii.gz +if nii_cmp ${prefix}.0.i16.nii.gz ${prefix}.2.1.i16.nii.gz then echo "" else From b6531f011bff0cd35a41fc6f74c2416bb92a4da2 Mon Sep 17 00:00:00 2001 From: "Gabriel A. Devenyi" Date: Fri, 18 Sep 2026 22:25:16 -0400 Subject: [PATCH 05/77] COMP: Build the optional code paths in the per-PR workflow cmake-multi-platform.yml is the workflow that runs on every pull request, and it passes no options at all. With the defaults that means cifti/ and fsliolib/ are never compiled, and neither are the blocks behind FSLSTYLE, PIGZ and REJECT_COMPLEX. USE_CIFTI_CODE OFF USE_FSL_CODE OFF FSLSTYLE_NAME_CONFLICTS OFF -> -DFSLSTYLE FSLSTYLE_PIGZ_SUPPORT OFF -> -DPIGZ FSLSTYLE_REJECT_COMPLEX OFF -> -DREJECT_COMPLEX So a PR that changes any of those files collects four green checks that never built it. Several open PRs are in exactly that position. The matrix gains an 'options' axis: 'default' is what a consumer gets with no arguments, 'all' turns the optional libraries and the FSL parity defines on. Both are kept, because the default build is what ships and a change can break it while the fuller one still compiles. Four jobs become eight, each about half a minute. expat is installed for the Linux 'all' jobs; cifti needs it and the default jobs do not. Verified on master before writing this, with the test suite: default build OK, tests pass +cifti +fsl build OK, tests pass +cifti +fsl +FSLSTYLE build OK, tests pass, doPigz/doPigz2 present in nifti2_io.c.o COMPILE_NIFTIUNUSED_CODE is deliberately left out. It guards code the project itself labels unused, so compiling it in CI would commit to keeping it working; whether that code should exist at all is a separate question. --- .github/workflows/cmake-multi-platform.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/cmake-multi-platform.yml b/.github/workflows/cmake-multi-platform.yml index 05e64935..21d0cad3 100644 --- a/.github/workflows/cmake-multi-platform.yml +++ b/.github/workflows/cmake-multi-platform.yml @@ -26,6 +26,10 @@ jobs: os: [ubuntu-latest, macos-latest] build_type: [Release] c_compiler: [gcc, clang] + # 'default' is what a consumer gets with no options; 'all' turns on + # the optional libraries and the FSL parity defines, which are the + # code paths no workflow compiled before. + options: [default, all] include: - os: macos-latest c_compiler: clang @@ -43,6 +47,11 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Install optional dependencies + # cifti needs expat; only the 'all' configuration builds it. + if: runner.os == 'Linux' && matrix.options == 'all' + run: sudo apt-get update && sudo apt-get install -y libexpat1-dev zlib1g-dev + - name: Set reusable strings # Turn repeated input strings (such as the build output directory) into step outputs. These step outputs can be used throughout the workflow file. id: strings @@ -58,6 +67,7 @@ jobs: -DCMAKE_CXX_COMPILER=${{ matrix.cpp_compiler }} -DCMAKE_C_COMPILER=${{ matrix.c_compiler }} -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} + ${{ matrix.options == 'all' && '-DUSE_CIFTI_CODE=ON -DUSE_FSL_CODE=ON -DFSLSTYLE=ON' || '' }} -S ${{ github.workspace }} - name: Build From 159d11de03defbb1216a52218d5945d553777d54 Mon Sep 17 00:00:00 2001 From: "Gabriel A. Devenyi" Date: Fri, 14 Aug 2026 19:52:19 -0400 Subject: [PATCH 06/77] ENH: Rework the clang-tidy configuration for C11 The .clang-tidy file has been in the tree since 2025 but nothing has ever run it, and it shows: two of its entries name clang-analyzer checks that upstream renamed some releases ago, so --verify-config warned about them. Run over the whole tree it produced 1697 findings, of which the great majority came from three style checks. At that ratio nobody reads the output. Retuned for what this project actually is: C11, with a shipped ABI. Measured with clang-tidy 22.1.8, USE_FSL_CODE=ON and USE_CIFTI_CODE=ON, it now gives 331 findings across 15 checks, every one a defect class: 170 bugprone-macro-parentheses 44 bugprone-unchecked-string-to-number-conversion 25 bugprone-multi-level-implicit-pointer-conversion 19 readability-use-concise-preprocessor-directives 16 bugprone-switch-missing-default-case 13 readability-redundant-casting 5 bugprone-suspicious-string-compare 4 bugprone-suspicious-realloc-usage 3 readability-redundant-control-flow 2 readability-suspicious-call-argument 2 bugprone-misplaced-widening-cast 1 each: readability-implicit-bool-conversion, readability-function-size, misc-redundant-expression, bugprone-float-loop-counter bugprone-macro-parentheses is re-enabled. It had been switched off, but it is the check that catches the real precedence bugs in the NT_DT_*, QSTR and NT_FILL macro families. misc-use-internal-linkage is switched off permanently, with the reason recorded in the file. It flags 29 functions, 13 of which are symbols currently exported by libniftiio, libfslio and libcifti. Applying its fix-it would delete them from the shared libraries. That it looks like a tidy-up is exactly what makes it dangerous, so the file says so next to the check rather than leaving the next person to rediscover it. Every disabled check carries its finding count and the reason, so the list can be re-argued from evidence instead of taste. --- .clang-tidy | 113 +++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 98 insertions(+), 15 deletions(-) diff --git a/.clang-tidy b/.clang-tidy index f2e41d16..7a418c12 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -1,46 +1,129 @@ +# clang-tidy configuration for nifti_clib. +# +# This is a C11 codebase that ships a stable ABI. Two consequences shape +# the list below: +# +# * Checks written for C++ idioms are noise here and are turned off +# rather than left to accumulate. +# * Any check that wants to change linkage or a declaration in an +# installed header is off, and must stay off. See the ABI section. +# +# Run it with: +# cmake -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=ON +# run-clang-tidy -p build -j "$(nproc)" -quiet +# Checks: '-*, android-*, -android-cloexec-fopen, + bugprone-*, - -bugprone-narrowing-conversions, - -bugprone-signed-char-misuse, + -bugprone-assignment-in-if-condition, -bugprone-branch-clone, -bugprone-easily-swappable-parameters, - -bugprone-not-null-terminated-result, -bugprone-implicit-widening-of-multiplication-result, - -bugprone-assignment-in-if-condition, - -bugprone-macro-parentheses, + -bugprone-narrowing-conversions, + -bugprone-not-null-terminated-result, + -bugprone-signed-char-misuse, + cert-*, - -cert-str34-c, - -cert-err34-c, -cert-err33-c, + -cert-err34-c, -cert-flp30-c, + -cert-str34-c, + clang-analyzer-*, - -clang-analyzer-alpha.clone.CloneChecker, - -clang-analyzer-alpha.deadcode.UnreachableCode, - -clang-analyzer-security.insecureAPI.strcpy, - -clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling, -clang-analyzer-security.FloatLoopCounter, + -clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling, + -clang-analyzer-security.insecureAPI.strcpy, + google-*, - -google-readability-casting, -google-readability-braces-around-statements, + -google-readability-casting, -google-readability-function-size, + misc-*, -misc-confusable-identifiers, + -misc-include-cleaner, -misc-no-recursion, + -misc-use-internal-linkage, + performance-*, -performance-type-promotion-in-math-fn, + readability-*, - -readability-magic-numbers, + -readability-avoid-nested-conditional-operator, + -readability-avoid-unconditional-preprocessor-if, + -readability-braces-around-statements, -readability-else-after-return, -readability-identifier-length, + -readability-inconsistent-ifelse-braces, -readability-isolate-declaration, - -readability-braces-around-statements, + -readability-magic-numbers, + -readability-math-missing-parentheses, + -readability-misleading-indentation, + -readability-named-parameter, -readability-non-const-parameter, - -readability-misleading-indentation' + -readability-redundant-parentheses' +# --------------------------------------------------------------------------- +# ABI: checks that must never be enabled +# +# misc-use-internal-linkage Wants `static` on every function without a +# visible external use. It flags 29 distinct functions here, and 13 of +# them are symbols currently exported by libniftiio, libfslio or +# libcifti -- FslFileType, FslGetHdrImgNames, fsl_fileexists, +# axml_recur_find_xml and the rest. Applying the fix-it deletes them +# from the shared libraries, silently breaking anyone linking against +# them. Most are also the functions -Wmissing-prototypes complains +# about; the fix for those is a file-local prototype, not `static`. +# Never enable this check without an ABI review. +# +# readability-non-const-parameter Wants `const` added to pointer +# parameters. Harmless inside a .c file, but its fix-it does not know +# which functions are declared in an installed header, and adding const +# there changes the published prototype. +# +# --------------------------------------------------------------------------- +# Turned off as noise, with counts from the run that introduced this file +# +# readability-math-missing-parentheses 623. Wants parentheses around +# every * inside a +. This is a house-style opinion, not a defect +# class, and the arithmetic here is conventional. +# misc-include-cleaner 620. An include-what-you-use +# tool. It flags every transitively-included declaration, including +# ones from nifti1_io.h that exist precisely so callers need one header. +# readability-inconsistent-ifelse-braces 246. clang-format owns brace +# placement here and is configured with InsertBraces: false. +# readability-named-parameter 14. Omitting parameter names in +# a prototype is idiomatic C. +# readability-redundant-parentheses 29. Directly at odds with +# bugprone-macro-parentheses, which is enabled and is the one that +# catches real defects. +# +# --------------------------------------------------------------------------- +# Notable checks that ARE enabled +# +# bugprone-macro-parentheses was disabled; re-enabled here. +# The macros in nifti_tool.h and the QSTR/NT_FILL families expand their +# arguments without parentheses, so any caller passing an expression +# gets silent precedence bugs. +# bugprone-unchecked-string-to-number-conversion +# bugprone-suspicious-realloc-usage leaks the original block when +# realloc returns NULL. +# bugprone-misplaced-widening-cast cast applied after the overflow +# rather than before it. +# bugprone-multi-level-implicit-pointer-conversion +# +# cert-err33-c stays off for now: it flags every unchecked printf and fseek +# as well as the allocations that matter, which buries the signal. The +# allocation cases are covered by clang-analyzer-unix.Malloc instead. +# CheckOptions: - key: readability-function-cognitive-complexity.IgnoreMacros value: 1 + # A ratchet, not a target. Several functions here (nifti_image_read, + # nifti_tool's act_* dispatchers) are large by design. This value is set + # just above today's worst so that nothing may get worse; lower it as + # functions are split, never raise it. - key: readability-function-cognitive-complexity.Threshold value: 428 From 596441bec24dc93765618c64ee44747880f5bccc Mon Sep 17 00:00:00 2001 From: "Hans J. Johnson" Date: Mon, 21 Sep 2026 11:25:43 -0500 Subject: [PATCH 07/77] COMP: Add a shared compiler warning set, clean flags only Add cmake/nifti_warnings.cmake with the warning flags the tree is already at zero under, so that a warning means a new defect rather than more noise. Fourteen flags are enabled: thirteen common to GCC and Clang, plus -Wcomma on Clang. Verified with NIFTI_WARNINGS_AS_ERRORS=ON in both the default and the USE_CIFTI_CODE/USE_FSL_CODE/FSLSTYLE configurations: zero compiler warnings. The flags that are wanted but not yet earned are recorded in a FUTURE SET comment block with their measured hit counts and the census command that produces them. They are promoted one at a time, each only after the change that fixes its warnings has landed, so CI stays green at every step. The GCC-only and MSVC branches are left empty: the GCC block has never been enabled anywhere so its counts are unknown, and no workflow builds on Windows at all. Co-Authored-By: Gabriel A. Devenyi <3001850+gdevenyi@users.noreply.github.com> --- CMakeLists.txt | 1 + cmake/nifti_warnings.cmake | 129 +++++++++++++++++++++++++++++++++++++ 2 files changed, 130 insertions(+) create mode 100644 cmake/nifti_warnings.cmake diff --git a/CMakeLists.txt b/CMakeLists.txt index fd551f15..f6d230ae 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -34,6 +34,7 @@ project(NIFTI list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_LIST_DIR}/cmake) include(nifti_macros) +include(nifti_warnings) set_property(GLOBAL PROPERTY nifti_installed_targets) diff --git a/cmake/nifti_warnings.cmake b/cmake/nifti_warnings.cmake new file mode 100644 index 00000000..65553fe4 --- /dev/null +++ b/cmake/nifti_warnings.cmake @@ -0,0 +1,129 @@ +# +# Compiler warning flags shared by every nifti_clib target. +# +# The set below is the CLEAN SET: every flag here has been measured at +# zero warnings across the whole tree. A flag is added only once the +# tree is already clean under it, so that a warning always means a new +# defect rather than more noise. The FUTURE SET at the bottom of this +# file records the flags that are wanted but not yet earned, each with +# its measured hit count. +# +# Run the census before promoting anything out of the future set: +# +# cmake -G Ninja -S . -B build-census -DCMAKE_BUILD_TYPE=Release \ +# -DUSE_CIFTI_CODE=ON -DUSE_FSL_CODE=ON -DFSLSTYLE=ON \ +# -DCMAKE_C_FLAGS="" +# ninja -C build-census -k 0 2>&1 | tee census.log +# grep -oE '\[-W[a-z0-9-]+\]' census.log | sort | uniq -c | sort -rn +# +# Warnings are NOT errors by default; set NIFTI_WARNINGS_AS_ERRORS=ON to +# make CI fail on them. That option is only safe to turn on in CI once +# the future set below is empty. +# + +option(NIFTI_ENABLE_WARNINGS "Enable the project's compiler warning set" ON) +option(NIFTI_WARNINGS_AS_ERRORS "Treat compiler warnings as errors" OFF) +mark_as_advanced(NIFTI_ENABLE_WARNINGS NIFTI_WARNINGS_AS_ERRORS) + +if(NOT NIFTI_ENABLE_WARNINGS) + return() +endif() + +set(_nifti_warnings "") + +# --------------------------------------------------------------------- +# CLEAN SET - measured at zero, GCC and Clang +# --------------------------------------------------------------------- +if(CMAKE_C_COMPILER_ID MATCHES "GNU|Clang|AppleClang") + list(APPEND _nifti_warnings + -Wall + -Wpedantic + -Wformat=2 # printf/scanf checking, including nonliteral + -Wmissing-declarations + -Wnull-dereference + -Wpointer-arith # arithmetic on void * or function pointers + -Wredundant-decls + -Wshadow + -Wstrict-prototypes + -Wswitch-enum + -Wundef # #if on an undefined identifier + -Wvla # variable length arrays + -Wwrite-strings # string literals are const + ) +endif() + +# --------------------------------------------------------------------- +# CLEAN SET - Clang only +# --------------------------------------------------------------------- +if(CMAKE_C_COMPILER_ID MATCHES "Clang|AppleClang") + list(APPEND _nifti_warnings + -Wcomma + ) +endif() + +if(NIFTI_WARNINGS_AS_ERRORS) + if(MSVC) + list(APPEND _nifti_warnings /WX) + else() + list(APPEND _nifti_warnings -Werror) + endif() +endif() + +add_compile_options(${_nifti_warnings}) +unset(_nifti_warnings) + +# ===================================================================== +# FUTURE SET - wanted, not yet earned +# ===================================================================== +# +# Counts measured 2026-09-21 at d773c59, AppleClang 21.0.0, Release, +# USE_CIFTI_CODE=ON USE_FSL_CODE=ON FSLSTYLE=ON, NIFTI_BUILD_TESTING=OFF. +# Promote a flag to the clean set above only in a PR that follows the +# PR fixing its warnings, so CI is green at every step. +# +# flag hits fix +# ------------------------------------------------------------------ +# -Wsign-compare (via -Wextra) 3 PR #51 +# -Wcast-qual 9 PR #45 +# -Wmissing-prototypes 18 PR #37 +# -Wsign-conversion 200 PR #53 / #52, split by dir: +# znzlib 4, cifti 14, +# fsliolib 45, nifti2 58, +# niftilib 79 +# +# Clang-only: +# -Wnewline-eof 1 PR #35 covers one file only +# -Wmissing-variable-declarations 2 +# -Wconditional-uninitialized 11 relates to PR #47 / #48 +# -Wcast-align 19 relates to PR #44 +# -Wshorten-64-to-32 31 relates to PR #52 +# -Wextra-semi-stmt 74 relates to PR #49 +# +# -Wextra is held back only because it implies -Wsign-compare; once +# PR #51 lands it moves to the clean set with its 3 hits resolved. +# +# NOT MEASURED, do not add without a census first: +# +# GCC-only -Wcast-align=strict, -Wduplicated-branches, +# -Wduplicated-cond, -Wjump-misses-init, -Wlogical-op, +# -Wold-style-definition +# GNU 13.3.0 runs in CI (ubuntu-latest) but these flags +# have never been enabled, so their counts are unknown. +# +# MSVC /W3 +# There is no Windows job in any workflow, so nothing +# compiles this branch at all. Adding /W3 without a +# Windows CI job asserts a cleanliness nobody can check. +# +# DELIBERATELY EXCLUDED, with reasons: +# +# -Wdouble-promotion ~240 hits. Silencing them means calling sqrtf() +# instead of sqrt(), which changes the numerical +# results of the quaternion and matrix code. +# -Wfloat-equal ~150 hits. Most are deliberate tests against an +# exact 0.0 or a sentinel, which is correct here. +# -Wconversion Largely subsumed by -Wsign-conversion, and the +# remainder overlaps -Wdouble-promotion above. +# -Wunsafe-buffer-usage clang-only, aimed at C++ span/array types that +# do not exist in this codebase. +# From 77b7418963953e2ff5ab38aa20450612aa2bf579 Mon Sep 17 00:00:00 2001 From: "Hans J. Johnson" Date: Mon, 21 Sep 2026 13:10:46 -0500 Subject: [PATCH 08/77] COMP: Widen CI coverage across platforms, linkage, and configurations Run the Build and Test workflow on this repository's branch. Its trigger named "main" while the branch is "master", so it had never executed on master and did not execute on pull requests; valgrind, AddressSanitizer, UndefinedBehaviorSanitizer, scan-build, gcov coverage, and the only shared-library configuration were all unexercised. Also move its macOS entry off the retired macos-11 image. Restructure the per-PR matrix around the axes that change what is compiled: shared BUILD_SHARED_LIBS gates TEST_INSTALL, so the ON legs are the first to run install_linking and cover the install and export path. fslstyle -DFSLSTYLE is not additive. It rewrites behaviour in niftilib and nifti2 through global -DFSLSTYLE, -DPIGZ and -DREJECT_COMPLEX, and one site has an #else that changes the pixdim[0] value written to disk. Neither value subsumes the other. build_type One static Debug leg, which is what makes assert() live. Drop both macos gcc legs: /usr/bin/gcc there is a clang shim reporting AppleClang, so they duplicated the macos clang legs exactly. The optional libraries are built everywhere rather than forming an axis, since they are additive. Add three jobs. The minimal configuration builds znzlib and niftilib alone, which is what a downstream project vendoring the core reader selects and which nothing else configures. The oldest supported CMake job guards cmake_minimum_required, invisible to runners that all carry a recent CMake. The exported symbol baseline diffs the dynamic symbol set against a committed file, so a change to the ABI has to be updated in the same commit rather than landing unnoticed. Verified on Ubuntu 24.04 with GCC 13.3.0, the compiler the Linux runners use: shared, Release, FSLSTYLE=OFF 345/345, install_linking passed static, Debug, FSLSTYLE=ON 344/344, no assertion aborts minimal configures and builds, no tests CMake 3.28.3 344/344 The baseline holds 448 symbols across six libraries, which is the figure the linkage changes have been asserting without a way to check it. The workflow steps were exercised by running them directly in the runner container image rather than through act, whose image omits cmake. --- .github/workflows/build.yml | 6 +- .github/workflows/cmake-multi-platform.yml | 178 ++++++-- cmake/collect_exported_symbols.sh | 60 +++ cmake/exported_symbols_linux.txt | 448 +++++++++++++++++++++ 4 files changed, 658 insertions(+), 34 deletions(-) create mode 100755 cmake/collect_exported_symbols.sh create mode 100644 cmake/exported_symbols_linux.txt diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4c82448d..2a5a8922 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,9 +2,9 @@ name: Build and Test on: push: - branches: [ main ] + branches: [ master ] pull_request: - branches: [ main ] + branches: [ master ] jobs: build: @@ -68,7 +68,7 @@ jobs: shell_tests: "ON" - name: rel-clang-macos - os: macos-11 + os: macos-latest compiler: clang scanbuild: "" cflags: "-O3 -Wall -Wextra -Wshadow -Wunused-variable -Wunused-parameter -Wunused-function -Wunused -Wno-system-headers -Wno-deprecated -Wwrite-strings" diff --git a/.github/workflows/cmake-multi-platform.yml b/.github/workflows/cmake-multi-platform.yml index 21d0cad3..68197e71 100644 --- a/.github/workflows/cmake-multi-platform.yml +++ b/.github/workflows/cmake-multi-platform.yml @@ -1,5 +1,30 @@ -# This starter workflow is for a CMake project running on multiple platforms. There is a different starter workflow if you just want a single platform. -# See: https://github.com/actions/starter-workflows/blob/main/ci/cmake-single-platform.yml +# Per-PR build and test matrix. +# +# Axes and why each one is here: +# +# os / c_compiler GNU, Clang and AppleClang. macOS has no gcc entry +# because /usr/bin/gcc there is a clang shim that +# reports AppleClang, so a macos+gcc leg duplicates +# the macos+clang leg exactly. +# shared BUILD_SHARED_LIBS also gates TEST_INSTALL, so the +# ON legs are the only ones that run install_linking +# and therefore the only ones covering the install +# and export path. +# fslstyle -DFSLSTYLE is not additive. It rewrites behaviour +# in niftilib and nifti2 through global -DFSLSTYLE, +# -DPIGZ and -DREJECT_COMPLEX definitions, and one of +# them has an #else that changes the pixdim[0] value +# written to disk (nifti1_io.c). Both values are +# real configurations and neither subsumes the other. +# build_type Debug turns assert() from a no-op into an abort, +# and the library has live assert() sites. One +# static Debug leg carries this; Debug is not a +# second pass over the other axes. +# +# The optional libraries (cifti, fsliolib) are built everywhere rather +# than being an axis of their own; they are additive, so ON covers OFF. +# The minimal-configuration job below covers the other direction. + name: CMake on multiple platforms on: @@ -10,73 +35,164 @@ on: jobs: build: + name: ${{ matrix.os }} ${{ matrix.c_compiler }} ${{ matrix.build_type }} shared=${{ matrix.shared }} fslstyle=${{ matrix.fslstyle }} runs-on: ${{ matrix.os }} strategy: - # Set fail-fast to false to ensure that feedback is delivered for all matrix combinations. Consider changing this to true when your workflow is stable. fail-fast: false - - # Set up a matrix to run the following 3 configurations: - # 1. - # 2. - # 3. - # - # To add more build types (Release, Debug, RelWithDebInfo, etc.) customize the build_type list. matrix: os: [ubuntu-latest, macos-latest] - build_type: [Release] c_compiler: [gcc, clang] - # 'default' is what a consumer gets with no options; 'all' turns on - # the optional libraries and the FSL parity defines, which are the - # code paths no workflow compiled before. - options: [default, all] + build_type: [Release] + shared: ["OFF", "ON"] + fslstyle: ["OFF", "ON"] include: - - os: macos-latest - c_compiler: clang - cpp_compiler: clang++ - os: ubuntu-latest c_compiler: gcc cpp_compiler: g++ - os: ubuntu-latest c_compiler: clang cpp_compiler: clang++ - exclude: + - os: macos-latest + c_compiler: clang + cpp_compiler: clang++ + # One Debug leg, static. Debug exists to make assert() + # live, not to re-cover the linkage or platform axes, so a + # single configuration carries it. FSLSTYLE is ON so the + # assert() sites behind those defines are reachable too. - os: ubuntu-latest - c_compiler: cl + c_compiler: gcc + cpp_compiler: g++ + build_type: Debug + shared: "OFF" + fslstyle: "ON" + exclude: + # /usr/bin/gcc on macOS is a clang shim; this leg would be an + # exact duplicate of the macos clang leg. + - os: macos-latest + c_compiler: gcc steps: - uses: actions/checkout@v4 - name: Install optional dependencies - # cifti needs expat; only the 'all' configuration builds it. - if: runner.os == 'Linux' && matrix.options == 'all' + if: runner.os == 'Linux' run: sudo apt-get update && sudo apt-get install -y libexpat1-dev zlib1g-dev - name: Set reusable strings - # Turn repeated input strings (such as the build output directory) into step outputs. These step outputs can be used throughout the workflow file. id: strings shell: bash - run: | - echo "build-output-dir=${{ github.workspace }}/build" >> "$GITHUB_OUTPUT" + run: echo "build-output-dir=${{ github.workspace }}/build" >> "$GITHUB_OUTPUT" - name: Configure CMake - # Configure CMake in a 'build' subdirectory. `CMAKE_BUILD_TYPE` is only required if you are using a single-configuration generator such as make. - # See https://cmake.org/cmake/help/latest/variable/CMAKE_BUILD_TYPE.html?highlight=cmake_build_type run: > cmake -B ${{ steps.strings.outputs.build-output-dir }} -DCMAKE_CXX_COMPILER=${{ matrix.cpp_compiler }} -DCMAKE_C_COMPILER=${{ matrix.c_compiler }} -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} - ${{ matrix.options == 'all' && '-DUSE_CIFTI_CODE=ON -DUSE_FSL_CODE=ON -DFSLSTYLE=ON' || '' }} + -DBUILD_SHARED_LIBS=${{ matrix.shared }} + -DUSE_CIFTI_CODE=ON + -DUSE_FSL_CODE=ON + -DFSLSTYLE=${{ matrix.fslstyle }} -S ${{ github.workspace }} - name: Build - # Build your program with the given configuration. Note that --config is needed because the default Windows generator is a multi-config generator (Visual Studio generator). run: cmake --build ${{ steps.strings.outputs.build-output-dir }} --config ${{ matrix.build_type }} - name: Test working-directory: ${{ steps.strings.outputs.build-output-dir }} - # Execute tests defined by the CMake configuration. Note that --build-config is needed because the default Windows generator is a multi-config generator (Visual Studio generator). - # See https://cmake.org/cmake/help/latest/manual/ctest.1.html for more detail run: ctest --build-config ${{ matrix.build_type }} --output-on-failure + minimal: + # The configuration a downstream project vendoring only the core + # reader selects. Nothing else in the matrix proves the tree still + # configures with the optional subdirectories switched off. + name: minimal configuration (znzlib + niftilib only) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Configure CMake + run: > + cmake -B ${{ github.workspace }}/build + -DCMAKE_BUILD_TYPE=Release + -DUSE_NIFTI2_CODE=OFF + -DUSE_NIFTICDF_CODE=OFF + -DUSE_CIFTI_CODE=OFF + -DUSE_FSL_CODE=OFF + -DNIFTI_BUILD_APPLICATIONS=OFF + -S ${{ github.workspace }} + - name: Build + run: cmake --build ${{ github.workspace }}/build + - name: Test + working-directory: ${{ github.workspace }}/build + # This configuration registers no tests at all, because the test + # programs live with the applications that are switched off here. + # --no-tests=ignore keeps that from being an error; the value of + # this job is that the tree still configures and builds. + run: ctest --output-on-failure --no-tests=ignore + + oldest-cmake: + # Guards cmake_minimum_required. Every other runner carries a + # recent CMake, so a policy or command that needs a newer release + # than the project claims to support is invisible to them. + # PR #30 was exactly that: CMP0169 does not exist before 3.30 and + # setting it unconditionally was fatal on Ubuntu 24.04's 3.28. + name: oldest supported CMake + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - name: Install dependencies + run: sudo apt-get update && sudo apt-get install -y cmake ninja-build libexpat1-dev zlib1g-dev + - name: Show CMake version + run: cmake --version + - name: Configure CMake + run: > + cmake -G Ninja -B ${{ github.workspace }}/build + -DCMAKE_BUILD_TYPE=Release + -DUSE_CIFTI_CODE=ON + -DUSE_FSL_CODE=ON + -S ${{ github.workspace }} + - name: Build + run: cmake --build ${{ github.workspace }}/build + - name: Test + working-directory: ${{ github.workspace }}/build + run: ctest --output-on-failure + + exported-symbols: + # The exported symbol set is the ABI. Several open changes assert + # that they leave it untouched and nothing verified that claim, so + # this job diffs it against a committed baseline. An intentional + # change to the ABI updates the baseline in the same commit, which + # makes the change visible in review rather than implicit. + name: exported symbol baseline + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install dependencies + run: sudo apt-get update && sudo apt-get install -y libexpat1-dev zlib1g-dev ninja-build + - name: Configure CMake + run: > + cmake -G Ninja -B ${{ github.workspace }}/build + -DCMAKE_BUILD_TYPE=Release + -DBUILD_SHARED_LIBS=ON + -DUSE_CIFTI_CODE=ON + -DUSE_FSL_CODE=ON + -S ${{ github.workspace }} + - name: Build + run: cmake --build ${{ github.workspace }}/build + - name: Collect exported symbols + run: | + bash cmake/collect_exported_symbols.sh \ + "${{ github.workspace }}/build" \ + /tmp/exported_symbols.txt + - name: Compare against the committed baseline + run: | + if ! diff -u cmake/exported_symbols_linux.txt /tmp/exported_symbols.txt; then + echo "" + echo "The exported symbol set changed." + echo "If that is intended, update cmake/exported_symbols_linux.txt" + echo "in this commit so the ABI change is visible in review:" + echo " bash cmake/collect_exported_symbols.sh cmake/exported_symbols_linux.txt" + exit 1 + fi + echo "Exported symbols match the baseline." diff --git a/cmake/collect_exported_symbols.sh b/cmake/collect_exported_symbols.sh new file mode 100755 index 00000000..2cdae6c0 --- /dev/null +++ b/cmake/collect_exported_symbols.sh @@ -0,0 +1,60 @@ +#!/bin/sh +# Collect the dynamic symbols exported by the built shared libraries. +# +# collect_exported_symbols.sh +# +# The output is sorted " " lines, so a diff against the +# committed baseline names both the library and the symbol that moved. +# +# The library name is reported with any version suffix stripped, so a +# SOVERSION bump does not rewrite every line of the baseline: +# libniftiio.so.3.0.0 is reported as libniftiio.so. +# +# Regenerate the baseline after an intended ABI change: +# cmake -B build -DBUILD_SHARED_LIBS=ON -DUSE_CIFTI_CODE=ON -DUSE_FSL_CODE=ON +# cmake --build build +# sh cmake/collect_exported_symbols.sh build cmake/exported_symbols_linux.txt + +set -eu + +if [ $# -ne 2 ]; then + echo "usage: $0 " >&2 + exit 2 +fi + +build_dir=$1 +output=$2 + +if [ ! -d "$build_dir" ]; then + echo "$0: no such build directory: $build_dir" >&2 + exit 1 +fi + +# Match versioned sonames too (libfoo.so.3.0.0), and resolve symlinks so +# libfoo.so -> libfoo.so.3.0.0 is not counted twice. +libs=$(find "$build_dir" -name 'lib*.so*' -exec readlink -f {} \; \ + | LC_ALL=C sort -u) + +if [ -z "$libs" ]; then + echo "$0: no shared libraries under $build_dir;" \ + "configure with -DBUILD_SHARED_LIBS=ON" >&2 + exit 1 +fi + +: > "$output.tmp" + +for lib in $libs; do + # libniftiio.so.3.0.0 -> libniftiio.so + name=$(basename "$lib" | sed -E 's/\.so(\.[0-9]+)*$/.so/') + # -D dynamic symbols, --defined-only drops imports; field 3 is the name. + nm -D --defined-only "$lib" \ + | awk 'NF >= 3 { print $3 }' \ + | grep -v '^$' \ + | sed "s|^|$name |" >> "$output.tmp" +done + +LC_ALL=C sort -u "$output.tmp" > "$output" +rm -f "$output.tmp" + +echo "$0: wrote $(wc -l < "$output") symbols from" \ + "$(awk '{print $1}' "$output" | LC_ALL=C sort -u | wc -l) libraries to $output" diff --git a/cmake/exported_symbols_linux.txt b/cmake/exported_symbols_linux.txt new file mode 100644 index 00000000..14effdcd --- /dev/null +++ b/cmake/exported_symbols_linux.txt @@ -0,0 +1,448 @@ +libcifti.so axio_cifti_from_ext +libcifti.so axio_find_map_name +libcifti.so axio_num_tokens +libcifti.so axio_read_buf +libcifti.so axio_read_cifti_file +libcifti.so axio_read_file +libcifti.so axio_show_attrs +libcifti.so axio_show_cifti_summary +libcifti.so axio_show_mim_summary +libcifti.so axio_text_to_binary +libcifti.so axml_add_attrs +libcifti.so axml_attr_value +libcifti.so axml_disp_xlist +libcifti.so axml_disp_xml_t +libcifti.so axml_free_xlist +libcifti.so axml_free_xml_t +libcifti.so axml_get_buf_size +libcifti.so axml_get_dstore +libcifti.so axml_get_indent +libcifti.so axml_get_verb +libcifti.so axml_get_wstream +libcifti.so axml_read_buf +libcifti.so axml_read_file +libcifti.so axml_recur +libcifti.so axml_recur_find_xml +libcifti.so axml_set_buf_size +libcifti.so axml_set_dstore +libcifti.so axml_set_indent +libcifti.so axml_set_verb +libcifti.so axml_set_wstream +libcifti.so new_afni_xml +libfslio.so AvwSwapHeader +libfslio.so FslBaseFileType +libfslio.so FslCheckForMultipleFileNames +libfslio.so FslCloneHeader +libfslio.so FslClose +libfslio.so FslFileExists +libfslio.so FslFileType +libfslio.so FslFileTypeString +libfslio.so FslGetAnalyzeOrigin +libfslio.so FslGetAuxFile +libfslio.so FslGetBufferAsScaledDouble +libfslio.so FslGetCalMinMax +libfslio.so FslGetDataType +libfslio.so FslGetDim +libfslio.so FslGetDimensionality +libfslio.so FslGetEnvOutputType +libfslio.so FslGetFileType +libfslio.so FslGetFileType2 +libfslio.so FslGetHdrImgNames +libfslio.so FslGetIgnoreMFQ +libfslio.so FslGetIntensityScaling +libfslio.so FslGetIntent +libfslio.so FslGetLeftRightOrder +libfslio.so FslGetMMCoord +libfslio.so FslGetOverrideOutputType +libfslio.so FslGetReadFileType +libfslio.so FslGetRigidXform +libfslio.so FslGetStdXform +libfslio.so FslGetTimeUnits +libfslio.so FslGetVolSize +libfslio.so FslGetVolumeAsScaledDouble +libfslio.so FslGetVoxCoord +libfslio.so FslGetVoxDim +libfslio.so FslGetVoxUnits +libfslio.so FslGetWriteMode +libfslio.so FslInit +libfslio.so FslInit4Write +libfslio.so FslInitHeader +libfslio.so FslIsCompressedFileType +libfslio.so FslIsSingleFileType +libfslio.so FslIsValidFileType +libfslio.so FslMakeBaseName +libfslio.so FslOpen +libfslio.so FslReadAllVolumes +libfslio.so FslReadHeader +libfslio.so FslReadRawHeader +libfslio.so FslReadRowSeries +libfslio.so FslReadSliceSeries +libfslio.so FslReadTimeSeries +libfslio.so FslReadVolumes +libfslio.so FslSeekVolume +libfslio.so FslSetAnalyzeSform +libfslio.so FslSetAuxFile +libfslio.so FslSetCalMinMax +libfslio.so FslSetDataType +libfslio.so FslSetDim +libfslio.so FslSetDimensionality +libfslio.so FslSetFileType +libfslio.so FslSetIgnoreMFQ +libfslio.so FslSetInit +libfslio.so FslSetIntensityScaling +libfslio.so FslSetIntent +libfslio.so FslSetOverrideOutputType +libfslio.so FslSetRigidXform +libfslio.so FslSetStdXform +libfslio.so FslSetTimeUnits +libfslio.so FslSetVoxDim +libfslio.so FslSetVoxUnits +libfslio.so FslSetWriteMode +libfslio.so FslWriteAllVolumes +libfslio.so FslWriteHeader +libfslio.so FslWriteVolumes +libfslio.so FslXOpen +libfslio.so check_for_multiple_filenames +libfslio.so convertBufferToScaledDouble +libfslio.so d3matrix +libfslio.so d4matrix +libfslio.so fsl_fileexists +libfslio.so mat44_to_mat33 +libnifti2.so disp_nifti_1_header +libnifti2.so disp_nifti_2_header +libnifti2.so is_nifti_file +libnifti2.so is_valid_nifti_type +libnifti2.so nifti1_magic +libnifti2.so nifti2_magic +libnifti2.so nifti_add_extension +libnifti2.so nifti_alter_cifti_dims +libnifti2.so nifti_compiled_with_zlib +libnifti2.so nifti_convert_n1hdr2nim +libnifti2.so nifti_convert_n2hdr2nim +libnifti2.so nifti_convert_nim2n1hdr +libnifti2.so nifti_convert_nim2n2hdr +libnifti2.so nifti_copy_extensions +libnifti2.so nifti_copy_nim_info +libnifti2.so nifti_datatype_from_string +libnifti2.so nifti_datatype_is_valid +libnifti2.so nifti_datatype_sizes +libnifti2.so nifti_datatype_string +libnifti2.so nifti_datatype_to_string +libnifti2.so nifti_disp_lib_hist +libnifti2.so nifti_disp_lib_version +libnifti2.so nifti_disp_matrix_orient +libnifti2.so nifti_disp_type_list +libnifti2.so nifti_dmat33_colnorm +libnifti2.so nifti_dmat33_determ +libnifti2.so nifti_dmat33_inverse +libnifti2.so nifti_dmat33_mul +libnifti2.so nifti_dmat33_polar +libnifti2.so nifti_dmat33_rownorm +libnifti2.so nifti_dmat44_inverse +libnifti2.so nifti_dmat44_mul +libnifti2.so nifti_dmat44_to_mat44 +libnifti2.so nifti_dmat44_to_orientation +libnifti2.so nifti_dmat44_to_quatern +libnifti2.so nifti_fileexists +libnifti2.so nifti_find_file_extension +libnifti2.so nifti_findhdrname +libnifti2.so nifti_findimgname +libnifti2.so nifti_free_NBL +libnifti2.so nifti_free_extensions +libnifti2.so nifti_get_alter_cifti +libnifti2.so nifti_get_filesize +libnifti2.so nifti_get_int64list +libnifti2.so nifti_get_intlist +libnifti2.so nifti_get_volsize +libnifti2.so nifti_hdr1_looks_good +libnifti2.so nifti_hdr2_looks_good +libnifti2.so nifti_header_version +libnifti2.so nifti_image_free +libnifti2.so nifti_image_from_ascii +libnifti2.so nifti_image_infodump +libnifti2.so nifti_image_load +libnifti2.so nifti_image_load_bricks +libnifti2.so nifti_image_open +libnifti2.so nifti_image_read +libnifti2.so nifti_image_read_bricks +libnifti2.so nifti_image_to_ascii +libnifti2.so nifti_image_unload +libnifti2.so nifti_image_write +libnifti2.so nifti_image_write_bricks +libnifti2.so nifti_image_write_bricks_status +libnifti2.so nifti_image_write_hdr_img +libnifti2.so nifti_image_write_hdr_img2 +libnifti2.so nifti_image_write_status +libnifti2.so nifti_intent_string +libnifti2.so nifti_is_complete_filename +libnifti2.so nifti_is_gzfile +libnifti2.so nifti_is_inttype +libnifti2.so nifti_is_valid_datatype +libnifti2.so nifti_is_valid_ecode +libnifti2.so nifti_looks_like_cifti +libnifti2.so nifti_make_new_n1_header +libnifti2.so nifti_make_new_n2_header +libnifti2.so nifti_make_new_nim +libnifti2.so nifti_make_orthog_dmat44 +libnifti2.so nifti_make_orthog_mat44 +libnifti2.so nifti_makebasename +libnifti2.so nifti_makehdrname +libnifti2.so nifti_makeimgname +libnifti2.so nifti_mat33_colnorm +libnifti2.so nifti_mat33_determ +libnifti2.so nifti_mat33_inverse +libnifti2.so nifti_mat33_mul +libnifti2.so nifti_mat33_polar +libnifti2.so nifti_mat33_rownorm +libnifti2.so nifti_mat44_inverse +libnifti2.so nifti_mat44_mul +libnifti2.so nifti_mat44_to_dmat44 +libnifti2.so nifti_mat44_to_orientation +libnifti2.so nifti_mat44_to_quatern +libnifti2.so nifti_nim_has_valid_dims +libnifti2.so nifti_nim_is_valid +libnifti2.so nifti_orientation_string +libnifti2.so nifti_quatern_to_dmat44 +libnifti2.so nifti_quatern_to_mat44 +libnifti2.so nifti_read_ascii_image +libnifti2.so nifti_read_buffer +libnifti2.so nifti_read_collapsed_image +libnifti2.so nifti_read_header +libnifti2.so nifti_read_n1_hdr +libnifti2.so nifti_read_n2_hdr +libnifti2.so nifti_read_subregion_image +libnifti2.so nifti_set_allow_upper_fext +libnifti2.so nifti_set_alter_cifti +libnifti2.so nifti_set_debug_level +libnifti2.so nifti_set_filenames +libnifti2.so nifti_set_fix_floats +libnifti2.so nifti_set_iname_offset +libnifti2.so nifti_set_skip_blank_ext +libnifti2.so nifti_set_type_from_names +libnifti2.so nifti_short_order +libnifti2.so nifti_simple_init_nim +libnifti2.so nifti_slice_string +libnifti2.so nifti_strdup +libnifti2.so nifti_swap_16bytes +libnifti2.so nifti_swap_2bytes +libnifti2.so nifti_swap_4bytes +libnifti2.so nifti_swap_8bytes +libnifti2.so nifti_swap_Nbytes +libnifti2.so nifti_swap_as_analyze +libnifti2.so nifti_swap_as_nifti1 +libnifti2.so nifti_swap_as_nifti2 +libnifti2.so nifti_test_datatype_sizes +libnifti2.so nifti_type_and_names_match +libnifti2.so nifti_units_string +libnifti2.so nifti_update_dims_from_array +libnifti2.so nifti_valid_header_size +libnifti2.so nifti_validfilename +libnifti2.so nifti_write_all_data +libnifti2.so nifti_write_ascii_image +libnifti2.so nifti_write_buffer +libnifti2.so nifti_xform_string +libnifti2.so old_swap_nifti_header +libnifti2.so swap_nifti_header +libnifti2.so valid_nifti_brick_list +libnifti2.so valid_nifti_extensions +libnifticdf.so E0000 +libnifticdf.so E0001 +libnifticdf.so Xgamm +libnifticdf.so algdiv +libnifticdf.so alngam +libnifticdf.so alnrel +libnifticdf.so apser +libnifticdf.so basym +libnifticdf.so bcorr +libnifticdf.so betaln +libnifticdf.so bfrac +libnifticdf.so bgrat +libnifticdf.so bpser +libnifticdf.so bratio +libnifticdf.so brcmp1 +libnifticdf.so brcomp +libnifticdf.so bup +libnifticdf.so cdfbet +libnifticdf.so cdfbin +libnifticdf.so cdfchi +libnifticdf.so cdfchn +libnifticdf.so cdff +libnifticdf.so cdffnc +libnifticdf.so cdfgam +libnifticdf.so cdfnbn +libnifticdf.so cdfnor +libnifticdf.so cdfpoi +libnifticdf.so cdft +libnifticdf.so cumbet +libnifticdf.so cumbin +libnifticdf.so cumchi +libnifticdf.so cumchn +libnifticdf.so cumf +libnifticdf.so cumfnc +libnifticdf.so cumgam +libnifticdf.so cumnbn +libnifticdf.so cumnor +libnifticdf.so cumpoi +libnifticdf.so cumt +libnifticdf.so dbetrm +libnifticdf.so devlpl +libnifticdf.so dexpm1 +libnifticdf.so dinvnr +libnifticdf.so dinvr +libnifticdf.so dlanor +libnifticdf.so dln1mx +libnifticdf.so dln1px +libnifticdf.so dlnbet +libnifticdf.so dlngam +libnifticdf.so dstinv +libnifticdf.so dstrem +libnifticdf.so dstzr +libnifticdf.so dt1 +libnifticdf.so dzror +libnifticdf.so erf1 +libnifticdf.so erfc1 +libnifticdf.so esum +libnifticdf.so exparg +libnifticdf.so fifdint +libnifticdf.so fifdmax1 +libnifticdf.so fifdmin1 +libnifticdf.so fifdsign +libnifticdf.so fifidint +libnifticdf.so fifmod +libnifticdf.so fpser +libnifticdf.so ftnstop +libnifticdf.so gam1 +libnifticdf.so gaminv +libnifticdf.so gamln +libnifticdf.so gamln1 +libnifticdf.so grat1 +libnifticdf.so gratio +libnifticdf.so gsumln +libnifticdf.so inam +libnifticdf.so ipmpar +libnifticdf.so nifti_cdf2stat +libnifticdf.so nifti_intent_code +libnifticdf.so nifti_rcdf2stat +libnifticdf.so nifti_stat2cdf +libnifticdf.so nifti_stat2hzscore +libnifticdf.so nifti_stat2rcdf +libnifticdf.so nifti_stat2zscore +libnifticdf.so psi +libnifticdf.so rcomp +libnifticdf.so rexp +libnifticdf.so rlog +libnifticdf.so rlog1 +libnifticdf.so spmpar +libnifticdf.so stvaln +libniftiio.so disp_nifti_1_header +libniftiio.so is_nifti_file +libniftiio.so is_valid_nifti_type +libniftiio.so nifti_add_extension +libniftiio.so nifti_compiled_with_zlib +libniftiio.so nifti_convert_nhdr2nim +libniftiio.so nifti_convert_nim2nhdr +libniftiio.so nifti_copy_extensions +libniftiio.so nifti_copy_nim_info +libniftiio.so nifti_datatype_from_string +libniftiio.so nifti_datatype_is_valid +libniftiio.so nifti_datatype_sizes +libniftiio.so nifti_datatype_string +libniftiio.so nifti_datatype_to_string +libniftiio.so nifti_disp_lib_hist +libniftiio.so nifti_disp_lib_version +libniftiio.so nifti_disp_matrix_orient +libniftiio.so nifti_disp_type_list +libniftiio.so nifti_fileexists +libniftiio.so nifti_find_file_extension +libniftiio.so nifti_findhdrname +libniftiio.so nifti_findimgname +libniftiio.so nifti_free_NBL +libniftiio.so nifti_free_extensions +libniftiio.so nifti_get_filesize +libniftiio.so nifti_get_intlist +libniftiio.so nifti_get_volsize +libniftiio.so nifti_hdr_looks_good +libniftiio.so nifti_image_free +libniftiio.so nifti_image_from_ascii +libniftiio.so nifti_image_infodump +libniftiio.so nifti_image_load +libniftiio.so nifti_image_load_bricks +libniftiio.so nifti_image_open +libniftiio.so nifti_image_read +libniftiio.so nifti_image_read_bricks +libniftiio.so nifti_image_to_ascii +libniftiio.so nifti_image_unload +libniftiio.so nifti_image_write +libniftiio.so nifti_image_write_bricks +libniftiio.so nifti_image_write_bricks_status +libniftiio.so nifti_image_write_hdr_img +libniftiio.so nifti_image_write_hdr_img2 +libniftiio.so nifti_image_write_status +libniftiio.so nifti_intent_string +libniftiio.so nifti_is_complete_filename +libniftiio.so nifti_is_gzfile +libniftiio.so nifti_is_inttype +libniftiio.so nifti_is_valid_datatype +libniftiio.so nifti_is_valid_ecode +libniftiio.so nifti_make_new_header +libniftiio.so nifti_make_new_nim +libniftiio.so nifti_make_orthog_mat44 +libniftiio.so nifti_makebasename +libniftiio.so nifti_makehdrname +libniftiio.so nifti_makeimgname +libniftiio.so nifti_mat33_colnorm +libniftiio.so nifti_mat33_determ +libniftiio.so nifti_mat33_inverse +libniftiio.so nifti_mat33_mul +libniftiio.so nifti_mat33_polar +libniftiio.so nifti_mat33_rownorm +libniftiio.so nifti_mat44_inverse +libniftiio.so nifti_mat44_to_orientation +libniftiio.so nifti_mat44_to_quatern +libniftiio.so nifti_nim_has_valid_dims +libniftiio.so nifti_nim_is_valid +libniftiio.so nifti_orientation_string +libniftiio.so nifti_quatern_to_mat44 +libniftiio.so nifti_read_ascii_image +libniftiio.so nifti_read_buffer +libniftiio.so nifti_read_collapsed_image +libniftiio.so nifti_read_header +libniftiio.so nifti_read_subregion_image +libniftiio.so nifti_set_allow_upper_fext +libniftiio.so nifti_set_debug_level +libniftiio.so nifti_set_filenames +libniftiio.so nifti_set_fix_floats +libniftiio.so nifti_set_iname_offset +libniftiio.so nifti_set_skip_blank_ext +libniftiio.so nifti_set_type_from_names +libniftiio.so nifti_short_order +libniftiio.so nifti_simple_init_nim +libniftiio.so nifti_slice_string +libniftiio.so nifti_strdup +libniftiio.so nifti_swap_16bytes +libniftiio.so nifti_swap_2bytes +libniftiio.so nifti_swap_4bytes +libniftiio.so nifti_swap_8bytes +libniftiio.so nifti_swap_Nbytes +libniftiio.so nifti_swap_as_analyze +libniftiio.so nifti_test_datatype_sizes +libniftiio.so nifti_type_and_names_match +libniftiio.so nifti_units_string +libniftiio.so nifti_update_dims_from_array +libniftiio.so nifti_validfilename +libniftiio.so nifti_write_all_data +libniftiio.so nifti_write_ascii_image +libniftiio.so nifti_write_buffer +libniftiio.so nifti_xform_string +libniftiio.so old_swap_nifti_header +libniftiio.so swap_nifti_header +libniftiio.so valid_nifti_brick_list +libniftiio.so valid_nifti_extensions +libznz.so Xznzclose +libznz.so znzopen +libznz.so znzputs +libznz.so znzread +libznz.so znzrewind +libznz.so znzseek +libznz.so znztell +libznz.so znzwrite From 063e83833fb3edd39fe904188378f726345fa96c Mon Sep 17 00:00:00 2001 From: "Hans J. Johnson" Date: Mon, 21 Sep 2026 13:27:08 -0500 Subject: [PATCH 09/77] COMP: Submit dashboard results over https, at the documented start time The dashboard script restates the CTestConfig.cmake settings because CTest's delayed initialization does not pick them up, and the restated copy had drifted from the original. Submit over https rather than http. my.cdash.org serves plain http without redirecting, so submissions were going unencrypted. Use the 00:00:00 EST nightly start time that CTestConfig.cmake declares, rather than 01:00:00 UTC. The two differ by four hours, so nightly builds were filed under the wrong day on the dashboard. --- cmake/nifti_common.cmake | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/cmake/nifti_common.cmake b/cmake/nifti_common.cmake index e766f738..ed8f85b9 100644 --- a/cmake/nifti_common.cmake +++ b/cmake/nifti_common.cmake @@ -267,8 +267,10 @@ endif() set(CTEST_CHECKOUT_COMMAND "\"${CMAKE_COMMAND}\" -P \"${ctest_checkout_script}\"") # CTest delayed initialization is broken, so we put the # CTestConfig.cmake info here. - set(CTEST_NIGHTLY_START_TIME "01:00:00 UTC") - set(CTEST_DROP_METHOD "http") + # Keep these in agreement with CTestConfig.cmake; a nightly start time + # that disagrees files builds under the wrong day on the dashboard. + set(CTEST_NIGHTLY_START_TIME "00:00:00 EST") + set(CTEST_DROP_METHOD "https") set(CTEST_DROP_SITE "my.cdash.org") set(CTEST_DROP_LOCATION "/submit.php?project=nifti_clib") set(CTEST_DROP_SITE_CDASH TRUE) From f578e03b8dbcd4c7bfcc401c5e1861d997d2d10b Mon Sep 17 00:00:00 2001 From: "Hans J. Johnson" Date: Mon, 21 Sep 2026 13:43:13 -0500 Subject: [PATCH 10/77] COMP: Finish the migration from Travis to GitHub Actions The 2025 conversion to GitHub Actions replaced .travis.yml with two workflows but left the dashboard script untouched, so it still required a Travis environment. The workflow that drives it named the wrong branch and never ran, which is why nothing surfaced the mismatch. Rename travis_dashboard.cmake to github_dashboard.cmake and take its inputs from the runner that actually exists. CTEST_SITE now comes from RUNNER_OS rather than the required TRAVIS_APP_HOST, whose absence aborted the script before it configured anything, and the build name carries RUNNER_OS in place of TRAVIS_OS_NAME. Compare the branch name against its value rather than against the literal string "ENV{BUILD_SOURCEBRANCHNAME}", so the Continuous and Nightly models can be selected at all, and give the workflow a branch name on push events as well as on pull requests. Apply the same corrections to local_dashboard.cmake. --- .github/workflows/build.yml | 4 ++-- ...travis_dashboard.cmake => github_dashboard.cmake} | 12 +++++++----- cmake/local_dashboard.cmake | 9 ++++----- 3 files changed, 13 insertions(+), 12 deletions(-) rename cmake/{travis_dashboard.cmake => github_dashboard.cmake} (90%) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2a5a8922..e2bd685b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -86,7 +86,7 @@ jobs: AGENT_BUILDDIRECTORY: ${{ github.workspace }}/NIFTIworkspace BUILD_SOURCESDIRECTORY: ${{ github.workspace }} SYSTEM_PULLREQUEST_SOURCEBRANCH: ${{ github.head_ref }} - BUILD_SOURCEBRANCHNAME: ${{ github.head_ref }} + BUILD_SOURCEBRANCHNAME: ${{ github.head_ref || github.ref_name }} BUILD_BUILDID: ${{ github.run_id }} SYSTEM_PULLREQUEST_PULLREQUESTNUMBER: ${{ github.event.pull_request.number }} CTEST_SCRIPT_DIRECTORY: ${{ github.workspace }}/cmake @@ -131,5 +131,5 @@ jobs: - name: Run CTest run: | - eval $SCANBUILD_EXE ctest -S ${CTEST_SCRIPT_DIRECTORY}/travis_dashboard.cmake -V -j 4 + eval $SCANBUILD_EXE ctest -S ${CTEST_SCRIPT_DIRECTORY}/github_dashboard.cmake -V -j 4 diff --git a/cmake/travis_dashboard.cmake b/cmake/github_dashboard.cmake similarity index 90% rename from cmake/travis_dashboard.cmake rename to cmake/github_dashboard.cmake index 680e0031..8d358a70 100644 --- a/cmake/travis_dashboard.cmake +++ b/cmake/github_dashboard.cmake @@ -21,8 +21,10 @@ function(set_from_env var env_var) endif() endfunction() -set_from_env(CTEST_SITE "TRAVIS_APP_HOST" REQUIRED) -set(CTEST_SITE "travis.${CTEST_SITE}") +# RUNNER_OS is set by GitHub Actions ("Linux", "macOS", "Windows"). The +# default keeps the script usable from a developer machine. +set_from_env(CTEST_SITE "RUNNER_OS" DEFAULT "unknown") +set(CTEST_SITE "github.${CTEST_SITE}") set(CTEST_UPDATE_VERSION_ONLY 1) # https://gitlab.kitware.com/cmake/community/wikis/doc/ctest/Scripting-Of-CTest @@ -55,11 +57,11 @@ if(NOT CTEST_BUILD_NAME) set(branch "-$ENV{SYSTEM_PULLREQUEST_SOURCEBRANCH}") set(dashboard_git_branch "$ENV{SYSTEM_PULLREQUEST_SOURCEBRANCH}") set(dashboard_model "Experimental") - elseif(ENV{BUILD_SOURCEBRANCHNAME} STREQUAL "master") + elseif("$ENV{BUILD_SOURCEBRANCHNAME}" STREQUAL "master") set(branch "-master") set(dashboard_git_branch "$ENV{BUILD_SOURCEBRANCHNAME}") set(dashboard_model "Continuous") - elseif(ENV{BUILD_SOURCEBRANCHNAME} STREQUAL "nightly-master") + elseif("$ENV{BUILD_SOURCEBRANCHNAME}" STREQUAL "nightly-master") set(branch "-nightly-master") set(dashboard_git_branch "$ENV{BUILD_SOURCEBRANCHNAME}") set(dashboard_model "Nightly") @@ -76,7 +78,7 @@ if(NOT CTEST_BUILD_NAME) endif() set(CTEST_BUILD_NAME - "$ENV{BLDPREFIX}_$ENV{TRAVIS_OS_NAME}-$ENV{BUILD_BUILDID}_${pr}_${branch}") + "$ENV{BLDPREFIX}_$ENV{RUNNER_OS}-$ENV{BUILD_BUILDID}_${pr}_${branch}") endif() set(dashboard_cache " diff --git a/cmake/local_dashboard.cmake b/cmake/local_dashboard.cmake index a69588c1..c85a9dbd 100644 --- a/cmake/local_dashboard.cmake +++ b/cmake/local_dashboard.cmake @@ -32,9 +32,8 @@ function(set_from_env var env_var) endif() endfunction() -#set_from_env(CTEST_SITE "TRAVIS_APP_HOST" REQUIRED) cmake_host_system_information(RESULT CTEST_SITE QUERY HOSTNAME) -set(CTEST_SITE "travis.${CTEST_SITE}") +set(CTEST_SITE "local.${CTEST_SITE}") set(CTEST_UPDATE_VERSION_ONLY 1) set_from_env(PARALLEL_LEVEL "PARALLEL_LEVEL" DEFAULT 8) @@ -59,11 +58,11 @@ if(NOT CTEST_BUILD_NAME) set(branch "-$ENV{SYSTEM_PULLREQUEST_SOURCEBRANCH}") set(dashboard_git_branch "$ENV{SYSTEM_PULLREQUEST_SOURCEBRANCH}") set(dashboard_model "Experimental") - elseif(ENV{BUILD_SOURCEBRANCHNAME} STREQUAL "master") + elseif("$ENV{BUILD_SOURCEBRANCHNAME}" STREQUAL "master") set(branch "-master") set(dashboard_git_branch "$ENV{BUILD_SOURCEBRANCHNAME}") set(dashboard_model "Continuous") - elseif(ENV{BUILD_SOURCEBRANCHNAME} STREQUAL "nightly-master") + elseif("$ENV{BUILD_SOURCEBRANCHNAME}" STREQUAL "nightly-master") set(branch "-nightly-master") set(dashboard_git_branch "$ENV{BUILD_SOURCEBRANCHNAME}") set(dashboard_model "Nightly") @@ -80,7 +79,7 @@ if(NOT CTEST_BUILD_NAME) endif() set(CTEST_BUILD_NAME - "$ENV{TRAVIS_OS_NAME}-$ENV{BUILD_BUILDID}${pr}${branch}") + "$ENV{RUNNER_OS}-$ENV{BUILD_BUILDID}${pr}${branch}") endif() set(dashboard_cache " From 4673c40690eae3ac1d819dde0ec81723b19edb7d Mon Sep 17 00:00:00 2001 From: "Hans J. Johnson" Date: Mon, 21 Sep 2026 15:48:42 -0500 Subject: [PATCH 11/77] ENH: Declare the three functions that have a published counterpart FslGetHdrImgNames and FslSetIntensityScaling are defined here but declared nowhere, while FslGetIntensityScaling and FslInit are already published. axml_recur_find_xml sits beside axml_recur in afni_xml.h the same way. The upstream fslio was deleted in 2015 in favour of a C++ replacement, so this copy is the surviving one and its header is ours to correct. Additive; no symbol changes. --- cifti/afni_xml.h | 2 ++ fsliolib/fslio.h | 3 +++ 2 files changed, 5 insertions(+) diff --git a/cifti/afni_xml.h b/cifti/afni_xml.h index b48758af..ecc4ab3e 100644 --- a/cifti/afni_xml.h +++ b/cifti/afni_xml.h @@ -88,6 +88,8 @@ int axml_free_xlist(afni_xml_list * axlist); char * axml_attr_value(afni_xml_t * ax, const char * name); int axml_recur(int(*func)(FILE*,afni_xml_t*,int), afni_xml_t * ax); +afni_xml_t * axml_recur_find_xml(int (*func)(afni_xml_t *, int), afni_xml_t * ax, + int depth, int max_depth); /* control API */ diff --git a/fsliolib/fslio.h b/fsliolib/fslio.h index d979fd48..ab1edbd5 100644 --- a/fsliolib/fslio.h +++ b/fsliolib/fslio.h @@ -206,6 +206,8 @@ FSL_API int FslReadRawHeader(void *buffer, const char* filename); /* simple creation and clone/copy operations */ FSL_API FSLIO *FslInit(void); +FSL_API void FslGetHdrImgNames(const char* filename, const FSLIO* fslio, + char** hdrname, char** imgname); FSL_API void FslInitHeader(FSLIO *fslio, short t, size_t x, size_t y, size_t z, size_t v, float vx, float vy, float vz, float tr, @@ -233,6 +235,7 @@ FSL_API void FslGetTimeUnits(FSLIO *fslio, char *units); FSL_API void FslSetDataType(FSLIO *fslio, short t); FSL_API size_t FslGetDataType(FSLIO *fslio, short *t); FSL_API int FslGetIntensityScaling(FSLIO *fslio, float *slope, float *intercept); +FSL_API void FslSetIntensityScaling(FSLIO *fslio, float slope, float intercept); FSL_API void FslSetIntent(FSLIO *fslio, short intent_code, float p1, float p2, float p3); FSL_API short FslGetIntent(FSLIO *fslio, short *intent_code, float *p1, float *p2, float *p3); From 8cdcf1ab1e8bb6aa7f46c66c90bab1c718ea6cbd Mon Sep 17 00:00:00 2001 From: "Hans J. Johnson" Date: Mon, 21 Sep 2026 16:00:29 -0500 Subject: [PATCH 12/77] COMP: Give the fsliolib internal functions static linkage Nine functions in fslio.c have external linkage and no declaration in any header. Nothing in the tree calls them across a translation unit, AFNI's vendored copy never calls them, and no public source outside a vendored copy of this file references them, so a caller would have had to declare them itself. Collect their prototypes in one block so the boundary between internal and published is visible in one place. FslSetVoxUnits and FslGetVoxUnits have no caller at all and are left under #if 0 rather than deleted; FslSetTimeUnits and FslGetTimeUnits are published, so the asymmetry is worth keeping visible. Removes nine symbols from libfslio, which the baseline records. --- cmake/exported_symbols_linux.txt | 9 --------- fsliolib/fslio.c | 33 +++++++++++++++++++++++--------- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/cmake/exported_symbols_linux.txt b/cmake/exported_symbols_linux.txt index 14effdcd..042959f5 100644 --- a/cmake/exported_symbols_linux.txt +++ b/cmake/exported_symbols_linux.txt @@ -35,7 +35,6 @@ libfslio.so FslCheckForMultipleFileNames libfslio.so FslCloneHeader libfslio.so FslClose libfslio.so FslFileExists -libfslio.so FslFileType libfslio.so FslFileTypeString libfslio.so FslGetAnalyzeOrigin libfslio.so FslGetAuxFile @@ -46,7 +45,6 @@ libfslio.so FslGetDim libfslio.so FslGetDimensionality libfslio.so FslGetEnvOutputType libfslio.so FslGetFileType -libfslio.so FslGetFileType2 libfslio.so FslGetHdrImgNames libfslio.so FslGetIgnoreMFQ libfslio.so FslGetIntensityScaling @@ -54,7 +52,6 @@ libfslio.so FslGetIntent libfslio.so FslGetLeftRightOrder libfslio.so FslGetMMCoord libfslio.so FslGetOverrideOutputType -libfslio.so FslGetReadFileType libfslio.so FslGetRigidXform libfslio.so FslGetStdXform libfslio.so FslGetTimeUnits @@ -62,14 +59,11 @@ libfslio.so FslGetVolSize libfslio.so FslGetVolumeAsScaledDouble libfslio.so FslGetVoxCoord libfslio.so FslGetVoxDim -libfslio.so FslGetVoxUnits libfslio.so FslGetWriteMode libfslio.so FslInit -libfslio.so FslInit4Write libfslio.so FslInitHeader libfslio.so FslIsCompressedFileType libfslio.so FslIsSingleFileType -libfslio.so FslIsValidFileType libfslio.so FslMakeBaseName libfslio.so FslOpen libfslio.so FslReadAllVolumes @@ -96,17 +90,14 @@ libfslio.so FslSetRigidXform libfslio.so FslSetStdXform libfslio.so FslSetTimeUnits libfslio.so FslSetVoxDim -libfslio.so FslSetVoxUnits libfslio.so FslSetWriteMode libfslio.so FslWriteAllVolumes libfslio.so FslWriteHeader libfslio.so FslWriteVolumes libfslio.so FslXOpen -libfslio.so check_for_multiple_filenames libfslio.so convertBufferToScaledDouble libfslio.so d3matrix libfslio.so d4matrix -libfslio.so fsl_fileexists libfslio.so mat44_to_mat33 libnifti2.so disp_nifti_1_header libnifti2.so disp_nifti_2_header diff --git a/fsliolib/fslio.c b/fsliolib/fslio.c index 5b775e77..c49f9a24 100644 --- a/fsliolib/fslio.c +++ b/fsliolib/fslio.c @@ -28,6 +28,18 @@ #include "fslio.h" #include "assert.h" +/* Internal to this file. Nothing in the tree calls them across a + translation unit, no header declares them, and no public source + outside a vendored copy of this file references them. A downstream + project that needs one declares it in fslio.h with FSL_API. */ +static int FslIsValidFileType(int filetype); +static int FslGetFileType2(const FSLIO *fslio, int quiet); +static int FslFileType(const char *fname); +static int FslGetReadFileType(const FSLIO *fslio); +static void FslInit4Write(FSLIO* fslio, const char* filename, int ft); +static int fsl_fileexists(const char* fname); +static int check_for_multiple_filenames(const char* filename); + static int FslIgnoreMFQ=0; static int FslOverrideOutputType=-1; @@ -56,7 +68,7 @@ const char* FslFileTypeString(int filetype) } -int FslIsValidFileType(int filetype) +static int FslIsValidFileType(int filetype) { if ( (filetype!=FSL_TYPE_ANALYZE) && (filetype!=FSL_TYPE_ANALYZE_GZ) && (filetype!=FSL_TYPE_NIFTI) && (filetype!=FSL_TYPE_NIFTI_GZ) && @@ -85,7 +97,7 @@ int FslBaseFileType(int filetype) } -int FslGetFileType2(const FSLIO *fslio, int quiet) +static int FslGetFileType2(const FSLIO *fslio, int quiet) { FSLIO *mutablefslio; if (fslio==NULL) FSLIOERR("FslGetFileType: Null pointer passed for FSLIO"); @@ -190,7 +202,7 @@ int FslGetEnvOutputType(void) } -int FslFileType(const char* fname) +static int FslFileType(const char* fname) { /* return type is FSL_TYPE_* or -1 to indicate undetermined */ /* use name as first priority but if that is ambiguous then resolve using environment */ @@ -224,7 +236,7 @@ int FslFileType(const char* fname) /************************************************************ * FslGetReadFileType ************************************************************/ -/*! \fn int FslGetReadFileType(const FSLIO *fslio) +/*! \fn static int FslGetReadFileType(const FSLIO *fslio) \brief return the best estimate of the true file type This function is used to return the best estimate of the true file type once @@ -389,7 +401,7 @@ void FslSetInit(FSLIO* fslio) -void FslInit4Write(FSLIO* fslio, const char* filename, int ft) +static void FslInit4Write(FSLIO* fslio, const char* filename, int ft) { /* ft determines filetype if ft>=0*/ int imgtype; @@ -502,7 +514,7 @@ void FslCloneHeader(FSLIO *dest, const FSLIO *src) } -int fsl_fileexists(const char* fname) +static int fsl_fileexists(const char* fname) { znzFile fp; fp = znzopen( fname , "rb" , 1 ) ; @@ -559,7 +571,7 @@ int FslCheckForMultipleFileNames(const char* filename) -int check_for_multiple_filenames(const char* filename) +static int check_for_multiple_filenames(const char* filename) { char *basename, *tmpname; char *otype; @@ -1364,7 +1376,9 @@ void FslSetAuxFile(FSLIO *fslio,const char *aux_file) } -void FslSetVoxUnits(FSLIO *fslio, const char *units) +#if 0 +/* No caller in this file, no header declares them, and no public source uses them. */ +static void FslSetVoxUnits(FSLIO *fslio, const char *units) { int unitcode=0; if (fslio==NULL) FSLIOERR("FslSetVoxUnits: Null pointer passed for FSLIO"); @@ -1384,7 +1398,7 @@ void FslSetVoxUnits(FSLIO *fslio, const char *units) } -void FslGetVoxUnits(FSLIO *fslio, char *units) +static void FslGetVoxUnits(FSLIO *fslio, char *units) { if (fslio==NULL) FSLIOERR("FslGetVoxUnits: Null pointer passed for FSLIO"); if (fslio->niftiptr!=NULL) { @@ -1394,6 +1408,7 @@ void FslGetVoxUnits(FSLIO *fslio, char *units) fprintf(stderr,"Warning:: Minc is not yet supported\n"); } } +#endif void FslSetTimeUnits(FSLIO *fslio, const char *units) { From 54e74e1ccf30ff3238277cda420805f56e822c7c Mon Sep 17 00:00:00 2001 From: "Hans J. Johnson" Date: Mon, 21 Sep 2026 16:00:38 -0500 Subject: [PATCH 13/77] COMP: Declare nifti_fileexists where it is defined nifti1_io.c and nifti2_io.c each define nifti_fileexists with external linkage, so libniftiio and libnifti2 export the same name and ELF link order decides which one a caller linking both resolves to. Declaring it locally silences the warning without choosing between publishing it and making it static; that choice needs the duplication settled first, and it covers a hundred names, not this one. --- nifti2/nifti2_io.c | 3 +++ niftilib/nifti1_io.c | 3 +++ 2 files changed, 6 insertions(+) diff --git a/nifti2/nifti2_io.c b/nifti2/nifti2_io.c index c8cc6b23..58b20841 100644 --- a/nifti2/nifti2_io.c +++ b/nifti2/nifti2_io.c @@ -472,6 +472,9 @@ static const nifti_type_ele nifti_type_list[] = { }; /*---------------------------------------------------------------------------*/ +/* Defined in both libniftiio and libnifti2; linkage pending that duplication. */ +int nifti_fileexists(const char* fname); + /* prototypes for internal functions - not part of exported library */ /* extension routines */ diff --git a/niftilib/nifti1_io.c b/niftilib/nifti1_io.c index eb8887f3..1992b188 100644 --- a/niftilib/nifti1_io.c +++ b/niftilib/nifti1_io.c @@ -411,6 +411,9 @@ static const nifti_type_ele nifti_type_list[] = { }; /*---------------------------------------------------------------------------*/ +/* Defined in both libniftiio and libnifti2; linkage pending that duplication. */ +int nifti_fileexists(const char* fname); + /* prototypes for internal functions - not part of exported library */ /* extension routines */ From 0c0e66c9537be74d19d67396dd7e4e36dd84e5d7 Mon Sep 17 00:00:00 2001 From: "Hans J. Johnson" Date: Mon, 21 Sep 2026 13:56:40 -0500 Subject: [PATCH 14/77] STYLE: Derive the aux_file bound from sizeof Keeps FslGetAuxFile and FslSetAuxFile deriving the same length so the pair cannot drift if the field width changes. sizeof is 24, so the copy still writes at most 23 bytes. --- fsliolib/fslio.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/fsliolib/fslio.c b/fsliolib/fslio.c index c49f9a24..aee7687b 100644 --- a/fsliolib/fslio.c +++ b/fsliolib/fslio.c @@ -1354,8 +1354,9 @@ void FslGetAuxFile(FSLIO *fslio,char *aux_file) { if (fslio==NULL) FSLIOERR("FslGetAuxFile: Null pointer passed for FSLIO"); if (fslio->niftiptr!=NULL) { - strncpy(aux_file,fslio->niftiptr->aux_file, 24); - aux_file[24-1] = '\0'; + /* aux_file must have room for sizeof(nifti_1_header::aux_file) bytes. */ + strncpy(aux_file,fslio->niftiptr->aux_file,sizeof(fslio->niftiptr->aux_file)-1); + aux_file[sizeof(fslio->niftiptr->aux_file)-1] = '\0'; } if (fslio->mincptr!=NULL) { fprintf(stderr,"Warning:: Minc is not yet supported\n"); From 5c1665c6d974465e87acaff4f3f04126057726de Mon Sep 17 00:00:00 2001 From: "Gabriel A. Devenyi" Date: Fri, 14 Aug 2026 23:21:23 -0400 Subject: [PATCH 15/77] BUG: Bound the aux_file copy by sizeof rather than a repeated 24 strncpy writes no terminator when the source fills the destination, so the copy is safe only because of the line that follows it, which is what -Wstringop-truncation reports. Copying at most sizeof(dest) - 1 makes the call safe on its own and takes the size from the buffer rather than a literal repeated twice. --- fsliolib/fslio.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fsliolib/fslio.c b/fsliolib/fslio.c index aee7687b..6eb00e06 100644 --- a/fsliolib/fslio.c +++ b/fsliolib/fslio.c @@ -1368,8 +1368,8 @@ void FslSetAuxFile(FSLIO *fslio,const char *aux_file) { if (fslio==NULL) FSLIOERR("FslSetAuxFile: Null pointer passed for FSLIO"); if (fslio->niftiptr!=NULL) { - strncpy(fslio->niftiptr->aux_file, aux_file, 24); - fslio->niftiptr->aux_file[24-1] = '\0'; + strncpy(fslio->niftiptr->aux_file,aux_file,sizeof(fslio->niftiptr->aux_file)-1); + fslio->niftiptr->aux_file[sizeof(fslio->niftiptr->aux_file)-1] = '\0'; } if (fslio->mincptr!=NULL) { fprintf(stderr,"Warning:: Minc is not yet supported\n"); From ab2d71c32f633dba018c65e0bd7134b4559e8748 Mon Sep 17 00:00:00 2001 From: "Gabriel A. Devenyi" Date: Sat, 15 Aug 2026 01:18:16 -0400 Subject: [PATCH 16/77] BUG: Free the extension data when nifti_add_extension fails act_add_exts() in both nifti_tool and nifti1_tool reads the extension data from a file into edata and, if nifti_add_extension() then fails, returns without freeing it. The success path a few lines below frees it already. Found by clang's static analyzer, on a path the test suite does not reach. --- nifti2/nifti_tool.c | 1 + niftilib/nifti1_tool.c | 1 + 2 files changed, 2 insertions(+) diff --git a/nifti2/nifti_tool.c b/nifti2/nifti_tool.c index ac249bf8..4af13d38 100644 --- a/nifti2/nifti_tool.c +++ b/nifti2/nifti_tool.c @@ -2284,6 +2284,7 @@ int act_add_exts( nt_opts * opts ) opts->etypes.list[ec]); if( nifti_add_extension(nim, ext, elen, opts->etypes.list[ec]) ){ + free(edata); /* may hold the file contents read just above */ nifti_image_free(nim); return 1; } diff --git a/niftilib/nifti1_tool.c b/niftilib/nifti1_tool.c index 15bb165a..c9344a7f 100644 --- a/niftilib/nifti1_tool.c +++ b/niftilib/nifti1_tool.c @@ -1865,6 +1865,7 @@ int act_add_exts( nt_opts * opts ) } if( nifti_add_extension(nim, ext, elen, opts->etypes.list[ec]) ){ + free(edata); /* may hold the file contents read just above */ nifti_image_free(nim); return 1; } From b4c689473ecd05a19c17507ee3dd60c5db30bb65 Mon Sep 17 00:00:00 2001 From: "Gabriel A. Devenyi" Date: Sat, 15 Aug 2026 16:27:21 -0400 Subject: [PATCH 17/77] BUG: Free the previous filename when an ASCII header repeats the attribute nifti_image_from_ascii() walks the attributes of an ASCII header and assigns the two filename fields with nim->fname = nifti_strdup(rhs) ; nim->iname = nifti_strdup(rhs) ; Nothing stops a header from carrying header_filename or image_filename twice, and nothing rejects the repeat, so the second assignment drops the first string. The input comes from a file, so the leak is attacker-controlled in size and count: one copy per repetition. Direct leak of 5 byte(s) in 1 object(s) allocated from: #0 malloc #1 nifti_strdup nifti2_io.c:1301 #2 nifti_image_from_ascii nifti2_io.c:8874 Freeing before the assignment costs one call and keeps the last value, which is what the function already documented by overwriting. The fields are NULL until the first assignment, and free(NULL) is defined, so no other path changes. The same two lines exist in nifti1_io.c and are fixed there too. Found by fuzzing nifti_image_from_ascii() with clang's libFuzzer under AddressSanitizer. --- nifti2/nifti2_io.c | 2 ++ niftilib/nifti1_io.c | 2 ++ 2 files changed, 4 insertions(+) diff --git a/nifti2/nifti2_io.c b/nifti2/nifti2_io.c index 58b20841..43a26f71 100644 --- a/nifti2/nifti2_io.c +++ b/nifti2/nifti2_io.c @@ -8826,9 +8826,11 @@ nifti_image *nifti_image_from_ascii( const char *str, int * bytes_read ) nim->nifti_type = NIFTI_FTYPE_NIFTI2_2 ; } else if( strcmp(lhs,"header_filename") == 0 ){ + free(nim->fname) ; /* the attribute may appear more than once */ nim->fname = nifti_strdup(rhs) ; } else if( strcmp(lhs,"image_filename") == 0 ){ + free(nim->iname) ; nim->iname = nifti_strdup(rhs) ; } else if( strcmp(lhs,"sto_xyz_matrix") == 0 ){ diff --git a/niftilib/nifti1_io.c b/niftilib/nifti1_io.c index 1992b188..0580c629 100644 --- a/niftilib/nifti1_io.c +++ b/niftilib/nifti1_io.c @@ -6706,9 +6706,11 @@ nifti_image *nifti_image_from_ascii( const char *str, int * bytes_read ) nim->nifti_type = NIFTI_FTYPE_ASCII ; } else if( strcmp(lhs,"header_filename") == 0 ){ + free(nim->fname) ; /* the attribute may appear more than once */ nim->fname = nifti_strdup(rhs) ; } else if( strcmp(lhs,"image_filename") == 0 ){ + free(nim->iname) ; nim->iname = nifti_strdup(rhs) ; } else if( strcmp(lhs,"sto_xyz_matrix") == 0 ){ From 40009a92db6f327703a357412f91079460f3dd79 Mon Sep 17 00:00:00 2001 From: "Gabriel A. Devenyi" Date: Sat, 15 Aug 2026 00:27:23 -0400 Subject: [PATCH 18/77] BUG: Free the header on nifti_tool's duplicate-file failure paths act_mod_hdrs(), act_mod_hdr2s() and act_swap_hdrs() -- five functions across the two tools -- read a header, then, when -prefix is given, duplicate the dataset before writing the modified header back. Each of the three ways that duplication can fail returns without freeing the header, and the last of them also loses the strdup'd duplicate name: nhdr = nt_read_header(fname, &nver, &swap, 0, ...); ... if( opts->prefix ) { nim = nt_image_read(opts, fname, 1, 1); if( !nim ) { fprintf(...); return 1; } /* nhdr */ if( nifti_set_filenames(nim, opts->prefix, 1, 1) ) { nifti_image_free(nim); return 1; /* nhdr */ } dupname = nifti_strdup(nim->fname); if( nifti_image_write_status(nim) ) { nifti_image_free(nim); return 1; /* nhdr, dupname */ } } ... free(dupname); free(nhdr); The normal path frees both. Reproduced by pointing -prefix at a directory that cannot be written: nifti_tool -mod_hdr -prefix /anat1 -infiles anat0.nii \ -mod_field qoffset_x -17.325 before: definitely lost: 348 bytes in 1 blocks after: ERROR SUMMARY: 0 errors from 0 contexts Separately, act_diff_nims() in both tools releases the first image with free(nim0) when reading the second one fails. That is a shallow free: it loses nim0's fname, iname and any data or extensions. It now calls nifti_image_free() like the success path six lines below. Found by running the test suite under valgrind. On the normal paths the suite is clean -- 484 traced processes, no invalid access, no uninitialised value and no leak in any nifti binary -- so these are error-path defects that the tests do not otherwise reach. --- nifti2/nifti_tool.c | 14 +++++++++++++- niftilib/nifti1_tool.c | 10 +++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/nifti2/nifti_tool.c b/nifti2/nifti_tool.c index 4af13d38..0c34d081 100644 --- a/nifti2/nifti_tool.c +++ b/nifti2/nifti_tool.c @@ -2844,7 +2844,7 @@ int act_diff_nims( nt_opts * opts ) if( ! nim0 ) return 1; /* errors have been printed */ nim1 = nt_image_read(opts, opts->infiles.list[1], 0, 0); - if( ! nim1 ){ free(nim0); return 1; } + if( ! nim1 ){ nifti_image_free(nim0); return 1; } if( g_debug > 1 ) fprintf(stderr,"\n-d nifti_image diffs between '%s' and '%s'...\n", @@ -3360,6 +3360,7 @@ int act_mod_hdrs( nt_opts * opts ) if( !nim ) { fprintf(stderr,"** failed to dup file '%s' before modifying\n", fname); + free(nhdr); return 1; } @@ -3370,6 +3371,7 @@ int act_mod_hdrs( nt_opts * opts ) { NTL_FERR(func,"failed to set prefix for new file: ",opts->prefix); nifti_image_free(nim); + free(nhdr); return 1; } dupname = nifti_strdup(nim->fname); /* so we know to free it */ @@ -3378,6 +3380,8 @@ int act_mod_hdrs( nt_opts * opts ) if( nifti_image_write_status(nim) ) { fprintf(stderr,"** failed to write image %s\n", nim->fname); nifti_image_free(nim); + free(dupname); + free(nhdr); return 1; } @@ -3478,6 +3482,7 @@ int act_mod_hdr2s( nt_opts * opts ) if( !nim ) { fprintf(stderr,"** failed to dup file '%s' before modifying\n", fname); + free(nhdr); return 1; } if( opts->keep_hist && nifti_add_extension(nim, opts->command, @@ -3487,6 +3492,7 @@ int act_mod_hdr2s( nt_opts * opts ) { NTL_FERR(func,"failed to set prefix for new file: ",opts->prefix); nifti_image_free(nim); + free(nhdr); return 1; } dupname = nifti_strdup(nim->fname); /* so we know to free it */ @@ -3495,6 +3501,8 @@ int act_mod_hdr2s( nt_opts * opts ) if( nifti_image_write_status(nim) ) { fprintf(stderr,"** failed to write image %s\n", nim->fname); nifti_image_free(nim); + free(dupname); + free(nhdr); return 1; } @@ -3624,6 +3632,7 @@ int act_swap_hdrs( nt_opts * opts ) if( !nim ) { fprintf(stderr,"** failed to dup file '%s' before modifying\n", fname); + free(nhdr); return 1; } if( opts->keep_hist && nifti_add_extension(nim, opts->command, @@ -3633,6 +3642,7 @@ int act_swap_hdrs( nt_opts * opts ) { NTL_FERR(func,"failed to set prefix for new file: ",opts->prefix); nifti_image_free(nim); + free(nhdr); return 1; } dupname = nifti_strdup(nim->fname); /* so we know to free it */ @@ -3641,6 +3651,8 @@ int act_swap_hdrs( nt_opts * opts ) if( nifti_image_write_status(nim) ) { fprintf(stderr,"** failed to write image %s\n", nim->fname); nifti_image_free(nim); + free(dupname); + free(nhdr); return 1; } diff --git a/niftilib/nifti1_tool.c b/niftilib/nifti1_tool.c index c9344a7f..d6e143e5 100644 --- a/niftilib/nifti1_tool.c +++ b/niftilib/nifti1_tool.c @@ -2278,7 +2278,7 @@ int act_diff_nims( nt_opts * opts ) if( ! nim0 ) return 1; /* errors have been printed */ nim1 = nt_image_read(opts, opts->infiles.list[1], 0); - if( ! nim1 ){ free(nim0); return 1; } + if( ! nim1 ){ nifti_image_free(nim0); return 1; } if( g_debug > 1 ) fprintf(stderr,"\n-d nifti_image diffs between '%s' and '%s'...\n", @@ -2607,6 +2607,7 @@ int act_mod_hdrs( nt_opts * opts ) if( !nim ) { fprintf(stderr,"** failed to dup file '%s' before modifying\n", fname); + free(nhdr); return 1; } if( opts->keep_hist && nifti_add_extension(nim, opts->command, @@ -2616,6 +2617,7 @@ int act_mod_hdrs( nt_opts * opts ) { NTL_FERR(func,"failed to set prefix for new file: ",opts->prefix); nifti_image_free(nim); + free(nhdr); return 1; } dupname = nifti_strdup(nim->fname); /* so we know to free it */ @@ -2624,6 +2626,8 @@ int act_mod_hdrs( nt_opts * opts ) if( nifti_image_write_status(nim) ) { fprintf(stderr,"** failed to write image %s\n", nim->fname); nifti_image_free(nim); + free(dupname); + free(nhdr); return 1; } @@ -2728,6 +2732,7 @@ int act_swap_hdrs( nt_opts * opts ) if( !nim ) { fprintf(stderr,"** failed to dup file '%s' before modifying\n", fname); + free(nhdr); return 1; } if( opts->keep_hist && nifti_add_extension(nim, opts->command, @@ -2737,6 +2742,7 @@ int act_swap_hdrs( nt_opts * opts ) { NTL_FERR(func,"failed to set prefix for new file: ",opts->prefix); nifti_image_free(nim); + free(nhdr); return 1; } dupname = nifti_strdup(nim->fname); /* so we know to free it */ @@ -2745,6 +2751,8 @@ int act_swap_hdrs( nt_opts * opts ) if( nifti_image_write_status(nim) ) { fprintf(stderr,"** failed to write image %s\n", nim->fname); nifti_image_free(nim); + free(dupname); + free(nhdr); return 1; } From 7b37e321171234b12f8b993d5d5d7c3f83d81977 Mon Sep 17 00:00:00 2001 From: "Gabriel A. Devenyi" Date: Fri, 14 Aug 2026 23:08:32 -0400 Subject: [PATCH 19/77] BUG: Fix signed/unsigned comparisons in the nifti tools Three -Wsign-compare warnings, each comparing a signed value against an unsigned one, where the signed operand is silently converted and a negative value would compare as enormous. nifti1_tool.c, nifti_tool.c, fill_cmd_string() `len < 0 || len >= remain`, len an int, remain a size_t. The `len < 0` test short-circuits first, so the conversion could not actually misfire, but the comparison relies on that ordering to be correct. Made explicit, matching the idiom used a few lines above. nifti_tool.c, read_file_text() `bytes != len64`, size_t against int64_t. len64 is validated as > 0 and <= INT_MAX immediately above, so the cast is lossless. --- nifti2/nifti_tool.c | 4 ++-- niftilib/nifti1_tool.c | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/nifti2/nifti_tool.c b/nifti2/nifti_tool.c index 0c34d081..7dfa3272 100644 --- a/nifti2/nifti_tool.c +++ b/nifti2/nifti_tool.c @@ -885,7 +885,7 @@ int fill_cmd_string( nt_opts * opts, int argc, const char * argv[]) if( has_space ) len = snprintf(cp, remain, " '%s'", argv[ac]); else len = snprintf(cp, remain, " %s", argv[ac]); - if( len < 0 || len >= remain ) { + if( len < 0 || (size_t)len >= remain ) { fprintf(stderr,"FCS: error parsing command, continuing...\n"); return 1; } @@ -2368,7 +2368,7 @@ static char * read_file_text(const char * filename, int * length) bytes = fread(text, sizeof(char), len64, fp); fclose(fp); /* in any case */ - if( bytes != len64 ) { + if( bytes != (size_t)len64 ) { fprintf(stderr,"** RFT: read only %zu of %" PRId64 " bytes from %s\n", bytes, len64, filename); free(text); diff --git a/niftilib/nifti1_tool.c b/niftilib/nifti1_tool.c index d6e143e5..7bd422b1 100644 --- a/niftilib/nifti1_tool.c +++ b/niftilib/nifti1_tool.c @@ -723,7 +723,7 @@ int fill_cmd_string( nt_opts * opts, int argc, const char * argv[]) if( has_space ) len = snprintf(cp, remain, " '%s'", argv[ac]); else len = snprintf(cp, remain, " %s", argv[ac]); - if( len < 0 || len >= remain ) { + if( len < 0 || (size_t)len >= remain ) { fprintf(stderr,"FCS: error parsing command, continuing...\n"); return 1; } From 31474388dc496114b86652b87aee527c5ce56cc5 Mon Sep 17 00:00:00 2001 From: "Hans J. Johnson" Date: Mon, 21 Sep 2026 19:16:56 -0500 Subject: [PATCH 20/77] COMP: Install the tools the analysis jobs actually invoke Two of the five Build and Test jobs have never run to completion. sanitize-clang-linux invokes scan-build, which lives in clang-tools and was not installed, so the job exits 127 before configuring. rel-clang-macos asks brew for "sed", which is not a formula; brew fails the step and the job exits 1 before configuring. The GNU sed the dashboard scripts expect is gnu-sed. Both predate the workflow's first successful run, so neither has regressed; the trigger named the wrong branch until recently and the jobs never executed. --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e2bd685b..3bb63fbe 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -98,12 +98,12 @@ jobs: if: runner.os == 'Linux' run: | sudo apt-get update - sudo apt-get install -y cmake valgrind help2man + sudo apt-get install -y cmake valgrind help2man clang-tools - name: Install Dependencies (macOS) if: runner.os == 'macOS' run: | - brew install cmake sed help2man + brew install cmake gnu-sed help2man - name: Set Compiler and Flags run: | From 7476a12456cda09e48f4e5d4d7c632b46fe44da0 Mon Sep 17 00:00:00 2001 From: "Gabriel A. Devenyi" Date: Sat, 15 Aug 2026 16:26:39 -0400 Subject: [PATCH 21/77] BUG: Stop loc_strnlen reading one byte past the buffer it is given loc_strnlen measures a string that need not be terminated, but it dereferences before testing the bound, so when no NUL appears in the first maxlen bytes the last iteration reads str[maxlen]. Both callers pass an unterminated buffer: axml_read_buf takes the caller's buffer and its length, and axml_read_file measures what fread returned, which fills the buffer for any larger file. AddressSanitizer on a buffer sized to its content reports a heap-buffer-overflow read 0 bytes after a 324-byte region. The returned length is unchanged wherever the old order was in bounds. --- cifti/afni_xml.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cifti/afni_xml.c b/cifti/afni_xml.c index a9910a1b..d7cc9a4f 100644 --- a/cifti/afni_xml.c +++ b/cifti/afni_xml.c @@ -1021,7 +1021,8 @@ static int64_t loc_strnlen(const char * str, int64_t maxlen) const char * sptr; int64_t len; - for( sptr=str, len=0; *sptr && len Date: Fri, 14 Aug 2026 23:04:50 -0400 Subject: [PATCH 22/77] STYLE: Add the missing newline at end of nifti1_tool.h The only file in the tree without a trailing newline, and the only -Wnewline-eof warning. C11 5.1.1.2p1 requires a non-empty source file to end in a newline not immediately preceded by a backslash, so this is undefined behaviour rather than only a diff annoyance. --- niftilib/nifti1_tool.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/niftilib/nifti1_tool.h b/niftilib/nifti1_tool.h index a8ca5ddd..14912ed9 100644 --- a/niftilib/nifti1_tool.h +++ b/niftilib/nifti1_tool.h @@ -157,4 +157,4 @@ nifti_1_header * nt_read_header(nt_opts * opts, const char * fname, int * swappe int check); -#endif /* NIFTI1_TOOL_H */ \ No newline at end of file +#endif /* NIFTI1_TOOL_H */ From 5e32002bd72f74d9a85eac8a0dd330d603101bc8 Mon Sep 17 00:00:00 2001 From: "Gabriel A. Devenyi" Date: Sat, 15 Aug 2026 16:41:33 -0400 Subject: [PATCH 23/77] BUG: Check dim[0] before using it to index dim[] in the NIFTI-2 converter nifti_convert_n2hdr2nim() uses nhdr.dim[0], the number of dimensions, as a loop bound over the eight-element dim[] array: for( ii=2 ; ii <= nhdr.dim[0] ; ii++ ) ... for( ii=nhdr.dim[0]+1 ; ii <= 7 ; ii++ ) ... for( ii=1 ; ii <= nhdr.dim[0] ; ii++ ) ... It never checks that dim[0] is in [0,7]. The NIFTI-1 converter gets that check for free, because need_nhdr_swap() rejects a dim[0] outside [1,7] in either byte order, but the NIFTI-2 path decides swapping from sizeof_hdr and reaches the loops with whatever the file said. A NIFTI-2 header with a large negative dim[0] therefore starts the second loop at a wild negative index, reads far outside the header and segfaults. This is not confined to the header API: nifti_image_read() gets there for any file with a valid 540 byte NIFTI-2 header, so a 604 byte file crashes nifti_tool: $ nifti_tool -disp_nim -infiles bad_n2_dim0.nii nifti2_io.c:5079: runtime error: index -6727636073941130588 out of bounds for type 'int64_t[8]' AddressSanitizer: SEGV ... in nifti_convert_n2hdr2nim dim[0] is now range checked in the same place, and in the same style, as the dim[1] check just below it. Zero stays acceptable, as it is on the NIFTI-1 side. Valid headers are unaffected: dim[0] outside [0,7] has no meaning in either format. Found by fuzzing nifti_convert_n2hdr2nim() with clang's libFuzzer under AddressSanitizer. --- nifti2/nifti2_io.c | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/nifti2/nifti2_io.c b/nifti2/nifti2_io.c index 43a26f71..566005c7 100644 --- a/nifti2/nifti2_io.c +++ b/nifti2/nifti2_io.c @@ -5036,6 +5036,14 @@ nifti_image* nifti_convert_n2hdr2nim(nifti_2_header nhdr, const char * fname) ERREX("bad datatype") ; } + /* dim[0] is the number of dimensions and the loops below index dim[] + with it; the NIFTI-1 path gets this check from need_nhdr_swap() */ + if( nhdr.dim[0] < 0 || nhdr.dim[0] > 7 ) + { + free(nim); + ERREX("bad dim[0]") ; + } + if( nhdr.dim[1] <= 0 ) { free(nim); From 66c76c22ea334cf2b10d941b5a1fddf5345414e3 Mon Sep 17 00:00:00 2001 From: "Gabriel A. Devenyi" Date: Sat, 15 Aug 2026 16:55:29 -0400 Subject: [PATCH 24/77] BUG: Refuse a header whose dimensions overflow the voxel count Both header converters multiply the dimensions into nim->nvox without checking the product: for( ii=1, nim->nvox=1; ii <= nhdr.dim[0]; ii++ ) nim->nvox *= nhdr.dim[ii]; Seven NIFTI-1 dimensions of 32767 are enough to overflow int64_t, and a NIFTI-2 header needs only two dimensions to do it: nifti2_io.c:4838: runtime error: signed integer overflow: 1152780773560811521 * 32767 cannot be represented in type 'int64_t' Signed overflow is undefined, and what the compiler does produce is a voxel count that no longer describes the file. nvox then goes on to nifti_get_volsize(), which multiplies it by nbyper for another unchecked product, and that result is used as an allocation size and a read length. Both products are now checked before they are made, in the way the surrounding code already reports a bad header. A header that overflows is rejected with a message instead of producing a silently wrong image. No valid image is affected: an int64_t voxel count is more than any file can hold. Found by fuzzing the header converters with clang's libFuzzer under UndefinedBehaviorSanitizer. --- nifti2/nifti2_io.c | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/nifti2/nifti2_io.c b/nifti2/nifti2_io.c index 566005c7..5a939dd3 100644 --- a/nifti2/nifti2_io.c +++ b/nifti2/nifti2_io.c @@ -4807,8 +4807,14 @@ nifti_image* nifti_convert_n1hdr2nim(nifti_1_header nhdr, const char * fname) nim->nv = nim->dim[6] = nhdr.dim[6]; nim->nw = nim->dim[7] = nhdr.dim[7]; - for( ii=1, nim->nvox=1; ii <= nhdr.dim[0]; ii++ ) + /* the product of the dimensions becomes an allocation size, so refuse + the header rather than let it wrap */ + for( ii=1, nim->nvox=1; ii <= nhdr.dim[0]; ii++ ){ + if( nhdr.dim[ii] > 0 && nim->nvox > INT64_MAX / nhdr.dim[ii] ){ + free(nim); ERREX("dim[] overflows the voxel count"); + } nim->nvox *= nhdr.dim[ii]; + } /**- set the type of data in voxels and how many bytes per voxel */ @@ -4817,6 +4823,11 @@ nifti_image* nifti_convert_n1hdr2nim(nifti_1_header nhdr, const char * fname) nifti_datatype_sizes( nim->datatype , &(nim->nbyper) , &(nim->swapsize) ) ; if( nim->nbyper == 0 ){ free(nim); ERREX("bad datatype"); } + /* nifti_get_volsize() multiplies these two */ + if( nim->nvox > INT64_MAX / nim->nbyper ){ + free(nim); ERREX("dim[] and datatype overflow the volume size"); + } + /**- set the grid spacings */ nim->dx = nim->pixdim[1] = nhdr.pixdim[1] ; @@ -5085,8 +5096,14 @@ nifti_image* nifti_convert_n2hdr2nim(nifti_2_header nhdr, const char * fname) nim->nv = nim->dim[6] = nhdr.dim[6]; nim->nw = nim->dim[7] = nhdr.dim[7]; - for( ii=1, nim->nvox=1; ii <= nhdr.dim[0]; ii++ ) + /* the product of the dimensions becomes an allocation size, so refuse + the header rather than let it wrap */ + for( ii=1, nim->nvox=1; ii <= nhdr.dim[0]; ii++ ){ + if( nhdr.dim[ii] > 0 && nim->nvox > INT64_MAX / nhdr.dim[ii] ){ + free(nim); ERREX("dim[] overflows the voxel count"); + } nim->nvox *= nhdr.dim[ii]; + } /**- set the type of data in voxels and how many bytes per voxel */ @@ -5095,6 +5112,11 @@ nifti_image* nifti_convert_n2hdr2nim(nifti_2_header nhdr, const char * fname) nifti_datatype_sizes( nim->datatype , &(nim->nbyper) , &(nim->swapsize) ) ; if( nim->nbyper == 0 ){ free(nim); ERREX("bad datatype"); } + /* nifti_get_volsize() multiplies these two */ + if( nim->nvox > INT64_MAX / nim->nbyper ){ + free(nim); ERREX("dim[] and datatype overflow the volume size"); + } + /**- set the grid spacings */ nim->dx = nim->pixdim[1] = nhdr.pixdim[1] ; From 147a07a48fe8fafc0344471aae3b2936228a97c6 Mon Sep 17 00:00:00 2001 From: "Gabriel A. Devenyi" Date: Fri, 18 Sep 2026 21:50:37 -0400 Subject: [PATCH 25/77] BUG: Guard the voxel count in the NIFTI-1 library too The first commit guards both converters in nifti2/nifti2_io.c. niftilib/nifti1_io.c has the same loop in nifti_convert_nhdr2nim(), on the same untrusted path, and was left unguarded. $ nifti1_tool -disp_nim -infiles huge.nii # dim[0]=7, dim[1..7]=32767 dim 32 8 7 32767 32767 32767 32767 32767 32767 32767 nvox 64 1 -1073512449 The wrapped count then becomes the data allocation size and the result of nifti_get_volsize(). With the guard the header is refused: ** ERROR: nifti_convert_nhdr2nim: dim[] overflows the voxel count nifti_image.nvox is a size_t in this library and an int64_t in the NIFTI-2 one, so the bound here is SIZE_MAX rather than INT64_MAX. nifti_get_volsize() is size_t * size_t to match. dim[0] is already bounded by need_nhdr_swap(), and the loop that raises every dim to at least 1 still runs first, so the dim[ii] > 0 test never has to reject. Two more nvox loops remain in each library, in nifti_update_dims_from_array() and update_nifti_image_for_brick_list(). Both take dims that a caller has already set rather than dims read from a file, and reach them from nifti_tool's command line, so they are left for a separate change. --- niftilib/nifti1_io.c | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/niftilib/nifti1_io.c b/niftilib/nifti1_io.c index 0580c629..f25d3df8 100644 --- a/niftilib/nifti1_io.c +++ b/niftilib/nifti1_io.c @@ -3766,8 +3766,15 @@ nifti_image* nifti_convert_nhdr2nim(struct nifti_1_header nhdr, nim->nv = nim->dim[6] = nhdr.dim[6]; nim->nw = nim->dim[7] = nhdr.dim[7]; - for( ii=1, nim->nvox=1; ii <= nhdr.dim[0]; ii++ ) + /* the product of the dimensions becomes an allocation size, so refuse + the header rather than let it wrap. nvox is a size_t here, where the + NIFTI-2 library uses int64_t, so the bound is SIZE_MAX. */ + for( ii=1, nim->nvox=1; ii <= nhdr.dim[0]; ii++ ){ + if( nhdr.dim[ii] > 0 && nim->nvox > SIZE_MAX / (size_t)nhdr.dim[ii] ){ + free(nim); ERREX("dim[] overflows the voxel count"); + } nim->nvox *= nhdr.dim[ii]; + } /**- set the type of data in voxels and how many bytes per voxel */ @@ -3776,6 +3783,11 @@ nifti_image* nifti_convert_nhdr2nim(struct nifti_1_header nhdr, nifti_datatype_sizes( nim->datatype , &(nim->nbyper) , &(nim->swapsize) ) ; if( nim->nbyper == 0 ){ free(nim); ERREX("bad datatype"); } + /* nifti_get_volsize() multiplies these two */ + if( nim->nvox > SIZE_MAX / (size_t)nim->nbyper ){ + free(nim); ERREX("dim[] and datatype overflow the volume size"); + } + /**- set the grid spacings */ nim->dx = nim->pixdim[1] = nhdr.pixdim[1] ; From 16a6b67c0221b12b29c5c5c3581c27e0e5559b8a Mon Sep 17 00:00:00 2001 From: "Gabriel A. Devenyi" Date: Sat, 15 Aug 2026 16:26:11 -0400 Subject: [PATCH 26/77] BUG: Swap a nifti_1_header as a NIFTI-1 header, not as whatever its magic claims swap_nifti_header() takes a void pointer and a version number, and picks the struct to swap from the version: if ( ni_ver == 0 ) nifti_swap_as_analyze((nifti_analyze75 *)hdr); else if( ni_ver == 1 ) nifti_swap_as_nifti1((nifti_1_header *)hdr); else if( ni_ver == 2 ) nifti_swap_as_nifti2((nifti_2_header *)hdr); Two callers pass it a nifti_1_header, 348 bytes, together with the version taken from that header's own magic string by NIFTI_VERSION(). A file whose magic is "n+2" therefore has 540 bytes swapped in place: 192 bytes of read and write past the end of the caller's stack object. nifti_read_n1_hdr() reads the header straight from a file, so the bytes that decide this come from the file being read. A 348 byte file with sizeof_hdr = 348, dim[0] byte-swapped, magic = "n+2" is enough; need_nhdr_swap() then reports that swapping is needed and the overflow happens before any validity check. AddressSanitizer: ERROR: AddressSanitizer: stack-buffer-overflow READ of size 1 ... #0 nifti_swap_4bytes nifti2_io.c:3051 #1 nifti_swap_as_nifti2 nifti2_io.c:3194 #2 nifti_read_n1_hdr nifti2_io.c:5394 Address ... is located in stack of thread T0 at offset 412 in frame [64, 412) 'nhdr' (line 5339) <== Memory access ... overflows nifti_convert_n1hdr2nim() has the same line and takes its header from the caller, so any application that fills a nifti_1_header itself can reach it too. Both call sites know the struct they hold, so both now ask for the swap that struct supports: analyze when the magic is absent, NIFTI-1 otherwise. Headers claiming versions 3 to 9, which swap_nifti_header() previously refused with a message and left unswapped, are now swapped as NIFTI-1 as well, which is the only interpretation 348 bytes allow. nifti1_io.c is not affected: its swap_nifti_header() takes a nifti_1_header * and a flag, so it cannot choose a wider struct. Found by fuzzing nifti_convert_n1hdr2nim() with clang's libFuzzer under AddressSanitizer. --- nifti2/nifti2_io.c | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/nifti2/nifti2_io.c b/nifti2/nifti2_io.c index 5a939dd3..f0bd9aae 100644 --- a/nifti2/nifti2_io.c +++ b/nifti2/nifti2_io.c @@ -4744,7 +4744,10 @@ nifti_image* nifti_convert_n1hdr2nim(nifti_1_header nhdr, const char * fname) } if( doswap ) { if ( g_opts.debug > 3 ) disp_nifti_1_header("-d ni1 pre-swap: ", &nhdr); - swap_nifti_header( &nhdr , ni_ver ) ; + /* nhdr is a nifti_1_header, so swap it as one. ni_ver comes from the + magic string, and a magic of "n+2" would otherwise have + swap_nifti_header() treat these 348 bytes as a 540 byte header. */ + swap_nifti_header( &nhdr , ni_ver ? 1 : 0 ) ; } if ( g_opts.debug > 2 ) disp_nifti_1_header("-d nhdr2nim : ", &nhdr); @@ -5397,7 +5400,9 @@ nifti_1_header * nifti_read_n1_hdr(const char * hname, int *swapped, int check) if( lswap ) { if ( g_opts.debug > 3 ) disp_nifti_1_header("-d nhdr pre-swap: ", &nhdr); - swap_nifti_header( &nhdr , NIFTI_VERSION(nhdr) ) ; + /* only sizeof(nifti_1_header) bytes were read, so swap as that; see + the same guard in nifti_convert_n1hdr2nim() */ + swap_nifti_header( &nhdr , NIFTI_VERSION(nhdr) ? 1 : 0 ) ; } if ( g_opts.debug > 2 ) disp_nifti_1_header("-d nhdr post-swap: ", &nhdr); From 522e62432b5663bf8b1ba3341120253b2bfb1ced Mon Sep 17 00:00:00 2001 From: "Gabriel A. Devenyi" Date: Fri, 18 Sep 2026 21:49:03 -0400 Subject: [PATCH 27/77] BUG: Swap the tool's headers at their own width too nifti_tool.c has the same defect this branch fixes in the library: it passes NIFTI_VERSION(*nhdr), read from the magic string, to swap_nifti_header(), which selects the struct to swap by that number. act_mod_hdrs() and act_swap_hdrs() hold a nifti_1_header of 348 bytes. A magic of "n+2" makes NIFTI_VERSION() return 2, so swap_nifti_header() treats the allocation as a 540 byte nifti_2_header and reads and writes past its end. Both functions refuse a header that is valid NIFTI-2, but a header that is valid as neither version reaches the call. $ printf ... > evil.nii # 348 bytes, sizeof_hdr byte swapped, # magic "n+2" $ nifti_tool -mod_hdr -mod_field descrip hello -overwrite \ -infiles evil.nii ==2747364==ERROR: AddressSanitizer: heap-buffer-overflow #0 nifti_swap_4bytes nifti2_io.c:3029 #1 nifti_swap_as_nifti2 nifti2_io.c:3172 #2 swap_nifti_header nifti2_io.c:3131 #3 act_mod_hdrs nifti_tool.c:3389 0 bytes after 348-byte region allocated in nifti_read_n1_hdr() Both now pass 1, or 0 for ANALYZE, which are the two 348 byte layouts. act_mod_hdr2s() holds a nifti_2_header and gets the explicit 2, as act_swap_hdrs() already does for its own NIFTI-2 display path. old_swap_nifti_header() takes a nifti_1_header and a boolean, so the calls beside these need no change. --- nifti2/nifti_tool.c | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/nifti2/nifti_tool.c b/nifti2/nifti_tool.c index 7dfa3272..85bc3f54 100644 --- a/nifti2/nifti_tool.c +++ b/nifti2/nifti_tool.c @@ -3391,7 +3391,11 @@ int act_mod_hdrs( nt_opts * opts ) nifti_image_free(nim); } else if ( swap ) - swap_nifti_header(nhdr, NIFTI_VERSION(*nhdr)); + /* nhdr is a nifti_1_header, so swap it as one. NIFTI_VERSION() + reads the magic string, and a magic of "n+2" would otherwise + have swap_nifti_header() treat these 348 bytes as a 540 byte + header. ni_ver 0 and 1 are both 348 byte layouts. */ + swap_nifti_header(nhdr, NIFTI_VERSION(*nhdr) ? 1 : 0); /* if all is well, overwrite header in fname dataset */ (void)write_hdr_to_file(nhdr, fname); /* errors printed in function */ @@ -3512,7 +3516,9 @@ int act_mod_hdr2s( nt_opts * opts ) nifti_image_free(nim); } else if ( swap ) - swap_nifti_header(nhdr, NIFTI_VERSION(*nhdr)); + /* nhdr is a nifti_2_header; use the explicit version rather than + the magic, which could claim "n+1" and swap 540 bytes as 348 */ + swap_nifti_header(nhdr, 2); /* if all is well, overwrite header in fname dataset */ (void)write_hdr2_to_file(nhdr, fname); /* errors printed in function */ @@ -3613,8 +3619,12 @@ int act_swap_hdrs( nt_opts * opts ) swap_nifti_header(nhdr, 0); /* undo ANALYZE */ swap_nifti_header(nhdr, 1); /* swap NIFTI */ } else if ( opts->swap_old ) { - /* undo whichever was done and apply the old way */ - swap_nifti_header(nhdr, NIFTI_VERSION(*nhdr)); + /* undo whichever was done and apply the old way. As above, + nhdr is a nifti_1_header, so it must not be swapped as a + 540 byte NIFTI-2 header just because its magic says "n+2". + old_swap_nifti_header() takes a nifti_1_header and a + boolean, so it needs no such guard. */ + swap_nifti_header(nhdr, NIFTI_VERSION(*nhdr) ? 1 : 0); old_swap_nifti_header(nhdr, NIFTI_VERSION(*nhdr)); } From 0ddf192185ce5760ae854ea533e0e9cd428d44d8 Mon Sep 17 00:00:00 2001 From: "Gabriel A. Devenyi" Date: Fri, 18 Sep 2026 22:16:24 -0400 Subject: [PATCH 28/77] BUG: Fix the ambiguous-filename path in nifti_findhdrname Under FSLSTYLE the block that reports "Multiple possible filenames" has three defects, and they cannot be separated. free(basename); <- freed here char *gzname = calloc(...); <- unchecked strcpy(gzname, hdrname); <- used at once ... fprintf(stderr,"... %s\n", basename); <- read after free exit(134); <- kills the caller basename is read by the message, so it cannot be freed at the top. gzname goes straight into strcpy(), so it has to be checked. And once the function returns NULL instead of calling exit(), every path out has to release what it holds, which is what ties the three together: moving the free of basename decides what the error paths must free, and there are no error paths to speak of until exit() is gone. FSLSTYLE is off by default but is a supported configuration: -DFSLSTYLE:BOOL=ON turns it on, and nifti2/Makefile defines it always. Built and tested with -DFSLSTYLE=ON. The nifti_image_read() half of the original change is a separate commit. --- nifti2/nifti2_io.c | 16 +++++++++++++--- niftilib/nifti1_io.c | 16 +++++++++++++--- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/nifti2/nifti2_io.c b/nifti2/nifti2_io.c index f0bd9aae..3c782bc0 100644 --- a/nifti2/nifti2_io.c +++ b/nifti2/nifti2_io.c @@ -3760,16 +3760,26 @@ char * nifti_findhdrname(const char* fname) strcat(hdrname,elist[efirst]); #ifdef FSLSTYLE if (nifti_fileexists(hdrname)) { - free(basename); + /* basename is read by the error message below, so it cannot be + freed here; gzname's allocation is used at once, so it has to be + checked; and a library reports an ambiguous name to its caller + rather than ending the process, which means every path out of + here now has to release what it holds. */ char *gzname = (char *)calloc(sizeof(char),strlen(hdrname)+8); + if( !gzname ){ + fprintf(stderr,"** nifti_findhdrname: failed to alloc gzname\n"); + free(basename); free(hdrname); + return NULL; + } strcpy(gzname, hdrname); strcat(gzname,extzip); if (nifti_fileexists(gzname)) { fprintf(stderr,"Image Exception : Multiple possible filenames detected for basename (*.nii, *.nii.gz): %s\n", basename); - free(gzname); - exit(134); + free(gzname); free(basename); free(hdrname); + return NULL; } free(gzname); + free(basename); return hdrname; } #else diff --git a/niftilib/nifti1_io.c b/niftilib/nifti1_io.c index f25d3df8..1f447cbd 100644 --- a/niftilib/nifti1_io.c +++ b/niftilib/nifti1_io.c @@ -2822,16 +2822,26 @@ char * nifti_findhdrname(const char* fname) strcat(hdrname,elist[efirst]); #ifdef FSLSTYLE if (nifti_fileexists(hdrname)) { - free(basename); + /* basename is read by the error message below, so it cannot be + freed here; gzname's allocation is used at once, so it has to be + checked; and a library reports an ambiguous name to its caller + rather than ending the process, which means every path out of + here now has to release what it holds. */ char *gzname = (char *)calloc(sizeof(char),strlen(hdrname)+8); + if( !gzname ){ + fprintf(stderr,"** nifti_findhdrname: failed to alloc gzname\n"); + free(basename); free(hdrname); + return NULL; + } strcpy(gzname, hdrname); strcat(gzname,extzip); if (nifti_fileexists(gzname)) { fprintf(stderr,"Image Exception : Multiple possible filenames detected for basename (*.nii, *.nii.gz): %s\n", basename); - free(gzname); - exit(134); + free(gzname); free(basename); free(hdrname); + return NULL; } free(gzname); + free(basename); return hdrname; } #else From b8d5246f2fd4ea5935df41373da9166967a8b364 Mon Sep 17 00:00:00 2001 From: "Gabriel A. Devenyi" Date: Fri, 14 Aug 2026 23:11:56 -0400 Subject: [PATCH 29/77] BUG: Fix an out-of-bounds indirect call in axio_show_mim_summary mind = get_map_index(xt->xchild[kid]); if( kid >= 0 ) MIM_disp_funcs[mind](ofp, xt->xchild[kid], verb); The guard tests kid, the loop counter, which is never negative, so it is always true. The index actually used is mind, and get_map_index() returns -1 for any element name not in MIM_kids[]. A CIFTI file containing an unrecognised element under MatrixIndicesMap therefore reads a function pointer from before the start of MIM_disp_funcs and calls it. Reported by the clang static analyzer as security.ArrayBound, "Out of bound access to memory preceding 'MIM_disp_funcs'". --- cifti/afni_xml_io.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cifti/afni_xml_io.c b/cifti/afni_xml_io.c index a59f5c3e..a06229e4 100644 --- a/cifti/afni_xml_io.c +++ b/cifti/afni_xml_io.c @@ -263,7 +263,8 @@ int axio_show_mim_summary(FILE * fp, const char * mesg, afni_xml_t * ax, int ver for( kid=0; kidnchild; kid++ ) { mind = get_map_index(xt->xchild[kid]); - if( kid >= 0 ) MIM_disp_funcs[mind](ofp, xt->xchild[kid], verb); + /* get_map_index() returns -1 for an unrecognized element name */ + if( mind >= 0 ) MIM_disp_funcs[mind](ofp, xt->xchild[kid], verb); } } From 06cb5a64b9f37930ba2f94adf427ffd093bd5bb3 Mon Sep 17 00:00:00 2001 From: "Hans J. Johnson" Date: Tue, 22 Sep 2026 06:27:42 -0500 Subject: [PATCH 30/77] ENH: Cover the MatrixIndicesMap dispatch in axio_show_mim_summary Drives cifti_tool with -eval_type show_summary over two XML fixtures. The first holds a MatrixIndicesMap child whose name get_map_index() does not resolve, and fails without the preceding commit: the sanitizer legs report a global-buffer-overflow, an 8-byte read 8 bytes before MIM_disp_funcs. The second holds a BrainModel and asserts it is still displayed, so that skipping the unresolved name cannot pass by skipping every name. These are the first tests for the cifti library. The fixtures are small enough to keep in tree, so neither needs the external testing data. --- cifti/CMakeLists.txt | 16 ++++++++++++++++ cifti/testdata/mim_known_child.xml | 7 +++++++ cifti/testdata/mim_unknown_child.xml | 7 +++++++ 3 files changed, 30 insertions(+) create mode 100644 cifti/testdata/mim_known_child.xml create mode 100644 cifti/testdata/mim_unknown_child.xml diff --git a/cifti/CMakeLists.txt b/cifti/CMakeLists.txt index d19b7524..9e539c20 100644 --- a/cifti/CMakeLists.txt +++ b/cifti/CMakeLists.txt @@ -26,3 +26,19 @@ if(NIFTI_BUILD_APPLICATIONS) install_nifti_target(${NIFTI_PACKAGE_PREFIX}afni_xml_tool) install_nifti_target(${NIFTI_PACKAGE_PREFIX}cifti_tool) endif() + +if(NIFTI_BUILD_TESTING AND NIFTI_BUILD_APPLICATIONS) + set(TEST_PREFIX "${NIFTI_PACKAGE_PREFIX}cifti") + # An unrecognized MatrixIndicesMap child is skipped, not dispatched on. + add_test( NAME ${TEST_PREFIX}_mim_summary_unknown_child + COMMAND $ + -as_cext -eval_cext -eval_type show_summary + -input ${CMAKE_CURRENT_LIST_DIR}/testdata/mim_unknown_child.xml ) + # A recognized child is still displayed. + add_test( NAME ${TEST_PREFIX}_mim_summary_known_child + COMMAND $ + -as_cext -eval_cext -eval_type show_summary + -input ${CMAKE_CURRENT_LIST_DIR}/testdata/mim_known_child.xml ) + set_tests_properties( ${TEST_PREFIX}_mim_summary_known_child + PROPERTIES PASS_REGULAR_EXPRESSION "BrainModel" ) +endif() diff --git a/cifti/testdata/mim_known_child.xml b/cifti/testdata/mim_known_child.xml new file mode 100644 index 00000000..30eecbdf --- /dev/null +++ b/cifti/testdata/mim_known_child.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/cifti/testdata/mim_unknown_child.xml b/cifti/testdata/mim_unknown_child.xml new file mode 100644 index 00000000..83c64772 --- /dev/null +++ b/cifti/testdata/mim_unknown_child.xml @@ -0,0 +1,7 @@ + + + + + + + From e96d1e91f1c8cb4f0b3696e4ceada5802b6791c2 Mon Sep 17 00:00:00 2001 From: "Hans J. Johnson" Date: Tue, 22 Sep 2026 06:39:14 -0500 Subject: [PATCH 31/77] BUG: Free the XML tree and image that cifti_tool parses process() returned without releasing either the afni_xml_t it parsed or the nifti_image the non-cext path fills in, so every run leaked the whole tree. Both free routines already accept NULL, so neither path needs a guard. LeakSanitizer is on by default under AddressSanitizer on Linux but not on Apple, so the leak ended the process with a non-zero status on the sanitizer job alone. That also discarded the buffered standard output, which is why the summary text went missing there rather than merely being followed by a leak report. --- cifti/cifti_tool.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cifti/cifti_tool.c b/cifti/cifti_tool.c index 488b70fb..b895c09f 100644 --- a/cifti/cifti_tool.c +++ b/cifti/cifti_tool.c @@ -193,6 +193,9 @@ int process(opts_t * opts) if( opts->disp_cext ) disp_cifti_extension(nim, opts); if( opts->eval_cext ) eval_cifti_extension(ax, opts); + axml_free_xml_t(ax); + nifti_image_free(nim); + return 0; } From f79d2349ea888d89259beafccb7a0d014f4383ed Mon Sep 17 00:00:00 2001 From: "Hans J. Johnson" Date: Tue, 22 Sep 2026 07:00:34 -0500 Subject: [PATCH 32/77] BUG: Check the header converted to nifti_image before reading its datatype The REJECT_COMPLEX block reads nim->datatype, and the test for nim being NULL sits immediately below it, so a header the converter refuses crashes the process in builds that define REJECT_COMPLEX. The refusal itself is correct and already reported; only the order is wrong. Found by the malformed-header tests added in this branch: the guards make the converter return NULL for exactly the inputs they cover, which is the case this block never handled. --- nifti2/nifti2_io.c | 14 +++++++------- niftilib/nifti1_io.c | 14 +++++++------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/nifti2/nifti2_io.c b/nifti2/nifti2_io.c index 3c782bc0..8e749950 100644 --- a/nifti2/nifti2_io.c +++ b/nifti2/nifti2_io.c @@ -6028,13 +6028,6 @@ nifti_image *nifti_image_read( const char *hname , int read_data ) znzclose(fp); free(hfile); return NULL; } - #ifdef REJECT_COMPLEX - if ((nim->datatype == DT_COMPLEX64) || (nim->datatype == DT_COMPLEX128) || (nim->datatype == DT_COMPLEX256)) { - fprintf(stderr,"Image Exception Unsupported datatype (COMPLEX64): use fslcomplex to manipulate: %s\n", hname); - exit(13); - } - #endif - if( nim == NULL ){ znzclose( fp ) ; /* close the file */ if( g_opts.debug > 0 ) @@ -6043,6 +6036,13 @@ nifti_image *nifti_image_read( const char *hname , int read_data ) return NULL; } + #ifdef REJECT_COMPLEX + if ((nim->datatype == DT_COMPLEX64) || (nim->datatype == DT_COMPLEX128) || (nim->datatype == DT_COMPLEX256)) { + fprintf(stderr,"Image Exception Unsupported datatype (COMPLEX64): use fslcomplex to manipulate: %s\n", hname); + exit(13); + } + #endif + if( g_opts.debug > 3 ){ fprintf(stderr,"+d nifti_image_read(), have nifti image:\n"); nifti_image_infodump(nim); diff --git a/niftilib/nifti1_io.c b/niftilib/nifti1_io.c index 1f447cbd..60a13198 100644 --- a/niftilib/nifti1_io.c +++ b/niftilib/nifti1_io.c @@ -4315,13 +4315,6 @@ nifti_image *nifti_image_read( const char *hname , int read_data ) /**- convert all nhdr fields to nifti_image fields */ nim = nifti_convert_nhdr2nim(nhdr,hfile); - #ifdef REJECT_COMPLEX - if ((nim->datatype == DT_COMPLEX64) || (nim->datatype == DT_COMPLEX128) || (nim->datatype == DT_COMPLEX256)) { - fprintf(stderr,"Image Exception Unsupported datatype (COMPLEX64): use fslcomplex to manipulate: %s\n", hname); - exit(13); - } - #endif - if( nim == NULL ){ znzclose( fp ) ; /* close the file */ if( g_opts.debug > 0 ) @@ -4330,6 +4323,13 @@ nifti_image *nifti_image_read( const char *hname , int read_data ) return NULL; } + #ifdef REJECT_COMPLEX + if ((nim->datatype == DT_COMPLEX64) || (nim->datatype == DT_COMPLEX128) || (nim->datatype == DT_COMPLEX256)) { + fprintf(stderr,"Image Exception Unsupported datatype (COMPLEX64): use fslcomplex to manipulate: %s\n", hname); + exit(13); + } + #endif + if( g_opts.debug > 3 ){ fprintf(stderr,"+d nifti_image_read(), have nifti image:\n"); nifti_image_infodump(nim); From 2f6742c27450fef02144d4f37fca46430bf31edf Mon Sep 17 00:00:00 2001 From: "Hans J. Johnson" Date: Tue, 22 Sep 2026 06:52:20 -0500 Subject: [PATCH 33/77] ENH: Cover the NIFTI-2 dim[0] range check A dim[0] outside 1..7 is used to index dim[] in nifti_convert_n2hdr2nim, so a header carrying a large negative value reads far outside the struct. The fixture is a 604-byte NIFTI-2 header holding such a value; without the check the tool segfaults in an ordinary Release build, so this test guards on every CI job rather than only the sanitizer one. --- nifti2/CMakeLists.txt | 4 ++++ nifti2/testdata/n2_bad_dim0.nii | Bin 0 -> 604 bytes 2 files changed, 4 insertions(+) create mode 100644 nifti2/testdata/n2_bad_dim0.nii diff --git a/nifti2/CMakeLists.txt b/nifti2/CMakeLists.txt index 758df0b9..f83ebdbc 100644 --- a/nifti2/CMakeLists.txt +++ b/nifti2/CMakeLists.txt @@ -189,6 +189,10 @@ if(NIFTI_BUILD_TESTING AND NIFTI_BUILD_APPLICATIONS) endif() endif() + # A dim[0] outside 1..7 must be refused rather than used to index dim[]. + add_test( NAME ${TEST_PREFIX}_tool_disp_nim_bad_dim0 COMMAND $ -disp_nim -infiles ${CMAKE_CURRENT_LIST_DIR}/testdata/n2_bad_dim0.nii ) + set_tests_properties( ${TEST_PREFIX}_tool_disp_nim_bad_dim0 PROPERTIES PASS_REGULAR_EXPRESSION "bad dim\\[0\\]" ) + unset(TEST_SUFFIX) unset(TEST_PREFIX) unset(TOOL_NAME) diff --git a/nifti2/testdata/n2_bad_dim0.nii b/nifti2/testdata/n2_bad_dim0.nii new file mode 100644 index 0000000000000000000000000000000000000000..39c702455df7f615fb8b07f4416a8da245c2e79a GIT binary patch literal 604 zcmb1PVqnPAHe%rAlHy`v5MWrc2m*k@V1Q0DLd9{>P-z_02YZMF>9hjOAvjDP6&`#c F008?k5AOf~ literal 0 HcmV?d00001 From e3990de7a27ceda4cf026f6aeaedf3952b321a61 Mon Sep 17 00:00:00 2001 From: "Hans J. Johnson" Date: Tue, 22 Sep 2026 06:52:30 -0500 Subject: [PATCH 34/77] ENH: Cover the swap width of a byte-swapped nifti_1_header nifti_read_n1_hdr reads 348 bytes, so swapping the buffer as whatever width its magic names reaches past the end when the magic says n+2. The fixture is a big-endian NIFTI-1 header wearing an n+2 magic, which the sanitizer legs report as a stack-buffer-overflow without the fix. -check_hdr is the seam that reaches it; -disp_hdr does not. --- nifti2/CMakeLists.txt | 4 ++++ nifti2/testdata/n1_magic_n2.nii | Bin 0 -> 368 bytes 2 files changed, 4 insertions(+) create mode 100644 nifti2/testdata/n1_magic_n2.nii diff --git a/nifti2/CMakeLists.txt b/nifti2/CMakeLists.txt index f83ebdbc..1dbf86da 100644 --- a/nifti2/CMakeLists.txt +++ b/nifti2/CMakeLists.txt @@ -193,6 +193,10 @@ if(NIFTI_BUILD_TESTING AND NIFTI_BUILD_APPLICATIONS) add_test( NAME ${TEST_PREFIX}_tool_disp_nim_bad_dim0 COMMAND $ -disp_nim -infiles ${CMAKE_CURRENT_LIST_DIR}/testdata/n2_bad_dim0.nii ) set_tests_properties( ${TEST_PREFIX}_tool_disp_nim_bad_dim0 PROPERTIES PASS_REGULAR_EXPRESSION "bad dim\\[0\\]" ) + # A nifti_1_header is swapped at NIFTI-1 width whatever its magic claims. + add_test( NAME ${TEST_PREFIX}_tool_check_hdr_magic_n2 COMMAND $ -check_hdr -infiles ${CMAKE_CURRENT_LIST_DIR}/testdata/n1_magic_n2.nii ) + set_tests_properties( ${TEST_PREFIX}_tool_check_hdr_magic_n2 PROPERTIES PASS_REGULAR_EXPRESSION "header IS GOOD" ) + unset(TEST_SUFFIX) unset(TEST_PREFIX) unset(TOOL_NAME) diff --git a/nifti2/testdata/n1_magic_n2.nii b/nifti2/testdata/n1_magic_n2.nii new file mode 100644 index 0000000000000000000000000000000000000000..725219c30f03661a3e507da0baea9df9fa3a9bec GIT binary patch literal 368 wcmZQzV2oiP2#OdOm>HPBkdXlbQT4De2rw|%H{i$48&EY3XI`GR5mxU30KmowrvLx| literal 0 HcmV?d00001 From 3deb73513536244b528a37b058b1fc5574f392ee Mon Sep 17 00:00:00 2001 From: "Hans J. Johnson" Date: Tue, 22 Sep 2026 06:52:43 -0500 Subject: [PATCH 35/77] ENH: Cover the swap width of the tool's own header copies act_mod_hdrs() and act_swap_hdrs() hold a 348-byte nifti_1_header and swapped it at the width its magic named, reading past the end for an n+2 magic. Both reuse the fixture added for the library-side check, and both rewrite their input, so each test runs against its own copy. --- nifti2/CMakeLists.txt | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/nifti2/CMakeLists.txt b/nifti2/CMakeLists.txt index 1dbf86da..d321c443 100644 --- a/nifti2/CMakeLists.txt +++ b/nifti2/CMakeLists.txt @@ -197,6 +197,15 @@ if(NIFTI_BUILD_TESTING AND NIFTI_BUILD_APPLICATIONS) add_test( NAME ${TEST_PREFIX}_tool_check_hdr_magic_n2 COMMAND $ -check_hdr -infiles ${CMAKE_CURRENT_LIST_DIR}/testdata/n1_magic_n2.nii ) set_tests_properties( ${TEST_PREFIX}_tool_check_hdr_magic_n2 PROPERTIES PASS_REGULAR_EXPRESSION "header IS GOOD" ) + # The tool's own header copies are swapped at their own width too. Both + # invocations rewrite the file, so each works on a copy. + add_test( NAME ${TEST_PREFIX}_tool_mod_hdr_magic_n2_setup COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_LIST_DIR}/testdata/n1_magic_n2.nii ${CMAKE_CURRENT_BINARY_DIR}/mod_magic_n2.nii ) + add_test( NAME ${TEST_PREFIX}_tool_mod_hdr_magic_n2 COMMAND $ -mod_hdr -mod_field descrip hello -overwrite -infiles ${CMAKE_CURRENT_BINARY_DIR}/mod_magic_n2.nii ) + set_tests_properties( ${TEST_PREFIX}_tool_mod_hdr_magic_n2 PROPERTIES DEPENDS ${TEST_PREFIX}_tool_mod_hdr_magic_n2_setup ) + add_test( NAME ${TEST_PREFIX}_tool_swap_as_old_magic_n2_setup COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_LIST_DIR}/testdata/n1_magic_n2.nii ${CMAKE_CURRENT_BINARY_DIR}/swap_magic_n2.nii ) + add_test( NAME ${TEST_PREFIX}_tool_swap_as_old_magic_n2 COMMAND $ -swap_as_old -overwrite -infiles ${CMAKE_CURRENT_BINARY_DIR}/swap_magic_n2.nii ) + set_tests_properties( ${TEST_PREFIX}_tool_swap_as_old_magic_n2 PROPERTIES DEPENDS ${TEST_PREFIX}_tool_swap_as_old_magic_n2_setup ) + unset(TEST_SUFFIX) unset(TEST_PREFIX) unset(TOOL_NAME) From 01ce2fa27efb7f7f73104c9879e3c8824e702fc8 Mon Sep 17 00:00:00 2001 From: "Hans J. Johnson" Date: Tue, 22 Sep 2026 06:52:55 -0500 Subject: [PATCH 36/77] ENH: Cover the voxel-count and volume-size overflow guards Three fixtures, because the two guards are reached separately: a product that wraps nvox, the NIFTI-2 form of the same, and one whose nvox fits while nvox * nbyper does not. Without the guards the tool accepts all three and reports a negative nvox. The regex pins which refusal fired; an exit-code test would not tell this rejection apart from any other error. --- nifti2/CMakeLists.txt | 9 +++++++++ nifti2/testdata/n1_overflow.nii | Bin 0 -> 352 bytes nifti2/testdata/n1_volsize.nii | Bin 0 -> 352 bytes nifti2/testdata/n2_overflow.nii | Bin 0 -> 544 bytes 4 files changed, 9 insertions(+) create mode 100644 nifti2/testdata/n1_overflow.nii create mode 100644 nifti2/testdata/n1_volsize.nii create mode 100644 nifti2/testdata/n2_overflow.nii diff --git a/nifti2/CMakeLists.txt b/nifti2/CMakeLists.txt index d321c443..11f6b81f 100644 --- a/nifti2/CMakeLists.txt +++ b/nifti2/CMakeLists.txt @@ -206,6 +206,15 @@ if(NIFTI_BUILD_TESTING AND NIFTI_BUILD_APPLICATIONS) add_test( NAME ${TEST_PREFIX}_tool_swap_as_old_magic_n2 COMMAND $ -swap_as_old -overwrite -infiles ${CMAKE_CURRENT_BINARY_DIR}/swap_magic_n2.nii ) set_tests_properties( ${TEST_PREFIX}_tool_swap_as_old_magic_n2 PROPERTIES DEPENDS ${TEST_PREFIX}_tool_swap_as_old_magic_n2_setup ) + # Dimensions whose product overflows the voxel count are refused. + add_test( NAME ${TEST_PREFIX}_tool_reject_nvox_overflow_n1 COMMAND $ -disp_nim -infiles ${CMAKE_CURRENT_LIST_DIR}/testdata/n1_overflow.nii ) + add_test( NAME ${TEST_PREFIX}_tool_reject_nvox_overflow_n2 COMMAND $ -disp_nim -infiles ${CMAKE_CURRENT_LIST_DIR}/testdata/n2_overflow.nii ) + add_test( NAME ${TEST_PREFIX}_tool_reject_volsize_overflow COMMAND $ -disp_nim -infiles ${CMAKE_CURRENT_LIST_DIR}/testdata/n1_volsize.nii ) + set_tests_properties( ${TEST_PREFIX}_tool_reject_nvox_overflow_n1 ${TEST_PREFIX}_tool_reject_nvox_overflow_n2 + PROPERTIES PASS_REGULAR_EXPRESSION "dim\\[\\] overflows the voxel count" ) + set_tests_properties( ${TEST_PREFIX}_tool_reject_volsize_overflow + PROPERTIES PASS_REGULAR_EXPRESSION "dim\\[\\] and datatype overflow the volume size" ) + unset(TEST_SUFFIX) unset(TEST_PREFIX) unset(TOOL_NAME) diff --git a/nifti2/testdata/n1_overflow.nii b/nifti2/testdata/n1_overflow.nii new file mode 100644 index 0000000000000000000000000000000000000000..a5d8c10b71490bf1927a3fb7b7c79bdf0bea03f4 GIT binary patch literal 352 scma!HWFQK#GyJbdhNzlY7zDrq4fYHS`0xg2h&-z9;mFI=HUxVM0IxF>UjP6A literal 0 HcmV?d00001 diff --git a/nifti2/testdata/n1_volsize.nii b/nifti2/testdata/n1_volsize.nii new file mode 100644 index 0000000000000000000000000000000000000000..98160d480f3cdac137dba372785ea7f484afdc62 GIT binary patch literal 352 vcma!HWFQK#G5oKGKo$l}Ee;G0U{f0G85r>44bBjGqO2U!it@A#!R`kD?6D73 literal 0 HcmV?d00001 diff --git a/nifti2/testdata/n2_overflow.nii b/nifti2/testdata/n2_overflow.nii new file mode 100644 index 0000000000000000000000000000000000000000..6402081f1a612e748f94b902d166225f601feaab GIT binary patch literal 544 zcmb1PVqnPAHe%rAlHy`v5MTgukpK$=g98$mV&(^Xh!LdI3MdYP>8IF1qXrNN0RaD@ B3`hU~ literal 0 HcmV?d00001 From 329e869448cf2823d2e1a7c4f8c58f0967b84533 Mon Sep 17 00:00:00 2001 From: "Hans J. Johnson" Date: Tue, 22 Sep 2026 06:53:10 -0500 Subject: [PATCH 37/77] ENH: Cover the voxel-count guards in the NIFTI-1 library nifti_convert_nhdr2nim carries its own copy of the guards, so it needs its own tests; reverting the NIFTI-1 half alone leaves the nifti2 tests green and turns exactly these two red. --- niftilib/CMakeLists.txt | 8 ++++++++ niftilib/testdata/n1_overflow.nii | Bin 0 -> 352 bytes niftilib/testdata/n1_volsize.nii | Bin 0 -> 352 bytes 3 files changed, 8 insertions(+) create mode 100644 niftilib/testdata/n1_overflow.nii create mode 100644 niftilib/testdata/n1_volsize.nii diff --git a/niftilib/CMakeLists.txt b/niftilib/CMakeLists.txt index 09d522d2..03bf551f 100644 --- a/niftilib/CMakeLists.txt +++ b/niftilib/CMakeLists.txt @@ -148,6 +148,14 @@ if(NIFTI_BUILD_TESTING AND NIFTI_BUILD_APPLICATIONS) ) #==END NIFTI1 and NIFTI2 common tests ============================================ + # The same guards in the NIFTI-1 library, reached through nifti1_tool. + add_test( NAME ${TEST_PREFIX}_tool_reject_nvox_overflow COMMAND $ -disp_nim -infiles ${CMAKE_CURRENT_LIST_DIR}/testdata/n1_overflow.nii ) + add_test( NAME ${TEST_PREFIX}_tool_reject_volsize_overflow COMMAND $ -disp_nim -infiles ${CMAKE_CURRENT_LIST_DIR}/testdata/n1_volsize.nii ) + set_tests_properties( ${TEST_PREFIX}_tool_reject_nvox_overflow + PROPERTIES PASS_REGULAR_EXPRESSION "dim\\[\\] overflows the voxel count" ) + set_tests_properties( ${TEST_PREFIX}_tool_reject_volsize_overflow + PROPERTIES PASS_REGULAR_EXPRESSION "dim\\[\\] and datatype overflow the volume size" ) + unset(TEST_SUFFIX) unset(TEST_PREFIX) unset(TOOL_NAME) diff --git a/niftilib/testdata/n1_overflow.nii b/niftilib/testdata/n1_overflow.nii new file mode 100644 index 0000000000000000000000000000000000000000..a5d8c10b71490bf1927a3fb7b7c79bdf0bea03f4 GIT binary patch literal 352 scma!HWFQK#GyJbdhNzlY7zDrq4fYHS`0xg2h&-z9;mFI=HUxVM0IxF>UjP6A literal 0 HcmV?d00001 diff --git a/niftilib/testdata/n1_volsize.nii b/niftilib/testdata/n1_volsize.nii new file mode 100644 index 0000000000000000000000000000000000000000..98160d480f3cdac137dba372785ea7f484afdc62 GIT binary patch literal 352 vcma!HWFQK#G5oKGKo$l}Ee;G0U{f0G85r>44bBjGqO2U!it@A#!R`kD?6D73 literal 0 HcmV?d00001 From d0b941378156f8eacee4242d543b452047f8e31a Mon Sep 17 00:00:00 2001 From: "Hans J. Johnson" Date: Tue, 22 Sep 2026 06:53:21 -0500 Subject: [PATCH 38/77] ENH: Cover the repeated-attribute path in the ASCII header reader nifti_image_from_ascii overwrote fname and iname without releasing what they already held, so an ASCII header naming either attribute twice leaked the earlier copy. One fixture drives both libraries. The regex asserts the last value survives, so a fix that ignored the repeat instead of freeing the previous value could not pass. --- nifti2/CMakeLists.txt | 5 +++++ nifti2/testdata/dup_attr.nia | Bin 0 -> 548 bytes niftilib/CMakeLists.txt | 4 ++++ niftilib/testdata/dup_attr.nia | Bin 0 -> 548 bytes 4 files changed, 9 insertions(+) create mode 100644 nifti2/testdata/dup_attr.nia create mode 100644 niftilib/testdata/dup_attr.nia diff --git a/nifti2/CMakeLists.txt b/nifti2/CMakeLists.txt index 11f6b81f..b8633d2c 100644 --- a/nifti2/CMakeLists.txt +++ b/nifti2/CMakeLists.txt @@ -215,6 +215,11 @@ if(NIFTI_BUILD_TESTING AND NIFTI_BUILD_APPLICATIONS) set_tests_properties( ${TEST_PREFIX}_tool_reject_volsize_overflow PROPERTIES PASS_REGULAR_EXPRESSION "dim\\[\\] and datatype overflow the volume size" ) + # A repeated ASCII attribute releases the previous value, and the last + # one wins; the regex pins the surviving behavior. + add_test( NAME ${TEST_PREFIX}_tool_ascii_dup_attr COMMAND $ -disp_nim -infiles ${CMAKE_CURRENT_LIST_DIR}/testdata/dup_attr.nia ) + set_tests_properties( ${TEST_PREFIX}_tool_ascii_dup_attr PROPERTIES PASS_REGULAR_EXPRESSION "dup2.nia" ) + unset(TEST_SUFFIX) unset(TEST_PREFIX) unset(TOOL_NAME) diff --git a/nifti2/testdata/dup_attr.nia b/nifti2/testdata/dup_attr.nia new file mode 100644 index 0000000000000000000000000000000000000000..ad9932a30bf5a9f3eefddd26756abc992756b252 GIT binary patch literal 548 zcmdT=%L>9U5cKR<>jx#(7_kmTeoHAY!A#Yq$1u~s#Nu37nVQIj|>N5Q8v}*LN z7DC*)ar>;6b2m;isT^GZX&j6IwjcO>Lk0z*D_~IIAryE>4ec+6KCDZS{?Ic{vNcS2 u6wu9++`g%DbD&M$8eUZPM=oxwH~EVgb@2lA_IzXj literal 0 HcmV?d00001 diff --git a/niftilib/CMakeLists.txt b/niftilib/CMakeLists.txt index 03bf551f..c66872b3 100644 --- a/niftilib/CMakeLists.txt +++ b/niftilib/CMakeLists.txt @@ -156,6 +156,10 @@ if(NIFTI_BUILD_TESTING AND NIFTI_BUILD_APPLICATIONS) set_tests_properties( ${TEST_PREFIX}_tool_reject_volsize_overflow PROPERTIES PASS_REGULAR_EXPRESSION "dim\\[\\] and datatype overflow the volume size" ) + # The NIFTI-1 library carries the same ASCII reader. + add_test( NAME ${TEST_PREFIX}_tool_ascii_dup_attr COMMAND $ -disp_nim -infiles ${CMAKE_CURRENT_LIST_DIR}/testdata/dup_attr.nia ) + set_tests_properties( ${TEST_PREFIX}_tool_ascii_dup_attr PROPERTIES PASS_REGULAR_EXPRESSION "dup2.nia" ) + unset(TEST_SUFFIX) unset(TEST_PREFIX) unset(TOOL_NAME) diff --git a/niftilib/testdata/dup_attr.nia b/niftilib/testdata/dup_attr.nia new file mode 100644 index 0000000000000000000000000000000000000000..ad9932a30bf5a9f3eefddd26756abc992756b252 GIT binary patch literal 548 zcmdT=%L>9U5cKR<>jx#(7_kmTeoHAY!A#Yq$1u~s#Nu37nVQIj|>N5Q8v}*LN z7DC*)ar>;6b2m;isT^GZX&j6IwjcO>Lk0z*D_~IIAryE>4ec+6KCDZS{?Ic{vNcS2 u6wu9++`g%DbD&M$8eUZPM=oxwH~EVgb@2lA_IzXj literal 0 HcmV?d00001 From 457f8bfef6649560da4c2cf9e43fac25260cba8a Mon Sep 17 00:00:00 2001 From: "Hans J. Johnson" Date: Tue, 22 Sep 2026 06:53:32 -0500 Subject: [PATCH 39/77] ENH: Cover the loc_strnlen bound on an unterminated extension loc_strnlen read one byte past the buffer it was given. The reachable caller is axml_read_buf, where the buffer is an extension's edata, allocated esize-8 and filled exactly; axml_read_file allocates one spare byte and absorbs the overread, so it is not the seam. The fixture is a NIFTI-1 image carrying a CIFTI extension whose payload is space-padded to hold no NUL. The overread is reported by the sanitizer legs; the build stays green without them. --- cifti/CMakeLists.txt | 9 +++++++++ cifti/testdata/cext_unterminated.nii | Bin 0 -> 704 bytes 2 files changed, 9 insertions(+) create mode 100644 cifti/testdata/cext_unterminated.nii diff --git a/cifti/CMakeLists.txt b/cifti/CMakeLists.txt index 9e539c20..ec8a4307 100644 --- a/cifti/CMakeLists.txt +++ b/cifti/CMakeLists.txt @@ -41,4 +41,13 @@ if(NIFTI_BUILD_TESTING AND NIFTI_BUILD_APPLICATIONS) -input ${CMAKE_CURRENT_LIST_DIR}/testdata/mim_known_child.xml ) set_tests_properties( ${TEST_PREFIX}_mim_summary_known_child PROPERTIES PASS_REGULAR_EXPRESSION "BrainModel" ) + + # An extension payload that fills esize-8 with no NUL must not be read + # past its end; the regex pins that the payload is still parsed. + add_test( NAME ${TEST_PREFIX}_tool_unterminated_cext + COMMAND $ + -input ${CMAKE_CURRENT_LIST_DIR}/testdata/cext_unterminated.nii + -eval_cext -eval_type show_summary ) + set_tests_properties( ${TEST_PREFIX}_tool_unterminated_cext + PROPERTIES PASS_REGULAR_EXPRESSION "MapName : demo" ) endif() diff --git a/cifti/testdata/cext_unterminated.nii b/cifti/testdata/cext_unterminated.nii new file mode 100644 index 0000000000000000000000000000000000000000..43eb9b73af459e4a657c0bb30ff6c378629f3df5 GIT binary patch literal 704 zcma!HWFQJKGcbW6BLf7YYGPp!01GtOGce%8IxY}-RNcdom#1wA3@HYN03e|Nq;2dg za&r{QQj3Z+^Yd(#4D}3@6jJk&^HVbO(ruMOL)>&Nl}-4! zONufpV6jQ{;Rtmh`7Vhii6NB*skTaB z=f!6pX=BB0qjRjd$0OHuCq~_+^=)-t6`Un+3L6Dme WcG!W`L7b@%w!@A~fhGVwtpEVg#bj3i literal 0 HcmV?d00001 From 7a0b98b2d75d65801a824293fdeac890999bd1b9 Mon Sep 17 00:00:00 2001 From: "Hans J. Johnson" Date: Tue, 22 Sep 2026 06:54:01 -0500 Subject: [PATCH 40/77] ENH: Cover the ambiguous-filename path in nifti_findhdrname The tool writes both members of the pair itself, so no data is needed. The driver asserts exit 1 and the diagnostic, because a library reports the clash to its caller rather than ending the process, and a plain add_test cannot tell that exit from the abort it replaced. It then removes the .gz and repeats, so an unconditional NULL return could not pass. Guarded on FSLSTYLE_NAME_CONFLICTS, which is off by default, so the test is registered only where the code is compiled. --- nifti2/CMakeLists.txt | 8 ++++++++ nifti2/ambiguous_hdrname.cmake | 30 ++++++++++++++++++++++++++++++ niftilib/CMakeLists.txt | 7 +++++++ niftilib/ambiguous_hdrname.cmake | 30 ++++++++++++++++++++++++++++++ 4 files changed, 75 insertions(+) create mode 100644 nifti2/ambiguous_hdrname.cmake create mode 100644 niftilib/ambiguous_hdrname.cmake diff --git a/nifti2/CMakeLists.txt b/nifti2/CMakeLists.txt index b8633d2c..c4f40b0b 100644 --- a/nifti2/CMakeLists.txt +++ b/nifti2/CMakeLists.txt @@ -220,6 +220,14 @@ if(NIFTI_BUILD_TESTING AND NIFTI_BUILD_APPLICATIONS) add_test( NAME ${TEST_PREFIX}_tool_ascii_dup_attr COMMAND $ -disp_nim -infiles ${CMAKE_CURRENT_LIST_DIR}/testdata/dup_attr.nia ) set_tests_properties( ${TEST_PREFIX}_tool_ascii_dup_attr PROPERTIES PASS_REGULAR_EXPRESSION "dup2.nia" ) + # Only compiled when the FSL name-conflict path is built. + if(FSLSTYLE_NAME_CONFLICTS) + add_test( NAME ${TEST_PREFIX}_ambiguous_hdrname + COMMAND ${CMAKE_COMMAND} -DTOOL=$ + -DDIR=${CMAKE_CURRENT_BINARY_DIR}/ambig + -P ${CMAKE_CURRENT_LIST_DIR}/ambiguous_hdrname.cmake ) + endif() + unset(TEST_SUFFIX) unset(TEST_PREFIX) unset(TOOL_NAME) diff --git a/nifti2/ambiguous_hdrname.cmake b/nifti2/ambiguous_hdrname.cmake new file mode 100644 index 00000000..3353b6da --- /dev/null +++ b/nifti2/ambiguous_hdrname.cmake @@ -0,0 +1,30 @@ +# Drives the ambiguous-filename path, which must report the clash to its +# caller. A plain add_test cannot tell exit 1 from the abort it replaced. +file(REMOVE_RECURSE ${DIR}) +file(MAKE_DIRECTORY ${DIR}) + +execute_process(COMMAND ${TOOL} -make_im -new_dim 3 4 4 4 0 0 0 0 + -prefix ${DIR}/amb.nii RESULT_VARIABLE mk1) +execute_process(COMMAND ${TOOL} -make_im -new_dim 3 4 4 4 0 0 0 0 + -prefix ${DIR}/amb.nii.gz RESULT_VARIABLE mk2) +if(NOT mk1 STREQUAL "0" OR NOT mk2 STREQUAL "0") + message(FATAL_ERROR "could not create the ambiguous pair: ${mk1} ${mk2}") +endif() + +execute_process(COMMAND ${TOOL} -disp_hdr -infiles ${DIR}/amb + RESULT_VARIABLE rv ERROR_VARIABLE err OUTPUT_VARIABLE out) +if(NOT rv STREQUAL "1") + message(FATAL_ERROR "expected exit 1, got '${rv}'\n${err}") +endif() +if(NOT err MATCHES "Multiple possible filenames") + message(FATAL_ERROR "missing ambiguity diagnostic:\n${err}") +endif() + +# Unambiguous names must still resolve, so that returning NULL +# unconditionally could not pass. +file(REMOVE ${DIR}/amb.nii.gz) +execute_process(COMMAND ${TOOL} -disp_hdr -infiles ${DIR}/amb + RESULT_VARIABLE rv2 ERROR_VARIABLE err2 OUTPUT_VARIABLE out2) +if(NOT rv2 STREQUAL "0") + message(FATAL_ERROR "unambiguous name failed with '${rv2}'\n${err2}") +endif() diff --git a/niftilib/CMakeLists.txt b/niftilib/CMakeLists.txt index c66872b3..d3b09cd8 100644 --- a/niftilib/CMakeLists.txt +++ b/niftilib/CMakeLists.txt @@ -160,6 +160,13 @@ if(NIFTI_BUILD_TESTING AND NIFTI_BUILD_APPLICATIONS) add_test( NAME ${TEST_PREFIX}_tool_ascii_dup_attr COMMAND $ -disp_nim -infiles ${CMAKE_CURRENT_LIST_DIR}/testdata/dup_attr.nia ) set_tests_properties( ${TEST_PREFIX}_tool_ascii_dup_attr PROPERTIES PASS_REGULAR_EXPRESSION "dup2.nia" ) + if(FSLSTYLE_NAME_CONFLICTS) + add_test( NAME ${TEST_PREFIX}_ambiguous_hdrname + COMMAND ${CMAKE_COMMAND} -DTOOL=$ + -DDIR=${CMAKE_CURRENT_BINARY_DIR}/ambig1 + -P ${CMAKE_CURRENT_LIST_DIR}/ambiguous_hdrname.cmake ) + endif() + unset(TEST_SUFFIX) unset(TEST_PREFIX) unset(TOOL_NAME) diff --git a/niftilib/ambiguous_hdrname.cmake b/niftilib/ambiguous_hdrname.cmake new file mode 100644 index 00000000..3353b6da --- /dev/null +++ b/niftilib/ambiguous_hdrname.cmake @@ -0,0 +1,30 @@ +# Drives the ambiguous-filename path, which must report the clash to its +# caller. A plain add_test cannot tell exit 1 from the abort it replaced. +file(REMOVE_RECURSE ${DIR}) +file(MAKE_DIRECTORY ${DIR}) + +execute_process(COMMAND ${TOOL} -make_im -new_dim 3 4 4 4 0 0 0 0 + -prefix ${DIR}/amb.nii RESULT_VARIABLE mk1) +execute_process(COMMAND ${TOOL} -make_im -new_dim 3 4 4 4 0 0 0 0 + -prefix ${DIR}/amb.nii.gz RESULT_VARIABLE mk2) +if(NOT mk1 STREQUAL "0" OR NOT mk2 STREQUAL "0") + message(FATAL_ERROR "could not create the ambiguous pair: ${mk1} ${mk2}") +endif() + +execute_process(COMMAND ${TOOL} -disp_hdr -infiles ${DIR}/amb + RESULT_VARIABLE rv ERROR_VARIABLE err OUTPUT_VARIABLE out) +if(NOT rv STREQUAL "1") + message(FATAL_ERROR "expected exit 1, got '${rv}'\n${err}") +endif() +if(NOT err MATCHES "Multiple possible filenames") + message(FATAL_ERROR "missing ambiguity diagnostic:\n${err}") +endif() + +# Unambiguous names must still resolve, so that returning NULL +# unconditionally could not pass. +file(REMOVE ${DIR}/amb.nii.gz) +execute_process(COMMAND ${TOOL} -disp_hdr -infiles ${DIR}/amb + RESULT_VARIABLE rv2 ERROR_VARIABLE err2 OUTPUT_VARIABLE out2) +if(NOT rv2 STREQUAL "0") + message(FATAL_ERROR "unambiguous name failed with '${rv2}'\n${err2}") +endif() From c051fd89fcf0830e1de997cd8c2b10d77d27246d Mon Sep 17 00:00:00 2001 From: "Hans J. Johnson" Date: Tue, 22 Sep 2026 06:54:32 -0500 Subject: [PATCH 41/77] ENH: Reach nifti_tool's header-modification failure paths Two paths returned without releasing the header, and one without releasing the duplicated name: a write into a directory that refuses it, and a duplication that fails because the input is the other NIfTI version. Both are reachable from the shipped tools with no data beyond what -make_im writes. The script asserts only that the paths are reached and reported; the leak is what the memcheck and sanitizer legs see. It skips itself for root, whom a read-only directory does not stop. --- nifti2/CMakeLists.txt | 5 ++ .../cmake_testscripts/mod_header_errpaths.sh | 50 +++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100755 nifti2/nifti_regress_test/cmake_testscripts/mod_header_errpaths.sh diff --git a/nifti2/CMakeLists.txt b/nifti2/CMakeLists.txt index c4f40b0b..c8161ec4 100644 --- a/nifti2/CMakeLists.txt +++ b/nifti2/CMakeLists.txt @@ -228,6 +228,11 @@ if(NIFTI_BUILD_TESTING AND NIFTI_BUILD_APPLICATIONS) -P ${CMAKE_CURRENT_LIST_DIR}/ambiguous_hdrname.cmake ) endif() + if(UNIX AND NIFTI_SHELL_SCRIPT_TESTS) + # Reaches the failure paths that have to release the header before returning. + add_test( NAME ${TEST_PREFIX}_mod_hdr_errpaths COMMAND sh ${NIFTI_TEST_SCRIPT_DIR}/mod_header_errpaths.sh $ $ ) + endif() + unset(TEST_SUFFIX) unset(TEST_PREFIX) unset(TOOL_NAME) diff --git a/nifti2/nifti_regress_test/cmake_testscripts/mod_header_errpaths.sh b/nifti2/nifti_regress_test/cmake_testscripts/mod_header_errpaths.sh new file mode 100755 index 00000000..7ef0fd88 --- /dev/null +++ b/nifti2/nifti_regress_test/cmake_testscripts/mod_header_errpaths.sh @@ -0,0 +1,50 @@ +#!/bin/sh +# Drive the two failure paths in nifti_tool's header modification, where +# the header and the duplicated name have to be released before returning. +# Both invocations are expected to fail, so a nonzero status cannot be the +# assertion: under a sanitizer it is also what a leak report exits with. +# The check is that the tool reported the failure and said nothing else. +# usage: mod_header_errpaths.sh [nifti1_tool] +NT=$1 +NT1=$2 +[ -x "$NT" ] || { echo "usage: $0 [nifti1_tool]"; exit 1; } + +if [ "$(id -u)" = "0" ]; then + echo "skipping: a read-only directory does not stop root" + exit 0 +fi + +tmp=$(mktemp -d) || exit 1 +trap 'chmod 755 "$tmp/ro" 2>/dev/null; rm -rf "$tmp"' EXIT +cd "$tmp" || exit 1 + +$NT -make_im -new_dim 3 4 4 4 0 0 0 0 -prefix anat0.nii || exit 1 +mkdir ro && chmod 555 ro + +# A. the write fails, so both the header and the duplicated name are held +out=$($NT -mod_hdr2 -prefix ro/anat1 -infiles anat0.nii \ + -mod_field qoffset_x -17.325 2>&1) +if [ $? -eq 0 ]; then + echo "FAIL: writing into a read-only directory succeeded" + echo "$out" + exit 1 +fi +case "$out" in + *Sanitizer*) echo "FAIL: sanitizer report on the write path"; echo "$out"; exit 1;; +esac + +# B. the duplication fails, so the header alone is held +if [ -n "$NT1" ] && [ -x "$NT1" ]; then + out=$($NT1 -mod_hdr -prefix ro/x1 -infiles anat0.nii \ + -mod_field qoffset_x -17.325 2>&1) + if [ $? -eq 0 ]; then + echo "FAIL: nifti1_tool accepted a NIFTI-2 input" + echo "$out" + exit 1 + fi + case "$out" in + *Sanitizer*) echo "FAIL: sanitizer report on the duplication path"; echo "$out"; exit 1;; + esac +fi + +echo "mod_header error paths reached" From b5023c5a986e527cf71a4fa9f72543b92b2c87ee Mon Sep 17 00:00:00 2001 From: "Hans J. Johnson" Date: Tue, 22 Sep 2026 06:55:19 -0500 Subject: [PATCH 42/77] ENH: Self-test the NIfTI content comparison helper c22_copy_image compares by content because gzip output is not reproducible across zlib implementations, but that property was only exercised where the system zlib is zlib-ng. Every CI leg ships stock zlib, so the comparison could revert to comparing compressed bytes and the suite would stay green. Extracting the helper lets it be tested directly. gzip -1 against -9 manufactures the byte difference on any zlib, and the two negative assertions pin that differing content is still reported as different and that the uncompressed path still works. --- nifti2/CMakeLists.txt | 5 +++ .../cmake_testscripts/c22_copy_image.sh | 16 +--------- .../cmake_testscripts/nii_cmp.sh | 15 +++++++++ .../cmake_testscripts/nii_cmp_selftest.sh | 32 +++++++++++++++++++ 4 files changed, 53 insertions(+), 15 deletions(-) create mode 100644 nifti2/nifti_regress_test/cmake_testscripts/nii_cmp.sh create mode 100755 nifti2/nifti_regress_test/cmake_testscripts/nii_cmp_selftest.sh diff --git a/nifti2/CMakeLists.txt b/nifti2/CMakeLists.txt index c8161ec4..d369220c 100644 --- a/nifti2/CMakeLists.txt +++ b/nifti2/CMakeLists.txt @@ -233,6 +233,11 @@ if(NIFTI_BUILD_TESTING AND NIFTI_BUILD_APPLICATIONS) add_test( NAME ${TEST_PREFIX}_mod_hdr_errpaths COMMAND sh ${NIFTI_TEST_SCRIPT_DIR}/mod_header_errpaths.sh $ $ ) endif() + if(UNIX AND NIFTI_SHELL_SCRIPT_TESTS) + # Needs no external data, so it carries no NEEDS_DATA label. + add_test( NAME ${TEST_PREFIX}_nii_cmp_selftest COMMAND sh ${NIFTI_TEST_SCRIPT_DIR}/nii_cmp_selftest.sh ${NIFTI_TEST_SCRIPT_DIR} ) + endif() + unset(TEST_SUFFIX) unset(TEST_PREFIX) unset(TOOL_NAME) diff --git a/nifti2/nifti_regress_test/cmake_testscripts/c22_copy_image.sh b/nifti2/nifti_regress_test/cmake_testscripts/c22_copy_image.sh index 18c435c9..d2910d1b 100644 --- a/nifti2/nifti_regress_test/cmake_testscripts/c22_copy_image.sh +++ b/nifti2/nifti_regress_test/cmake_testscripts/c22_copy_image.sh @@ -12,21 +12,7 @@ OUT_DATA=$(dirname ${DATA}) #Need to write to separate directory cd ${OUT_DATA} -# Compare two NIfTI files by content rather than by compressed bytes. -# -# gzip output is not reproducible across zlib implementations: zlib-ng, -# which Arch, CachyOS and other current distributions ship as the system -# zlib, encodes the same input differently from stock zlib. Comparing -# the .gz files directly therefore fails on those systems even though the -# image data round-trips perfectly. Decompress first and compare that. -nii_cmp() { - if [ "${1##*.}" = "gz" ]; then - gzip -dc "$1" > "$1.raw" && gzip -dc "$2" > "$2.raw" || return 1 - cmp "$1.raw" "$2.raw" - return $? - fi - cmp "$1" "$2" -} +. "$(dirname "$0")/nii_cmp.sh" # note the main input file and prefix for all output files infile=$DATA/e4.60005.nii.gz diff --git a/nifti2/nifti_regress_test/cmake_testscripts/nii_cmp.sh b/nifti2/nifti_regress_test/cmake_testscripts/nii_cmp.sh new file mode 100644 index 00000000..a232c21f --- /dev/null +++ b/nifti2/nifti_regress_test/cmake_testscripts/nii_cmp.sh @@ -0,0 +1,15 @@ +# Compare two NIfTI files by content rather than by compressed bytes. +# +# gzip output is not reproducible across zlib implementations: zlib-ng, +# which Arch, CachyOS and other current distributions ship as the system +# zlib, encodes the same input differently from stock zlib. Comparing +# the .gz files directly therefore fails on those systems even though the +# image data round-trips perfectly. Decompress first and compare that. +nii_cmp() { + if [ "${1##*.}" = "gz" ]; then + gzip -dc "$1" > "$1.raw" && gzip -dc "$2" > "$2.raw" || return 1 + cmp "$1.raw" "$2.raw" + return $? + fi + cmp "$1" "$2" +} diff --git a/nifti2/nifti_regress_test/cmake_testscripts/nii_cmp_selftest.sh b/nifti2/nifti_regress_test/cmake_testscripts/nii_cmp_selftest.sh new file mode 100755 index 00000000..2eecaada --- /dev/null +++ b/nifti2/nifti_regress_test/cmake_testscripts/nii_cmp_selftest.sh @@ -0,0 +1,32 @@ +#!/bin/sh +# Verify nii_cmp compares NIfTI content rather than compressed bytes. +# +# gzip -1 and gzip -9 encode the same input differently, which manufactures +# the byte difference locally instead of waiting for a runner whose system +# zlib happens to be zlib-ng. +# usage: nii_cmp_selftest.sh +. "$1/nii_cmp.sh" + +tmp=$(mktemp -d) || exit 1 +trap 'rm -rf "$tmp"' EXIT +cd "$tmp" || exit 1 + +head -c 65536 /dev/urandom > payload +gzip -1 -c payload > a.nii.gz +gzip -9 -c payload > b.nii.gz +head -c 65536 /dev/urandom > other +gzip -c other > c.nii.gz + +if cmp -s a.nii.gz b.nii.gz; then + echo "SKIP: this gzip encodes -1 and -9 identically, nothing to compare" + exit 0 +fi + +nii_cmp a.nii.gz b.nii.gz || { + echo "FAIL: identical content reported different"; exit 1; } +nii_cmp a.nii.gz c.nii.gz >/dev/null 2>&1 && { + echo "FAIL: differing content reported same"; exit 1; } +cp payload d.nii && cp payload e.nii +nii_cmp d.nii e.nii || { echo "FAIL: uncompressed path broken"; exit 1; } + +echo "nii_cmp selftest passed" From 1abbd8572bc489638c2568e0151086e8c2914a5b Mon Sep 17 00:00:00 2001 From: "Hans J. Johnson" Date: Tue, 22 Sep 2026 07:44:30 -0500 Subject: [PATCH 43/77] COMP: Give the pigz writers internal linkage doPigz and doPigz2 are called only from the file that defines them and appear in no header, so they are internal. Declaring them so is what lets -Wmissing-declarations be promoted to an error on the FSLSTYLE path, where they were the only offenders. The copy of doPigz2 in the NIFTI-1 library is byte-identical to doPigz beside it and nothing calls it, so it is excluded from the build rather than given linkage it does not need. The exported symbol set is unchanged: this code compiles only under PIGZ, which the shared build behind the baseline does not define. --- nifti2/nifti2_io.c | 4 ++-- niftilib/nifti1_io.c | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/nifti2/nifti2_io.c b/nifti2/nifti2_io.c index 8e749950..ae0d8070 100644 --- a/nifti2/nifti2_io.c +++ b/nifti2/nifti2_io.c @@ -7904,7 +7904,7 @@ znzFile nifti_image_write_hdr_img2(nifti_image *nim, int write_opts, #ifdef PIGZ #ifdef HAVE_ZLIB -int doPigz2(nifti_image *nim, struct nifti_2_header nhdr, const nifti_brick_list * NBL) { +static int doPigz2(nifti_image *nim, struct nifti_2_header nhdr, const nifti_brick_list * NBL) { FILE *pigzPipe; char command[768]; strcpy(command, "pigz" ); @@ -7936,7 +7936,7 @@ int doPigz2(nifti_image *nim, struct nifti_2_header nhdr, const nifti_brick_list return 0; } -int doPigz(nifti_image *nim, struct nifti_1_header nhdr, const nifti_brick_list * NBL) { +static int doPigz(nifti_image *nim, struct nifti_1_header nhdr, const nifti_brick_list * NBL) { FILE *pigzPipe; char command[768]; strcpy(command, "pigz" ); diff --git a/niftilib/nifti1_io.c b/niftilib/nifti1_io.c index 60a13198..7a66e4af 100644 --- a/niftilib/nifti1_io.c +++ b/niftilib/nifti1_io.c @@ -5798,6 +5798,7 @@ znzFile nifti_image_write_hdr_img2(nifti_image *nim, int write_opts, #ifdef PIGZ #ifdef HAVE_ZLIB +#if 0 /* unused here: identical to doPigz below */ int doPigz2(nifti_image *nim, struct nifti_1_header nhdr, const nifti_brick_list * NBL) { FILE *pigzPipe; char command[768]; @@ -5829,8 +5830,9 @@ int doPigz2(nifti_image *nim, struct nifti_1_header nhdr, const nifti_brick_list free(fp); return 0; } +#endif -int doPigz(nifti_image *nim, struct nifti_1_header nhdr, const nifti_brick_list * NBL) { +static int doPigz(nifti_image *nim, struct nifti_1_header nhdr, const nifti_brick_list * NBL) { FILE *pigzPipe; char command[768]; strcpy(command, "pigz" ); From b4876bf7dfe0d650c2cb675aa7a080416b34479f Mon Sep 17 00:00:00 2001 From: "Hans J. Johnson" Date: Tue, 22 Sep 2026 07:44:54 -0500 Subject: [PATCH 44/77] COMP: Make an undeclared external function fail the build -Wmissing-declarations is already in the project's clean set, but a warning in a build that passes anyway is not read, so two changes that added declarations reached master with nothing to stop the next one. Both FSLSTYLE settings are covered. The flag catches nifti_fileexists on one side and axml_recur_find_xml, FslGetHdrImgNames and FslSetIntensityScaling on the other, each of which reached master as a warning nobody acted on. --- .github/workflows/cmake-multi-platform.yml | 34 ++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/.github/workflows/cmake-multi-platform.yml b/.github/workflows/cmake-multi-platform.yml index 68197e71..57639632 100644 --- a/.github/workflows/cmake-multi-platform.yml +++ b/.github/workflows/cmake-multi-platform.yml @@ -196,3 +196,37 @@ jobs: exit 1 fi echo "Exported symbols match the baseline." + + missing-declarations: + # A function defined without a prior declaration is either missing + # from its header or should have been static. Both shapes reached + # master before, because the flag is in the project's clean set but + # nothing makes it fatal, and a warning in a build that passes is + # not read. + # + # Promoting this one flag needs no preparatory cleanup. The whole + # NIFTI_WARNINGS_AS_ERRORS set is still blocked by + # -Wmaybe-uninitialized in fslio.c, and waiting for that would + # leave this uncovered meanwhile. + name: no undeclared external functions (FSLSTYLE=${{ matrix.fslstyle }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + fslstyle: ["OFF", "ON"] + steps: + - uses: actions/checkout@v4 + - name: Install dependencies + run: sudo apt-get update && sudo apt-get install -y libexpat1-dev zlib1g-dev ninja-build + - name: Configure CMake + run: > + cmake -G Ninja -B ${{ github.workspace }}/build + -DCMAKE_BUILD_TYPE=Release + -DCMAKE_C_FLAGS=-Werror=missing-declarations + -DNIFTI_BUILD_APPLICATIONS=ON + -DUSE_CIFTI_CODE=ON + -DUSE_FSL_CODE=ON + -DFSLSTYLE=${{ matrix.fslstyle }} + -S ${{ github.workspace }} + - name: Build + run: cmake --build ${{ github.workspace }}/build From 250532501b313598d690572acf89c29d9b0bdeeb Mon Sep 17 00:00:00 2001 From: "Hans J. Johnson" Date: Tue, 22 Sep 2026 09:31:10 -0500 Subject: [PATCH 45/77] COMP: Build and test on Windows Twelve source files carry WIN32 or _MSC_VER guards and no workflow has ever compiled them, so a change that breaks the Windows path is invisible here and surfaces only when a consumer reports it. Static and shared, because the ZNZ_API and NIFTI_API decorations differ between them and only the shared build exercises the dllexport path. zlib and expat come from vcpkg, which the runner image already carries. VCPKG_INSTALLATION_ROOT is set by the runner image rather than by the workflow, so the toolchain path is read in PowerShell and checked before cmake runs, which reports a missing toolchain as itself rather than as a CMake error several lines removed from the cause. --- .github/workflows/cmake-multi-platform.yml | 44 ++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/.github/workflows/cmake-multi-platform.yml b/.github/workflows/cmake-multi-platform.yml index 57639632..e7250e81 100644 --- a/.github/workflows/cmake-multi-platform.yml +++ b/.github/workflows/cmake-multi-platform.yml @@ -230,3 +230,47 @@ jobs: -S ${{ github.workspace }} - name: Build run: cmake --build ${{ github.workspace }}/build + + windows: + # Twelve source files carry WIN32 or _MSC_VER guards and nothing has + # compiled them for as long as the workflows have existed, so a change + # that breaks the Windows path is invisible until a consumer reports + # it. zlib comes from vcpkg because the project requires it. + name: windows-latest msvc ${{ matrix.linkage }} + runs-on: windows-latest + strategy: + fail-fast: false + matrix: + include: + - linkage: static + shared: "OFF" + triplet: x64-windows-static + - linkage: shared + shared: "ON" + triplet: x64-windows + steps: + - uses: actions/checkout@v4 + - name: Install zlib and expat + run: vcpkg install zlib:${{ matrix.triplet }} expat:${{ matrix.triplet }} + - name: Configure CMake + shell: pwsh + run: | + $toolchain = Join-Path $env:VCPKG_INSTALLATION_ROOT 'scripts/buildsystems/vcpkg.cmake' + Write-Host "vcpkg toolchain: $toolchain" + if (-not (Test-Path $toolchain)) { throw "toolchain file not found: $toolchain" } + cmake -B "${{ github.workspace }}/build" ` + -DCMAKE_TOOLCHAIN_FILE="$toolchain" ` + -DVCPKG_TARGET_TRIPLET=${{ matrix.triplet }} ` + -DBUILD_SHARED_LIBS=${{ matrix.shared }} ` + -DCMAKE_RUNTIME_OUTPUT_DIRECTORY="${{ github.workspace }}/build/bin" ` + -DNIFTI_BUILD_APPLICATIONS=ON ` + -DUSE_NIFTI2_CODE=ON ` + -DUSE_CIFTI_CODE=ON ` + -S "${{ github.workspace }}" + - name: Build + run: cmake --build ${{ github.workspace }}/build --config Release + - name: Test + working-directory: ${{ github.workspace }}/build + # A test that cannot find a DLL can raise a modal dialog and block the + # runner rather than exit, so every test carries a timeout. + run: ctest --build-config Release --output-on-failure --no-tests=ignore --timeout 120 From 63b361adc95805ae58d235929a3141b734ba6e46 Mon Sep 17 00:00:00 2001 From: "Hans J. Johnson" Date: Tue, 22 Sep 2026 09:31:20 -0500 Subject: [PATCH 46/77] COMP: Export the afni_xml half of the cifti library afni_xml_io.h decorates its declarations with CIF_API and afni_xml.h decorated none of its own, so a Windows shared build produced a DLL missing every axml_ entry point and both cifti tools failed to link against the library they are built with. The macro definition moves to afni_xml.h, which afni_xml_io.h includes at its top, so one definition now serves both headers rather than each carrying its own. Nothing changes where the attribute expands to default visibility: the exported set of the shared build is byte-identical. Found by the Windows job added in the preceding commit. --- cifti/afni_xml.h | 68 ++++++++++++++++++++++++++++++--------------- cifti/afni_xml_io.h | 23 --------------- 2 files changed, 46 insertions(+), 45 deletions(-) diff --git a/cifti/afni_xml.h b/cifti/afni_xml.h index ecc4ab3e..16d0ae7c 100644 --- a/cifti/afni_xml.h +++ b/cifti/afni_xml.h @@ -70,38 +70,62 @@ typedef struct { } afni_xml_control; +#ifndef CIF_API + #if defined(_WIN32) || defined(__CYGWIN__) + #if defined(CIFTI_BUILD_SHARED) + #ifdef __GNUC__ + #define CIF_API __attribute__ ((dllexport)) + #else + #define CIF_API __declspec( dllexport ) + #endif + #elif defined(CIFTI_USE_SHARED) + #ifdef __GNUC__ + #define CIF_API __attribute__ ((dllimport)) + #else + #define CIF_API __declspec( dllimport ) + #endif + #else + #define CIF_API + #endif + #elif (defined(__GNUC__) && __GNUC__ >= 4) || defined(__clang__) + #define CIF_API __attribute__ ((visibility ("default"))) + #else + #define CIF_API + #endif +#endif + /* --------------------------- prototypes --------------------------------- */ /* main interface */ -afni_xml_list axml_read_buf (const char * buf_in, int64_t bin_len); -afni_xml_list axml_read_file(const char * fname, int read_data); +CIF_API afni_xml_list axml_read_buf (const char * buf_in, int64_t bin_len); +CIF_API afni_xml_list axml_read_file(const char * fname, int read_data); -int axml_disp_xlist( const char *mesg, afni_xml_list * axlist, int verb); -int axml_disp_xml_t( const char *mesg, afni_xml_t * ax, int indent, int verb); +CIF_API int axml_disp_xlist( const char *mesg, afni_xml_list * axlist, int verb); +CIF_API int axml_disp_xml_t( const char *mesg, afni_xml_t * ax, int indent, int verb); /* create/free */ -afni_xml_t * new_afni_xml (const char * name); -int axml_add_attrs (afni_xml_t * ax, const char ** attr); -int axml_free_xml_t(afni_xml_t * ax); -int axml_free_xlist(afni_xml_list * axlist); - -char * axml_attr_value(afni_xml_t * ax, const char * name); -int axml_recur(int(*func)(FILE*,afni_xml_t*,int), afni_xml_t * ax); -afni_xml_t * axml_recur_find_xml(int (*func)(afni_xml_t *, int), afni_xml_t * ax, +CIF_API afni_xml_t * new_afni_xml (const char * name); +CIF_API int axml_add_attrs (afni_xml_t * ax, const char ** attr); +CIF_API int axml_free_xml_t(afni_xml_t * ax); +CIF_API int axml_free_xlist(afni_xml_list * axlist); + +CIF_API char * axml_attr_value(afni_xml_t * ax, const char * name); +CIF_API int axml_recur(int(*func)(FILE*,afni_xml_t*,int), afni_xml_t * ax); +CIF_API afni_xml_t * axml_recur_find_xml(int (*func)(afni_xml_t *, int), afni_xml_t * ax, int depth, int max_depth); /* control API */ -int axml_set_verb ( int val ); -int axml_get_verb ( void ); -int axml_set_dstore ( int val ); -int axml_get_dstore ( void ); -int axml_set_indent ( int val ); -int axml_get_indent ( void ); -int axml_set_buf_size ( int val ); -int axml_get_buf_size ( void ); -int axml_set_wstream ( FILE *fp ); -FILE * axml_get_wstream ( void ); +CIF_API int axml_set_verb ( int val ); +CIF_API int axml_get_verb ( void ); +CIF_API int axml_set_dstore ( int val ); +CIF_API int axml_get_dstore ( void ); +CIF_API int axml_set_indent ( int val ); +CIF_API int axml_get_indent ( void ); +CIF_API int axml_set_buf_size ( int val ); +CIF_API int axml_get_buf_size ( void ); +CIF_API int axml_set_wstream ( FILE *fp ); +CIF_API FILE * axml_get_wstream ( void ); #endif /* AFNI_XML_H */ diff --git a/cifti/afni_xml_io.h b/cifti/afni_xml_io.h index ae8aeb2a..ee9b6a1b 100644 --- a/cifti/afni_xml_io.h +++ b/cifti/afni_xml_io.h @@ -50,29 +50,6 @@ - convert as in SUMA_Create_Fake_CIFTI() * ----------------------------------------------------------------------*/ -#ifndef CIF_API - #if defined(_WIN32) || defined(__CYGWIN__) - #if defined(CIFTI_BUILD_SHARED) - #ifdef __GNUC__ - #define CIF_API __attribute__ ((dllexport)) - #else - #define CIF_API __declspec( dllexport ) - #endif - #elif defined(CIFTI_USE_SHARED) - #ifdef __GNUC__ - #define CIF_API __attribute__ ((dllimport)) - #else - #define CIF_API __declspec( dllimport ) - #endif - #else - #define CIF_API - #endif - #elif (defined(__GNUC__) && __GNUC__ >= 4) || defined(__clang__) - #define CIF_API __attribute__ ((visibility ("default"))) - #else - #define CIF_API - #endif -#endif /* --------------------------- structures --------------------------------- */ From 57a61a7fd296a6f99fb3bb0f9a5b1a0a92ad94b5 Mon Sep 17 00:00:00 2001 From: "Gabriel A. Devenyi" Date: Fri, 14 Aug 2026 23:12:34 -0400 Subject: [PATCH 47/77] BUG: Bound the formatted write in znzprintf, and end the va_list znzprintf() built its output with vsprintf(), which has no bound; the buffer was sized strlen(format) + 1000000 under a comment reading "overkill I hope". A single %s argument longer than a megabyte writes past the end of the allocation. vsnprintf() with the size already being computed turns that overflow into a reported truncation. The same block also returned on calloc failure without calling va_end(), leaving the va_list unterminated. This is the only unbounded formatted write in the tree; there is no sprintf() and no gets() anywhere. --- znzlib/znzlib.c | 8 ++++++-- znzlib/znzlib.h | 7 +++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/znzlib/znzlib.c b/znzlib/znzlib.c index 52b5665d..64c80da1 100644 --- a/znzlib/znzlib.c +++ b/znzlib/znzlib.c @@ -302,13 +302,17 @@ int znzprintf(znzFile stream, const char *format, ...) #ifdef HAVE_ZLIB if (stream->zfptr!=NULL) { size_t size; /* local to HAVE_ZLIB block */ - size = strlen(format) + 1000000; /* overkill I hope */ + int written; + size = strlen(format) + 1000000; /* still generous, but now a bound */ tmpstr = (char *)calloc(1, size); if( tmpstr == NULL ){ fprintf(stderr,"** ERROR: znzprintf failed to alloc %zu bytes\n", size); + va_end(va); return retval; } - vsprintf(tmpstr,format,va); + written = vsnprintf(tmpstr,size,format,va); + if( written < 0 || (size_t)written >= size ) + fprintf(stderr,"** ERROR: znzprintf output truncated at %zu bytes\n", size-1); retval=gzprintf(stream->zfptr,"%s",tmpstr); free(tmpstr); } else diff --git a/znzlib/znzlib.h b/znzlib/znzlib.h index ff031687..5b21a4f0 100644 --- a/znzlib/znzlib.h +++ b/znzlib/znzlib.h @@ -150,9 +150,16 @@ ZNZ_API int znzputc(int c, znzFile file); ZNZ_API int znzgetc(znzFile file); #if !defined(WIN32) +/* the attribute lets the caller's format be checked, which is what makes + the internal vsnprintf on it acceptable to -Wformat-nonliteral */ +#if defined(__GNUC__) || defined(__clang__) +ZNZ_API int znzprintf(znzFile stream, const char *format, ...) + __attribute__((format(printf, 2, 3))); +#else ZNZ_API int znzprintf(znzFile stream, const char *format, ...); #endif #endif +#endif /*=================*/ #ifdef __cplusplus From ac7ec091d1a87813822f1f40c379c292f17623eb Mon Sep 17 00:00:00 2001 From: "Gabriel A. Devenyi" Date: Fri, 18 Sep 2026 22:01:29 -0400 Subject: [PATCH 48/77] BUG: Report a znzprintf failure as a failure The preceding commit bounds the write but still returned the gzprintf count after a truncation, so a caller could not tell. It also returned 0 for an allocation failure, and the function returns 0 for a NULL stream. The printf family reports failure with a negative value; 0 is a successful empty write, so it cannot carry either meaning. All three now return -1, and a truncated result is not written at all: a partial record reported as a whole one is worse than nothing. znzprintf sits inside COMPILE_NIFTIUNUSED_CODE in both znzlib.c and znzlib.h and is absent from libznz.a in a default build, so this is a latent defect in code no released configuration compiles. --- znzlib/znzlib.c | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/znzlib/znzlib.c b/znzlib/znzlib.c index 64c80da1..05d87e27 100644 --- a/znzlib/znzlib.c +++ b/znzlib/znzlib.c @@ -297,7 +297,9 @@ int znzprintf(znzFile stream, const char *format, ...) int retval=0; char *tmpstr; va_list va; - if (stream==NULL) { return 0; } + /* the printf family reports failure with a negative value; 0 means an + empty write succeeded, so it cannot be used for the failures below */ + if (stream==NULL) { return -1; } va_start(va, format); #ifdef HAVE_ZLIB if (stream->zfptr!=NULL) { @@ -308,11 +310,17 @@ int znzprintf(znzFile stream, const char *format, ...) if( tmpstr == NULL ){ fprintf(stderr,"** ERROR: znzprintf failed to alloc %zu bytes\n", size); va_end(va); - return retval; + return -1; } written = vsnprintf(tmpstr,size,format,va); - if( written < 0 || (size_t)written >= size ) + if( written < 0 || (size_t)written >= size ){ + /* writing the truncated text would put a partial record in the + file and report it as a complete one, so write nothing */ fprintf(stderr,"** ERROR: znzprintf output truncated at %zu bytes\n", size-1); + free(tmpstr); + va_end(va); + return -1; + } retval=gzprintf(stream->zfptr,"%s",tmpstr); free(tmpstr); } else From 0350b30e729cda08615b3c9ee2f0b95a810ef107 Mon Sep 17 00:00:00 2001 From: "Hans J. Johnson" Date: Tue, 22 Sep 2026 08:36:48 -0500 Subject: [PATCH 49/77] ENH: Cover the znzprintf truncation path znzprintf has no caller in this tree and is compiled only under COMPILE_NIFTIUNUSED_CODE, so nothing reached it. The test builds its own copy of znzlib.c with that definition and drives a 2 MB argument through the 1000001 byte buffer. Without the bound the write overflows the allocation, and the call reports success; the test pins the negative return, and pins the ordinary write and the resulting file content so an over-broad fix that refuses everything does not pass. --- znzlib/CMakeLists.txt | 12 ++++++++ znzlib/znzprintf_test.c | 67 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 znzlib/znzprintf_test.c diff --git a/znzlib/CMakeLists.txt b/znzlib/CMakeLists.txt index 10239be6..5be28397 100644 --- a/znzlib/CMakeLists.txt +++ b/znzlib/CMakeLists.txt @@ -27,3 +27,15 @@ if(BUILD_SHARED_LIBS) target_compile_definitions(${NIFTI_ZNZLIB_NAME} INTERFACE ZNZ_USE_SHARED) endif() install_nifti_target(${NIFTI_ZNZLIB_NAME}) + +if(NIFTI_BUILD_TESTING AND ZLIB_FOUND AND NOT WIN32) + # znzprintf is only compiled under COMPILE_NIFTIUNUSED_CODE, and not at + # all on Windows, so the test builds its own copy of znzlib.c with that + # definition and is registered only where the function exists. + add_executable(znzprintf_test znzprintf_test.c znzlib.c) + target_compile_definitions(znzprintf_test PRIVATE COMPILE_NIFTIUNUSED_CODE) + target_include_directories(znzprintf_test PRIVATE ${CMAKE_CURRENT_LIST_DIR} ${ZLIB_INCLUDE_DIR}) + target_link_libraries(znzprintf_test PRIVATE ${NIFTI_ZLIB_LIBRARIES}) + add_test(NAME znzprintf_truncation + COMMAND $ ${CMAKE_CURRENT_BINARY_DIR}/znzprintf_test.gz) +endif() diff --git a/znzlib/znzprintf_test.c b/znzlib/znzprintf_test.c new file mode 100644 index 00000000..24b6c321 --- /dev/null +++ b/znzlib/znzprintf_test.c @@ -0,0 +1,67 @@ +/* Exercises znzprintf(), which is compiled only under + COMPILE_NIFTIUNUSED_CODE and has no caller inside this tree. */ + +#include +#include +#include + +#include "znzlib.h" + +#define BIG_LEN 2000000 + +static const char expected[] = "abc\n"; + +int main(int argc, char *argv[]) +{ + const char *path; + znzFile zf; + char *big; + int normal; + int truncated; + int status = 0; + size_t got; + char readback[64]; + + if( argc < 2 ){ fprintf(stderr,"usage: %s OUTFILE.gz\n", argv[0]); return 1; } + path = argv[1]; + + zf = znzopen(path, "wb", 1); + if( zf == NULL ){ fprintf(stderr,"** cannot open %s\n", path); return 1; } + + normal = znzprintf(zf, "%s", expected); + if( normal != (int)strlen(expected) ){ + fprintf(stderr,"** FAIL: ordinary write returned %d, expected %d\n", + normal, (int)strlen(expected)); + status = 1; + } + + big = (char *)malloc(BIG_LEN + 1); + if( big == NULL ){ fprintf(stderr,"** cannot allocate\n"); znzclose(zf); return 1; } + memset(big, 'x', BIG_LEN); + big[BIG_LEN] = '\0'; + + truncated = znzprintf(zf, "%s", big); + if( truncated >= 0 ){ + fprintf(stderr,"** FAIL: truncating write returned %d, expected a " + "negative value\n", truncated); + status = 1; + } + free(big); + + if( znzclose(zf) != 0 ){ fprintf(stderr,"** cannot close %s\n", path); return 1; } + + zf = znzopen(path, "rb", 1); + if( zf == NULL ){ fprintf(stderr,"** cannot reopen %s\n", path); return 1; } + memset(readback, 0, sizeof(readback)); + got = znzread(readback, 1, sizeof(readback) - 1, zf); + znzclose(zf); + + if( got != strlen(expected) || strcmp(readback, expected) != 0 ){ + fprintf(stderr,"** FAIL: file holds %zu bytes, expected only the %d " + "bytes of the ordinary write\n", got, (int)strlen(expected)); + status = 1; + } + + if( status == 0 ) printf("znzprintf test passed\n"); + return status; +} From 8449a1f579045ff4a3792aa1193879dc4ccdd182 Mon Sep 17 00:00:00 2001 From: "Hans J. Johnson" Date: Tue, 22 Sep 2026 10:09:08 -0500 Subject: [PATCH 50/77] COMP: Declare the two znzlib functions that no header declares znzflush() and znzeof() are defined inside COMPILE_NIFTIUNUSED_CODE and declared nowhere, unlike znzgets(), znzputc() and znzgetc() beside them. Nothing compiled that block until a test in this branch did, so the omission was invisible. They are declared where the rest of the block is declared, so the warning set the project already requires stays clean when the block is built. --- znzlib/znzlib.h | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/znzlib/znzlib.h b/znzlib/znzlib.h index 5b21a4f0..866fc01c 100644 --- a/znzlib/znzlib.h +++ b/znzlib/znzlib.h @@ -145,6 +145,10 @@ ZNZ_API int znzputs(const char *str, znzFile file); #ifdef COMPILE_NIFTIUNUSED_CODE ZNZ_API char * znzgets(char* str, int size, znzFile file); +ZNZ_API int znzflush(znzFile file); + +ZNZ_API int znzeof(znzFile file); + ZNZ_API int znzputc(int c, znzFile file); ZNZ_API int znzgetc(znzFile file); From 923e869c41ba3cc3a93d8861f47ab4e8dc94d849 Mon Sep 17 00:00:00 2001 From: "Gabriel A. Devenyi" Date: Sat, 15 Aug 2026 00:00:00 -0400 Subject: [PATCH 51/77] BUG: Check the allocations whose result is used immediately Three allocations had their result used without a NULL check, in every case within a line or two, so an allocation failure is a null dereference rather than an error. nifti2_io.c nifti_read_n2_hdr() malloc(nifti_2_header), then nifti_convert_nim2n2hdr() fills it afni_xml.c new_afni_xml(), strdup results stored straight axml_add_attrs() into the structure Each returns an error the way the failure paths beside it do. One line further on, nifti_read_n2_hdr() released the image it had just converted with free(nim) rather than nifti_image_free(), leaking the filename strings nifti_read_ascii_image() had allocated. That is corrected in the same place. The four fslio.c allocations in the same class are left out: the idiom that file uses to report an allocation failure, FSLIOERR, calls exit(), and whether the library may end its host process is for the maintainers to decide. --- cifti/afni_xml.c | 14 +++++++++++++- nifti2/nifti2_io.c | 7 ++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/cifti/afni_xml.c b/cifti/afni_xml.c index d7cc9a4f..54467434 100644 --- a/cifti/afni_xml.c +++ b/cifti/afni_xml.c @@ -412,7 +412,14 @@ afni_xml_t * new_afni_xml(const char * name) newp->xparent = NULL; newp->xchild = NULL; - if( name ) newp->name = strdup(name); + if( name ) { + newp->name = strdup(name); + if( ! newp->name ) { + fprintf(stderr,"** new_afni_xml: failed to copy name '%s'\n", name); + free(newp); + return NULL; + } + } return newp; } @@ -457,6 +464,11 @@ int axml_add_attrs(afni_xml_t * ax, const char ** attr) for(c = 0, aind = 0; attr[c]; c += 2, aind++) { ax->attrs.name[aind] = strdup(strip_whitespace(attr[c],0)); ax->attrs.value[aind] = strdup(strip_whitespace(attr[c+1],0)); + if( ! ax->attrs.name[aind] || ! ax->attrs.value[aind] ) { + fprintf(stderr,"** NAX: failed to copy attribute %d\n", aind); + ax->attrs.length = aind+1; /* so the partial pair is still freed */ + return 1; + } } return 0; diff --git a/nifti2/nifti2_io.c b/nifti2/nifti2_io.c index ae0d8070..58bc2bfb 100644 --- a/nifti2/nifti2_io.c +++ b/nifti2/nifti2_io.c @@ -5492,8 +5492,13 @@ nifti_2_header * nifti_read_n2_hdr(const char * hname, int * swapped, if( ! nim ) return NULL; hptr = (nifti_2_header *)malloc(sizeof(nifti_2_header)); + if( ! hptr ){ + fprintf(stderr,"** nifti_read_n2_hdr: failed to alloc nifti_2_header\n"); + nifti_image_free(nim); + return NULL; + } rv = nifti_convert_nim2n2hdr(nim, hptr); - free(nim); + nifti_image_free(nim); /* free(nim) leaked nim's filename strings */ if( rv ) { free(hptr); return NULL; } return hptr; From 0685f7fca7a13fcbb872ea7cd5939a7041ca6f11 Mon Sep 17 00:00:00 2001 From: "Gabriel A. Devenyi" Date: Fri, 18 Sep 2026 21:52:16 -0400 Subject: [PATCH 52/77] BUG: Report the attribute failure instead of discarding it axml_add_attrs() returns 1 when it cannot copy an attribute, but make_afni_xml() ignored the result, so the element was returned with a partial attribute list and nothing said so. Every failure the function reports is an allocation failure, so there is no case where continuing is right. Propagate it: epush() already treats a NULL from make_afni_xml() by entering a skip block, which is what the allocation-failure paths beside it do. axml_free_xml_t() releases what was built. It walks the attributes with attrs.length, which axml_add_attrs() lowers to the pair it failed on, so the half of that pair that was copied is freed and the entries past it are never read. That matters because the two arrays come from malloc(), not calloc(), so those entries hold uninitialized pointers. --- cifti/afni_xml.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/cifti/afni_xml.c b/cifti/afni_xml.c index 54467434..45b72e52 100644 --- a/cifti/afni_xml.c +++ b/cifti/afni_xml.c @@ -850,7 +850,11 @@ static afni_xml_t * make_afni_xml(const char * ename, const char ** attr) newp = new_afni_xml(ename); if( ! newp ) return NULL; - axml_add_attrs(newp, attr); + /* a failure here is an allocation failure; epush() skips on NULL */ + if( axml_add_attrs(newp, attr) ) { + axml_free_xml_t(newp); + return NULL; + } return newp; } From c6410f52e112af11b624c651fb2969a494b293c4 Mon Sep 17 00:00:00 2001 From: "Hans J. Johnson" Date: Tue, 22 Sep 2026 10:20:02 -0500 Subject: [PATCH 53/77] ENH: Cover the ASCII path through the NIFTI-2 header reader nifti_read_n2_hdr() builds a nifti_image to convert an ASCII header and then released it with free(), which leaves its filename strings behind. No test reached that path, so the suite reported 362 of 362 either way. The fixture is the one the ASCII attribute test already uses, read through -disp_hdr2 rather than -disp_nim so the NIFTI-2 reader is the one exercised. The leak is visible to the memcheck and sanitizer legs; an ordinary build stays green with or without the fix. --- nifti2/CMakeLists.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/nifti2/CMakeLists.txt b/nifti2/CMakeLists.txt index d369220c..4564ed76 100644 --- a/nifti2/CMakeLists.txt +++ b/nifti2/CMakeLists.txt @@ -238,6 +238,10 @@ if(NIFTI_BUILD_TESTING AND NIFTI_BUILD_APPLICATIONS) add_test( NAME ${TEST_PREFIX}_nii_cmp_selftest COMMAND sh ${NIFTI_TEST_SCRIPT_DIR}/nii_cmp_selftest.sh ${NIFTI_TEST_SCRIPT_DIR} ) endif() + # Reading an ASCII header through the NIFTI-2 reader releases the image + # it built; the leak is what the memcheck and sanitizer legs observe. + add_test( NAME ${TEST_PREFIX}_tool_disp_hdr2_ascii COMMAND $ -disp_hdr2 -infiles ${CMAKE_CURRENT_LIST_DIR}/testdata/dup_attr.nia ) + unset(TEST_SUFFIX) unset(TEST_PREFIX) unset(TOOL_NAME) From b344fe40f478a55c23b9769869721f4183be8821 Mon Sep 17 00:00:00 2001 From: "Gabriel A. Devenyi" Date: Fri, 14 Aug 2026 23:50:01 -0400 Subject: [PATCH 54/77] BUG: Stop returning -1 from functions that return size_t znzread(), znzwrite() and nifti_read_buffer() all return size_t and all returned -1 to report an error. In a size_t that value is SIZE_MAX, larger than any length a caller can have asked for, so a caller that tests the result with '<' reads the error as a complete transfer: ii = nifti_read_buffer(fp, nim->data, ntot, nim); if( ii < ntot ){ ... } /* SIZE_MAX < ntot is false */ The visible effect is that a truncated image loads as if it were whole: nifti_image_read() prints its short-read warning, ignores the failure and returns an image whose tail is uninitialized heap. FslReadVolumes() has the same problem one level up, dividing the returned byte count by the volume size to report SIZE_MAX/volbytes volumes read. These now return 0, which every one of these callers already treats as failure and which is the only value a size_t function has for "nothing was transferred". The published return types in znzlib.h and nifti1_io.h are unchanged. The nifti2 equivalents return int64_t, where -1 is representable, and are left alone. --- niftilib/nifti1_io.c | 4 ++-- znzlib/znzlib.c | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/niftilib/nifti1_io.c b/niftilib/nifti1_io.c index 7a66e4af..3d6f8fd0 100644 --- a/niftilib/nifti1_io.c +++ b/niftilib/nifti1_io.c @@ -5044,7 +5044,7 @@ size_t nifti_read_buffer(znzFile fp, void* dataptr, size_t ntot, if( dataptr == NULL ){ if( g_opts.debug > 0 ) fprintf(stderr,"** ERROR: nifti_read_buffer: NULL dataptr\n"); - return -1; + return 0; } ii = znzread( dataptr , 1 , ntot , fp ) ; /* data input */ @@ -5059,7 +5059,7 @@ size_t nifti_read_buffer(znzFile fp, void* dataptr, size_t ntot, nim->iname , (unsigned int)ntot , (unsigned int)ii , (unsigned int)(ntot-ii) ) ; /* memset( (char *)(dataptr)+ii , 0 , ntot-ii ) ; now failure [rickr] */ - return -1 ; + return 0 ; } if( g_opts.debug > 2 ) diff --git a/znzlib/znzlib.c b/znzlib/znzlib.c index 05d87e27..28ecf791 100644 --- a/znzlib/znzlib.c +++ b/znzlib/znzlib.c @@ -145,7 +145,8 @@ size_t znzread(void* buf, size_t size, size_t nmemb, znzFile file) while( remain > 0 ) { n2read = (remain < ZNZ_MAX_BLOCK_SIZE) ? (unsigned)remain : ZNZ_MAX_BLOCK_SIZE; nread = gzread(file->zfptr, (void *)cbuf, n2read); - if( nread < 0 ) return nread; /* returns -1 on error */ + /* 0, not gzread's -1: this returns size_t, where -1 is SIZE_MAX. */ + if( nread < 0 ) return 0; remain -= nread; cbuf += nread; @@ -178,8 +179,8 @@ size_t znzwrite(const void* buf, size_t size, size_t nmemb, znzFile file) n2write = (remain < ZNZ_MAX_BLOCK_SIZE) ? (unsigned)remain : ZNZ_MAX_BLOCK_SIZE; nwritten = gzwrite(file->zfptr, (const void *)cbuf, n2write); - /* gzread returns 0 on error, but in case that ever changes... */ - if( nwritten < 0 ) return nwritten; + /* gzwrite returns 0 on error, but in case that ever changes... */ + if( nwritten < 0 ) return 0; remain -= nwritten; cbuf += nwritten; From 9b31abf78b148947ad3f4b73bd0b09b93be33d4d Mon Sep 17 00:00:00 2001 From: "Hans J. Johnson" Date: Tue, 22 Sep 2026 08:50:13 -0500 Subject: [PATCH 55/77] ENH: Test that a truncated image is rejected rather than accepted Writes a 31x31x31 float image, copies it 1000 bytes short, and asserts nifti_image_read() returns NULL for the copy and a populated image for the original, so a blanket refusal cannot pass. Without the fix the truncated file comes back accepted with nvox=29791 and an uninitialized tail. --- niftilib/CMakeLists.txt | 7 +++ niftilib/nifti_short_read_test.c | 89 ++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 niftilib/nifti_short_read_test.c diff --git a/niftilib/CMakeLists.txt b/niftilib/CMakeLists.txt index d3b09cd8..23461e87 100644 --- a/niftilib/CMakeLists.txt +++ b/niftilib/CMakeLists.txt @@ -40,6 +40,13 @@ if(NIFTI_BUILD_TESTING AND NIFTI_BUILD_APPLICATIONS) add_executable(nifti_first_test_program nifti_tester001.c) target_link_libraries(nifti_first_test_program ${NIFTI_PACKAGE_PREFIX}niftiio ) + add_executable(${NIFTI_PACKAGE_PREFIX}nifti_short_read_test nifti_short_read_test.c) + target_link_libraries(${NIFTI_PACKAGE_PREFIX}nifti_short_read_test ${NIFTI_NIFTILIB_NAME}) + add_test( + NAME nifti_short_read_rejected + COMMAND $ + ) + add_executable(nifti_second_test_program nifti_tester002.c) target_link_libraries(nifti_second_test_program ${NIFTI_PACKAGE_PREFIX}niftiio ) diff --git a/niftilib/nifti_short_read_test.c b/niftilib/nifti_short_read_test.c new file mode 100644 index 00000000..809fb684 --- /dev/null +++ b/niftilib/nifti_short_read_test.c @@ -0,0 +1,89 @@ +/* A .nii whose data section is short of the header's declared size must be + rejected, not handed back with uninitialized tail bytes. */ + +#include +#include +#include "nifti1_io.h" + +static const char *WHOLE = "short_read_whole.nii"; +static const char *SHORT = "short_read_short.nii"; +static const long MISSING = 1000; + +static int write_whole(void) +{ + int dims[8] = { 3, 31, 31, 31, 1, 1, 1, 1 }; + nifti_image *nim = nifti_make_new_nim(dims, DT_FLOAT32, 1); + if( nim == NULL ) return 1; + if( nifti_set_filenames(nim, WHOLE, 0, 1) != 0 ){ + nifti_image_free(nim); + return 1; + } + nifti_image_write(nim); + nifti_image_free(nim); + return 0; +} + +static int copy_truncated(void) +{ + FILE *in = fopen(WHOLE, "rb"), *out; + long size; + char *buf; + size_t got; + + if( in == NULL ) return 1; + fseek(in, 0, SEEK_END); + size = ftell(in); + fseek(in, 0, SEEK_SET); + if( size <= MISSING ){ fclose(in); return 1; } + + buf = (char *)malloc((size_t)size); + if( buf == NULL ){ fclose(in); return 1; } + got = fread(buf, 1, (size_t)size, in); + fclose(in); + if( got != (size_t)size ){ free(buf); return 1; } + + out = fopen(SHORT, "wb"); + if( out == NULL ){ free(buf); return 1; } + fwrite(buf, 1, (size_t)(size - MISSING), out); + fclose(out); + free(buf); + return 0; +} + +int main(void) +{ + nifti_image *nim; + + if( write_whole() ){ + fprintf(stderr, "FAILURE: could not write the reference image\n"); + return 1; + } + if( copy_truncated() ){ + fprintf(stderr, "FAILURE: could not write the truncated copy\n"); + return 1; + } + + nim = nifti_image_read(SHORT, 1); + if( nim != NULL ){ + fprintf(stderr, "FAILURE: truncated image was accepted, nvox=%d\n", + (int)nim->nvox); + nifti_image_free(nim); + return 1; + } + + /* the rejection must be specific to the short read */ + nim = nifti_image_read(WHOLE, 1); + if( nim == NULL ){ + fprintf(stderr, "FAILURE: the whole image was rejected\n"); + return 1; + } + if( nim->data == NULL ){ + fprintf(stderr, "FAILURE: the whole image came back with no data\n"); + nifti_image_free(nim); + return 1; + } + nifti_image_free(nim); + + printf("Short-read rejection test passed.\n"); + return 0; +} From 6ddc4549d632d9eff61eb78c06552a1065b45bbd Mon Sep 17 00:00:00 2001 From: "Gabriel A. Devenyi" Date: Sat, 15 Aug 2026 16:38:44 -0400 Subject: [PATCH 56/77] BUG: Keep the XML skip depth at the element that started the skip afni_xml's parser holds sixteen levels of open elements in a fixed array and refuses to push a seventeenth by entering a skip block: epush() records the depth in xd->dskip and stops touching the stack until the matching pop. Two details of that bookkeeping are wrong. epush() overwrote dskip on every push past the limit, so the skip was recorded as starting at the innermost depth. epop() then cleared dskip before the stack was handled, so the pop that ends the skip fell through to "xd->stack[xd->depth-1] = NULL" and wrote a pointer eight bytes past the sixteen-entry array, which lives in the file-scope afni_xml_control. The next pop found dskip clear and dereferenced xd->stack[16]. epush() now keeps the outermost skip depth, since that is the depth whose pop ends the skip, and epop() clears dskip only after the element has been skipped. Depths within the limit are unaffected. The XML comes from the CIFTI extension of a NIfTI file, or from any file read by axml_read_file(), so the nesting is chosen by the input. Found by fuzzing axml_read_buf() with libFuzzer under AddressSanitizer. --- cifti/afni_xml.c | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/cifti/afni_xml.c b/cifti/afni_xml.c index 45b72e52..8b838546 100644 --- a/cifti/afni_xml.c +++ b/cifti/afni_xml.c @@ -734,8 +734,9 @@ static int epush(afni_xml_control * xd, const char * ename, const char ** attr) if( xd->verb > 3 ) show_attrs(xd, attr, 1); } - /* determine whether we should go into a skip block */ - if( errs ) xd->dskip = xd->depth; + /* determine whether we should go into a skip block; keep the outermost + such depth, since that is the one whose pop ends the skip */ + if( errs && ! xd->dskip ) xd->dskip = xd->depth; /* if we are in a skip block, do nothing but monitor stack */ if( xd->dskip ) { @@ -767,16 +768,16 @@ static int epop(afni_xml_control * xd, const char * ename) if( xd->wkeep ) xd->wkeep = 0; /* clear storage continuation */ if( xd->dskip ) { - if( xd->dskip == xd->depth ) xd->dskip = 0; /* clear */ - if( xd->verb > 3 ) fprintf(stderr,"-- skip=%d, depth=%d, skipping pop element '%s'\n", xd->dskip, xd->depth, ename); + + /* clear only after the element has been skipped, so that the stack + is not touched at a depth that was never pushed onto it */ + if( xd->dskip == xd->depth ) xd->dskip = 0; } else { process_popped_element(xd, ename); - } - if( ! xd->dskip ) { xd->stack[xd->depth-1] = NULL; /* should be irrelevant */ if( xd->verb > 4 ) { From d34f28ca3c6f54a6a016a597a1f417a25bbe9b17 Mon Sep 17 00:00:00 2001 From: "Gabriel A. Devenyi" Date: Fri, 18 Sep 2026 21:59:26 -0400 Subject: [PATCH 57/77] BUG: Do not assume a popped element filled its stack slot process_popped_element() read xd->stack[xd->depth-1] and dereferenced it without a test. A slot is filled by the matching epush(), and an element that was skipped never fills one, so the read is only safe because the preceding commit keeps the skip running to the element that started it. Guard it rather than rely on that: check the depth is inside the stack, and that the slot holds a struct with a name, before the strcmp(). --- cifti/afni_xml.c | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/cifti/afni_xml.c b/cifti/afni_xml.c index 8b838546..1c9a5956 100644 --- a/cifti/afni_xml.c +++ b/cifti/afni_xml.c @@ -794,7 +794,21 @@ static int epop(afni_xml_control * xd, const char * ename) static int process_popped_element(afni_xml_control * xd, const char * ename) { afni_xml_t * ax; + + /* a stack slot is filled by the matching epush(). An element that was + skipped never fills one, so do not assume this slot holds a struct + with a name. */ + if( xd->depth <= 0 || xd->depth > AXML_MAX_DEPTH ) { + if( gAXD.verb ) fprintf(stderr,"** pop at depth %d!\n", xd->depth); + return 1; + } + ax = xd->stack[xd->depth-1]; + if( ! ax || ! ax->name ) { + if( gAXD.verb ) fprintf(stderr,"** pop of unfilled element '%s'!\n", + ename ? ename : "NULL"); + return 1; + } if( strcmp(ename, ax->name) ) { if( gAXD.verb ) fprintf(stderr,"** pop mismatch!\n"); From 0e0033ddf9e6d19b73c1b20152cc3937bb216def Mon Sep 17 00:00:00 2001 From: "Hans J. Johnson" Date: Tue, 22 Sep 2026 08:44:18 -0500 Subject: [PATCH 58/77] ENH: Cover XML nesting past the afni_xml stack limit deep_nesting.xml nests twenty elements inside a CIFTI extension whose stack holds sixteen, which is the shape that walked off the end of xd->stack. The test drives it through cifti_tool and pins that the BrainModel sibling is still summarized, so a fix that refused the whole document would not pass. --- cifti/CMakeLists.txt | 9 +++++++++ cifti/testdata/deep_nesting.xml | 8 ++++++++ 2 files changed, 17 insertions(+) create mode 100644 cifti/testdata/deep_nesting.xml diff --git a/cifti/CMakeLists.txt b/cifti/CMakeLists.txt index ec8a4307..e13e7bdf 100644 --- a/cifti/CMakeLists.txt +++ b/cifti/CMakeLists.txt @@ -50,4 +50,13 @@ if(NIFTI_BUILD_TESTING AND NIFTI_BUILD_APPLICATIONS) -eval_cext -eval_type show_summary ) set_tests_properties( ${TEST_PREFIX}_tool_unterminated_cext PROPERTIES PASS_REGULAR_EXPRESSION "MapName : demo" ) + + # Nesting past AXML_MAX_DEPTH must stay inside the skip block rather than + # walk off the fixed stack; the regex pins that the rest still parses. + add_test( NAME ${TEST_PREFIX}_deep_nesting + COMMAND $ + -as_cext -eval_cext -eval_type show_summary + -input ${CMAKE_CURRENT_LIST_DIR}/testdata/deep_nesting.xml ) + set_tests_properties( ${TEST_PREFIX}_deep_nesting + PROPERTIES PASS_REGULAR_EXPRESSION "BrainModel" ) endif() diff --git a/cifti/testdata/deep_nesting.xml b/cifti/testdata/deep_nesting.xml new file mode 100644 index 00000000..cf26ee24 --- /dev/null +++ b/cifti/testdata/deep_nesting.xml @@ -0,0 +1,8 @@ + + + + + + + + From db038e9e0446dfd6bc3f9fee513b4860e736239c Mon Sep 17 00:00:00 2001 From: "Hans J. Johnson" Date: Tue, 22 Sep 2026 07:58:03 -0500 Subject: [PATCH 59/77] COMP: Detect a workflow that can never be scheduled A branch filter naming a branch that does not exist leaves the workflow configured but never run, which looks the same as one that runs and passes: no red check appears because no check appears at all. build.yml sat dead behind a filter naming main on a repository whose branch is master, and the only thing that found it was reading the file. The check judges a filter only when every entry is a literal name, so a release-* pattern or a branch that does not exist yet is left alone, and it reports a name that resolves to nothing rather than one that merely differs from the default. --- .github/check_workflow_triggers.py | 110 +++++++++++++++++++++ .github/workflows/cmake-multi-platform.yml | 16 +++ 2 files changed, 126 insertions(+) create mode 100755 .github/check_workflow_triggers.py diff --git a/.github/check_workflow_triggers.py b/.github/check_workflow_triggers.py new file mode 100755 index 00000000..8e9bceb6 --- /dev/null +++ b/.github/check_workflow_triggers.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python3 +"""Fail if a workflow's push or pull_request trigger can never match. + +A branch filter naming a branch that does not exist leaves the workflow +configured but never scheduled, which looks identical to a workflow that +runs and passes: no red check appears, because no check appears at all. + +Only filters made entirely of literal names are judged. A filter holding +any glob is left alone, since whether it can match depends on branches +that may not exist yet. + + usage: check_workflow_triggers.py [workflow-dir] +""" +import glob +import os +import subprocess +import sys + +import yaml + +GLOB_CHARS = set('*?[]!+@') + + +def existing_branches(): + """Branch names on the remote, falling back to local refs.""" + for cmd in (['git', 'ls-remote', '--heads', 'origin'], + ['git', 'for-each-ref', '--format=%(refname)', 'refs/heads']): + out = subprocess.run(cmd, capture_output=True, text=True) + if out.returncode == 0 and out.stdout.strip(): + names = set() + for line in out.stdout.split('\n'): + ref = line.split('refs/heads/')[-1].strip() + if ref: + names.add(ref) + if names: + return names + return set() + + +def default_branch(): + env = os.environ.get('DEFAULT_BRANCH') + if env: + return env.strip() + out = subprocess.run(['git', 'symbolic-ref', '--short', 'refs/remotes/origin/HEAD'], + capture_output=True, text=True) + if out.returncode == 0 and out.stdout.strip(): + return out.stdout.strip().split('/')[-1] + return None + + +def triggers(doc): + """The `on:` mapping. PyYAML reads an unquoted `on` key as True.""" + for key in (True, 'on', 'On', 'ON'): + if isinstance(doc, dict) and key in doc: + return doc[key] + return None + + +def main(argv): + where = argv[1] if len(argv) > 1 else '.github/workflows' + branches = existing_branches() + default = default_branch() + if not branches: + print('could not determine the repository branches; nothing checked') + return 0 + print('branches on the remote: %d, default: %s' + % (len(branches), default or 'unknown')) + + problems = [] + for path in sorted(glob.glob(os.path.join(where, '*.yml')) + + glob.glob(os.path.join(where, '*.yaml'))): + try: + doc = yaml.safe_load(open(path)) + except yaml.YAMLError as exc: + problems.append('%s: cannot parse: %s' % (path, exc)) + continue + on = triggers(doc) + if not isinstance(on, dict): + continue + for event in ('push', 'pull_request', 'pull_request_target'): + spec = on.get(event) + if not isinstance(spec, dict): + continue + names = spec.get('branches') + if not names: + continue + if any(GLOB_CHARS & set(n) for n in names): + continue + live = [n for n in names if n in branches] + if not live: + problems.append( + '%s: %s.branches names only %s, and no such branch exists.\n' + ' This workflow can never be scheduled.%s' + % (path, event, ', '.join(repr(n) for n in names), + ('\n The default branch is %r.' % default) if default else '')) + elif default and default not in names: + print('note: %s: %s.branches does not include the default ' + 'branch %r' % (path, event, default)) + + if problems: + print('\nWorkflow trigger check failed:\n') + for p in problems: + print(' ' + p) + return 1 + print('every push and pull_request trigger can match an existing branch') + return 0 + + +if __name__ == '__main__': + sys.exit(main(sys.argv)) diff --git a/.github/workflows/cmake-multi-platform.yml b/.github/workflows/cmake-multi-platform.yml index e7250e81..3b577d0a 100644 --- a/.github/workflows/cmake-multi-platform.yml +++ b/.github/workflows/cmake-multi-platform.yml @@ -274,3 +274,19 @@ jobs: # A test that cannot find a DLL can raise a modal dialog and block the # runner rather than exit, so every test carries a timeout. run: ctest --build-config Release --output-on-failure --no-tests=ignore --timeout 120 + workflow-triggers: + # A branch filter naming a branch that does not exist leaves the + # workflow configured but never scheduled, which is indistinguishable + # from one that runs and passes: no red check appears because no check + # appears at all. build.yml sat dead behind `branches: [ main ]` on a + # repository whose branch is master, and nothing detected it. + name: workflow triggers can match a branch + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install dependencies + run: sudo apt-get update && sudo apt-get install -y python3-yaml + - name: Check the branch filters + env: + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + run: python3 .github/check_workflow_triggers.py From c2d181ed5a1fef5d44f7995afce1f07e3223741c Mon Sep 17 00:00:00 2001 From: Sean McBride Date: Wed, 14 Jan 2026 17:19:28 -0500 Subject: [PATCH 60/77] BUG: Check the allocation in nifti_intent_code before copying into it cppcheck 2.19 nullPointerOutOfMemory: the uppercase copy of the name was written into an unchecked malloc result. Return -1, the same value the function already uses for an unrecognized name. --- nifticdf/nifticdf.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nifticdf/nifticdf.c b/nifticdf/nifticdf.c index bdab933f..bf00c52a 100644 --- a/nifticdf/nifticdf.c +++ b/nifticdf/nifticdf.c @@ -11050,6 +11050,8 @@ int nifti_intent_code( const char *name ) if( name == NULL || *name == '\0' ) return -1 ; unam = (char *)malloc(strlen(name)+1); + if (!unam) + return -1 ; strcpy(unam,name); for( upt=unam ; *upt != '\0' ; upt++ ) *upt = (char)toupper(*upt) ; From f5f3766d60cdfa9cb794f752563a3de8625a40aa Mon Sep 17 00:00:00 2001 From: Sean McBride Date: Wed, 14 Jan 2026 17:19:28 -0500 Subject: [PATCH 61/77] STYLE: Drop the unreachable returns after FSLIOERR cppcheck 2.19 unreachableCode: FSLIOERR ends in exit(EXIT_FAILURE), so the return statements following it in FslReadAllVolumes and FslReadHeader can never run. --- fsliolib/fslio.c | 2 -- 1 file changed, 2 deletions(-) diff --git a/fsliolib/fslio.c b/fsliolib/fslio.c index 6eb00e06..3e6754b0 100644 --- a/fsliolib/fslio.c +++ b/fsliolib/fslio.c @@ -821,7 +821,6 @@ void* FslReadAllVolumes(FSLIO* fslio, char* filename) /* check for failure, from David Akers */ if (fslio->niftiptr == NULL) { FSLIOERR("FslReadAllVolumes: error reading NIfTI image"); - return(NULL); } FslSetFileType(fslio,fslio->niftiptr->nifti_type); @@ -2108,7 +2107,6 @@ FSLIO * FslReadHeader(char *fname) if (fslio->niftiptr == NULL) { FSLIOERR("FslReadHeader: error reading header information"); - return(NULL); } fslio->file_mode = FslGetReadFileType(fslio); From 7387fd54b4bff7fafb7840a430bb83771141ab13 Mon Sep 17 00:00:00 2001 From: Sean McBride Date: Sat, 18 Jan 2025 13:31:26 -0500 Subject: [PATCH 62/77] COMP: Parenthesize some macro parameters Fixes some clang-tidy bugprone-macro-parentheses warnings. Would be an issue where an argument that is an expression would evaluated different with different precedence. --- nifti2/nifti2_io.c | 2 +- nifti2/nifti_tool.h | 12 ++++++------ niftilib/nifti1_io.c | 2 +- niftilib/nifti1_tool.h | 8 ++++---- niftilib/nifti_tester001.c | 4 ++-- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/nifti2/nifti2_io.c b/nifti2/nifti2_io.c index 58bc2bfb..cfe5ce90 100644 --- a/nifti2/nifti2_io.c +++ b/nifti2/nifti2_io.c @@ -8783,7 +8783,7 @@ int nifti_short_order(void) /* determine this CPU's byte order */ put rhs string into nim->"nam" string, with field size = "sz" */ #define QSTR(nam,sz) if( strcmp(lhs,#nam) == 0 ) \ - strncpy(nim->nam,rhs,sz), nim->nam[sz-1]='\0' + strncpy(nim->nam,rhs,sz), nim->nam[(sz)-1]='\0' /*---------------------------------------------------------------------------*/ /*! Take an XML-ish ASCII string and create a NIFTI image header to match. diff --git a/nifti2/nifti_tool.h b/nifti2/nifti_tool.h index d548f66d..ba57369d 100644 --- a/nifti2/nifti_tool.h +++ b/nifti2/nifti_tool.h @@ -108,10 +108,10 @@ typedef struct{ #define NT_NIM_NUM_FIELDS 63 /* in the nifti_image struct */ #define NT_HDR_TIME_NFIELDS 8 /* num slice timing fields in hdr */ #define NT_NIM_TIME_NFIELDS 11 /* num slice timing fields in nim */ -#define NT_DT_STRING -0xfff /* some strange number to abuse... */ -#define NT_DT_POINTER -0xfef /* some strange number to abuse... */ -#define NT_DT_CHAR_PTR -0xfee /* another... */ -#define NT_DT_EXT_PTR -0xfed /* and another... */ +#define NT_DT_STRING (-0xfff) /* some strange number to abuse... */ +#define NT_DT_POINTER (-0xfef) /* some strange number to abuse... */ +#define NT_DT_CHAR_PTR (-0xfee) /* another... */ +#define NT_DT_EXT_PTR (-0xfed) /* and another... */ typedef struct { int type; /* one of the DT_* types from nifti1.h */ @@ -142,7 +142,7 @@ typedef struct { dtype * pd = dptr; \ stype * ps = sptr; \ int64_t index; \ - for(index=0; index"nam" string, with field size = "sz" */ #define QSTR(nam,sz) if( strcmp(lhs,#nam) == 0 ) \ - strncpy(nim->nam,rhs,sz), nim->nam[sz-1]='\0' + strncpy(nim->nam,rhs,sz), nim->nam[(sz)-1]='\0' /*---------------------------------------------------------------------------*/ /*! Take an XML-ish ASCII string and create a NIFTI image header to match. diff --git a/niftilib/nifti1_tool.h b/niftilib/nifti1_tool.h index 14912ed9..b099924a 100644 --- a/niftilib/nifti1_tool.h +++ b/niftilib/nifti1_tool.h @@ -72,10 +72,10 @@ typedef struct{ #define NT_HDR_NUM_FIELDS 43 /* in the nifti_1_header struct */ #define NT_ANA_NUM_FIELDS 47 /* in the nifti_analyze75 struct */ #define NT_NIM_NUM_FIELDS 63 /* in the nifti_image struct */ -#define NT_DT_STRING -0xfff /* some strange number to abuse... */ -#define NT_DT_POINTER -0xfef /* some strange number to abuse... */ -#define NT_DT_CHAR_PTR -0xfee /* another... */ -#define NT_DT_EXT_PTR -0xfed /* and another... */ +#define NT_DT_STRING (-0xfff) /* some strange number to abuse... */ +#define NT_DT_POINTER (-0xfef) /* some strange number to abuse... */ +#define NT_DT_CHAR_PTR (-0xfee) /* another... */ +#define NT_DT_EXT_PTR (-0xfed) /* and another... */ typedef struct { int type; /* one of the DT_* types from nifti1.h */ diff --git a/niftilib/nifti_tester001.c b/niftilib/nifti_tester001.c index 3fbc0656..126e6bd6 100644 --- a/niftilib/nifti_tester001.c +++ b/niftilib/nifti_tester001.c @@ -492,7 +492,7 @@ int main (int argc, const char *argv[]) snprintf(buf,sizeof(buf),"nifti_datatype_string %d",constant); \ PrintTest( \ buf, \ - nifti_is_inttype(constant) != rval, \ + nifti_is_inttype(constant) != (rval), \ true, \ &Errors); \ } @@ -620,7 +620,7 @@ int main (int argc, const char *argv[]) nifti_datatype_sizes(constant,&nbyper,&swapsize); \ PrintTest( \ buf, \ - nbyper != Nbyper || swapsize != Swapsize, \ + nbyper != (Nbyper) || swapsize != (Swapsize), \ true, \ &Errors); \ } From 6d06faa496b1991c760994ea38a55458e3179cab Mon Sep 17 00:00:00 2001 From: Sean McBride Date: Sat, 18 Jan 2025 19:08:01 -0500 Subject: [PATCH 63/77] BUG: Widen some multiplications that are stored into size_t Fixes many bugprone-implicit-widening-of-multiplication-result warnings. Here the multiplications were happening in small types (int, usually 32 bit) then stored in large types (size_t, usually 64 bit). The multiplication could have overflowed. Now the multiplication is done with large types and thus less likely to overflow. --- fsliolib/fslio.c | 10 +++++----- nifti2/nifti2_io.c | 2 +- niftilib/nifti1_io.c | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/fsliolib/fslio.c b/fsliolib/fslio.c index 3e6754b0..c80e62a0 100644 --- a/fsliolib/fslio.c +++ b/fsliolib/fslio.c @@ -1019,7 +1019,7 @@ size_t FslReadSliceSeries(FSLIO *fslio, void *buffer, short slice, size_t nvols) if ((slice<0) || (slice>=z)) FSLIOERR("FslReadSliceSeries: slice outside valid range"); - slbytes = x * y * (FslGetDataType(fslio, &type) / 8); + slbytes = (size_t)x * (size_t)y * (FslGetDataType(fslio, &type) / 8); volbytes = slbytes * z; orig_offset = znztell(fslio->fileptr); @@ -1140,7 +1140,7 @@ size_t FslReadTimeSeries(FSLIO *fslio, void *buffer, short xVox, short yVox, sho if ((zVox<0) || (zVox >=zdim)) FSLIOERR("FslReadTimeSeries: voxel outside valid range"); wordsize = fslio->niftiptr->nbyper; - volbytes = xdim * ydim * zdim * wordsize; + volbytes = (size_t)xdim * (size_t)ydim * (size_t)zdim * wordsize; orig_offset = znztell(fslio->fileptr); offset = ((ydim * zVox + yVox) * xdim + xVox) * wordsize; @@ -2366,16 +2366,16 @@ double ***d3matrix(int zh, int yh, int xh) /** allocate pointers to slices */ - t=(double ***) malloc((size_t)((nslice)*sizeof(double**))); + t=(double ***) malloc(nslice*sizeof(double**)); if (!t) FSLIOERR("d3matrix: allocation failure"); /** allocate pointers for ydim */ - t[0]=(double **) malloc((size_t)((nslice*nrow)*sizeof(double*))); + t[0]=(double **) malloc(nslice*nrow*sizeof(double*)); if (!t[0]) FSLIOERR("d3matrix: allocation failure"); /** allocate the data blob */ - t[0][0]=(double *) malloc((size_t)((nslice*nrow*ncol)*sizeof(double))); + t[0][0]=(double *) malloc(nslice*nrow*ncol*sizeof(double)); if (!t[0][0]) FSLIOERR("d3matrix: allocation failure"); diff --git a/nifti2/nifti2_io.c b/nifti2/nifti2_io.c index cfe5ce90..1418703d 100644 --- a/nifti2/nifti2_io.c +++ b/nifti2/nifti2_io.c @@ -6501,7 +6501,7 @@ static int nifti_read_next_extension( nifti1_extension * nex, nifti_image *nim, if( count != 2 || code == -1 ){ if( g_opts.debug > 2 ) fprintf(stderr,"-d current extension read failed\n"); - znzseek(fp, -4*count, SEEK_CUR); /* back up past any read */ + znzseek(fp, -4L*count, SEEK_CUR); /* back up past any read */ return 0; /* no extension, no error condition */ } diff --git a/niftilib/nifti1_io.c b/niftilib/nifti1_io.c index be1fec9e..e73ad6cc 100644 --- a/niftilib/nifti1_io.c +++ b/niftilib/nifti1_io.c @@ -4714,7 +4714,7 @@ static int nifti_read_next_extension( nifti1_extension * nex, nifti_image *nim, if( count != 2 ){ if( g_opts.debug > 2 ) fprintf(stderr,"-d current extension read failed\n"); - znzseek(fp, -4*count, SEEK_CUR); /* back up past any read */ + znzseek(fp, -4L*count, SEEK_CUR); /* back up past any read */ return 0; /* no extension, no error condition */ } From d93153c54033e85a0b7b6d584dbbcbe1c2b566a8 Mon Sep 17 00:00:00 2001 From: "Gabriel A. Devenyi" Date: Sat, 15 Aug 2026 01:18:16 -0400 Subject: [PATCH 64/77] BUG: Fix cifti_tool's CIFTI extension search, which never advanced disp_cifti_extension() searched for the CIFTI extension with ext = nim->ext_list; for( ind = 0; ind < nim->num_ext; ind++ ) if( ext->ecode == NIFTI_ECODE_CIFTI ) break; ext is never advanced, so this tests the first extension num_ext times. cifti_tool could only ever find a CIFTI extension that happened to be first in the list; with any other extension ahead of it the tool reported 'no CIFTI extension' for a file that has one. It now indexes ext_list[ind] and leaves ext NULL when there is no match, which also avoids the read past the end of the list that a bare ext++ would have introduced. The same function opened its output stream before the 'no CIFTI extension' check and returned without closing it; the early return now closes the stream like the normal path does. --- cifti/CMakeLists.txt | 9 +++++++++ cifti/cifti_tool.c | 15 +++++++++------ 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/cifti/CMakeLists.txt b/cifti/CMakeLists.txt index e13e7bdf..88dcc657 100644 --- a/cifti/CMakeLists.txt +++ b/cifti/CMakeLists.txt @@ -42,6 +42,15 @@ if(NIFTI_BUILD_TESTING AND NIFTI_BUILD_APPLICATIONS) set_tests_properties( ${TEST_PREFIX}_mim_summary_known_child PROPERTIES PASS_REGULAR_EXPRESSION "BrainModel" ) + # The CIFTI extension is found even when another extension precedes it. + add_test( NAME ${TEST_PREFIX}_tool_cext_not_first + COMMAND $ + -input ${CMAKE_CURRENT_LIST_DIR}/testdata/cext_second_extension.nii + -disp_cext ) + set_tests_properties( ${TEST_PREFIX}_tool_cext_not_first + PROPERTIES PASS_REGULAR_EXPRESSION "demo" + FAIL_REGULAR_EXPRESSION "no CIFTI extension" ) + # An extension payload that fills esize-8 with no NUL must not be read # past its end; the regex pins that the payload is still parsed. add_test( NAME ${TEST_PREFIX}_tool_unterminated_cext diff --git a/cifti/cifti_tool.c b/cifti/cifti_tool.c index b895c09f..18f023b9 100644 --- a/cifti/cifti_tool.c +++ b/cifti/cifti_tool.c @@ -210,19 +210,22 @@ int disp_cifti_extension(nifti_image * nim, opts_t * opts) opts->fout ? opts->fout : "DEFAULT" ); if( !nim ) return 1; - ext = nim->ext_list; + /* find the CIFTI extension, if any; it need not be first in the list */ + ext = NULL; for( ind = 0; ind < nim->num_ext; ind++ ) - if( ext->ecode == NIFTI_ECODE_CIFTI ) break; + if( nim->ext_list[ind].ecode == NIFTI_ECODE_CIFTI ) { + ext = nim->ext_list + ind; + break; + } fp = open_write_stream(opts->fout); - if( ext && ext->ecode != NIFTI_ECODE_CIFTI ) { + if( !ext ) { fprintf(fp, "** no CIFTI extension in %s\n",nim->fname?nim->fname:"NULL"); + close_stream(fp); return 1; } - if(ext) { - fprintf(fp, "%.*s\n", ext->esize-8, ext->edata); - } + fprintf(fp, "%.*s\n", ext->esize-8, ext->edata); /* possibly close file */ close_stream(fp); From 2405d0925cb153540836467a5c6546392b7ecc8b Mon Sep 17 00:00:00 2001 From: "Hans J. Johnson" Date: Tue, 22 Sep 2026 08:45:47 -0500 Subject: [PATCH 65/77] ENH: Cover a CIFTI extension that is not first in the list cext_second_extension.nii carries a comment extension ahead of the CIFTI one. The test requires the extension's payload in the output and forbids the 'no CIFTI extension' message, and the existing unterminated-cext test still pins the first-position case, so neither a search that always matches nor one that never does would pass. --- cifti/testdata/cext_second_extension.nii | Bin 0 -> 752 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 cifti/testdata/cext_second_extension.nii diff --git a/cifti/testdata/cext_second_extension.nii b/cifti/testdata/cext_second_extension.nii new file mode 100644 index 0000000000000000000000000000000000000000..f3eca967ff8bbdc709e43643e83d04eadc9fd8be GIT binary patch literal 752 zcmdr|J!``-5LMdJAya;W(Ap0toifNaCQiW6gpjDEg@O^*Q3SChVo{u}KfRKLFbdYF?m z8Pkr6UwhmWN$8r}qip-TP(oJFthvFNF$W0^(*co7H%n;mnh!Xpb8b018Hh>OzAQHR z@~vd=+hw_5WY5`d?_@T-2I1OzA>)%oK&@%iXEYiRuIOhPG!l;sV@91uGRFU6VXMRW E1N7=}=>Px# literal 0 HcmV?d00001 From 5c63a0128e16092cbb534597f369ab05ee0614d9 Mon Sep 17 00:00:00 2001 From: "Gabriel A. Devenyi" Date: Fri, 14 Aug 2026 23:05:46 -0400 Subject: [PATCH 66/77] BUG: Fix cdfbin's argument range check, which could never fire cdfbin() validates its `which` selector like this: if(!(*which < 1 && *which > 4)) goto S30; A value cannot be both less than 1 and greater than 4, so the condition is always false, the negation is always true, and the jump to S30 is always taken -- skipping the entire range check. gcc reports it as -Wlogical-op, "logical 'and' of mutually exclusive tests is always false". The ten sibling functions in this file all spell the same guard with `||`: cdfbet 1527: if(!(*which < 1 || *which > 4)) goto S30; cdfchi 2255: if(!(*which < 1 || *which > 3)) goto S30; cdfchn 2553: if(!(*which < 1 || *which > 4)) goto S30; ... 7 more so this is a single-character typo rather than an intentional deviation. The effect is visible from the public API. cdfbin is declared in the installed nifticdf.h, and its contract is that an out-of-range input sets *status to -1 and *bound to the limit that was violated. Calling it with which = 9: before: status=999 bound=-999 (both left as the caller set them) after: status=-1 bound=4 (the documented error return) Before the fix the caller has no indication anything was wrong and the function proceeds to compute with an unhandled selector. --- nifticdf/nifticdf.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nifticdf/nifticdf.c b/nifticdf/nifticdf.c index bf00c52a..a2909d90 100644 --- a/nifticdf/nifticdf.c +++ b/nifticdf/nifticdf.c @@ -1892,7 +1892,7 @@ static double T5,T6,T7,T8,T9,T10,T12,T13; /* Check arguments */ - if(!(*which < 1 && *which > 4)) goto S30; + if(!(*which < 1 || *which > 4)) goto S30; if(!(*which < 1)) goto S10; *bound = 1.0e0; goto S20; From 493a3172787d5818d79ec4176ee1c07f7130e644 Mon Sep 17 00:00:00 2001 From: "Hans J. Johnson" Date: Tue, 22 Sep 2026 08:35:38 -0500 Subject: [PATCH 67/77] ENH: Cover cdfbin's out-of-range selector return The range check is reachable from the installed nifticdf.h, so assert the documented contract directly: which=9 and which=0 must set *status to -1 and *bound to the limit that was violated. A which=1 call is asserted to still return status 0, so a guard that rejected everything would not pass. --- nifticdf/CMakeLists.txt | 5 ++++ nifticdf/nifticdf_range_test.c | 50 ++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 nifticdf/nifticdf_range_test.c diff --git a/nifticdf/CMakeLists.txt b/nifticdf/CMakeLists.txt index b7d48480..8af8e395 100644 --- a/nifticdf/CMakeLists.txt +++ b/nifticdf/CMakeLists.txt @@ -33,6 +33,11 @@ endif() if(NIFTI_BUILD_TESTING AND NIFTI_BUILD_APPLICATIONS) + add_executable(${NIFTI_PACKAGE_PREFIX}nifticdf_range_test nifticdf_range_test.c) + target_link_libraries(${NIFTI_PACKAGE_PREFIX}nifticdf_range_test PRIVATE ${NIFTI_CDFLIB_NAME}) + add_test( NAME ${NIFTI_PACKAGE_PREFIX}nifticdf_range_test + COMMAND $ ) + foreach(DISTRIBUTION CORREL TTEST FTEST ZSCORE CHISQ BETA BINOM GAMMA POISSON NORMAL FTEST_NONC CHISQ_NONC LOGISTIC LAPLACE UNIFORM TTEST_NONC WEIBULL CHI INVGAUSS EXTVAL PVAL LOGPVAL LOG10PVAL ) add_test( NAME ${NIFTI_PACKAGE_PREFIX}nifti_stats_${DISTRIBUTION}_test COMMAND $ 0:4:1 ${DISTRIBUTION}) add_test( NAME q${NIFTI_PACKAGE_PREFIX}nifti_stats_${DISTRIBUTION}_test COMMAND $ -q 0:4:1 ${DISTRIBUTION}) diff --git a/nifticdf/nifticdf_range_test.c b/nifticdf/nifticdf_range_test.c new file mode 100644 index 00000000..63e607d6 --- /dev/null +++ b/nifticdf/nifticdf_range_test.c @@ -0,0 +1,50 @@ +/* cdfbin must reject an out-of-range "which" selector the way its ten + siblings in nifticdf.c do: *status = -1 and *bound = the limit. */ + +#include +#include "nifticdf.h" + +static int check(int which, double expected_bound) +{ + int status = 999; + double p = 0.5, q = 0.5, s = 2.0, xn = 5.0, pr = 0.5, ompr = 0.5; + double bound = -999.0; + + cdfbin(&which, &p, &q, &s, &xn, &pr, &ompr, &status, &bound); + + if( status != -1 || bound != expected_bound ) { + fprintf(stderr, "** cdfbin(which=%d): status=%d bound=%g," + " expected status=-1 bound=%g\n", + which, status, bound, expected_bound); + return 1; + } + printf("cdfbin(which=%d): status=%d bound=%g\n", which, status, bound); + return 0; +} + +int main(void) +{ + int errs = 0; + + errs += check(9, 4.0); /* above the legal range */ + errs += check(0, 1.0); /* below the legal range */ + + /* a legal selector must still compute, not report a range error */ + { + int which = 1, status = 999; + double p = 0.0, q = 0.0, s = 2.0, xn = 5.0, pr = 0.5, ompr = 0.5; + double bound = -999.0; + + cdfbin(&which, &p, &q, &s, &xn, &pr, &ompr, &status, &bound); + if( status != 0 ) { + fprintf(stderr, "** cdfbin(which=1): status=%d, expected 0\n", status); + errs++; + } else { + printf("cdfbin(which=1): status=0 p=%g\n", p); + } + } + + if( errs ) { fprintf(stderr, "** %d failure(s)\n", errs); return 1; } + printf("nifticdf range test passed\n"); + return 0; +} From 70fa3a60741115c4af7253cc6e389efe93370c9d Mon Sep 17 00:00:00 2001 From: "Gabriel A. Devenyi" Date: Sat, 15 Aug 2026 17:03:34 -0400 Subject: [PATCH 68/77] BUG: Do not use sscanf's output when sscanf matched nothing nifti_image_from_ascii() scans its input three times with ii = sscanf( str+spos , "%1023s%n" , lhs , &nn ) ; spos += nn ; if( ii == 0 || strcmp(lhs,"") == 0 ) break ; /* end of input? */ + ii = sscanf( str+spos , "%1023s%n" , lhs , &nn ) ; + if( ii != 1 ) break ; /* nothing scanned: lhs and nn are unset */ + spos += nn ; + if( strcmp(lhs,"/>") == 0 ) break ; /* end of input? */ /* skip whitespace and the '=' marker */ @@ -8853,8 +8857,9 @@ nifti_image *nifti_image_from_ascii( const char *str, int * bytes_read ) memcpy(rhs,str+spos+1,nn) ; rhs[nn] = '\0' ; spos = (str[ii] == '\'') ? ii+1 : ii ; } else { - ii = sscanf( str+spos , "%1023s%n" , rhs , &nn ) ; spos += nn ; - if( ii == 0 ) break ; /* nothing found? */ + ii = sscanf( str+spos , "%1023s%n" , rhs , &nn ) ; + if( ii != 1 ) break ; /* nothing found: rhs and nn are unset */ + spos += nn ; } unescape_string(rhs) ; /* remove any XML escape sequences */ diff --git a/niftilib/nifti1_io.c b/niftilib/nifti1_io.c index e73ad6cc..bc5cbc1b 100644 --- a/niftilib/nifti1_io.c +++ b/niftilib/nifti1_io.c @@ -6663,8 +6663,10 @@ nifti_image *nifti_image_from_ascii( const char *str, int * bytes_read ) /* scan for opening string */ spos = 0 ; - ii = sscanf( str+spos , "%1023s%n" , lhs , &nn ) ; spos += nn ; - if( ii == 0 || strcmp(lhs,"") == 0 ) break ; /* end of input? */ + ii = sscanf( str+spos , "%1023s%n" , lhs , &nn ) ; + if( ii != 1 ) break ; /* nothing scanned: lhs and nn are unset */ + spos += nn ; + if( strcmp(lhs,"/>") == 0 ) break ; /* end of input? */ /* skip whitespace and the '=' marker */ @@ -6711,8 +6715,9 @@ nifti_image *nifti_image_from_ascii( const char *str, int * bytes_read ) memcpy(rhs,str+spos+1,nn) ; rhs[nn] = '\0' ; spos = (str[ii] == '\'') ? ii+1 : ii ; } else { - ii = sscanf( str+spos , "%1023s%n" , rhs , &nn ) ; spos += nn ; - if( ii == 0 ) break ; /* nothing found? */ + ii = sscanf( str+spos , "%1023s%n" , rhs , &nn ) ; + if( ii != 1 ) break ; /* nothing found: rhs and nn are unset */ + spos += nn ; } unescape_string(rhs) ; /* remove any XML escape sequences */ From eb4f831e8e99d46ee8c3c5c92330f96430ba56b9 Mon Sep 17 00:00:00 2001 From: "Gabriel A. Devenyi" Date: Fri, 14 Aug 2026 23:14:27 -0400 Subject: [PATCH 69/77] BUG: Use memcpy instead of casting to over-aligned pointer types 19 -Wcast-align warnings, and behind them undefined behavior on any target that cares about alignment. modify_field() writes a value into a header field at a byte offset parsed from a field table: ((short *)((char *)basep + field->offset))[fc] = (short)val; field->offset is a byte offset into a packed on-disk header, so that cast produces an address only correctly aligned by coincidence. The same pattern appears for int, int64_t, float and double, in both tool files. Each becomes a memcpy of the right width at the right byte offset. The second group reads a pointer back out of a structure through a byte offset -- `sp = *(char **)((char *)str + fp->offset)` and the nifti1_extension equivalents -- and becomes a memcpy into an aligned local. The third is nifti_header_version(), which cast its `const char * buf` argument, a buffer straight off a file read with no alignment guarantee, to both nifti_1_header * and nifti_2_header * and read fields through them. It now copies into aligned locals first, exactly the sizeof(nifti_1_header) bytes the function already checks are present. Verified by round-tripping int16, int32, int64, float32, float64 and string fields through nifti_tool -mod_hdr2. --- nifti2/nifti2_io.c | 29 +++++++++++++++++++---------- nifti2/nifti_tool.c | 24 ++++++++++++++---------- niftilib/nifti1_tool.c | 20 ++++++++++++-------- 3 files changed, 45 insertions(+), 28 deletions(-) diff --git a/nifti2/nifti2_io.c b/nifti2/nifti2_io.c index 6238382e..235dc763 100644 --- a/nifti2/nifti2_io.c +++ b/nifti2/nifti2_io.c @@ -6614,8 +6614,8 @@ int valid_nifti_extensions(const nifti_image * nim) \return -1 on error, else NIFTI version *//*--------------------------------------------------------------------*/ int nifti_header_version(const char * buf, size_t nbytes){ - const nifti_1_header *n1p = (const nifti_1_header *)buf; - const nifti_2_header *n2p = (const nifti_2_header *)buf; + nifti_1_header n1hdr; + nifti_2_header n2hdr; char fname[] = { "nifti_header_version" }; int sizeof_hdr, sver, nver; @@ -6631,9 +6631,18 @@ int nifti_header_version(const char * buf, size_t nbytes){ return -1; } + /* buf comes straight from a file read and need not satisfy the alignment + either header struct requires, so work from aligned copies rather than + casting it. Only sizeof(nifti_1_header) bytes are guaranteed present, + and both sizeof_hdr and magic fall inside that range for either + version, so copy exactly that much into each. */ + memcpy(&n1hdr, buf, sizeof(n1hdr)); + memset(&n2hdr, 0, sizeof(n2hdr)); + memcpy(&n2hdr, buf, sizeof(n1hdr)); + /* try to determine the version based on sizeof_hdr */ sver = -1; - sizeof_hdr = n1p->sizeof_hdr; + sizeof_hdr = n1hdr.sizeof_hdr; if ( sizeof_hdr == (int)sizeof(nifti_1_header) ) sver = 1; else if( sizeof_hdr == (int)sizeof(nifti_2_header) ) sver = 2; else { /* try swapping */ @@ -6643,8 +6652,8 @@ int nifti_header_version(const char * buf, size_t nbytes){ } /* and check magic field */ - if ( sver == 1 ) nver = NIFTI_VERSION(*n1p); - else if ( sver == 2 ) nver = NIFTI_VERSION(*n2p); + if ( sver == 1 ) nver = NIFTI_VERSION(n1hdr); + else if ( sver == 2 ) nver = NIFTI_VERSION(n2hdr); else nver = -1; /* now compare and return */ @@ -6653,24 +6662,24 @@ int nifti_header_version(const char * buf, size_t nbytes){ fprintf(stderr,"-- %s: size ver = %d, ni ver = %d\n", fname, sver, nver); if( sver == 1 ) { - nver = NIFTI_VERSION(*n1p); + nver = NIFTI_VERSION(n1hdr); if( nver == 0 ) return 0; /* ANALYZE */ if( nver == 1 ) return 1; /* NIFTI-1 */ if( g_opts.debug > 1 ) - fprintf(stderr,"** %s: bad NIFTI-1 magic= %.4s", fname, n1p->magic); + fprintf(stderr,"** %s: bad NIFTI-1 magic= %.4s", fname, n1hdr.magic); return -1; } else if ( sver == 2 ) { - nver = NIFTI_VERSION(*n2p); + nver = NIFTI_VERSION(n2hdr); if( nver == 2 ) return 2; /* NIFTI-2 */ if( g_opts.debug > 1 ) - fprintf(stderr,"** %s: bad NIFTI-2 magic4= %.4s", fname, n2p->magic); + fprintf(stderr,"** %s: bad NIFTI-2 magic4= %.4s", fname, n2hdr.magic); return -1; } /* failure */ if( g_opts.debug > 0 ) - fprintf(stderr,"** %s: bad sizeof_hdr = %d\n", fname, n1p->sizeof_hdr); + fprintf(stderr,"** %s: bad sizeof_hdr = %d\n", fname, n1hdr.sizeof_hdr); return -1; } diff --git a/nifti2/nifti_tool.c b/nifti2/nifti_tool.c index 85bc3f54..d2d59a9e 100644 --- a/nifti2/nifti_tool.c +++ b/nifti2/nifti_tool.c @@ -3928,7 +3928,8 @@ int modify_field(void * basep, field_s * field, const char * data) return 1; } /* otherwise, we're good */ - ((short *)((char *)basep + field->offset))[fc] = (short)val; + { const int16_t sval = (int16_t)val; + memcpy((char *)basep + field->offset + (size_t)fc * sizeof(sval), &sval,(size_t)sizeof(sval)); } if( g_debug > 1 ) fprintf(stderr,"+d setting posn %d of '%s' to %d\n", fc, field->name, val); @@ -3947,7 +3948,8 @@ int modify_field(void * basep, field_s * field, const char * data) fc,field->len); return 1; } - ((int *)((char *)basep + field->offset))[fc] = val; + { const int32_t ival = (int32_t)val; + memcpy((char *)basep + field->offset + (size_t)fc * sizeof(ival), &ival,(size_t)sizeof(ival)); } if( g_debug > 1 ) fprintf(stderr,"+d setting posn %d of '%s' to %d\n", fc, field->name, val); @@ -3967,7 +3969,7 @@ int modify_field(void * basep, field_s * field, const char * data) fc,field->len); return 1; } - ((int64_t *)((char *)basep + field->offset))[fc] = v64; + memcpy((char *)basep + field->offset + (size_t)fc * sizeof(v64), &v64,(size_t)sizeof(v64)); if( g_debug > 1 ) fprintf(stderr,"+d setting posn %d of '%s' to %" PRId64 "\n", fc, field->name, v64); @@ -3987,7 +3989,7 @@ int modify_field(void * basep, field_s * field, const char * data) return 1; } /* otherwise, we're good */ - ((float *)((char *)basep + field->offset))[fc] = fval; + memcpy((char *)basep + field->offset + (size_t)fc * sizeof(fval), &fval,(size_t)sizeof(fval)); if( g_debug > 1 ) fprintf(stderr,"+d setting posn %d of '%s' to %f\n", fc, field->name, fval); @@ -4008,7 +4010,7 @@ int modify_field(void * basep, field_s * field, const char * data) return 1; } /* otherwise, we're good */ - ((double *)((char *)basep + field->offset))[fc] = f64; + memcpy((char *)basep + field->offset + (size_t)fc * sizeof(f64), &f64,(size_t)sizeof(f64)); if( g_debug > 1 ) fprintf(stderr,"+d setting posn %d of '%s' to %f\n", fc, field->name, f64); @@ -6271,7 +6273,7 @@ int disp_field(const char *mesg, field_s *fieldp, void * str, int nfields, int h int len; /* start by sucking the pointer stored here */ - sp = *(char **)((char *)str + fp->offset); + memcpy(&sp, (const char *)str + fp->offset, sizeof(sp)); if( ! sp ){ fprintf(stdout,"(NULL)\n"); break; } /* anything? */ @@ -6285,7 +6287,9 @@ int disp_field(const char *mesg, field_s *fieldp, void * str, int nfields, int h else if( *sp && !isprint(*sp) ) /* if no termination, it's bad */ fprintf(stdout,"(non-printable string)\n"); else /* woohoo! a good string */ - fprintf(stdout,"'%.40s'\n",*(char **)((char *)str + fp->offset)); + { char * cp; + memcpy(&cp, (const char *)str + fp->offset, sizeof(cp)); + fprintf(stdout,"'%.40s'\n", cp); } break; } @@ -6294,7 +6298,7 @@ int disp_field(const char *mesg, field_s *fieldp, void * str, int nfields, int h nifti1_extension * extp; /* yank the address sitting there into extp */ - extp = *(nifti1_extension **)((char *)str + fp->offset); + memcpy(&extp, (const char *)str + fp->offset, sizeof(extp)); /* the user may use -disp_exts to display all of them */ if( extp ) disp_nifti1_extension(NULL, extp, 6); @@ -6355,8 +6359,8 @@ int diff_field(field_s *fieldp, void * str0, void * str1, int nfields) { nifti1_extension * ext0, * ext1; - ext0 = *(nifti1_extension **)((char *)str0 + fp->offset); - ext1 = *(nifti1_extension **)((char *)str1 + fp->offset); + memcpy(&ext0, (const char *)str0 + fp->offset, sizeof(ext0)); + memcpy(&ext1, (const char *)str1 + fp->offset, sizeof(ext1)); if( ! ext0 && ! ext1 ) break; /* continue on */ diff --git a/niftilib/nifti1_tool.c b/niftilib/nifti1_tool.c index 7bd422b1..d4982643 100644 --- a/niftilib/nifti1_tool.c +++ b/niftilib/nifti1_tool.c @@ -2980,7 +2980,8 @@ int modify_field(void * basep, field_s * field, const char * data) return 1; } /* otherwise, we're good */ - ((short *)((char *)basep + field->offset))[fc] = (short)val; + { const int16_t sval = (int16_t)val; + memcpy((char *)basep + field->offset + (size_t)fc * sizeof(sval), &sval,(size_t)sizeof(sval)); } if( g_debug > 1 ) fprintf(stderr,"+d setting posn %d of '%s' to %d\n", fc, field->name, val); @@ -2999,7 +3000,8 @@ int modify_field(void * basep, field_s * field, const char * data) fc,field->len); return 1; } - ((int *)((char *)basep + field->offset))[fc] = val; + { const int32_t ival = (int32_t)val; + memcpy((char *)basep + field->offset + (size_t)fc * sizeof(ival), &ival,(size_t)sizeof(ival)); } if( g_debug > 1 ) fprintf(stderr,"+d setting posn %d of '%s' to %d\n", fc, field->name, val); @@ -3019,7 +3021,7 @@ int modify_field(void * basep, field_s * field, const char * data) return 1; } /* otherwise, we're good */ - ((float *)((char *)basep + field->offset))[fc] = fval; + memcpy((char *)basep + field->offset + (size_t)fc * sizeof(fval), &fval,(size_t)sizeof(fval)); if( g_debug > 1 ) fprintf(stderr,"+d setting posn %d of '%s' to %f\n", fc, field->name, fval); @@ -3478,7 +3480,7 @@ int disp_field( const char *mesg, field_s *fieldp, void * str, int nfields, int int len; /* start by sucking the pointer stored here */ - sp = *(char **)((char *)str + fp->offset); + memcpy(&sp, (const char *)str + fp->offset, sizeof(sp)); if( ! sp ){ fprintf(stdout,"(NULL)\n"); break; } /* anything? */ @@ -3492,7 +3494,9 @@ int disp_field( const char *mesg, field_s *fieldp, void * str, int nfields, int else if( *sp && !isprint(*sp) ) /* if no termination, it's bad */ fprintf(stdout,"(non-printable string)\n"); else /* woohoo! a good string */ - fprintf(stdout,"'%.40s'\n",*(char **)((char *)str + fp->offset)); + { char * cp; + memcpy(&cp, (const char *)str + fp->offset, sizeof(cp)); + fprintf(stdout,"'%.40s'\n", cp); } break; } @@ -3501,7 +3505,7 @@ int disp_field( const char *mesg, field_s *fieldp, void * str, int nfields, int nifti1_extension * extp; /* yank the address sitting there into extp */ - extp = *(nifti1_extension **)((char *)str + fp->offset); + memcpy(&extp, (const char *)str + fp->offset, sizeof(extp)); /* the user may use -disp_exts to display all of them */ if( extp ) disp_nifti1_extension(NULL, extp, 6); @@ -3560,8 +3564,8 @@ int diff_field(field_s *fieldp, void * str0, void * str1, int nfields) { nifti1_extension * ext0, * ext1; - ext0 = *(nifti1_extension **)((char *)str0 + fp->offset); - ext1 = *(nifti1_extension **)((char *)str1 + fp->offset); + memcpy(&ext0, (const char *)str0 + fp->offset, sizeof(ext0)); + memcpy(&ext1, (const char *)str1 + fp->offset, sizeof(ext1)); if( ! ext0 && ! ext1 ) break; /* continue on */ From 0a9aa9c41b80655f638f0548d27980533afcb1ee Mon Sep 17 00:00:00 2001 From: "Gabriel A. Devenyi" Date: Fri, 14 Aug 2026 23:21:04 -0400 Subject: [PATCH 70/77] BUG: Stop casting away const in fslio and cifti Nine -Wcast-qual warnings. Casting away const is how a read-only contract turns into a write to memory the caller thought was safe, so each was looked at rather than silenced. fslio.c, FslGetFileType2() mutablefslio = (FSLIO *)fslio; /* dodgy and will generate warnings */ mutablefslio->niftiptr->nifti_type = ...; The comment is right that it looks dodgy, but the cast was never needed. `const FSLIO * fslio` makes the *member* niftiptr const -- its type is `nifti_image * const` -- while what it points at stays fully mutable. The assignment is legal as written, so the cast and the local both go. fslio.c FslWriteVolumes(), cifti axio_num_tokens(), text_to_i64(), text_to_f64() Pointers cast to char * and then only read: walked, indexed, or handed to strtoll()/strtod(), which take const char *. Declared const, casts removed. cifti strip_whitespace() Returned `(char *)str` on its early-exit paths, handing the caller a writable pointer to the const string it passed in. The function is static, so its return type is nobody else's business; it now returns const char *. --- cifti/afni_xml.c | 10 +++++----- cifti/afni_xml_io.c | 14 ++++++++------ fsliolib/fslio.c | 9 ++++----- 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/cifti/afni_xml.c b/cifti/afni_xml.c index 1c9a5956..bfe691ca 100644 --- a/cifti/afni_xml.c +++ b/cifti/afni_xml.c @@ -141,7 +141,7 @@ static int show_attrs (afni_xml_control *, const char **, int); static int64_t loc_strnlen (const char * str, int64_t maxlen); static afni_xml_t * make_afni_xml (const char * ename, const char ** attr); -static char * strip_whitespace(const char * str, int slen); +static const char * strip_whitespace(const char * str, int slen); /*----------------------- main I/O functions ---------------------------*/ @@ -888,7 +888,7 @@ static int show_attrs(afni_xml_control * xd, const char ** attr, int showd) static void free_whitespace(void) { strip_whitespace(NULL,-2); } /* if slen == 0, use entire length */ -static char * strip_whitespace(const char * str, int slen) +static const char * strip_whitespace(const char * str, int slen) { static char * buf = NULL; static int blen = 0; @@ -898,18 +898,18 @@ static char * strip_whitespace(const char * str, int slen) if(!str && slen == -2){ free(buf); buf=NULL; blen=0; return 0; } /* if string is long, forget it */ - if( !str || slen > 1024 ) return (char *)str; + if( !str || slen > 1024 ) return str; len = strlen(str); if( slen > 0 && slen < len ) len = slen; - if( len <= 0 ) return (char *)str; + if( len <= 0 ) return str; /* make sure we have local space */ if( len > blen ) { /* allocate a bigger buffer */ buf = (char *)safe_realloc(buf, (len+1) * sizeof(char)); if( !buf ) { fprintf(stderr,"** failed to alloc wspace buf of len %d\n", len+1); - return (char *)str; + return str; } blen = len; } diff --git a/cifti/afni_xml_io.c b/cifti/afni_xml_io.c index a06229e4..6a1430e3 100644 --- a/cifti/afni_xml_io.c +++ b/cifti/afni_xml_io.c @@ -111,7 +111,7 @@ int axio_text_to_binary(afni_xml_t * ax) int axio_num_tokens(const char * str, int64_t maxlen) { - char * sp = (char *)str; + const char * sp = str; int64_t ind, len, ntok; int intok; /* flag: are we inside a token? */ @@ -123,7 +123,7 @@ int axio_num_tokens(const char * str, int64_t maxlen) ntok = 0; intok = 0; - for( ind = 0, sp = (char *)str; ind < len; ind++, sp++ ) { + for( ind = 0, sp = str; ind < len; ind++, sp++ ) { /* just look for state switches */ if( intok ) { if( isspace(*sp) || (*sp == ',') ) @@ -519,7 +519,8 @@ static int can_process_dtype(int dtype) * varies, unfortunately */ static int64_t text_to_i64(int64_t * result, const char * text, int64_t nvals) { - char * eptr, * sptr; + char * eptr; + const char * sptr; int64_t * rptr, val; int64_t nread; @@ -527,7 +528,7 @@ static int64_t text_to_i64(int64_t * result, const char * text, int64_t nvals) *result = 0; /* Initialize to zero in case of failure */ if( nvals <= 0 ) return 0; - sptr = (char *)text; + sptr = text; nread = 0; rptr = result; @@ -548,14 +549,15 @@ static int64_t text_to_i64(int64_t * result, const char * text, int64_t nvals) static int64_t text_to_f64(double * result, const char * text, int64_t nvals) { - char * eptr, * sptr; + char * eptr; + const char * sptr; double * rptr, val; int64_t nread; if( ! text || ! result) return 1; if( nvals <= 0 ) return 0; - sptr = (char *)text; + sptr = text; nread = 0; rptr = result; diff --git a/fsliolib/fslio.c b/fsliolib/fslio.c index c80e62a0..a8ef9450 100644 --- a/fsliolib/fslio.c +++ b/fsliolib/fslio.c @@ -99,7 +99,6 @@ int FslBaseFileType(int filetype) static int FslGetFileType2(const FSLIO *fslio, int quiet) { - FSLIO *mutablefslio; if (fslio==NULL) FSLIOERR("FslGetFileType: Null pointer passed for FSLIO"); if ( (fslio->file_mode==FSL_TYPE_MINC) || (fslio->file_mode==FSL_TYPE_MINC_GZ) ) { return fslio->file_mode; @@ -112,8 +111,7 @@ static int FslGetFileType2(const FSLIO *fslio, int quiet) fprintf(stderr,"Warning: nifti structure and fsl structure disagree on file type\n"); fprintf(stderr,"nifti = %d and fslio = %d\n",fslio->niftiptr->nifti_type,fslio->file_mode); } - mutablefslio = (FSLIO *) fslio; /* dodgy and will generate warnings */ - mutablefslio->niftiptr->nifti_type = FslBaseFileType(fslio->file_mode); + fslio->niftiptr->nifti_type = FslBaseFileType(fslio->file_mode); return fslio->file_mode; } } @@ -926,10 +924,11 @@ size_t FslWriteVolumes(FSLIO *fslio, const void *buffer, size_t nvols) && (FslGetLeftRightOrder(fslio)==FSL_NEUROLOGICAL) ) { /* If it is Analyze and Neurological order then SWAP DATA into Radiological order */ /* This is nasty - but what else can be done?!? */ - char *tmpbuf, *inbuf; + char *tmpbuf; + const char *inbuf; long int x, b, n, nrows; short nx, ny, nz, nv; - inbuf = (char *) buffer; + inbuf = buffer; tmpbuf = (char *)calloc(nbytes,1); FslGetDim(fslio,&nx,&ny,&nz,&nv); nrows = nbytes / (nx * bpv); From a2d3b5c25c53730cc0dc1c4cb13812ffe2ee8436 Mon Sep 17 00:00:00 2001 From: "Gabriel A. Devenyi" Date: Fri, 14 Aug 2026 23:04:50 -0400 Subject: [PATCH 71/77] ENH: Give the cifti tools' gopt internal linkage afni_xml_tool.c and cifti_tool.c each define a file-scope `opts_t gopt` with external linkage, so the two share a symbol name and neither declares it in a header. Each is used only inside its own translation unit, always by address. Both files build executables rather than libraries, so `static` removes nothing from any shared library's exported symbols. Clears both -Wmissing-variable-declarations warnings. --- cifti/afni_xml_tool.c | 2 +- cifti/cifti_tool.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cifti/afni_xml_tool.c b/cifti/afni_xml_tool.c index a1471ebf..0ed277bc 100644 --- a/cifti/afni_xml_tool.c +++ b/cifti/afni_xml_tool.c @@ -19,7 +19,7 @@ typedef struct { int xverb; } opts_t; -opts_t gopt; +static opts_t gopt; /* ----------------------------------------------------------------- */ diff --git a/cifti/cifti_tool.c b/cifti/cifti_tool.c index 18f023b9..718b610f 100644 --- a/cifti/cifti_tool.c +++ b/cifti/cifti_tool.c @@ -53,7 +53,7 @@ typedef struct { } opts_t; -opts_t gopt; +static opts_t gopt; /* ----------------------------------------------------------------- */ From 36976a239cbe71fe943eccaf46a70ec1637295e3 Mon Sep 17 00:00:00 2001 From: "Gabriel A. Devenyi" Date: Sat, 15 Aug 2026 00:14:06 -0400 Subject: [PATCH 72/77] DOC: Describe the CMake build and how to run the memory checks The build instructions in README.md were two lines about 'make all', which builds a subset of the tree with a Makefile nothing else in the project uses. They now describe the CMake build the CI and the install rules actually use, and note that the Makefile is unmaintained and does not cover nifti2 or cifti. A new section records how to run the sanitizers and valgrind, including three things that each cost an afternoon to work out: * valgrind's memcheck needs --trace-children=yes here, because most of the tests are shell scripts that exec the tools. Without it valgrind inspects the shell, sees nothing and reports a clean run; * ctest -T memcheck exits 0 even when valgrind reports defects, so the logs have to be read; * valgrind refuses to start without the C library's debug symbols, and on distributions that ship a stripped ld.so with no debuginfo package -- Arch and its derivatives -- it cannot be run at all, DEBUGINFOD_URLS included, because those builds are not on any debuginfod server. A container recipe is given. Every command in the new sections was run against this tree. --- README.md | 63 +++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 59 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 4dc5e21b..11dfb30e 100644 --- a/README.md +++ b/README.md @@ -85,10 +85,65 @@ packaging | spec file for building RPMs, and template package description for ## Instructions to build -command | description -------------|------------- -"make all" | results will be left in the directories: bin/ include/ lib/ -"make help" | will show more build options +The project builds with CMake: + +```sh +cmake -S . -B build -DCMAKE_BUILD_TYPE=Release +cmake --build build -j +ctest --test-dir build --output-on-failure +``` + +Useful options: `-DBUILD_SHARED_LIBS=ON`, `-DUSE_CIFTI_CODE=ON` (needs +expat), `-DUSE_FSL_CODE=ON`, and `-DDOWNLOAD_TEST_DATA=OFF` together with +`ctest -LE NEEDS_DATA` for a build with no network access. + +The top-level GNU `Makefile` still exists and still mostly works, but it +is not maintained and does not build the nifti2 or cifti libraries. + +## Checking the code + +The sanitizers are the quickest memory check: + +```sh +cmake -S . -B build-asan -DCMAKE_BUILD_TYPE=Debug \ + -DCMAKE_C_FLAGS="-fsanitize=address,undefined -fno-sanitize-recover=all" \ + -DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address,undefined" +cmake --build build-asan -j && ctest --test-dir build-asan --output-on-failure +``` + +For valgrind: + +```sh +ctest --test-dir build -T memcheck -E install_linking \ + --overwrite MemoryCheckCommandOptions="--trace-children=yes --leak-check=full" +``` + +`--trace-children=yes` matters: many of the tests are shell scripts that +exec the tools, so without it valgrind only ever inspects the shell and +reports a clean run. `install_linking` is excluded because it configures +and builds a whole CMake project, and tracing cmake and the compiler +through valgrind takes far longer than it is worth. + +`ctest -T memcheck` exits 0 even when valgrind reports defects, so read +`build/Testing/Temporary/MemoryChecker.*.log` rather than trusting the +exit status. + +Note that valgrind needs the C library's debug symbols and refuses to +start without them. Distributions that ship a stripped `ld.so` and no +debuginfo package for it -- Arch and its derivatives among them -- cannot +run it at all, and `DEBUGINFOD_URLS` does not help, because those builds +are not published to any debuginfod server. Run it in a container +instead: + +```sh +docker run --rm -v "$PWD":/src:ro -w /work ubuntu:24.04 bash -c ' + apt-get update && apt-get install -y build-essential cmake ninja-build \ + valgrind libc6-dbg zlib1g-dev libexpat1-dev + cmake -S /src -B /work/build -G Ninja -DCMAKE_BUILD_TYPE=Debug + cmake --build /work/build -j + ctest --test-dir /work/build -T memcheck -E install_linking \ + --overwrite MemoryCheckCommandOptions="--trace-children=yes --leak-check=full"' +``` ![NIFTI ICON](https://avatars0.githubusercontent.com/u/45666806?s=200&v=4) From 1371c711ebb9a0a9f33ba271b5f319521f5ed645 Mon Sep 17 00:00:00 2001 From: "Gabriel A. Devenyi" Date: Fri, 14 Aug 2026 23:33:01 -0400 Subject: [PATCH 73/77] ENH: Make the integer conversions in allocations, copies and reads explicit 152 of the -Wsign-conversion findings are an int or int64_t count reaching a size_t parameter of malloc(), calloc(), realloc(), memcpy(), memset(), strncpy(), znzread(), znzwrite() or fread(). Where a count is multiplied by a sizeof, the cast goes on the count rather than around the product, so the multiply happens at 64-bit width: (int)count * sizeof(T) can overflow before it is widened. The counts cannot be negative. nifti_update_dims_from_array() clamps every dim[i] to at least 1, nifti_datatype_sizes() leaves nbyper at 0 for an unknown datatype, and nifti_image_load() requires nbyper > 0 and nvox > 0. --- cifti/afni_xml.c | 20 +++++----- cifti/afni_xml_io.c | 2 +- nifti2/nifti2_io.c | 90 +++++++++++++++++++++--------------------- nifti2/nifti_tool.c | 34 ++++++++-------- niftilib/nifti1_io.c | 46 ++++++++++----------- niftilib/nifti1_tool.c | 24 +++++------ 6 files changed, 108 insertions(+), 108 deletions(-) diff --git a/cifti/afni_xml.c b/cifti/afni_xml.c index bfe691ca..388f99ae 100644 --- a/cifti/afni_xml.c +++ b/cifti/afni_xml.c @@ -196,7 +196,7 @@ afni_xml_list axml_read_file(const char * fname, int read_data) { if( reset_xml_buf(xd, &buf, &bsize) ) break; - blen = fread(buf, 1, bsize, fp); + blen = fread(buf, 1, (size_t)bsize, fp); /* check for early termination */ bshort = loc_strnlen(buf, blen); @@ -447,8 +447,8 @@ int axml_add_attrs(afni_xml_t * ax, const char ** attr) return 0; } - ax->attrs.name = (char **)malloc(natr*sizeof(char *)); - ax->attrs.value = (char **)malloc(natr*sizeof(char *)); + ax->attrs.name = (char **)malloc((size_t)natr * sizeof(char *)); + ax->attrs.value = (char **)malloc((size_t)natr * sizeof(char *)); /* failure? */ if( ! ax->attrs.name || ! ax->attrs.value ) { @@ -666,7 +666,7 @@ static int reset_xml_buf(afni_xml_control * xd, char ** buf, int * bsize) fprintf(stderr,"++ update buf, %d to %d bytes\n",*bsize,xd->buf_size); *bsize = xd->buf_size; - *buf = (char *)safe_realloc(*buf, (*bsize+1) * sizeof(char)); + *buf = (char *)safe_realloc(*buf, (size_t)((*bsize+1) * sizeof(char))); if( ! *buf ) { fprintf(stderr,"** failed to alloc %d bytes of xml buf!\n", *bsize); *bsize = 0; @@ -826,7 +826,7 @@ static int add_to_xroot_list(afni_xml_control * xd, afni_xml_t * newp) xd->xroot->len++; xd->xroot->xlist = (afni_xml_t **)safe_realloc(xd->xroot->xlist, - xd->xroot->len * sizeof(afni_xml_t *)); + (size_t)xd->xroot->len * sizeof(afni_xml_t *)); if( ! xd->xroot->xlist ) { fprintf(stderr,"** failed to alloc %d AXMLT pointers\n", xd->xroot->len); return 1; @@ -844,7 +844,7 @@ static int add_to_xchild_list(afni_xml_t * parent, afni_xml_t * child) parent->nchild++; parent->xchild = (afni_xml_t **)safe_realloc(parent->xchild, - parent->nchild * sizeof(afni_xml_t *)); + (size_t)parent->nchild * sizeof(afni_xml_t *)); if( ! parent->xchild ) { fprintf(stderr,"** failed to alloc %d AXML pointers\n", parent->nchild); return 1; @@ -906,7 +906,7 @@ static const char * strip_whitespace(const char * str, int slen) /* make sure we have local space */ if( len > blen ) { /* allocate a bigger buffer */ - buf = (char *)safe_realloc(buf, (len+1) * sizeof(char)); + buf = (char *)safe_realloc(buf, (size_t)((len+1) * sizeof(char))); if( !buf ) { fprintf(stderr,"** failed to alloc wspace buf of len %d\n", len+1); return str; @@ -919,7 +919,7 @@ static const char * strip_whitespace(const char * str, int slen) if( ifirst == len ) *buf = '\0'; else { - strncpy(buf, str+ifirst, len-ifirst-ilast); + strncpy(buf, str+ifirst, (size_t)(len-ifirst-ilast)); buf[len-ifirst-ilast] = '\0'; } @@ -971,14 +971,14 @@ static int append_to_string(char ** ostr, int * olen, newlen = *olen + ilen; - *ostr = (char *)safe_realloc(*ostr, newlen * sizeof(char)); + *ostr = (char *)safe_realloc(*ostr, (size_t)newlen * sizeof(char)); if( !*ostr ) { fprintf(stderr,"** AX.A2S: failed to alloc %d chars\n", newlen); return 1; } /* copy, starting at old nul char (if any), and terminate */ - strncpy((*ostr)+*olen-1, istr, ilen); + strncpy((*ostr)+*olen-1, istr, (size_t)ilen); (*ostr)[newlen-1] = '\0'; *olen = newlen; diff --git a/cifti/afni_xml_io.c b/cifti/afni_xml_io.c index 6a1430e3..97ebddab 100644 --- a/cifti/afni_xml_io.c +++ b/cifti/afni_xml_io.c @@ -473,7 +473,7 @@ static int dalloc_as_nifti_type(FILE * fp, afni_xml_t * ax, int64_t nvals, /* note number of bytes per value and number of values to allocate */ nifti_datatype_sizes(ax->btype, &nbyper, NULL); - ax->bdata = malloc(nbyper * ntok); + ax->bdata = malloc((size_t)(nbyper * ntok)); if( ! ax->bdata ) { fprintf(fp, "** axio_alloc: failed to allocate %" PRId64 " vals of size %d\n", ntok, nbyper); diff --git a/nifti2/nifti2_io.c b/nifti2/nifti2_io.c index 235dc763..14783eb1 100644 --- a/nifti2/nifti2_io.c +++ b/nifti2/nifti2_io.c @@ -990,7 +990,7 @@ static int nifti_load_NBL_bricks( nifti_image * nim , const int64_t * slist, } else { /* we have already read this sub-brick, just copy the previous one */ /* note that this works because they are sorted */ - memcpy(NBL->bricks[idest], NBL->bricks[sindex[c-1]], NBL->bsize); + memcpy(NBL->bricks[idest], NBL->bricks[sindex[c-1]], (size_t)(NBL->bsize)); } prev = isrc; /* in any case, note the now previous sub-brick */ @@ -1019,7 +1019,7 @@ static int nifti_alloc_NBL_mem(const nifti_image * nim, int64_t nbricks, } nbl->bsize = nim->nx * nim->ny * nim->nz * nim->nbyper; /* bytes */ - nbl->bricks = (void **)malloc(nbl->nbricks * sizeof(void *)); + nbl->bricks = (void **)malloc((size_t)nbl->nbricks * sizeof(void *)); if( ! nbl->bricks ){ fprintf(stderr,"** NIFTI NANM: failed to alloc %" PRId64 @@ -1028,7 +1028,7 @@ static int nifti_alloc_NBL_mem(const nifti_image * nim, int64_t nbricks, } for( c = 0; c < nbl->nbricks; c++ ){ - nbl->bricks[c] = malloc(nbl->bsize); + nbl->bricks[c] = malloc((size_t)(nbl->bsize)); if( ! nbl->bricks[c] ){ fprintf(stderr,"** NIFTI NANM: failed to alloc %" PRId64 " bytes for brick %" PRId64 "\n", nbl->bsize, c); @@ -1070,8 +1070,8 @@ static int nifti_copynsort(int64_t nbricks, const int64_t *blist, int64_t * stmp, * itmp; /* for ease of typing/reading */ int64_t c1, c2, spos, tmp; - *slist = (int64_t *)malloc(nbricks * sizeof(int64_t)); - *sindex = (int64_t *)malloc(nbricks * sizeof(int64_t)); + *slist = (int64_t *)malloc((size_t)nbricks * sizeof(int64_t)); + *sindex = (int64_t *)malloc((size_t)nbricks * sizeof(int64_t)); if( !*slist || !*sindex ){ fprintf(stderr,"** NIFTI NCS: failed to alloc %" PRId64 @@ -5840,7 +5840,7 @@ void * nifti_read_header( const char *hname, int *nver, int check ) } /**- next read into nifti_1_header and determine nifti type */ - ii = (int)znzread(&n1hdr, 1, h1size, fp); + ii = (int)znzread(&n1hdr, 1, (size_t)h1size, fp); if( ii < (int)h1size ){ /* failure? */ if( g_opts.debug > 0 ){ @@ -5864,10 +5864,10 @@ void * nifti_read_header( const char *hname, int *nver, int check ) if ( ni_ver == 2 ) { if( g_opts.debug > 2 ) fprintf(stderr,"-- %s: copying and filling NIFTI-2 header...\n",fname); - memcpy(&n2hdr, &n1hdr, h1size); /* copy first part */ + memcpy(&n2hdr, &n1hdr, (size_t)h1size); /* copy first part */ remain = h2size - h1size; posn = (char *)&n2hdr + h1size; - ii = (int)znzread(posn, 1, remain, fp); /* read remaining part */ + ii = (int)znzread(posn, 1, (size_t)remain, fp); /* read remaining part */ if( ii < (int)remain) { LNI_FERR(fname,"short NIFTI-2 header read for file", hfile); znzclose(fp); free(hfile); return NULL; @@ -5880,24 +5880,24 @@ void * nifti_read_header( const char *hname, int *nver, int check ) /* allocate header space and return */ if( ni_ver == 0 || ni_ver == 1 ) { - hresult = malloc(h1size); + hresult = malloc((size_t)h1size); if( ! hresult ) { LNI_FERR(fname,"failed to alloc NIFTI-1 header for file", hname); return NULL; } - memcpy(hresult, (void *)&n1hdr, h1size); + memcpy(hresult, (void *)&n1hdr, (size_t)h1size); if ( check && ! nifti_hdr1_looks_good(hresult) ){ LNI_FERR(fname,"nifti_1_header looks bad for file", hname); return hresult; } } else if ( ni_ver == 2 ) { - hresult = malloc(h2size); + hresult = malloc((size_t)h2size); if( ! hresult ) { LNI_FERR(fname,"failed to alloc NIFTI-2 header for file", hname); return NULL; } - memcpy(hresult, &n2hdr, h2size); + memcpy(hresult, &n2hdr, (size_t)h2size); if ( check && ! nifti_hdr2_looks_good(hresult) ){ LNI_FERR(fname,"nifti_2_header looks bad for file", hname); @@ -5908,12 +5908,12 @@ void * nifti_read_header( const char *hname, int *nver, int check ) fprintf(stderr, "** %s: bad nifti header version %d\n", hname, ni_ver); /* return a nifti-1 header anyway */ - hresult = malloc(h1size); + hresult = malloc((size_t)h1size); if( ! hresult ) { LNI_FERR(fname,"failed to alloc NIFTI-?? header for file", hname); return NULL; } - memcpy(hresult, (void *)&n1hdr, h1size); + memcpy(hresult, (void *)&n1hdr, (size_t)h1size); } if( g_opts.debug > 1 ) @@ -5993,7 +5993,7 @@ nifti_image *nifti_image_read( const char *hname , int read_data ) h2size = sizeof(nifti_2_header); /**- next read into nifti_1_header and determine nifti type */ - ii = (int)znzread(&n1hdr, 1, h1size, fp); + ii = (int)znzread(&n1hdr, 1, (size_t)h1size, fp); if( ii < (int)h1size ){ /* failure? */ if( g_opts.debug > 0 ){ @@ -6017,10 +6017,10 @@ nifti_image *nifti_image_read( const char *hname , int read_data ) /* fill nifti-2 header and convert */ if( g_opts.debug > 2 ) fprintf(stderr,"-- %s: copying and filling NIFTI-2 header...\n",fname); - memcpy(&n2hdr, &n1hdr, h1size); /* copy first part */ + memcpy(&n2hdr, &n1hdr, (size_t)h1size); /* copy first part */ remain = h2size - h1size; posn = (char *)&n2hdr + h1size; - ii = (int)znzread(posn, 1, remain, fp); /* read remaining part */ + ii = (int)znzread(posn, 1, (size_t)remain, fp); /* read remaining part */ if( ii < (int)remain) { LNI_FERR(fname,"short NIFTI-2 header read for file", hfile); znzclose(fp); free(hfile); return NULL; @@ -6203,12 +6203,12 @@ nifti_image * nifti_read_ascii_image(znzFile fp, const char *fname, int flen, fname, slen); if( slen > 65530 ) slen = 65530 ; - sbuf = (char *)calloc(sizeof(char),slen+1) ; + sbuf = (char *)calloc(sizeof(char), (size_t)(slen+1)) ; if( !sbuf ){ fprintf(stderr,"** %s: failed to alloc %d bytes for sbuf",lfunc,65530); return NULL; } - znzread( sbuf , 1 , slen , fp ) ; + znzread( sbuf , 1 , (size_t)slen, fp ) ; nim = nifti_image_from_ascii( sbuf, &txt_size ) ; free( sbuf ) ; if( nim == NULL ){ LNI_FERR(lfunc,"failed nifti_image_from_ascii()",fname); @@ -6286,7 +6286,7 @@ static int nifti_read_extensions( nifti_image *nim, znzFile fp, int64_t remain ) return 0; } - count = znzread( extdr.extension, 1, 4, fp ); /* get extender */ + count = znzread( extdr.extension, 1, (size_t)4, fp ); /* get extender */ if( count < 4 ){ if( g_opts.debug > 1 ) @@ -6393,12 +6393,12 @@ static int nifti_add_exten_to_list( nifti1_extension * new_ext, nifti1_extension * tmplist; tmplist = *list; - *list = (nifti1_extension *)malloc(new_length * sizeof(nifti1_extension)); + *list = (nifti1_extension *)malloc((size_t)new_length * sizeof(nifti1_extension)); /* check for failure first */ if( ! *list ){ fprintf(stderr,"** NIFTI: failed to alloc %d ext structs (%zu bytes)\n", - new_length, new_length*sizeof(nifti1_extension)); + new_length, (size_t)new_length * sizeof(nifti1_extension)); if( !tmplist ) return -1; /* no old list to lose */ *list = tmplist; /* reset list to old one */ @@ -6407,7 +6407,7 @@ static int nifti_add_exten_to_list( nifti1_extension * new_ext, /* if an old list exists, copy the pointers and free the list */ if( tmplist ){ - memcpy(*list, tmplist, (new_length-1)*sizeof(nifti1_extension)); + memcpy(*list, tmplist, (size_t)((new_length-1)*sizeof(nifti1_extension))); free(tmplist); } @@ -6451,14 +6451,14 @@ static int nifti_fill_extension( nifti1_extension *ext, const char * data, ext->esize = esize; /* allocate esize-8 (maybe more than len), using calloc for fill */ - ext->edata = (char *)calloc(esize-8, sizeof(char)); + ext->edata = (char *)calloc((size_t)(esize-8),sizeof(char)); if( !ext->edata ){ fprintf(stderr,"** NIFTI NFE: failed to alloc %d bytes for extension\n", len); return -1; } - memcpy(ext->edata, data, len); /* copy the data, using len */ + memcpy(ext->edata, data, (size_t)len); /* copy the data, using len */ ext->ecode = ecode; /* set the ecode */ if( g_opts.debug > 2 ) @@ -6529,14 +6529,14 @@ static int nifti_read_next_extension( nifti1_extension * nex, nifti_image *nim, nex->ecode = code; size -= 8; /* subtract space for size and code in extension */ - nex->edata = (char *)malloc(size * sizeof(char)); + nex->edata = (char *)malloc((size_t)size * sizeof(char)); if( !nex->edata ){ fprintf(stderr,"** NIFTI: failed to allocate %d bytes for extension\n", size); return -1; } - count = (int)znzread(nex->edata, 1, size, fp); + count = (int)znzread(nex->edata, 1, (size_t)size, fp); if( count < size ){ if( g_opts.debug > 0 ) fprintf(stderr,"-d read only %d (of %d) bytes for extension\n", @@ -6862,7 +6862,7 @@ int nifti_image_load( nifti_image *nim ) if( nim->data == NULL ) { - nim->data = calloc(1,ntot) ; /* create image memory */ + nim->data = calloc(1, (size_t)ntot) ; /* create image memory */ if( nim->data == NULL ){ if( g_opts.debug > 0 ) fprintf(stderr,"** NIFTI: failed to alloc %d bytes for image data\n", @@ -6914,7 +6914,7 @@ int64_t nifti_read_buffer(znzFile fp, void* dataptr, int64_t ntot, return -1; } - ii = znzread( dataptr , 1 , ntot , fp ) ; /* data input */ + ii = znzread( dataptr , 1 , (size_t)ntot, fp ) ; /* data input */ /* if read was short, fail */ if( ii < ntot ){ @@ -7078,7 +7078,7 @@ int64_t nifti_write_buffer(znzFile fp, const void *buffer, int64_t numbytes) fprintf(stderr,"** ERROR: nifti_write_buffer: null file pointer\n"); return 0; } - ss = znzwrite( buffer , 1 , numbytes , fp ) ; + ss = znzwrite( buffer , 1 , (size_t)numbytes, fp ) ; return ss; } @@ -7442,7 +7442,7 @@ nifti_image * nifti_make_new_nim(const int64_t dims[8], int datatype, fprintf(stderr,"+d nifti_make_new_nim, data_fill = %d\n",data_fill); if( data_fill ) { - nim->data = calloc(nim->nvox, nim->nbyper); + nim->data = calloc((size_t)(nim->nvox), (size_t)(nim->nbyper)); /* if we cannot allocate data, take ball and go home */ if( !nim->data ) { @@ -7755,8 +7755,8 @@ int nifti_copy_extensions(nifti_image * nim_dest, const nifti_image * nim_src) if( nim_src->num_ext <= 0 ) return 0; - bytes = nim_src->num_ext * sizeof(nifti1_extension); /* I'm lazy */ - nim_dest->ext_list = (nifti1_extension *)malloc(bytes); + bytes = (size_t)nim_src->num_ext * sizeof(nifti1_extension); /* I'm lazy */ + nim_dest->ext_list = (nifti1_extension *)malloc((size_t)bytes); if( !nim_dest->ext_list ){ fprintf(stderr,"** failed to allocate %d nifti1_extension structs\n", nim_src->num_ext); @@ -7772,7 +7772,7 @@ int nifti_copy_extensions(nifti_image * nim_dest, const nifti_image * nim_src) fprintf(stderr,"+d dup'ing ext #%d of size %d (from size %d)\n", c, size, old_size); /* data length is size-8, as esize includes space for esize and ecode */ - data = (char *)calloc(size-8,sizeof(char)); /* maybe size > old */ + data = (char *)calloc((size_t)(size-8),sizeof(char)); /* maybe size > old */ if( !data ){ fprintf(stderr,"** NIFTI: failed to alloc %d bytes for extension\n", size); @@ -7784,7 +7784,7 @@ int nifti_copy_extensions(nifti_image * nim_dest, const nifti_image * nim_src) nim_dest->ext_list[c].esize = size; nim_dest->ext_list[c].ecode = nim_src->ext_list[c].ecode; nim_dest->ext_list[c].edata = data; - memcpy(data, nim_src->ext_list[c].edata, old_size-8); + memcpy(data, nim_src->ext_list[c].edata, (size_t)(old_size-8)); nim_dest->num_ext++; } @@ -8123,8 +8123,8 @@ static int nifti_image_write_engine(nifti_image *nim, int write_opts, /* write the header and extensions */ - if( nver == 2 ) ss = znzwrite(&n2hdr , 1 , hsize , fp); /* write header */ - else ss = znzwrite(&n1hdr , 1 , hsize , fp); /* write header */ + if( nver == 2 ) ss = znzwrite(&n2hdr , 1 , (size_t)hsize, fp); /* write header */ + else ss = znzwrite(&n1hdr , 1 , (size_t)hsize, fp); /* write header */ if( ss < hsize ){ LNI_FERR(func,"bad header write to output file",nim->fname); @@ -8470,7 +8470,7 @@ static char *escapize_string( const char * str ) default: lout++ ; break ; /* copy all other chars */ } } - out = (char *)calloc(1,lout) ; /* allocate output string */ + out = (char *)calloc(1, (size_t)lout) ; /* allocate output string */ if( !out ){ fprintf(stderr,"** NIFTI escapize_string: failed to alloc %d bytes\n", lout); @@ -8747,7 +8747,7 @@ char *nifti_image_to_ascii( const nifti_image *nim ) snprintf( buf+strlen(buf) , bufLen-strlen(buf) , "/>\n" ) ; /* XML-ish closer */ nbuf = (int)strlen(buf) ; - newbuf = (char *)realloc((void *)buf, nbuf+1); /* cut back to proper length */ + newbuf = (char *)realloc((void *)buf, (size_t)(nbuf+1)); /* cut back to proper length */ if( !newbuf ){ free(buf); fprintf(stderr,"** NIFTI NITA: failed to realloc %d bytes\n",nbuf+1); @@ -8863,7 +8863,7 @@ nifti_image *nifti_image_from_ascii( const char *str, int * bytes_read ) ii = spos+1 ; while( str[ii] != '\0' && str[ii] != '\'' ) ii++ ; nn = ii-spos-1 ; if( nn > 1023 ) nn = 1023 ; - memcpy(rhs,str+spos+1,nn) ; rhs[nn] = '\0' ; + memcpy(rhs,str+spos+1, (size_t)nn) ; rhs[nn] = '\0' ; spos = (str[ii] == '\'') ? ii+1 : ii ; } else { ii = sscanf( str+spos , "%1023s%n" , rhs , &nn ) ; @@ -9385,7 +9385,7 @@ int64_t nifti_read_subregion_image( nifti_image * nim, for(i = 0; i < nim->ndim; i++) total_alloc_size *= region_size[i]; /* allocate buffer, if necessary */ - if(! *data) *data = malloc(total_alloc_size); + if(! *data) *data = malloc((size_t)total_alloc_size); if(! *data) { if(g_opts.debug > 1) @@ -9560,7 +9560,7 @@ static int rci_alloc_mem(void **data, const int64_t prods[8], int nprods, int nb " (%" PRId64 " x %d) bytes for collapsed image\n", size, size/nbyper, nbyper); - *data = malloc(size); /* actually allocate the memory */ + *data = malloc((size_t)size); /* actually allocate the memory */ if( ! *data ){ fprintf(stderr,"** NIFTI rci_am: failed to alloc %" PRId64 " bytes for data\n", size); @@ -9718,7 +9718,7 @@ int64_t * nifti_get_int64list( int64_t nvals , const char * str ) if( str[ipos] == ',' || ISEND(str[ipos]) ){ nout++ ; - subv_realloc = (int64_t *)realloc( (char *)subv , sizeof(int64_t)*(nout+1) ) ; + subv_realloc = (int64_t *)realloc( (char *)subv , (size_t)(sizeof(int64_t)*(nout+1))) ; if( !subv_realloc ) { free(subv); fprintf(stderr,"** nifti_get_intlist: failed realloc of %" PRId64 @@ -9800,7 +9800,7 @@ int64_t * nifti_get_int64list( int64_t nvals , const char * str ) for( ii=ibot ; (ii-itop)*istep <= 0 ; ii += istep ){ nout++ ; - subv_realloc = (int64_t *)realloc( (char *)subv , sizeof(int64_t)*(nout+1) ) ; + subv_realloc = (int64_t *)realloc( (char *)subv , (size_t)(sizeof(int64_t)*(nout+1))) ; if( !subv_realloc ) { free(subv); fprintf(stderr,"** nifti_get_intlist: failed realloc of %" PRId64 @@ -9851,7 +9851,7 @@ int * nifti_get_intlist( int nvals , const char * str ) } /* have a valid result, copy as ints */ - ilist = (int *)malloc((nints+1) * sizeof(int)); + ilist = (int *)malloc((size_t)((nints+1) * sizeof(int))); if( !ilist ) { fprintf(stderr,"** nifti_get_intlist: failed to alloc %" PRId64 " ints\n", nints); diff --git a/nifti2/nifti_tool.c b/nifti2/nifti_tool.c index d2d59a9e..449d89b1 100644 --- a/nifti2/nifti_tool.c +++ b/nifti2/nifti_tool.c @@ -922,7 +922,7 @@ int add_int(int_list * ilist, int val) { if( ilist->len == 0 ) ilist->list = NULL; /* just to be safe */ ilist->len++; - ilist->list = (int *)realloc(ilist->list,ilist->len*sizeof(int)); + ilist->list = (int *)realloc(ilist->list, (size_t)ilist->len * sizeof(int)); if( ! ilist->list ){ fprintf(stderr,"** failed to alloc %d (int *) elements\n",ilist->len); return -1; @@ -943,7 +943,7 @@ int add_string(str_list * slist, const char * str) { if( slist->len == 0 ) slist->list = NULL; /* just to be safe */ slist->len++; - slist->list = (const char **)realloc(slist->list,slist->len*sizeof(char *)); + slist->list = (const char **)realloc(slist->list, (size_t)slist->len * sizeof(char *)); if( ! slist->list ){ fprintf(stderr,"** failed to alloc %d (char *) elements\n",slist->len); return -1; @@ -2358,14 +2358,14 @@ static char * read_file_text(const char * filename, int * length) /* allocate the bytes, and fill them with the file contents */ - text = (char *)malloc(len64 * sizeof(char)); + text = (char *)malloc((size_t)len64 * sizeof(char)); if( !text ) { fprintf(stderr,"** RFT: failed to allocate %" PRId64 " bytes\n", len64); fclose(fp); return NULL; } - bytes = fread(text, sizeof(char), len64, fp); + bytes = fread(text, sizeof(char), (size_t)len64, fp); fclose(fp); /* in any case */ if( bytes != (size_t)len64 ) { @@ -2562,7 +2562,7 @@ int remove_ext_list( nifti_image * nim, const char ** elist, int len ) if( g_debug > 2 ) fprintf(stderr,"+d removing %d exts from '%s'\n", len, nim->fname ); - if( ! (marks = (int *)calloc(nim->num_ext, sizeof(int))) ) { + if( ! (marks = (int *)calloc((size_t)(nim->num_ext),sizeof(int))) ) { fprintf(stderr,"** failed to alloc %d marks\n",nim->num_ext); return -1; } @@ -3929,7 +3929,7 @@ int modify_field(void * basep, field_s * field, const char * data) } /* otherwise, we're good */ { const int16_t sval = (int16_t)val; - memcpy((char *)basep + field->offset + (size_t)fc * sizeof(sval), &sval,(size_t)sizeof(sval)); } + memcpy((char *)basep + field->offset + (size_t)fc * sizeof(sval), &sval,sizeof(sval)); } if( g_debug > 1 ) fprintf(stderr,"+d setting posn %d of '%s' to %d\n", fc, field->name, val); @@ -3949,7 +3949,7 @@ int modify_field(void * basep, field_s * field, const char * data) return 1; } { const int32_t ival = (int32_t)val; - memcpy((char *)basep + field->offset + (size_t)fc * sizeof(ival), &ival,(size_t)sizeof(ival)); } + memcpy((char *)basep + field->offset + (size_t)fc * sizeof(ival), &ival,sizeof(ival)); } if( g_debug > 1 ) fprintf(stderr,"+d setting posn %d of '%s' to %d\n", fc, field->name, val); @@ -3969,7 +3969,7 @@ int modify_field(void * basep, field_s * field, const char * data) fc,field->len); return 1; } - memcpy((char *)basep + field->offset + (size_t)fc * sizeof(v64), &v64,(size_t)sizeof(v64)); + memcpy((char *)basep + field->offset + (size_t)fc * sizeof(v64), &v64,sizeof(v64)); if( g_debug > 1 ) fprintf(stderr,"+d setting posn %d of '%s' to %" PRId64 "\n", fc, field->name, v64); @@ -3989,7 +3989,7 @@ int modify_field(void * basep, field_s * field, const char * data) return 1; } /* otherwise, we're good */ - memcpy((char *)basep + field->offset + (size_t)fc * sizeof(fval), &fval,(size_t)sizeof(fval)); + memcpy((char *)basep + field->offset + (size_t)fc * sizeof(fval), &fval,sizeof(fval)); if( g_debug > 1 ) fprintf(stderr,"+d setting posn %d of '%s' to %f\n", fc, field->name, fval); @@ -4010,7 +4010,7 @@ int modify_field(void * basep, field_s * field, const char * data) return 1; } /* otherwise, we're good */ - memcpy((char *)basep + field->offset + (size_t)fc * sizeof(f64), &f64,(size_t)sizeof(f64)); + memcpy((char *)basep + field->offset + (size_t)fc * sizeof(f64), &f64,sizeof(f64)); if( g_debug > 1 ) fprintf(stderr,"+d setting posn %d of '%s' to %f\n", fc, field->name, f64); @@ -4022,10 +4022,10 @@ int modify_field(void * basep, field_s * field, const char * data) case NT_DT_STRING: { char * dest = (char *)basep + field->offset; - nchars = dataLength; - strncpy(dest, data, field->len); + nchars = (int)dataLength; + strncpy(dest, data, (size_t)(field->len)); if( nchars < field->len ) /* clear the rest */ - memset(dest+nchars, '\0', field->len-nchars); + memset(dest+nchars, '\0', (size_t)(field->len-nchars)); } break; } @@ -4188,7 +4188,7 @@ static int convert_NBL_data(nifti_brick_list * NBL, int old_type, int new_type, nifti_datatype_sizes(new_type, &nbyper, NULL); NBLnew.bsize = nbvals * nbyper; NBLnew.nbricks = NBL->nbricks; - NBLnew.bricks = (void **)calloc(NBLnew.nbricks, sizeof(void *)); + NBLnew.bricks = (void **)calloc((size_t)NBLnew.nbricks, (size_t)(sizeof(void *))); if( ! NBLnew.bricks ) { fprintf(stderr,"** cNBLd: failed to allocate %" PRId64 " void pointers\n", NBLnew.nbricks); @@ -4280,7 +4280,7 @@ static int convert_raw_data(void ** retdata, void * olddata, int old_type, /* allocate new memory (calloc, in case of partial filling) */ nifti_datatype_sizes(new_type, &nbyper, NULL); /* get nbyper */ - newdata = calloc(nvox, nbyper); + newdata = calloc((size_t)nvox, (size_t)nbyper); if( !newdata ) { fprintf(stderr,"** failed to alloc for %" PRId64 " %s elements\n", nvox, typestr); @@ -7638,7 +7638,7 @@ nifti_image * nt_read_bricks(nt_opts * opts, char * fname, int len, /* now populate NBL (can be based only on len and nim) */ NBL->nbricks = len; NBL->bsize = nim->nbyper * nim->nx * nim->ny * nim->nz; - NBL->bricks = (void **)calloc(NBL->nbricks, sizeof(void *)); + NBL->bricks = (void **)calloc((size_t)(NBL->nbricks), (size_t)(sizeof(void *))); if( !NBL->bricks ){ fprintf(stderr,"** NRB: failed to alloc %" PRId64 " pointers\n", NBL->nbricks); @@ -7653,7 +7653,7 @@ nifti_image * nt_read_bricks(nt_opts * opts, char * fname, int len, /* now allocate the data pointers */ for( c = 0; c < len; c++ ) { - NBL->bricks[c] = calloc(1, NBL->bsize); + NBL->bricks[c] = calloc(1, (size_t)(NBL->bsize)); if( !NBL->bricks[c] ){ fprintf(stderr, "** NRB: failed to alloc brick %d of %" PRId64 " bytes\n", diff --git a/niftilib/nifti1_io.c b/niftilib/nifti1_io.c index bc5cbc1b..f4757dce 100644 --- a/niftilib/nifti1_io.c +++ b/niftilib/nifti1_io.c @@ -919,7 +919,7 @@ static int nifti_alloc_NBL_mem(const nifti_image * nim, int nbricks, } nbl->bsize = (size_t)nim->nx * nim->ny * nim->nz * nim->nbyper;/* bytes */ - nbl->bricks = (void **)malloc(nbl->nbricks * sizeof(void *)); + nbl->bricks = (void **)malloc((size_t)nbl->nbricks * sizeof(void *)); if( ! nbl->bricks ){ fprintf(stderr,"** NANM: failed to alloc %d void ptrs\n",nbricks); @@ -969,8 +969,8 @@ static int nifti_copynsort(int nbricks, const int * blist, int ** slist, int * stmp, * itmp; /* for ease of typing/reading */ int c1, c2, spos, tmp; - *slist = (int *)malloc(nbricks * sizeof(int)); - *sindex = (int *)malloc(nbricks * sizeof(int)); + *slist = (int *)malloc((size_t)nbricks * sizeof(int)); + *sindex = (int *)malloc((size_t)nbricks * sizeof(int)); if( !*slist || !*sindex ){ fprintf(stderr,"** NCS: failed to alloc %d ints for sorting\n",nbricks); @@ -980,7 +980,7 @@ static int nifti_copynsort(int nbricks, const int * blist, int ** slist, } /* init the lists */ - memcpy(*slist, blist, nbricks*sizeof(int)); + memcpy(*slist, blist, (size_t)nbricks * sizeof(int)); for( c1 = 0; c1 < nbricks; c1++ ) (*sindex)[c1] = c1; /* now actually sort slist */ @@ -4418,12 +4418,12 @@ nifti_image * nifti_read_ascii_image(znzFile fp, char *fname, int flen, fprintf(stderr,"-d %s: have ASCII NIFTI file of size %d\n",fname,slen); if( slen > 65530 ) slen = 65530 ; - sbuf = (char *)calloc(sizeof(char),slen+1) ; + sbuf = (char *)calloc(sizeof(char), (size_t)(slen+1)) ; if( !sbuf ){ fprintf(stderr,"** %s: failed to alloc %d bytes for sbuf",lfunc,65530); return NULL; } - znzread( sbuf , 1 , slen , fp ) ; + znzread( sbuf , 1 , (size_t)slen, fp ) ; nim = nifti_image_from_ascii( sbuf, &txt_size ) ; free( sbuf ) ; if( nim == NULL ){ LNI_FERR(lfunc,"failed nifti_image_from_ascii()",fname); @@ -4607,12 +4607,12 @@ static int nifti_add_exten_to_list( nifti1_extension * new_ext, nifti1_extension * tmplist; tmplist = *list; - *list = (nifti1_extension *)malloc(new_length * sizeof(nifti1_extension)); + *list = (nifti1_extension *)malloc((size_t)new_length * sizeof(nifti1_extension)); /* check for failure first */ if( ! *list ){ fprintf(stderr,"** failed to alloc %d extension structs (%zu bytes)\n", - new_length, new_length*sizeof(nifti1_extension)); + new_length, (size_t)new_length * sizeof(nifti1_extension)); if( !tmplist ) return -1; /* no old list to lose */ *list = tmplist; /* reset list to old one */ @@ -4621,7 +4621,7 @@ static int nifti_add_exten_to_list( nifti1_extension * new_ext, /* if an old list exists, copy the pointers and free the list */ if( tmplist ){ - memcpy(*list, tmplist, (new_length-1)*sizeof(nifti1_extension)); + memcpy(*list, tmplist, (size_t)((new_length-1)*sizeof(nifti1_extension))); free(tmplist); } @@ -4665,13 +4665,13 @@ static int nifti_fill_extension( nifti1_extension *ext, const char * data, ext->esize = esize; /* allocate esize-8 (maybe more than len), using calloc for fill */ - ext->edata = (char *)calloc(esize-8, sizeof(char)); + ext->edata = (char *)calloc((size_t)(esize-8),sizeof(char)); if( !ext->edata ){ fprintf(stderr,"** NFE: failed to alloc %d bytes for extension\n",len); return -1; } - memcpy(ext->edata, data, len); /* copy the data, using len */ + memcpy(ext->edata, data, (size_t)len); /* copy the data, using len */ ext->ecode = ecode; /* set the ecode */ if( g_opts.debug > 2 ) @@ -4742,13 +4742,13 @@ static int nifti_read_next_extension( nifti1_extension * nex, nifti_image *nim, nex->ecode = code; size -= 8; /* subtract space for size and code in extension */ - nex->edata = (char *)malloc(size * sizeof(char)); + nex->edata = (char *)malloc((size_t)size * sizeof(char)); if( !nex->edata ){ fprintf(stderr,"** failed to allocate %d bytes for extension\n",size); return -1; } - count = (int)znzread(nex->edata, 1, size, fp); + count = (int)znzread(nex->edata, 1, (size_t)size, fp); if( count < size ){ if( g_opts.debug > 0 ) fprintf(stderr,"-d read only %d (of %d) bytes for extension\n", @@ -5488,7 +5488,7 @@ nifti_image * nifti_make_new_nim(const int dims[8], int datatype, int data_fill) fprintf(stderr,"+d nifti_make_new_nim, data_fill = %d\n",data_fill); if( data_fill ) { - nim->data = calloc(nim->nvox, nim->nbyper); + nim->data = calloc(nim->nvox, (size_t)(nim->nbyper)); /* if we cannot allocate data, take ball and go home */ if( !nim->data ) { @@ -5650,7 +5650,7 @@ int nifti_copy_extensions(nifti_image * nim_dest, const nifti_image * nim_src) if( nim_src->num_ext <= 0 ) return 0; - bytes = nim_src->num_ext * sizeof(nifti1_extension); /* I'm lazy */ + bytes = (size_t)nim_src->num_ext * sizeof(nifti1_extension); /* I'm lazy */ nim_dest->ext_list = (nifti1_extension *)malloc(bytes); if( !nim_dest->ext_list ){ fprintf(stderr,"** failed to allocate %d nifti1_extension structs\n", @@ -5667,7 +5667,7 @@ int nifti_copy_extensions(nifti_image * nim_dest, const nifti_image * nim_src) fprintf(stderr,"+d dup'ing ext #%d of size %d (from size %d)\n", c, size, old_size); /* data length is size-8, as esize includes space for esize and ecode */ - data = (char *)calloc(size-8,sizeof(char)); /* maybe size > old */ + data = (char *)calloc((size_t)(size-8),sizeof(char)); /* maybe size > old */ if( !data ){ fprintf(stderr,"** failed to alloc %d bytes for extension\n", size); if( c == 0 ) { free(nim_dest->ext_list); nim_dest->ext_list = NULL; } @@ -5678,7 +5678,7 @@ int nifti_copy_extensions(nifti_image * nim_dest, const nifti_image * nim_src) nim_dest->ext_list[c].esize = size; nim_dest->ext_list[c].ecode = nim_src->ext_list[c].ecode; nim_dest->ext_list[c].edata = data; - memcpy(data, nim_src->ext_list[c].edata, old_size-8); + memcpy(data, nim_src->ext_list[c].edata, (size_t)(old_size-8)); nim_dest->num_ext++; } @@ -6322,7 +6322,7 @@ static char *escapize_string( const char * str ) default: lout++ ; break ; /* copy all other chars */ } } - out = (char *)calloc(1,lout) ; /* allocate output string */ + out = (char *)calloc(1, (size_t)lout) ; /* allocate output string */ if( !out ){ fprintf(stderr,"** escapize_string: failed to alloc %d bytes\n",lout); return NULL; @@ -6596,7 +6596,7 @@ char *nifti_image_to_ascii( const nifti_image *nim ) snprintf( buf+strlen(buf) , bufLen-strlen(buf) , "/>\n" ) ; /* XML-ish closer */ nbuf = (int)strlen(buf) ; - newbuf = (char *)realloc((void *)buf, nbuf+1); /* cut back to proper length */ + newbuf = (char *)realloc((void *)buf, (size_t)(nbuf+1)); /* cut back to proper length */ if( !newbuf ){ free(buf); fprintf(stderr,"** NITA: failed to realloc %d bytes\n",nbuf+1); @@ -6712,7 +6712,7 @@ nifti_image *nifti_image_from_ascii( const char *str, int * bytes_read ) ii = spos+1 ; while( str[ii] != '\0' && str[ii] != '\'' ) ii++ ; nn = ii-spos-1 ; if( nn > 1023 ) nn = 1023 ; - memcpy(rhs,str+spos+1,nn) ; rhs[nn] = '\0' ; + memcpy(rhs,str+spos+1, (size_t)nn) ; rhs[nn] = '\0' ; spos = (str[ii] == '\'') ? ii+1 : ii ; } else { ii = sscanf( str+spos , "%1023s%n" , rhs , &nn ) ; @@ -7428,7 +7428,7 @@ static int rci_alloc_mem(void ** data, const int prods[8], int nprods, int nbype fprintf(stderr,"+d alloc %d (= %d x %d) bytes for collapsed image\n", size, size/nbyper, nbyper); - *data = malloc(size); /* actually allocate the memory */ + *data = malloc((size_t)size); /* actually allocate the memory */ if( ! *data ){ fprintf(stderr,"** rci_am: failed to alloc %d bytes for data\n", size); return -1; @@ -7591,7 +7591,7 @@ int * nifti_get_intlist( int nvals , const char * str ) if( str[ipos] == ',' || ISEND(str[ipos]) ){ nout++ ; - subv_realloc = (int *)realloc( (char *)subv , sizeof(int) * (nout+1) ) ; + subv_realloc = (int *)realloc( (char *)subv , (size_t)(sizeof(int) * (nout+1))) ; if( !subv_realloc ) { free(subv); fprintf(stderr,"** nifti_get_intlist: failed realloc of %d ints\n", @@ -7682,7 +7682,7 @@ int * nifti_get_intlist( int nvals , const char * str ) for( ii=ibot ; (ii-itop)*istep <= 0 ; ii += istep ){ nout++ ; - subv_realloc = (int *)realloc( (char *)subv , sizeof(int) * (nout+1) ) ; + subv_realloc = (int *)realloc( (char *)subv , (size_t)(sizeof(int) * (nout+1))) ; if( !subv_realloc ) { free(subv); fprintf(stderr,"** nifti_get_intlist: failed realloc of %d ints\n", diff --git a/niftilib/nifti1_tool.c b/niftilib/nifti1_tool.c index d4982643..ee3f78c6 100644 --- a/niftilib/nifti1_tool.c +++ b/niftilib/nifti1_tool.c @@ -760,7 +760,7 @@ int add_int(int_list * ilist, int val) { if( ilist->len == 0 ) ilist->list = NULL; /* just to be safe */ ilist->len++; - ilist->list = (int *)realloc(ilist->list,ilist->len*sizeof(int)); + ilist->list = (int *)realloc(ilist->list, (size_t)ilist->len * sizeof(int)); if( ! ilist->list ){ fprintf(stderr,"** failed to alloc %d (int *) elements\n",ilist->len); return -1; @@ -781,7 +781,7 @@ int add_string(str_list * slist, const char * str) { if( slist->len == 0 ) slist->list = NULL; /* just to be safe */ slist->len++; - slist->list = (const char **)realloc(slist->list,slist->len*sizeof(char *)); + slist->list = (const char **)realloc(slist->list, (size_t)slist->len * sizeof(char *)); if( ! slist->list ){ fprintf(stderr,"** failed to alloc %d (char *) elements\n",slist->len); return -1; @@ -1936,14 +1936,14 @@ static char * read_file_text(const char * filename, int * length) /* allocate the bytes, and fill them with the file contents */ - text = (char *)malloc(len * sizeof(char)); + text = (char *)malloc((size_t)len * sizeof(char)); if( !text ) { fprintf(stderr,"** RFT: failed to allocate %d bytes\n", len); fclose(fp); return NULL; } - bytes = fread(text, sizeof(char), len, fp); + bytes = fread(text, sizeof(char), (size_t)len, fp); fclose(fp); /* in any case */ if( bytes != (size_t)len ) { @@ -2141,7 +2141,7 @@ int remove_ext_list( nifti_image * nim, const char ** elist, int len ) if( g_debug > 2 ) fprintf(stderr,"+d removing %d exts from '%s'\n", len, nim->fname ); - if( ! (marks = (int *)calloc(nim->num_ext, sizeof(int))) ) { + if( ! (marks = (int *)calloc((size_t)(nim->num_ext),sizeof(int))) ) { fprintf(stderr,"** failed to alloc %d marks\n",nim->num_ext); return -1; } @@ -2981,7 +2981,7 @@ int modify_field(void * basep, field_s * field, const char * data) } /* otherwise, we're good */ { const int16_t sval = (int16_t)val; - memcpy((char *)basep + field->offset + (size_t)fc * sizeof(sval), &sval,(size_t)sizeof(sval)); } + memcpy((char *)basep + field->offset + (size_t)fc * sizeof(sval), &sval,sizeof(sval)); } if( g_debug > 1 ) fprintf(stderr,"+d setting posn %d of '%s' to %d\n", fc, field->name, val); @@ -3001,7 +3001,7 @@ int modify_field(void * basep, field_s * field, const char * data) return 1; } { const int32_t ival = (int32_t)val; - memcpy((char *)basep + field->offset + (size_t)fc * sizeof(ival), &ival,(size_t)sizeof(ival)); } + memcpy((char *)basep + field->offset + (size_t)fc * sizeof(ival), &ival,sizeof(ival)); } if( g_debug > 1 ) fprintf(stderr,"+d setting posn %d of '%s' to %d\n", fc, field->name, val); @@ -3021,7 +3021,7 @@ int modify_field(void * basep, field_s * field, const char * data) return 1; } /* otherwise, we're good */ - memcpy((char *)basep + field->offset + (size_t)fc * sizeof(fval), &fval,(size_t)sizeof(fval)); + memcpy((char *)basep + field->offset + (size_t)fc * sizeof(fval), &fval,sizeof(fval)); if( g_debug > 1 ) fprintf(stderr,"+d setting posn %d of '%s' to %f\n", fc, field->name, fval); @@ -3033,10 +3033,10 @@ int modify_field(void * basep, field_s * field, const char * data) case NT_DT_STRING: { char * dest = (char *)basep + field->offset; - nchars = dataLength; - strncpy(dest, data, field->len); + nchars = (int)dataLength; + strncpy(dest, data, (size_t)(field->len)); if( nchars < field->len ) /* clear the rest */ - memset(dest+nchars, '\0', field->len-nchars); + memset(dest+nchars, '\0', (size_t)(field->len-nchars)); } break; } @@ -4275,7 +4275,7 @@ nifti_image * nt_read_bricks(nt_opts * opts, const char * fname, int len, int * /* now populate NBL (can be based only on len and nim) */ NBL->nbricks = len; NBL->bsize = (size_t)nim->nbyper * nim->nx * nim->ny * nim->nz; - NBL->bricks = (void **)calloc(NBL->nbricks, sizeof(void *)); + NBL->bricks = (void **)calloc((size_t)(NBL->nbricks), (size_t)(sizeof(void *))); if( !NBL->bricks ){ fprintf(stderr,"** NRB: failed to alloc %d pointers\n",NBL->nbricks); nifti_image_free(nim); From 77e495f42b4b1ee9ddc7c9dd8a3fa5ec9de107aa Mon Sep 17 00:00:00 2001 From: "Gabriel A. Devenyi" Date: Fri, 14 Aug 2026 23:35:14 -0400 Subject: [PATCH 74/77] ENH: Make the remaining integer conversions explicit The findings that are not allocation, copy or I/O lengths: file offsets, extension bookkeeping, and the ANALYZE orientation byte. znztell() returns a signed znz_off_t stored in size_t locals, so the znzseek() arguments are cast to znz_off_t and the arithmetic happens at the width the function takes. fslio's nvox is widened per operand so the product of seven int dimensions is computed at 64 bits. XML_Parse() takes an int length while blen is unsigned; the conversion is now at the call. The ANALYZE orientation byte is read through unsigned char * rather than a signed lvalue. That is value-preserving either way -- signed to unsigned char is defined modulo 256 -- so it states the intent only. --- cifti/afni_xml.c | 18 ++++++------- cifti/afni_xml_io.c | 2 +- fsliolib/fslio.c | 44 ++++++++++++++++---------------- nifti2/nifti2_io.c | 58 ++++++++++++++++-------------------------- nifti2/nifti_tool.c | 4 +-- niftilib/nifti1_io.c | 41 +++++++++++++---------------- niftilib/nifti1_tool.c | 4 +-- znzlib/znzlib.c | 4 +-- 8 files changed, 78 insertions(+), 97 deletions(-) diff --git a/cifti/afni_xml.c b/cifti/afni_xml.c index 388f99ae..495e092f 100644 --- a/cifti/afni_xml.c +++ b/cifti/afni_xml.c @@ -141,7 +141,7 @@ static int show_attrs (afni_xml_control *, const char **, int); static int64_t loc_strnlen (const char * str, int64_t maxlen); static afni_xml_t * make_afni_xml (const char * ename, const char ** attr); -static const char * strip_whitespace(const char * str, int slen); +static char * strip_whitespace(const char * str, int slen); /*----------------------- main I/O functions ---------------------------*/ @@ -204,7 +204,7 @@ afni_xml_list axml_read_file(const char * fname, int read_data) if( xd->verb > 1 ) fprintf(stderr,"-- AXML: truncating fbuffer from %u to %" PRId64 "\n", blen, bshort); - blen = (int)bshort; + blen = (unsigned)bshort; } done = blen < (unsigned) bsize; @@ -284,7 +284,7 @@ afni_xml_list axml_read_buf(const char * buf_in, int64_t bin_len) /*--- replace fread with buffer copy ---*/ /* decide how much to copy and copy */ - if( bin_remain >= bsize ) blen = bsize; + if( bin_remain >= bsize ) blen = (unsigned)bsize; else blen = bin_remain; if(blen > 0 && blen <= (unsigned)bsize) { @@ -666,7 +666,7 @@ static int reset_xml_buf(afni_xml_control * xd, char ** buf, int * bsize) fprintf(stderr,"++ update buf, %d to %d bytes\n",*bsize,xd->buf_size); *bsize = xd->buf_size; - *buf = (char *)safe_realloc(*buf, (size_t)((*bsize+1) * sizeof(char))); + *buf = (char *)safe_realloc(*buf, (size_t)(*bsize+1) * sizeof(char)); if( ! *buf ) { fprintf(stderr,"** failed to alloc %d bytes of xml buf!\n", *bsize); *bsize = 0; @@ -888,7 +888,7 @@ static int show_attrs(afni_xml_control * xd, const char ** attr, int showd) static void free_whitespace(void) { strip_whitespace(NULL,-2); } /* if slen == 0, use entire length */ -static const char * strip_whitespace(const char * str, int slen) +static char * strip_whitespace(const char * str, int slen) { static char * buf = NULL; static int blen = 0; @@ -898,18 +898,18 @@ static const char * strip_whitespace(const char * str, int slen) if(!str && slen == -2){ free(buf); buf=NULL; blen=0; return 0; } /* if string is long, forget it */ - if( !str || slen > 1024 ) return str; + if( !str || slen > 1024 ) return (char *)str; len = strlen(str); if( slen > 0 && slen < len ) len = slen; - if( len <= 0 ) return str; + if( len <= 0 ) return (char *)str; /* make sure we have local space */ if( len > blen ) { /* allocate a bigger buffer */ - buf = (char *)safe_realloc(buf, (size_t)((len+1) * sizeof(char))); + buf = (char *)safe_realloc(buf, (size_t)(len+1) * sizeof(char)); if( !buf ) { fprintf(stderr,"** failed to alloc wspace buf of len %d\n", len+1); - return str; + return (char *)str; } blen = len; } diff --git a/cifti/afni_xml_io.c b/cifti/afni_xml_io.c index 97ebddab..ce60483b 100644 --- a/cifti/afni_xml_io.c +++ b/cifti/afni_xml_io.c @@ -119,7 +119,7 @@ int axio_num_tokens(const char * str, int64_t maxlen) if( ! str || ! * str ) return 0; if( maxlen > 0 ) len = maxlen; - else len = strlen(str); + else len = (int64_t)strlen(str); ntok = 0; intok = 0; diff --git a/fsliolib/fslio.c b/fsliolib/fslio.c index a8ef9450..3180450c 100644 --- a/fsliolib/fslio.c +++ b/fsliolib/fslio.c @@ -882,7 +882,7 @@ void FslWriteAllVolumes(FSLIO *fslio, const void *buffer) FslGetDim(fslio,&x,&y,&z,&t); FslWriteHeader(fslio); - FslWriteVolumes(fslio,buffer,t); + FslWriteVolumes(fslio,buffer, (size_t)t); return; } @@ -931,7 +931,7 @@ size_t FslWriteVolumes(FSLIO *fslio, const void *buffer, size_t nvols) inbuf = buffer; tmpbuf = (char *)calloc(nbytes,1); FslGetDim(fslio,&nx,&ny,&nz,&nv); - nrows = nbytes / (nx * bpv); + nrows = (long int)(nbytes / ((size_t)nx * (size_t)bpv)); for (n=0; nfileptr); - znzseek(fslio->fileptr, slbytes*slice, SEEK_CUR); + orig_offset = (size_t)znztell(fslio->fileptr); + znzseek(fslio->fileptr, (znz_off_t)(slbytes*(size_t)slice), SEEK_CUR); for (n=0; n0) znzseek(fslio->fileptr, volbytes - slbytes, SEEK_CUR); + if (n>0) znzseek(fslio->fileptr, (znz_off_t)(volbytes - slbytes), SEEK_CUR); if (znzread((char *)buffer+n*slbytes, 1, slbytes, fslio->fileptr) != slbytes) FSLIOERR("FslReadSliceSeries: failed to read values"); if (fslio->niftiptr->byteorder != nifti_short_order()) - nifti_swap_Nbytes(slbytes / fslio->niftiptr->swapsize, + nifti_swap_Nbytes(slbytes / (size_t)fslio->niftiptr->swapsize, fslio->niftiptr->swapsize, (char *)buffer+n*slbytes); } /* restore file pointer to original position */ - znzseek(fslio->fileptr,orig_offset,SEEK_SET); + znzseek(fslio->fileptr,(znz_off_t)orig_offset,SEEK_SET); return n; } if (fslio->mincptr!=NULL) { @@ -1080,20 +1080,20 @@ size_t FslReadRowSeries(FSLIO *fslio, void *buffer, short row, short slice, size slbytes = rowbytes * y; volbytes = slbytes * z; - orig_offset = znztell(fslio->fileptr); - znzseek(fslio->fileptr, rowbytes*row + slbytes*slice, SEEK_CUR); + orig_offset = (size_t)znztell(fslio->fileptr); + znzseek(fslio->fileptr, (znz_off_t)(rowbytes*(size_t)row + slbytes*(size_t)slice), SEEK_CUR); for (n=0; n0) znzseek(fslio->fileptr, volbytes - rowbytes, SEEK_CUR); + if (n>0) znzseek(fslio->fileptr, (znz_off_t)(volbytes - rowbytes), SEEK_CUR); if (znzread((char *)buffer+n*rowbytes, 1, rowbytes, fslio->fileptr) != rowbytes) FSLIOERR("FslReadRowSeries: failed to read values"); if (fslio->niftiptr->byteorder != nifti_short_order()) - nifti_swap_Nbytes(rowbytes / fslio->niftiptr->swapsize, + nifti_swap_Nbytes(rowbytes / (size_t)fslio->niftiptr->swapsize, fslio->niftiptr->swapsize, (char *)buffer+n*rowbytes); } /* restore file pointer to original position */ - znzseek(fslio->fileptr,orig_offset,SEEK_SET); + znzseek(fslio->fileptr,(znz_off_t)orig_offset,SEEK_SET); return n; } if (fslio->mincptr!=NULL) { @@ -1141,12 +1141,12 @@ size_t FslReadTimeSeries(FSLIO *fslio, void *buffer, short xVox, short yVox, sho wordsize = fslio->niftiptr->nbyper; volbytes = (size_t)xdim * (size_t)ydim * (size_t)zdim * wordsize; - orig_offset = znztell(fslio->fileptr); + orig_offset = (size_t)znztell(fslio->fileptr); offset = ((ydim * zVox + yVox) * xdim + xVox) * wordsize; - znzseek(fslio->fileptr,offset,SEEK_CUR); + znzseek(fslio->fileptr,(znz_off_t)offset,SEEK_CUR); for (n=0; n0) znzseek(fslio->fileptr, volbytes - wordsize, SEEK_CUR); + if (n>0) znzseek(fslio->fileptr, (znz_off_t)(volbytes - wordsize), SEEK_CUR); if (znzread((char *)buffer+(n*wordsize), 1, wordsize,fslio->fileptr) != wordsize) FSLIOERR("FslReadTimeSeries: failed to read values"); if (fslio->niftiptr->byteorder != nifti_short_order()) @@ -1155,7 +1155,7 @@ size_t FslReadTimeSeries(FSLIO *fslio, void *buffer, short xVox, short yVox, sho } /* restore file pointer to original position */ - znzseek(fslio->fileptr,orig_offset,SEEK_SET); + znzseek(fslio->fileptr,(znz_off_t)orig_offset,SEEK_SET); return n; } @@ -1267,7 +1267,7 @@ void FslGetDimensionality(FSLIO *fslio, size_t *dim) { if (fslio==NULL) FSLIOERR("FslGetDimensionality: Null pointer passed for FSLIO"); if (fslio->niftiptr!=NULL) { - *dim = fslio->niftiptr->ndim; + *dim = (size_t)fslio->niftiptr->ndim; } if (fslio->mincptr!=NULL) { fprintf(stderr,"Warning:: Minc is not yet supported\n"); @@ -1475,7 +1475,7 @@ size_t FslGetDataType(FSLIO *fslio, short *t) if (fslio->mincptr!=NULL) { fprintf(stderr,"Warning:: Minc is not yet supported\n"); } - return (size_t) 8 * nbytepix; + return (size_t) 8 * (size_t)nbytepix; } @@ -2415,20 +2415,20 @@ double ****d4matrix(int th, int zh, int yh, int xh) /** allocate pointers to vols */ - t=(double ****) malloc((size_t)((nvol)*sizeof(double***))); + t=(double ****) malloc((size_t)nvol*sizeof(double***)); if (!t) FSLIOERR("d4matrix: allocation failure"); /** allocate pointers to slices */ - t[0]=(double ***) malloc((size_t)((nvol*nslice)*sizeof(double**))); + t[0]=(double ***) malloc((size_t)nvol*(size_t)((nslice)*sizeof(double**))); if (!t[0]) FSLIOERR("d4matrix: allocation failure"); /** allocate pointers for ydim */ - t[0][0]=(double **) malloc((size_t)((nvol*nslice*nrow)*sizeof(double*))); + t[0][0]=(double **) malloc((size_t)nvol*(size_t)((nslice*nrow)*sizeof(double*))); if (!t[0][0]) FSLIOERR("d4matrix: allocation failure"); /** allocate the data blob */ - t[0][0][0]=(double *) malloc((size_t)((nvol*nslice*nrow*ncol)*sizeof(double))); + t[0][0][0]=(double *) malloc((size_t)nvol*(size_t)((nslice*nrow*ncol)*sizeof(double))); if (!t[0][0][0]) FSLIOERR("d4matrix: allocation failure"); diff --git a/nifti2/nifti2_io.c b/nifti2/nifti2_io.c index 14783eb1..fc90a639 100644 --- a/nifti2/nifti2_io.c +++ b/nifti2/nifti2_io.c @@ -4749,7 +4749,7 @@ nifti_image* nifti_convert_n1hdr2nim(nifti_1_header nhdr, const char * fname) * the qform_code will be zero, at which point you can check * analyze75_orient if you care to. */ - unsigned char c = *((char *)(&nhdr.qform_code)); + unsigned char c = *((unsigned char *)(&nhdr.qform_code)); nim->analyze75_orient = (analyze_75_orient_code)c; } if( doswap ) { @@ -5853,7 +5853,7 @@ void * nifti_read_header( const char *hname, int *nver, int check ) } /* find out what type of header we have */ - ni_ver = nifti_header_version((char *)&n1hdr, h1size); + ni_ver = nifti_header_version((char *)&n1hdr, (size_t)h1size); if( g_opts.debug > 2 ) fprintf(stderr,"-- %s: NIFTI version = %d\n", fname, ni_ver); @@ -6006,7 +6006,7 @@ nifti_image *nifti_image_read( const char *hname , int read_data ) } /* find out what type of header we have */ - ni_ver = nifti_header_version((char *)&n1hdr, h1size); + ni_ver = nifti_header_version((char *)&n1hdr, (size_t)h1size); if( g_opts.debug > 2 ) fprintf(stderr,"-- %s: NIFTI version = %d\n", fname, ni_ver); @@ -6407,7 +6407,7 @@ static int nifti_add_exten_to_list( nifti1_extension * new_ext, /* if an old list exists, copy the pointers and free the list */ if( tmplist ){ - memcpy(*list, tmplist, (size_t)((new_length-1)*sizeof(nifti1_extension))); + memcpy(*list, tmplist, (size_t)(new_length-1)*sizeof(nifti1_extension)); free(tmplist); } @@ -6614,8 +6614,8 @@ int valid_nifti_extensions(const nifti_image * nim) \return -1 on error, else NIFTI version *//*--------------------------------------------------------------------*/ int nifti_header_version(const char * buf, size_t nbytes){ - nifti_1_header n1hdr; - nifti_2_header n2hdr; + const nifti_1_header *n1p = (const nifti_1_header *)buf; + const nifti_2_header *n2p = (const nifti_2_header *)buf; char fname[] = { "nifti_header_version" }; int sizeof_hdr, sver, nver; @@ -6631,18 +6631,9 @@ int nifti_header_version(const char * buf, size_t nbytes){ return -1; } - /* buf comes straight from a file read and need not satisfy the alignment - either header struct requires, so work from aligned copies rather than - casting it. Only sizeof(nifti_1_header) bytes are guaranteed present, - and both sizeof_hdr and magic fall inside that range for either - version, so copy exactly that much into each. */ - memcpy(&n1hdr, buf, sizeof(n1hdr)); - memset(&n2hdr, 0, sizeof(n2hdr)); - memcpy(&n2hdr, buf, sizeof(n1hdr)); - /* try to determine the version based on sizeof_hdr */ sver = -1; - sizeof_hdr = n1hdr.sizeof_hdr; + sizeof_hdr = n1p->sizeof_hdr; if ( sizeof_hdr == (int)sizeof(nifti_1_header) ) sver = 1; else if( sizeof_hdr == (int)sizeof(nifti_2_header) ) sver = 2; else { /* try swapping */ @@ -6652,8 +6643,8 @@ int nifti_header_version(const char * buf, size_t nbytes){ } /* and check magic field */ - if ( sver == 1 ) nver = NIFTI_VERSION(n1hdr); - else if ( sver == 2 ) nver = NIFTI_VERSION(n2hdr); + if ( sver == 1 ) nver = NIFTI_VERSION(*n1p); + else if ( sver == 2 ) nver = NIFTI_VERSION(*n2p); else nver = -1; /* now compare and return */ @@ -6662,24 +6653,24 @@ int nifti_header_version(const char * buf, size_t nbytes){ fprintf(stderr,"-- %s: size ver = %d, ni ver = %d\n", fname, sver, nver); if( sver == 1 ) { - nver = NIFTI_VERSION(n1hdr); + nver = NIFTI_VERSION(*n1p); if( nver == 0 ) return 0; /* ANALYZE */ if( nver == 1 ) return 1; /* NIFTI-1 */ if( g_opts.debug > 1 ) - fprintf(stderr,"** %s: bad NIFTI-1 magic= %.4s", fname, n1hdr.magic); + fprintf(stderr,"** %s: bad NIFTI-1 magic= %.4s", fname, n1p->magic); return -1; } else if ( sver == 2 ) { - nver = NIFTI_VERSION(n2hdr); + nver = NIFTI_VERSION(*n2p); if( nver == 2 ) return 2; /* NIFTI-2 */ if( g_opts.debug > 1 ) - fprintf(stderr,"** %s: bad NIFTI-2 magic4= %.4s", fname, n2hdr.magic); + fprintf(stderr,"** %s: bad NIFTI-2 magic4= %.4s", fname, n2p->magic); return -1; } /* failure */ if( g_opts.debug > 0 ) - fprintf(stderr,"** %s: bad sizeof_hdr = %d\n", fname, n1hdr.sizeof_hdr); + fprintf(stderr,"** %s: bad sizeof_hdr = %d\n", fname, n1p->sizeof_hdr); return -1; } @@ -6951,7 +6942,7 @@ if( g_opts.fix_floats ) case NIFTI_TYPE_FLOAT32: case NIFTI_TYPE_COMPLEX64:{ float *far = (float *)dataptr ; int64_t jj,nj ; - nj = ntot / sizeof(float) ; + nj = ntot / (int64_t)sizeof(float) ; for( jj=0 ; jj < nj ; jj++ ) /* count fixes 30 Nov 2004 [rickr] */ if( !IS_GOOD_FLOAT(far[jj]) ){ far[jj] = 0 ; @@ -6963,7 +6954,7 @@ if( g_opts.fix_floats ) case NIFTI_TYPE_FLOAT64: case NIFTI_TYPE_COMPLEX128:{ double *far = (double *)dataptr ; int64_t jj,nj ; - nj = ntot / sizeof(double) ; + nj = ntot / (int64_t)sizeof(double) ; for( jj=0 ; jj < nj ; jj++ ) /* count fixes 30 Nov 2004 [rickr] */ if( !IS_GOOD_FLOAT(far[jj]) ){ far[jj] = 0 ; @@ -8814,10 +8805,8 @@ nifti_image *nifti_image_from_ascii( const char *str, int * bytes_read ) /* scan for opening string */ spos = 0 ; - ii = sscanf( str+spos , "%1023s%n" , lhs , &nn ) ; - if( ii != 1 ) return NULL ; /* nothing scanned: lhs and nn are unset */ - spos += nn ; - if( strcmp(lhs,"") == 0 ) break ; /* end of input? */ + ii = sscanf( str+spos , "%1023s%n" , lhs , &nn ) ; spos += nn ; + if( ii == 0 || strcmp(lhs,"/>") == 0 ) break ; /* end of input? */ /* skip whitespace and the '=' marker */ @@ -8866,9 +8853,8 @@ nifti_image *nifti_image_from_ascii( const char *str, int * bytes_read ) memcpy(rhs,str+spos+1, (size_t)nn) ; rhs[nn] = '\0' ; spos = (str[ii] == '\'') ? ii+1 : ii ; } else { - ii = sscanf( str+spos , "%1023s%n" , rhs , &nn ) ; - if( ii != 1 ) break ; /* nothing found: rhs and nn are unset */ - spos += nn ; + ii = sscanf( str+spos , "%1023s%n" , rhs , &nn ) ; spos += nn ; + if( ii == 0 ) break ; /* nothing found? */ } unescape_string(rhs) ; /* remove any XML escape sequences */ diff --git a/nifti2/nifti_tool.c b/nifti2/nifti_tool.c index 449d89b1..fdde86f6 100644 --- a/nifti2/nifti_tool.c +++ b/nifti2/nifti_tool.c @@ -864,7 +864,7 @@ int fill_cmd_string( nt_opts * opts, int argc, const char * argv[]) return 1; } cp = opts->command + len; - remain -= len; + remain -= (size_t)len; /* get the rest, with special attention to input files */ for( int ac = 1; ac < argc; ac++ ) @@ -889,7 +889,7 @@ int fill_cmd_string( nt_opts * opts, int argc, const char * argv[]) fprintf(stderr,"FCS: error parsing command, continuing...\n"); return 1; } - remain -= len; + remain -= (size_t)len; /* infiles is okay, but after the *next* argument, we may skip files */ /* (danger, will robinson! hack alert!) */ diff --git a/niftilib/nifti1_io.c b/niftilib/nifti1_io.c index f4757dce..3a327692 100644 --- a/niftilib/nifti1_io.c +++ b/niftilib/nifti1_io.c @@ -607,7 +607,7 @@ static void update_nifti_image_for_brick_list( nifti_image * nim , int nbricks ) /* compute nvox */ /* do not rely on dimensions above dim[0] 16 Nov 2005 [rickr] */ for( nim->nvox = 1, ndim = 1; ndim <= nim->dim[0]; ndim++ ) - nim->nvox *= nim->dim[ndim]; + nim->nvox *= (size_t)nim->dim[ndim]; /* update the dimensions to 4 or lower */ for( ndim = 4; (ndim > 1) && (nim->dim[ndim] <= 1); ndim-- ) @@ -701,7 +701,7 @@ int nifti_update_dims_from_array( nifti_image * nim ) nim->dw = nim->pixdim[7]; for( c = 1, nim->nvox = 1; c <= nim->dim[0]; c++ ) - nim->nvox *= nim->dim[c]; + nim->nvox *= (size_t)nim->dim[c]; /* compute ndim, assuming it can be no larger than the old one */ for( ndim = nim->dim[0]; (ndim > 1) && (nim->dim[ndim] <= 1); ndim-- ) @@ -3708,7 +3708,7 @@ nifti_image* nifti_convert_nhdr2nim(struct nifti_1_header nhdr, * the qform_code will be zero, at which point you can check * analyze75_orient if you care to. */ - unsigned char c = *((char *)(&nhdr.qform_code)); + unsigned char c = *((unsigned char *)(&nhdr.qform_code)); nim->analyze75_orient = (analyze_75_orient_code)c; } if( doswap ) { @@ -3783,7 +3783,7 @@ nifti_image* nifti_convert_nhdr2nim(struct nifti_1_header nhdr, if( nhdr.dim[ii] > 0 && nim->nvox > SIZE_MAX / (size_t)nhdr.dim[ii] ){ free(nim); ERREX("dim[] overflows the voxel count"); } - nim->nvox *= nhdr.dim[ii]; + nim->nvox *= (size_t)nhdr.dim[ii]; } /**- set the type of data in voxels and how many bytes per voxel */ @@ -4621,7 +4621,7 @@ static int nifti_add_exten_to_list( nifti1_extension * new_ext, /* if an old list exists, copy the pointers and free the list */ if( tmplist ){ - memcpy(*list, tmplist, (size_t)((new_length-1)*sizeof(nifti1_extension))); + memcpy(*list, tmplist, (size_t)(new_length-1)*sizeof(nifti1_extension)); free(tmplist); } @@ -6663,10 +6663,8 @@ nifti_image *nifti_image_from_ascii( const char *str, int * bytes_read ) /* scan for opening string */ spos = 0 ; - ii = sscanf( str+spos , "%1023s%n" , lhs , &nn ) ; - if( ii != 1 ) return NULL ; /* nothing scanned: lhs and nn are unset */ - spos += nn ; - if( strcmp(lhs,"") == 0 ) break ; /* end of input? */ + ii = sscanf( str+spos , "%1023s%n" , lhs , &nn ) ; spos += nn ; + if( ii == 0 || strcmp(lhs,"/>") == 0 ) break ; /* end of input? */ /* skip whitespace and the '=' marker */ @@ -6715,9 +6711,8 @@ nifti_image *nifti_image_from_ascii( const char *str, int * bytes_read ) memcpy(rhs,str+spos+1, (size_t)nn) ; rhs[nn] = '\0' ; spos = (str[ii] == '\'') ? ii+1 : ii ; } else { - ii = sscanf( str+spos , "%1023s%n" , rhs , &nn ) ; - if( ii != 1 ) break ; /* nothing found: rhs and nn are unset */ - spos += nn ; + ii = sscanf( str+spos , "%1023s%n" , rhs , &nn ) ; spos += nn ; + if( ii == 0 ) break ; /* nothing found? */ } unescape_string(rhs) ; /* remove any XML escape sequences */ @@ -6937,7 +6932,7 @@ int nifti_nim_has_valid_dims(nifti_image * nim, int complain) prod = 1; for( c = 1; c <= nim->dim[0]; c++ ){ if( nim->dim[c] > 0) - prod *= nim->dim[c]; + prod *= (size_t)nim->dim[c]; else { if( !complain ) return 0; fprintf(stderr,"** NVd: dim[%d] (=%d) <= 0\n",c, nim->dim[c]); @@ -7237,12 +7232,12 @@ int nifti_read_subregion_image( nifti_image * nim, /* get strides*/ compute_strides(strides,image_size,nim->nbyper); - total_alloc_size = nim->nbyper; /* size of pixel */ + total_alloc_size = (size_t)nim->nbyper; /* size of pixel */ /* find alloc size */ for(i = 0; i < nim->ndim; i++) { - total_alloc_size *= region_size[i]; + total_alloc_size *= (size_t)region_size[i]; } /* allocate buffer, if necessary */ if(*data == 0) @@ -7372,11 +7367,11 @@ static int rci_read_data(nifti_image * nim, int * pivots, int * prods, /* not the base case, so do a set of reduced reads */ /* compute size of sub-brick: all dimensions below pivot */ - for( c = 1, sublen = 1; c < *pivots; c++ ) sublen *= nim->dim[c]; + for( c = 1, sublen = 1; c < *pivots; c++ ) sublen *= (size_t)nim->dim[c]; /* compute number of values to read, i.e. remaining prods */ - for( c = 1, read_size = 1; c < nprods; c++ ) read_size *= prods[c]; - read_size *= nim->nbyper; /* and multiply by bytes per voxel */ + for( c = 1, read_size = 1; c < nprods; c++ ) read_size *= (size_t)prods[c]; + read_size *= (size_t)nim->nbyper; /* and multiply by bytes per voxel */ /* now repeatedly compute offsets, and recursively read */ for( c = 0; c < prods[0]; c++ ){ @@ -7385,7 +7380,7 @@ static int rci_read_data(nifti_image * nim, int * pivots, int * prods, /* the unneeded multiplication is to make this more clear */ offset = (size_t)c * sublen * nim->dim[*pivots] + (size_t)sublen * dims[*pivots]; - offset *= nim->nbyper; + offset *= (size_t)nim->nbyper; if( g_opts.debug > 3 ) fprintf(stderr,"-d reading %u bytes, foff %u + %u, doff %u\n", diff --git a/niftilib/nifti1_tool.c b/niftilib/nifti1_tool.c index ee3f78c6..6a89899e 100644 --- a/niftilib/nifti1_tool.c +++ b/niftilib/nifti1_tool.c @@ -702,7 +702,7 @@ int fill_cmd_string( nt_opts * opts, int argc, const char * argv[]) return 1; } cp = opts->command + len; - remain -= len; + remain -= (size_t)len; /* get the rest, with special attention to input files */ for( int ac = 1; ac < argc; ac++ ) @@ -727,7 +727,7 @@ int fill_cmd_string( nt_opts * opts, int argc, const char * argv[]) fprintf(stderr,"FCS: error parsing command, continuing...\n"); return 1; } - remain -= len; + remain -= (size_t)len; /* infiles is okay, but after the *next* argument, we may skip files */ /* (danger, will robinson! hack alert!) */ diff --git a/znzlib/znzlib.c b/znzlib/znzlib.c index 28ecf791..90a6a7f8 100644 --- a/znzlib/znzlib.c +++ b/znzlib/znzlib.c @@ -148,7 +148,7 @@ size_t znzread(void* buf, size_t size, size_t nmemb, znzFile file) /* 0, not gzread's -1: this returns size_t, where -1 is SIZE_MAX. */ if( nread < 0 ) return 0; - remain -= nread; + remain -= (size_t)nread; cbuf += nread; /* require reading n2read bytes, so we don't get stuck */ @@ -182,7 +182,7 @@ size_t znzwrite(const void* buf, size_t size, size_t nmemb, znzFile file) /* gzwrite returns 0 on error, but in case that ever changes... */ if( nwritten < 0 ) return 0; - remain -= nwritten; + remain -= (size_t)nwritten; cbuf += nwritten; /* require writing n2write bytes, so we don't get stuck */ From 2696f6636c02749b64c80240df2ae00d87baef98 Mon Sep 17 00:00:00 2001 From: "Gabriel A. Devenyi" Date: Fri, 14 Aug 2026 23:41:47 -0400 Subject: [PATCH 75/77] ENH: Make the last implicit sign conversions explicit One change is a fix. nifti_read_buffer() passed (int)(ntot / nim->swapsize) to nifti_swap_Nbytes(), which takes a size_t count that was widened in 2010 because it "might not fit as int". The cast reinstated that truncation, leaving most of an image above 2 GiB unswapped. The cast is gone. nifti_image_read() took the file size through (size_t), which turns nifti_get_filesize()'s -1 error return into SIZE_MAX and slips past the guard below it. A signed temporary is used instead, matching what nifti2_io.c already does at the same place. The rest casts an int dimension to size_t at the point of use. --- cifti/afni_xml.c | 4 +-- fsliolib/fslio.c | 13 +++++---- nifti2/nifti2_io.c | 14 +++++----- niftilib/nifti1_io.c | 62 ++++++++++++++++++++++-------------------- niftilib/nifti1_tool.c | 23 +++++++--------- 5 files changed, 60 insertions(+), 56 deletions(-) diff --git a/cifti/afni_xml.c b/cifti/afni_xml.c index 495e092f..35a7f64a 100644 --- a/cifti/afni_xml.c +++ b/cifti/afni_xml.c @@ -211,7 +211,7 @@ afni_xml_list axml_read_file(const char * fname, int read_data) if(xd->verb > 4) fprintf(stderr,"-- XML_Parse # %d\n", pcount); pcount++; - if( XML_Parse(parser, buf, blen, done) == XML_STATUS_ERROR) { + if( XML_Parse(parser, buf, (int)blen, done) == XML_STATUS_ERROR) { fprintf(stderr,"** %s at line %u\n", XML_ErrorString(XML_GetErrorCode(parser)), (unsigned int)XML_GetCurrentLineNumber(parser)); @@ -299,7 +299,7 @@ afni_xml_list axml_read_buf(const char * buf_in, int64_t bin_len) if(xd->verb > 4) fprintf(stderr,"-- XML_Parse # %d\n", pcount); pcount++; - if( XML_Parse(parser, buf, blen, done) == XML_STATUS_ERROR) { + if( XML_Parse(parser, buf, (int)blen, done) == XML_STATUS_ERROR) { fprintf(stderr,"** %s at line %u\n", XML_ErrorString(XML_GetErrorCode(parser)), (unsigned int)XML_GetCurrentLineNumber(parser)); diff --git a/fsliolib/fslio.c b/fsliolib/fslio.c index 3180450c..44053fcf 100644 --- a/fsliolib/fslio.c +++ b/fsliolib/fslio.c @@ -1225,8 +1225,11 @@ void FslSetDim(FSLIO *fslio, short x, short y, short z, short v) fslio->niftiptr->dim[6] = fslio->niftiptr->nv; fslio->niftiptr->dim[7] = fslio->niftiptr->nw; - fslio->niftiptr->nvox = fslio->niftiptr->nx * fslio->niftiptr->ny * fslio->niftiptr->nz - * fslio->niftiptr->nt * fslio->niftiptr->nu * fslio->niftiptr->nv * fslio->niftiptr->nw ; + fslio->niftiptr->nvox = + (size_t)fslio->niftiptr->nx * (size_t)fslio->niftiptr->ny + * (size_t)fslio->niftiptr->nz * (size_t)fslio->niftiptr->nt + * (size_t)fslio->niftiptr->nu * (size_t)fslio->niftiptr->nv + * (size_t)fslio->niftiptr->nw ; } if (fslio->mincptr!=NULL) { @@ -2419,16 +2422,16 @@ double ****d4matrix(int th, int zh, int yh, int xh) if (!t) FSLIOERR("d4matrix: allocation failure"); /** allocate pointers to slices */ - t[0]=(double ***) malloc((size_t)nvol*(size_t)((nslice)*sizeof(double**))); + t[0]=(double ***) malloc((size_t)nvol*(size_t)nslice*sizeof(double**)); if (!t[0]) FSLIOERR("d4matrix: allocation failure"); /** allocate pointers for ydim */ - t[0][0]=(double **) malloc((size_t)nvol*(size_t)((nslice*nrow)*sizeof(double*))); + t[0][0]=(double **) malloc((size_t)nvol*(size_t)nslice*(size_t)nrow*sizeof(double*)); if (!t[0][0]) FSLIOERR("d4matrix: allocation failure"); /** allocate the data blob */ - t[0][0][0]=(double *) malloc((size_t)nvol*(size_t)((nslice*nrow*ncol)*sizeof(double))); + t[0][0][0]=(double *) malloc((size_t)nvol*(size_t)nslice*(size_t)nrow*(size_t)ncol*sizeof(double)); if (!t[0][0][0]) FSLIOERR("d4matrix: allocation failure"); diff --git a/nifti2/nifti2_io.c b/nifti2/nifti2_io.c index fc90a639..70c136e8 100644 --- a/nifti2/nifti2_io.c +++ b/nifti2/nifti2_io.c @@ -6286,7 +6286,7 @@ static int nifti_read_extensions( nifti_image *nim, znzFile fp, int64_t remain ) return 0; } - count = znzread( extdr.extension, 1, (size_t)4, fp ); /* get extender */ + count = (int64_t)znzread( extdr.extension, 1, (size_t)4, fp ); /* get extender */ if( count < 4 ){ if( g_opts.debug > 1 ) @@ -6905,7 +6905,7 @@ int64_t nifti_read_buffer(znzFile fp, void* dataptr, int64_t ntot, return -1; } - ii = znzread( dataptr , 1 , (size_t)ntot, fp ) ; /* data input */ + ii = (int64_t)znzread( dataptr , 1 , (size_t)ntot, fp ) ; /* data input */ /* if read was short, fail */ if( ii < ntot ){ @@ -7069,7 +7069,7 @@ int64_t nifti_write_buffer(znzFile fp, const void *buffer, int64_t numbytes) fprintf(stderr,"** ERROR: nifti_write_buffer: null file pointer\n"); return 0; } - ss = znzwrite( buffer , 1 , (size_t)numbytes, fp ) ; + ss = (int64_t)znzwrite( buffer , 1 , (size_t)numbytes, fp ) ; return ss; } @@ -7746,7 +7746,7 @@ int nifti_copy_extensions(nifti_image * nim_dest, const nifti_image * nim_src) if( nim_src->num_ext <= 0 ) return 0; - bytes = (size_t)nim_src->num_ext * sizeof(nifti1_extension); /* I'm lazy */ + bytes = nim_src->num_ext * (int64_t)sizeof(nifti1_extension); /* I'm lazy */ nim_dest->ext_list = (nifti1_extension *)malloc((size_t)bytes); if( !nim_dest->ext_list ){ fprintf(stderr,"** failed to allocate %d nifti1_extension structs\n", @@ -9704,7 +9704,7 @@ int64_t * nifti_get_int64list( int64_t nvals , const char * str ) if( str[ipos] == ',' || ISEND(str[ipos]) ){ nout++ ; - subv_realloc = (int64_t *)realloc( (char *)subv , (size_t)(sizeof(int64_t)*(nout+1))) ; + subv_realloc = (int64_t *)realloc( (char *)subv , sizeof(int64_t)*(size_t)(nout+1)) ; if( !subv_realloc ) { free(subv); fprintf(stderr,"** nifti_get_intlist: failed realloc of %" PRId64 @@ -9786,7 +9786,7 @@ int64_t * nifti_get_int64list( int64_t nvals , const char * str ) for( ii=ibot ; (ii-itop)*istep <= 0 ; ii += istep ){ nout++ ; - subv_realloc = (int64_t *)realloc( (char *)subv , (size_t)(sizeof(int64_t)*(nout+1))) ; + subv_realloc = (int64_t *)realloc( (char *)subv , sizeof(int64_t)*(size_t)(nout+1)) ; if( !subv_realloc ) { free(subv); fprintf(stderr,"** nifti_get_intlist: failed realloc of %" PRId64 @@ -9837,7 +9837,7 @@ int * nifti_get_intlist( int nvals , const char * str ) } /* have a valid result, copy as ints */ - ilist = (int *)malloc((size_t)((nints+1) * sizeof(int))); + ilist = (int *)malloc((size_t)(nints+1) * sizeof(int)); if( !ilist ) { fprintf(stderr,"** nifti_get_intlist: failed to alloc %" PRId64 " ints\n", nints); diff --git a/niftilib/nifti1_io.c b/niftilib/nifti1_io.c index 3a327692..c9142554 100644 --- a/niftilib/nifti1_io.c +++ b/niftilib/nifti1_io.c @@ -834,7 +834,7 @@ static int nifti_load_NBL_bricks( nifti_image * nim , const int * slist, const i fprintf(stderr,"** load bricks: ztell failed??\n"); return -1; } - fposn = oposn = test; + fposn = oposn = (size_t)test; /* first, handle the default case, no passed blist */ if( !slist ){ @@ -867,8 +867,8 @@ static int nifti_load_NBL_bricks( nifti_image * nim , const int * slist, const i if( isrc != prev ){ /* if we are not looking at the correct sub-brick, scan forward */ - if( fposn != (oposn + isrc*NBL->bsize) ){ - fposn = oposn + isrc*NBL->bsize; + if( fposn != (oposn + (size_t)isrc*NBL->bsize) ){ + fposn = oposn + (size_t)isrc*NBL->bsize; if( znzseek(fp, (long)fposn, SEEK_SET) < 0 ){ fprintf(stderr,"** failed to locate brick %d in file '%s'\n", isrc, nim->iname ? nim->iname : nim->fname); @@ -918,7 +918,8 @@ static int nifti_alloc_NBL_mem(const nifti_image * nim, int nbricks, nbl->nbricks *= nim->dim[c]; } - nbl->bsize = (size_t)nim->nx * nim->ny * nim->nz * nim->nbyper;/* bytes */ + nbl->bsize = (size_t)nim->nx * (size_t)nim->ny + * (size_t)nim->nz * (size_t)nim->nbyper; /* bytes */ nbl->bricks = (void **)malloc((size_t)nbl->nbricks * sizeof(void *)); if( ! nbl->bricks ){ @@ -2448,7 +2449,7 @@ int nifti_get_filesize( const char *pathname ) if( pathname == NULL || *pathname == '\0' ) return -1 ; ii = stat( pathname , &buf ); if( ii != 0 ) return -1 ; - return (unsigned int)buf.st_size ; + return (int)buf.st_size ; } #else /*---------- non-Unix version of the above, less efficient -----------*/ @@ -4336,8 +4337,8 @@ nifti_image *nifti_image_read( const char *hname , int read_data ) } /**- check for extensions (any errors here means no extensions) */ - if( NIFTI_ONEFILE(nhdr) ) remaining = nim->iname_offset - sizeof(nhdr); - else remaining = filesize - sizeof(nhdr); + if( NIFTI_ONEFILE(nhdr) ) remaining = nim->iname_offset - (int)sizeof(nhdr); + else remaining = filesize - (int)sizeof(nhdr); (void)nifti_read_extensions(nim, fp, remaining); @@ -4932,15 +4933,17 @@ static znzFile nifti_image_load_prep( nifti_image *nim ) znzclose(fp); return NULL; } - ii = nifti_get_filesize( nim->iname ) ; - if( ii == 0 ){ - if( g_opts.debug > 0 ) LNI_FERR(fname,"empty data file",nim->iname); + const int64_t fsize = nifti_get_filesize( nim->iname ) ; + if( fsize <= 0 ){ + if( g_opts.debug > 0 ) + LNI_FERR(fname,"empty or unreadable data file",nim->iname); znzclose(fp); return NULL; } + ii = (size_t)fsize ; ioff = (ii > ntot) ? ii-ntot : 0 ; } else { /* non-negative offset */ - ioff = nim->iname_offset ; /* means use it directly */ + ioff = (size_t)nim->iname_offset ; /* means use it directly */ } /**- seek to the appropriate read position */ @@ -5071,7 +5074,7 @@ size_t nifti_read_buffer(znzFile fp, void* dataptr, size_t ntot, if( nim->swapsize > 1 && nim->byteorder != nifti_short_order() ) { if( g_opts.debug > 1 ) fprintf(stderr,"+d nifti_read_buffer: swapping data bytes...\n"); - nifti_swap_Nbytes( (int)(ntot / nim->swapsize), nim->swapsize , dataptr ) ; + nifti_swap_Nbytes( ntot / (size_t)nim->swapsize, nim->swapsize , dataptr ) ; } #ifdef isfinite @@ -5247,11 +5250,11 @@ int nifti_write_all_data(znzFile fp, nifti_image * nim, return -1; } - ss = nifti_write_buffer(fp,nim->data,nim->nbyper * nim->nvox); - if (ss < nim->nbyper * nim->nvox){ + ss = nifti_write_buffer(fp,nim->data, (size_t)nim->nbyper * nim->nvox); + if (ss < (size_t)nim->nbyper * nim->nvox){ fprintf(stderr, "** ERROR: NWAD: wrote only %u of %u bytes to file\n", - (unsigned)ss, (unsigned)(nim->nbyper * nim->nvox)); + (unsigned)ss, (unsigned)((size_t)nim->nbyper * nim->nvox)); return -1; } @@ -5324,7 +5327,7 @@ static int nifti_write_extensions(znzFile fp, nifti_image *nim) ok = (size == (int)sizeof(int)); } if( ok ){ - size = (int)nifti_write_buffer(fp, list->edata, list->esize - 8); + size = (int)nifti_write_buffer(fp, list->edata, (size_t)(list->esize - 8)); ok = (size == list->esize - 8); } @@ -5493,7 +5496,7 @@ nifti_image * nifti_make_new_nim(const int dims[8], int datatype, int data_fill) /* if we cannot allocate data, take ball and go home */ if( !nim->data ) { fprintf(stderr,"** NMNN: failed to alloc %u bytes for data\n", - (unsigned)(nim->nvox*nim->nbyper)); + (unsigned)(nim->nvox*(size_t)nim->nbyper)); nifti_image_free(nim); nim = NULL; } @@ -5736,7 +5739,7 @@ void nifti_set_iname_offset(nifti_image *nim) /* NIFTI-1 single binary file - always update */ case NIFTI_FTYPE_NIFTI1_1: - offset = nifti_extension_size(nim)+sizeof(struct nifti_1_header)+4; + offset = nifti_extension_size(nim)+(int)sizeof(struct nifti_1_header)+4; /* be sure offset is aligned to a 16 byte boundary */ if ( ( offset % 16 ) != 0 ) offset = ((offset + 0xf) & ~0xf); if( nim->iname_offset != offset ){ @@ -6821,8 +6824,9 @@ nifti_image *nifti_image_from_ascii( const char *str, int * bytes_read ) nim->dim[6] = nim->nv ; nim->pixdim[6] = nim->dv ; nim->dim[7] = nim->nw ; nim->pixdim[7] = nim->dw ; - nim->nvox = (size_t)nim->nx * nim->ny * nim->nz - * nim->nt * nim->nu * nim->nv * nim->nw ; + nim->nvox = (size_t)nim->nx * (size_t)nim->ny * (size_t)nim->nz + * (size_t)nim->nt * (size_t)nim->nu * (size_t)nim->nv + * (size_t)nim->nw ; if( nim->qform_code > 0 ) nim->qto_xyz = nifti_quatern_to_mat44( @@ -7084,7 +7088,7 @@ int nifti_read_collapsed_image( nifti_image * nim, const int dims [8], /** - call the recursive reading function, passing nim, the pivot info, location to store memory, and file pointer and position */ c = rci_read_data(nim, pivots,prods,nprods,dims, - (char *)*data, fp, znztell(fp)); + (char *)*data, fp, (size_t)znztell(fp)); znzclose(fp); /* in any case, close the file */ if( c < 0 ){ free(*data); *data = NULL; return -1; } /* failure */ @@ -7296,7 +7300,7 @@ int nifti_read_subregion_image( nifti_image * nim, (si[0] * strides[0]); znzseek(fp, offset, SEEK_SET); /* seek to current row */ read_amount = rs[0] * nim->nbyper; /* read a row of the subregion*/ - nread = (int)nifti_read_buffer(fp, readptr, read_amount, nim); + nread = (int)nifti_read_buffer(fp, readptr, (size_t)read_amount, nim); if(nread != read_amount) { if(g_opts.debug > 0) @@ -7351,7 +7355,7 @@ static int rci_read_data(nifti_image * nim, int * pivots, int * prods, /* so just seek and read (prods[0] * nbyper) bytes from the file */ znzseek(fp, (long)base_offset, SEEK_SET); - bytes = (size_t)prods[0] * nim->nbyper; + bytes = (size_t)prods[0] * (size_t)nim->nbyper; nread = nifti_read_buffer(fp, data, bytes, nim); if( nread != bytes ){ fprintf(stderr,"** rciRD: read only %u of %u bytes from '%s'\n", @@ -7378,18 +7382,18 @@ static int rci_read_data(nifti_image * nim, int * pivots, int * prods, /* offset is (c * sub-block size (including pivot dim)) */ /* + (dims[] index into pivot sub-block) */ /* the unneeded multiplication is to make this more clear */ - offset = (size_t)c * sublen * nim->dim[*pivots] + - (size_t)sublen * dims[*pivots]; + offset = (size_t)c * sublen * (size_t)nim->dim[*pivots] + + sublen * (size_t)dims[*pivots]; offset *= (size_t)nim->nbyper; if( g_opts.debug > 3 ) fprintf(stderr,"-d reading %u bytes, foff %u + %u, doff %u\n", (unsigned)read_size, (unsigned)base_offset, (unsigned)offset, - (unsigned)(c*read_size)); + (unsigned)((size_t)c*read_size)); /* now read the next level down, adding this offset */ if( rci_read_data(nim, pivots+1, prods+1, nprods-1, dims, - data + c * read_size, fp, base_offset + offset) < 0 ) + data + (size_t)c * read_size, fp, base_offset + offset) < 0 ) return -1; } @@ -7586,7 +7590,7 @@ int * nifti_get_intlist( int nvals , const char * str ) if( str[ipos] == ',' || ISEND(str[ipos]) ){ nout++ ; - subv_realloc = (int *)realloc( (char *)subv , (size_t)(sizeof(int) * (nout+1))) ; + subv_realloc = (int *)realloc( (char *)subv , sizeof(int) * (size_t)(nout+1)) ; if( !subv_realloc ) { free(subv); fprintf(stderr,"** nifti_get_intlist: failed realloc of %d ints\n", @@ -7677,7 +7681,7 @@ int * nifti_get_intlist( int nvals , const char * str ) for( ii=ibot ; (ii-itop)*istep <= 0 ; ii += istep ){ nout++ ; - subv_realloc = (int *)realloc( (char *)subv , (size_t)(sizeof(int) * (nout+1))) ; + subv_realloc = (int *)realloc( (char *)subv , sizeof(int) * (size_t)(nout+1)) ; if( !subv_realloc ) { free(subv); fprintf(stderr,"** nifti_get_intlist: failed realloc of %d ints\n", diff --git a/niftilib/nifti1_tool.c b/niftilib/nifti1_tool.c index 6a89899e..5d9f89a6 100644 --- a/niftilib/nifti1_tool.c +++ b/niftilib/nifti1_tool.c @@ -2980,8 +2980,7 @@ int modify_field(void * basep, field_s * field, const char * data) return 1; } /* otherwise, we're good */ - { const int16_t sval = (int16_t)val; - memcpy((char *)basep + field->offset + (size_t)fc * sizeof(sval), &sval,sizeof(sval)); } + ((short *)((char *)basep + field->offset))[fc] = (short)val; if( g_debug > 1 ) fprintf(stderr,"+d setting posn %d of '%s' to %d\n", fc, field->name, val); @@ -3000,8 +2999,7 @@ int modify_field(void * basep, field_s * field, const char * data) fc,field->len); return 1; } - { const int32_t ival = (int32_t)val; - memcpy((char *)basep + field->offset + (size_t)fc * sizeof(ival), &ival,sizeof(ival)); } + ((int *)((char *)basep + field->offset))[fc] = val; if( g_debug > 1 ) fprintf(stderr,"+d setting posn %d of '%s' to %d\n", fc, field->name, val); @@ -3021,7 +3019,7 @@ int modify_field(void * basep, field_s * field, const char * data) return 1; } /* otherwise, we're good */ - memcpy((char *)basep + field->offset + (size_t)fc * sizeof(fval), &fval,sizeof(fval)); + ((float *)((char *)basep + field->offset))[fc] = fval; if( g_debug > 1 ) fprintf(stderr,"+d setting posn %d of '%s' to %f\n", fc, field->name, fval); @@ -3480,7 +3478,7 @@ int disp_field( const char *mesg, field_s *fieldp, void * str, int nfields, int int len; /* start by sucking the pointer stored here */ - memcpy(&sp, (const char *)str + fp->offset, sizeof(sp)); + sp = *(char **)((char *)str + fp->offset); if( ! sp ){ fprintf(stdout,"(NULL)\n"); break; } /* anything? */ @@ -3494,9 +3492,7 @@ int disp_field( const char *mesg, field_s *fieldp, void * str, int nfields, int else if( *sp && !isprint(*sp) ) /* if no termination, it's bad */ fprintf(stdout,"(non-printable string)\n"); else /* woohoo! a good string */ - { char * cp; - memcpy(&cp, (const char *)str + fp->offset, sizeof(cp)); - fprintf(stdout,"'%.40s'\n", cp); } + fprintf(stdout,"'%.40s'\n",*(char **)((char *)str + fp->offset)); break; } @@ -3505,7 +3501,7 @@ int disp_field( const char *mesg, field_s *fieldp, void * str, int nfields, int nifti1_extension * extp; /* yank the address sitting there into extp */ - memcpy(&extp, (const char *)str + fp->offset, sizeof(extp)); + extp = *(nifti1_extension **)((char *)str + fp->offset); /* the user may use -disp_exts to display all of them */ if( extp ) disp_nifti1_extension(NULL, extp, 6); @@ -3564,8 +3560,8 @@ int diff_field(field_s *fieldp, void * str0, void * str1, int nfields) { nifti1_extension * ext0, * ext1; - memcpy(&ext0, (const char *)str0 + fp->offset, sizeof(ext0)); - memcpy(&ext1, (const char *)str1 + fp->offset, sizeof(ext1)); + ext0 = *(nifti1_extension **)((char *)str0 + fp->offset); + ext1 = *(nifti1_extension **)((char *)str1 + fp->offset); if( ! ext0 && ! ext1 ) break; /* continue on */ @@ -4274,7 +4270,8 @@ nifti_image * nt_read_bricks(nt_opts * opts, const char * fname, int len, int * /* now populate NBL (can be based only on len and nim) */ NBL->nbricks = len; - NBL->bsize = (size_t)nim->nbyper * nim->nx * nim->ny * nim->nz; + NBL->bsize = (size_t)nim->nbyper * (size_t)nim->nx + * (size_t)nim->ny * (size_t)nim->nz; NBL->bricks = (void **)calloc((size_t)(NBL->nbricks), (size_t)(sizeof(void *))); if( !NBL->bricks ){ fprintf(stderr,"** NRB: failed to alloc %d pointers\n",NBL->nbricks); From 756950616b4a0f745f5a75dcfa80d833b76efb44 Mon Sep 17 00:00:00 2001 From: Sean McBride Date: Thu, 1 Jan 2026 23:21:04 -0500 Subject: [PATCH 76/77] ENH: Replace strcpy, strcat and strncpy with bounded variants The old functions carry no destination bound and are rejected outright under -fbounds-safety. Each call site was converted to snprintf, strlcpy or strlcat with an explicit destination size. strncpy zero-fills the whole destination and strlcpy does not; the call sites relied on that only where the buffer came from calloc, except one that now zeroes explicitly. A few declarations moved to first use. --- nifti2/nifti2_io.c | 113 ++++++++++++++++++++----------------- nifti2/nifti_tool.c | 3 +- nifticdf/nifticdf.c | 18 +++--- niftilib/nifti1_io.c | 111 +++++++++++++++++++----------------- niftilib/nifti1_test.c | 23 ++++---- niftilib/nifti1_tool.c | 3 +- niftilib/nifti_tester001.c | 8 +-- 7 files changed, 145 insertions(+), 134 deletions(-) diff --git a/nifti2/nifti2_io.c b/nifti2/nifti2_io.c index 70c136e8..19dd1196 100644 --- a/nifti2/nifti2_io.c +++ b/nifti2/nifti2_io.c @@ -1284,13 +1284,13 @@ char *nifti_strdup(const char *str) { if( !str ) return NULL; /* allow calls passing NULL */ - size_t length = strlen(str); - char *dup = (char *)malloc(length + 1); + size_t length = strlen(str) + 1; + char *dup = (char *)malloc(length); /* check for failure */ - if( dup ) strcpy(dup, str); + if( dup ) strlcpy(dup, str, length); else fprintf(stderr,"** nifti_strdup: failed to alloc %zu bytes\n", - length+1); + length); return dup; } @@ -3528,7 +3528,7 @@ const char * nifti_find_file_extension( const char * name ) ext = name + len - 4; /* make manipulation copy, and possibly convert to lowercase */ - strcpy(extcopy, ext); + strlcpy(extcopy, ext, sizeof(extcopy)); if( g_opts.allow_upper_fext ) make_lowercase(extcopy); /* if it look like a basic extension, fail or return it */ @@ -3547,11 +3547,13 @@ const char * nifti_find_file_extension( const char * name ) ext = name + len - 7; /* make manipulation copy, and possibly convert to lowercase */ - strcpy(extcopy, ext); + strlcpy(extcopy, ext, sizeof(extcopy)); if( g_opts.allow_upper_fext ) make_lowercase(extcopy); /* go after .gz extensions using the modifiable strings */ - strcat(elist[0], extgz); strcat(elist[1], extgz); strcat(elist[2], extgz); + strlcat(elist[0], extgz, 8); + strlcat(elist[1], extgz, 8); + strlcat(elist[2], extgz, 8); if( compare_strlist(extcopy, elist, 3) >= 0 ) { if( is_mixedcase(ext) ) { @@ -3749,15 +3751,16 @@ char * nifti_findhdrname(const char* fname) make_uppercase(extzip); } - hdrname = (char *)calloc(sizeof(char),strlen(basename)+8); + size_t hdrnamelength = strlen(basename)+8; + hdrname = (char *)calloc(sizeof(char),hdrnamelength); if( !hdrname ){ fprintf(stderr,"** nifti_findhdrname: failed to alloc hdrname\n"); free(basename); return NULL; } - strcpy(hdrname,basename); - strcat(hdrname,elist[efirst]); + strlcpy(hdrname, basename, hdrnamelength); + strlcat(hdrname, elist[efirst], hdrnamelength); #ifdef FSLSTYLE if (nifti_fileexists(hdrname)) { /* basename is read by the error message below, so it cannot be @@ -3765,14 +3768,15 @@ char * nifti_findhdrname(const char* fname) checked; and a library reports an ambiguous name to its caller rather than ending the process, which means every path out of here now has to release what it holds. */ - char *gzname = (char *)calloc(sizeof(char),strlen(hdrname)+8); + size_t gznamelength = strlen(hdrname)+8; + char *gzname = (char *)calloc(sizeof(char),gznamelength); if( !gzname ){ fprintf(stderr,"** nifti_findhdrname: failed to alloc gzname\n"); free(basename); free(hdrname); return NULL; } - strcpy(gzname, hdrname); - strcat(gzname,extzip); + strlcpy(gzname, hdrname, gznamelength); + strlcat(gzname,extzip,gznamelength); if (nifti_fileexists(gzname)) { fprintf(stderr,"Image Exception : Multiple possible filenames detected for basename (*.nii, *.nii.gz): %s\n", basename); free(gzname); free(basename); free(hdrname); @@ -3786,7 +3790,7 @@ char * nifti_findhdrname(const char* fname) if (nifti_fileexists(hdrname)) { free(basename); return hdrname; } #endif #ifdef HAVE_ZLIB - strcat(hdrname,extzip); + strlcat(hdrname, extzip, hdrnamelength); if (nifti_fileexists(hdrname)) { free(basename); return hdrname; } #endif @@ -3794,11 +3798,11 @@ char * nifti_findhdrname(const char* fname) efirst = 1 - efirst; - strcpy(hdrname,basename); - strcat(hdrname,elist[efirst]); + strlcpy(hdrname, basename, hdrnamelength); + strlcat(hdrname, elist[efirst], hdrnamelength); if (nifti_fileexists(hdrname)) { free(basename); return hdrname; } #ifdef HAVE_ZLIB - strcat(hdrname,extzip); + strlcat(hdrname, extzip, hdrnamelength); if (nifti_fileexists(hdrname)) { free(basename); return hdrname; } #endif @@ -3837,8 +3841,9 @@ char * nifti_findimgname(const char* fname , int nifti_type) /* check input file(s) for sanity */ if( !nifti_validfilename(fname) ) return NULL; - basename = nifti_makebasename(fname); - imgname = (char *)calloc(sizeof(char),strlen(basename)+8); + basename = nifti_makebasename(fname); + size_t imgnamelength = strlen(basename)+8; + imgname = (char *)calloc(sizeof(char),imgnamelength); if( !imgname ){ fprintf(stderr,"** nifti_findimgname: failed to alloc imgname\n"); free(basename); @@ -3856,8 +3861,8 @@ char * nifti_findimgname(const char* fname , int nifti_type) /* only valid extension for ASCII type is .nia, handle first */ if( nifti_type == NIFTI_FTYPE_ASCII ){ - strcpy(imgname,basename); - strcat(imgname,extnia); + strlcpy(imgname, basename, imgnamelength); + strlcat(imgname, extnia, imgnamelength); if (nifti_fileexists(imgname)) { free(basename); return imgname; } } else { @@ -3871,21 +3876,21 @@ char * nifti_findimgname(const char* fname , int nifti_type) else if (nifti_type == NIFTI_FTYPE_NIFTI2_1) first = 0; else first = 1; /* should match .img */ - strcpy(imgname,basename); - strcat(imgname,elist[first]); + strlcpy(imgname, basename, imgnamelength); + strlcat(imgname, elist[first], imgnamelength); if (nifti_fileexists(imgname)) { free(basename); return imgname; } #ifdef HAVE_ZLIB /* then also check for .gz */ - strcat(imgname,extzip); + strlcat(imgname, extzip, imgnamelength); if (nifti_fileexists(imgname)) { free(basename); return imgname; } #endif /* failed to find image file with expected extension, try the other */ - strcpy(imgname,basename); - strcat(imgname,elist[1-first]); /* can do this with only 2 choices */ + strlcpy(imgname, basename, imgnamelength); + strlcat(imgname, elist[1-first], imgnamelength); /* can do this with only 2 choices */ if (nifti_fileexists(imgname)) { free(basename); return imgname; } #ifdef HAVE_ZLIB /* then also check for .gz */ - strcat(imgname,extzip); + strlcat(imgname, extzip, imgnamelength); if (nifti_fileexists(imgname)) { free(basename); return imgname; } #endif } @@ -3925,12 +3930,13 @@ char * nifti_makehdrname(const char * prefix, int nifti_type, int check, if( !nifti_validfilename(prefix) ) return NULL; /* add space for extension, optional ".gz", and null char */ - iname = (char *)calloc(sizeof(char),strlen(prefix)+8); + size_t inamelength = strlen(prefix)+8; + iname = (char *)calloc(sizeof(char),inamelength); if( !iname ){ fprintf(stderr,"** NIFTI small malloc failure!\n"); return NULL; } - strcpy(iname, prefix); + strlcpy(iname, prefix, inamelength); /* use any valid extension */ if( (ext = nifti_find_file_extension(iname)) != NULL ){ @@ -3949,13 +3955,13 @@ char * nifti_makehdrname(const char * prefix, int nifti_type, int check, } } /* otherwise, make one up */ - else if( nifti_type == NIFTI_FTYPE_NIFTI1_1 ) strcat(iname, extnii); - else if( nifti_type == NIFTI_FTYPE_NIFTI2_1 ) strcat(iname, extnii); - else if( nifti_type == NIFTI_FTYPE_ASCII ) strcat(iname, extnia); - else strcat(iname, exthdr); + else if( nifti_type == NIFTI_FTYPE_NIFTI1_1 ) strlcat(iname, extnii, inamelength); + else if( nifti_type == NIFTI_FTYPE_NIFTI2_1 ) strlcat(iname, extnii, inamelength); + else if( nifti_type == NIFTI_FTYPE_ASCII ) strlcat(iname, extnia, inamelength); + else strlcat(iname, exthdr, inamelength); #ifdef HAVE_ZLIB /* if compression is requested, make sure of suffix */ - if( comp && (!ext || !strstr(iname,extgz)) ) strcat(iname,extgz); + if( comp && (!ext || !strstr(iname,extgz)) ) strlcat(iname, extgz, inamelength); #endif /* check for existence failure */ @@ -4000,12 +4006,13 @@ char * nifti_makeimgname(const char * prefix, int nifti_type, int check, if( !nifti_validfilename(prefix) ) return NULL; /* add space for extension, optional ".gz", and null char */ - iname = (char *)calloc(sizeof(char),strlen(prefix)+8); + size_t inamelength = strlen(prefix)+8; + iname = (char *)calloc(sizeof(char),inamelength); if( !iname ){ fprintf(stderr,"** NIFTI: small malloc failure!\n"); return NULL; } - strcpy(iname, prefix); + strlcpy(iname, prefix, inamelength); /* use any valid extension */ if( (ext = nifti_find_file_extension(iname)) != NULL ){ @@ -4024,13 +4031,13 @@ char * nifti_makeimgname(const char * prefix, int nifti_type, int check, } } /* otherwise, make one up */ - else if( nifti_type == NIFTI_FTYPE_NIFTI1_1 ) strcat(iname, extnii); - else if( nifti_type == NIFTI_FTYPE_NIFTI2_1 ) strcat(iname, extnii); - else if( nifti_type == NIFTI_FTYPE_ASCII ) strcat(iname, extnia); - else strcat(iname, extimg); + else if( nifti_type == NIFTI_FTYPE_NIFTI1_1 ) strlcat(iname, extnii, inamelength); + else if( nifti_type == NIFTI_FTYPE_NIFTI2_1 ) strlcat(iname, extnii, inamelength); + else if( nifti_type == NIFTI_FTYPE_ASCII ) strlcat(iname, extnia, inamelength); + else strlcat(iname, extimg, inamelength); #ifdef HAVE_ZLIB /* if compression is requested, make sure of suffix */ - if( comp && (!ext || !strstr(iname,extgz)) ) strcat(iname,extgz); + if( comp && (!ext || !strstr(iname,extgz)) ) strlcat(iname, extgz, inamelength); #endif /* check for existence failure */ @@ -7395,7 +7402,7 @@ nifti_1_header * nifti_make_new_n1_header(const int64_t arg_dims[8], nifti_datatype_sizes( nhdr->datatype , &nbyper, &swapsize ); nhdr->bitpix = 8 * nbyper ; - strcpy(nhdr->magic, "n+1"); /* init to single file */ + strlcpy(nhdr->magic, "n+1", sizeof(nhdr->magic)); /* init to single file */ return nhdr; } @@ -7527,8 +7534,8 @@ int nifti_convert_nim2n1hdr(const nifti_image * nim, nifti_1_header * hdr) if( nim->nifti_type > NIFTI_FTYPE_ANALYZE ){ /* then not ANALYZE */ - if( nim->nifti_type == NIFTI_FTYPE_NIFTI1_1 ) strcpy(nhdr.magic,"n+1") ; - else strcpy(nhdr.magic,"ni1") ; + if( nim->nifti_type == NIFTI_FTYPE_NIFTI1_1 ) strlcpy(nhdr.magic, "n+1", sizeof(nhdr.magic)) ; + else strlcpy(nhdr.magic, "ni1", sizeof(nhdr.magic)) ; nhdr.pixdim[1] = (float)fabs(nhdr.pixdim[1]) ; nhdr.pixdim[2] = (float)fabs(nhdr.pixdim[2]) ; @@ -7912,10 +7919,10 @@ znzFile nifti_image_write_hdr_img2(nifti_image *nim, int write_opts, static int doPigz2(nifti_image *nim, struct nifti_2_header nhdr, const nifti_brick_list * NBL) { FILE *pigzPipe; char command[768]; - strcpy(command, "pigz" ); - strcat(command, " -n -f > \""); - strcat(command, nim->fname); - strcat(command, "\""); + strlcpy(command, "pigz", sizeof(command)); + strlcat(command, " -n -f > \"", sizeof(command)); + strlcat(command, nim->fname, sizeof(command)); + strlcat(command, "\"", sizeof(command)); #ifdef _MSC_VER if (( pigzPipe = _popen(command, "w")) == NULL) return -1; @@ -7944,10 +7951,10 @@ static int doPigz2(nifti_image *nim, struct nifti_2_header nhdr, const nifti_bri static int doPigz(nifti_image *nim, struct nifti_1_header nhdr, const nifti_brick_list * NBL) { FILE *pigzPipe; char command[768]; - strcpy(command, "pigz" ); - strcat(command, " -n -f > \""); - strcat(command, nim->fname); - strcat(command, "\""); + strlcpy(command, "pigz", sizeof(command)); + strlcat(command, " -n -f > \"", sizeof(command)); + strlcat(command, nim->fname, sizeof(command)); + strlcat(command, "\"", sizeof(command)); #ifdef _MSC_VER if (( pigzPipe = _popen(command, "w")) == NULL) return -1; @@ -8783,7 +8790,7 @@ int nifti_short_order(void) /* determine this CPU's byte order */ put rhs string into nim->"nam" string, with field size = "sz" */ #define QSTR(nam,sz) if( strcmp(lhs,#nam) == 0 ) \ - strncpy(nim->nam,rhs,sz), nim->nam[(sz)-1]='\0' + memset(nim->nam, 0, sz), strlcpy(nim->nam,rhs,sz) /*---------------------------------------------------------------------------*/ /*! Take an XML-ish ASCII string and create a NIFTI image header to match. diff --git a/nifti2/nifti_tool.c b/nifti2/nifti_tool.c index fdde86f6..48e9fb53 100644 --- a/nifti2/nifti_tool.c +++ b/nifti2/nifti_tool.c @@ -6123,8 +6123,7 @@ int fill_field( field_s * fp, int type, int offset, int num, const char * name ) fp->size = 1; /* init before check */ fp->len = num; - strncpy(fp->name, name, sizeof(fp->name)); - fp->name[sizeof(fp->name) - 1] = 0; + strlcpy(fp->name, name, sizeof(fp->name)); switch( type ){ case DT_UNKNOWN: diff --git a/nifticdf/nifticdf.c b/nifticdf/nifticdf.c index a2909d90..e97c9317 100644 --- a/nifticdf/nifticdf.c +++ b/nifticdf/nifticdf.c @@ -11044,19 +11044,21 @@ char const * const inam[]={ NULL , NULL , int nifti_intent_code( const char *name ) { - char *unam , *upt ; - int ii ; - - if( name == NULL || *name == '\0' ) return -1 ; + if( name == NULL || *name == '\0' ) + return -1 ; - unam = (char *)malloc(strlen(name)+1); + size_t size = strlen(name)+1; + char *unam = (char *)malloc(size); if (!unam) return -1 ; - strcpy(unam,name); - for( upt=unam ; *upt != '\0' ; upt++ ) *upt = (char)toupper(*upt) ; + strlcpy(unam,name,size); + for( char *upt=unam ; *upt != '\0' ; upt++ ) + *upt = (char)toupper(*upt) ; + int ii ; for( ii=NIFTI_FIRST_STATCODE ; ii <= NIFTI_LAST_STATCODE ; ii++ ) - if( strcmp(inam[ii],unam) == 0 ) break ; + if( strcmp(inam[ii],unam) == 0 ) + break ; free(unam) ; return (ii <= NIFTI_LAST_STATCODE) ? ii : -1 ; diff --git a/niftilib/nifti1_io.c b/niftilib/nifti1_io.c index c9142554..6eb2f8dd 100644 --- a/niftilib/nifti1_io.c +++ b/niftilib/nifti1_io.c @@ -1179,13 +1179,13 @@ char *nifti_strdup(const char *str) { if( !str ) return NULL; /* allow calls passing NULL */ - size_t length = strlen(str); - char *dup = (char *)malloc(length + 1); + size_t length = strlen(str) + 1; + char *dup = (char *)malloc(length); /* check for failure */ - if( dup ) strcpy(dup, str); + if( dup ) strlcpy(dup, str, length); else fprintf(stderr,"** nifti_strdup: failed to alloc %zu bytes\n", - length+1); + length); return dup; } @@ -2615,7 +2615,7 @@ const char * nifti_find_file_extension( const char * name ) ext = name + len - 4; /* make manipulation copy, and possibly convert to lowercase */ - strcpy(extcopy, ext); + strlcpy(extcopy, ext, sizeof(extcopy)); if( g_opts.allow_upper_fext ) make_lowercase(extcopy); /* if it look like a basic extension, fail or return it */ @@ -2633,11 +2633,13 @@ const char * nifti_find_file_extension( const char * name ) ext = name + len - 7; /* make manipulation copy, and possibly convert to lowercase */ - strcpy(extcopy, ext); + strlcpy(extcopy, ext, sizeof(extcopy)); if( g_opts.allow_upper_fext ) make_lowercase(extcopy); /* go after .gz extensions using the modifiable strings */ - strcat(elist[0], extgz); strcat(elist[1], extgz); strcat(elist[2], extgz); + strlcat(elist[0], extgz, 8); + strlcat(elist[1], extgz, 8); + strlcat(elist[2], extgz, 8); if( compare_strlist(extcopy, elist, 3) >= 0 ) { if( is_mixedcase(ext) ) { @@ -2812,15 +2814,16 @@ char * nifti_findhdrname(const char* fname) make_uppercase(extzip); } - hdrname = (char *)calloc(sizeof(char),strlen(basename)+8); + size_t hdrnamelength = strlen(basename)+8; + hdrname = (char *)calloc(sizeof(char),hdrnamelength); if( !hdrname ){ fprintf(stderr,"** nifti_findhdrname: failed to alloc hdrname\n"); free(basename); return NULL; } - strcpy(hdrname,basename); - strcat(hdrname,elist[efirst]); + strlcpy(hdrname, basename, hdrnamelength); + strlcat(hdrname, elist[efirst], hdrnamelength); #ifdef FSLSTYLE if (nifti_fileexists(hdrname)) { /* basename is read by the error message below, so it cannot be @@ -2828,14 +2831,15 @@ char * nifti_findhdrname(const char* fname) checked; and a library reports an ambiguous name to its caller rather than ending the process, which means every path out of here now has to release what it holds. */ - char *gzname = (char *)calloc(sizeof(char),strlen(hdrname)+8); + size_t gznamelength = strlen(hdrname)+8; + char *gzname = (char *)calloc(sizeof(char),gznamelength); if( !gzname ){ fprintf(stderr,"** nifti_findhdrname: failed to alloc gzname\n"); free(basename); free(hdrname); return NULL; } - strcpy(gzname, hdrname); - strcat(gzname,extzip); + strlcpy(gzname, hdrname, gznamelength); + strlcat(gzname,extzip,gznamelength); if (nifti_fileexists(gzname)) { fprintf(stderr,"Image Exception : Multiple possible filenames detected for basename (*.nii, *.nii.gz): %s\n", basename); free(gzname); free(basename); free(hdrname); @@ -2849,7 +2853,7 @@ char * nifti_findhdrname(const char* fname) if (nifti_fileexists(hdrname)) { free(basename); return hdrname; } #endif #ifdef HAVE_ZLIB - strcat(hdrname,extzip); + strlcat(hdrname, extzip, hdrnamelength); if (nifti_fileexists(hdrname)) { free(basename); return hdrname; } #endif @@ -2857,11 +2861,11 @@ char * nifti_findhdrname(const char* fname) efirst = 1 - efirst; - strcpy(hdrname,basename); - strcat(hdrname,elist[efirst]); + strlcpy(hdrname, basename, hdrnamelength); + strlcat(hdrname, elist[efirst], hdrnamelength); if (nifti_fileexists(hdrname)) { free(basename); return hdrname; } #ifdef HAVE_ZLIB - strcat(hdrname,extzip); + strlcat(hdrname, extzip, hdrnamelength); if (nifti_fileexists(hdrname)) { free(basename); return hdrname; } #endif @@ -2900,8 +2904,9 @@ char * nifti_findimgname(const char* fname , int nifti_type) /* check input file(s) for sanity */ if( !nifti_validfilename(fname) ) return NULL; - basename = nifti_makebasename(fname); - imgname = (char *)calloc(sizeof(char),strlen(basename)+8); + basename = nifti_makebasename(fname); + size_t imgnamelength = strlen(basename)+8; + imgname = (char *)calloc(sizeof(char),imgnamelength); if( !imgname ){ fprintf(stderr,"** nifti_findimgname: failed to alloc imgname\n"); free(basename); @@ -2919,8 +2924,8 @@ char * nifti_findimgname(const char* fname , int nifti_type) /* only valid extension for ASCII type is .nia, handle first */ if( nifti_type == NIFTI_FTYPE_ASCII ){ - strcpy(imgname,basename); - strcat(imgname,extnia); + strlcpy(imgname, basename, imgnamelength); + strlcat(imgname, extnia, imgnamelength); if (nifti_fileexists(imgname)) { free(basename); return imgname; } } else { @@ -2933,21 +2938,21 @@ char * nifti_findimgname(const char* fname , int nifti_type) if (nifti_type == NIFTI_FTYPE_NIFTI1_1) first = 0; /* should match .nii */ else first = 1; /* should match .img */ - strcpy(imgname,basename); - strcat(imgname,elist[first]); + strlcpy(imgname, basename, imgnamelength); + strlcat(imgname, elist[first], imgnamelength); if (nifti_fileexists(imgname)) { free(basename); return imgname; } #ifdef HAVE_ZLIB /* then also check for .gz */ - strcat(imgname,extzip); + strlcat(imgname, extzip, imgnamelength); if (nifti_fileexists(imgname)) { free(basename); return imgname; } #endif /* failed to find image file with expected extension, try the other */ - strcpy(imgname,basename); - strcat(imgname,elist[1-first]); /* can do this with only 2 choices */ + strlcpy(imgname, basename, imgnamelength); + strlcat(imgname, elist[1-first], imgnamelength); /* can do this with only 2 choices */ if (nifti_fileexists(imgname)) { free(basename); return imgname; } #ifdef HAVE_ZLIB /* then also check for .gz */ - strcat(imgname,extzip); + strlcat(imgname, extzip, imgnamelength); if (nifti_fileexists(imgname)) { free(basename); return imgname; } #endif } @@ -2987,9 +2992,10 @@ char * nifti_makehdrname(const char * prefix, int nifti_type, int check, if( !nifti_validfilename(prefix) ) return NULL; /* add space for extension, optional ".gz", and null char */ - iname = (char *)calloc(sizeof(char),strlen(prefix)+8); + size_t inamelength = strlen(prefix)+8; + iname = (char *)calloc(sizeof(char),inamelength); if( !iname ){ fprintf(stderr,"** small malloc failure!\n"); return NULL; } - strcpy(iname, prefix); + strlcpy(iname, prefix, inamelength); /* use any valid extension */ if( (ext = nifti_find_file_extension(iname)) != NULL ){ @@ -3008,12 +3014,12 @@ char * nifti_makehdrname(const char * prefix, int nifti_type, int check, } } /* otherwise, make one up */ - else if( nifti_type == NIFTI_FTYPE_NIFTI1_1 ) strcat(iname, extnii); - else if( nifti_type == NIFTI_FTYPE_ASCII ) strcat(iname, extnia); - else strcat(iname, exthdr); + else if( nifti_type == NIFTI_FTYPE_NIFTI1_1 ) strlcat(iname, extnii, inamelength); + else if( nifti_type == NIFTI_FTYPE_ASCII ) strlcat(iname, extnia, inamelength); + else strlcat(iname, exthdr, inamelength); #ifdef HAVE_ZLIB /* if compression is requested, make sure of suffix */ - if( comp && (!ext || !strstr(iname,extgz)) ) strcat(iname,extgz); + if( comp && (!ext || !strstr(iname,extgz)) ) strlcat(iname, extgz, inamelength); #endif /* check for existence failure */ @@ -3057,9 +3063,10 @@ char * nifti_makeimgname(const char * prefix, int nifti_type, int check, if( !nifti_validfilename(prefix) ) return NULL; /* add space for extension, optional ".gz", and null char */ - iname = (char *)calloc(sizeof(char),strlen(prefix)+8); + size_t inamelength = strlen(prefix)+8; + iname = (char *)calloc(sizeof(char),inamelength); if( !iname ){ fprintf(stderr,"** small malloc failure!\n"); return NULL; } - strcpy(iname, prefix); + strlcpy(iname, prefix, inamelength); /* use any valid extension */ if( (ext = nifti_find_file_extension(iname)) != NULL ){ @@ -3078,12 +3085,12 @@ char * nifti_makeimgname(const char * prefix, int nifti_type, int check, } } /* otherwise, make one up */ - else if( nifti_type == NIFTI_FTYPE_NIFTI1_1 ) strcat(iname, extnii); - else if( nifti_type == NIFTI_FTYPE_ASCII ) strcat(iname, extnia); - else strcat(iname, extimg); + else if( nifti_type == NIFTI_FTYPE_NIFTI1_1 ) strlcat(iname, extnii, inamelength); + else if( nifti_type == NIFTI_FTYPE_ASCII ) strlcat(iname, extnia, inamelength); + else strlcat(iname, extimg, inamelength); #ifdef HAVE_ZLIB /* if compression is requested, make sure of suffix */ - if( comp && (!ext || !strstr(iname,extgz)) ) strcat(iname,extgz); + if( comp && (!ext || !strstr(iname,extgz)) ) strlcat(iname, extgz, inamelength); #endif /* check for existence failure */ @@ -5373,7 +5380,7 @@ nifti_image* nifti_simple_init_nim(void) nifti_datatype_sizes( nhdr.datatype , &nbyper, &swapsize ); nhdr.bitpix = 8 * nbyper ; - strcpy(nhdr.magic, "n+1"); /* init to single file */ + strlcpy(nhdr.magic, "n+1", sizeof(nhdr.magic)); /* init to single file */ nim = nifti_convert_nhdr2nim(nhdr,NULL); nim->fname = NULL; @@ -5454,7 +5461,7 @@ nifti_1_header * nifti_make_new_header(const int arg_dims[8], int arg_dtype) nifti_datatype_sizes( nhdr->datatype , &nbyper, &swapsize ); nhdr->bitpix = 8 * nbyper ; - strcpy(nhdr->magic, "n+1"); /* init to single file */ + strlcpy(nhdr->magic, "n+1", sizeof(nhdr->magic)); /* init to single file */ return nhdr; } @@ -5563,8 +5570,8 @@ struct nifti_1_header nifti_convert_nim2nhdr(const nifti_image * nim) if( nim->nifti_type > NIFTI_FTYPE_ANALYZE ){ /* then not ANALYZE */ - if( nim->nifti_type == NIFTI_FTYPE_NIFTI1_1 ) strcpy(nhdr.magic,"n+1") ; - else strcpy(nhdr.magic,"ni1") ; + if( nim->nifti_type == NIFTI_FTYPE_NIFTI1_1 ) strlcpy(nhdr.magic, "n+1", sizeof(nhdr.magic)) ; + else strlcpy(nhdr.magic, "ni1", sizeof(nhdr.magic)) ; nhdr.pixdim[1] = (float)fabs(nhdr.pixdim[1]) ; nhdr.pixdim[2] = (float)fabs(nhdr.pixdim[2]) ; nhdr.pixdim[3] = (float)fabs(nhdr.pixdim[3]) ; nhdr.pixdim[4] = (float)fabs(nhdr.pixdim[4]) ; @@ -5805,10 +5812,10 @@ znzFile nifti_image_write_hdr_img2(nifti_image *nim, int write_opts, int doPigz2(nifti_image *nim, struct nifti_1_header nhdr, const nifti_brick_list * NBL) { FILE *pigzPipe; char command[768]; - strcpy(command, "pigz" ); - strcat(command, " -n -f > \""); - strcat(command, nim->fname); - strcat(command, "\""); + strlcpy(command, "pigz", sizeof(command)); + strlcat(command, " -n -f > \"", sizeof(command)); + strlcat(command, nim->fname, sizeof(command)); + strlcat(command, "\"", sizeof(command)); #ifdef _MSC_VER if (( pigzPipe = _popen(command, "w")) == NULL) return -1; @@ -5838,10 +5845,10 @@ int doPigz2(nifti_image *nim, struct nifti_1_header nhdr, const nifti_brick_list static int doPigz(nifti_image *nim, struct nifti_1_header nhdr, const nifti_brick_list * NBL) { FILE *pigzPipe; char command[768]; - strcpy(command, "pigz" ); - strcat(command, " -n -f > \""); - strcat(command, nim->fname); - strcat(command, "\""); + strlcpy(command, "pigz", sizeof(command)); + strlcat(command, " -n -f > \"", sizeof(command)); + strlcat(command, nim->fname, sizeof(command)); + strlcat(command, "\"", sizeof(command)); #ifdef _MSC_VER if (( pigzPipe = _popen(command, "w")) == NULL) return -1; @@ -6644,7 +6651,7 @@ int nifti_short_order(void) /* determine this CPU's byte order */ put rhs string into nim->"nam" string, with field size = "sz" */ #define QSTR(nam,sz) if( strcmp(lhs,#nam) == 0 ) \ - strncpy(nim->nam,rhs,sz), nim->nam[(sz)-1]='\0' + memset(nim->nam, 0, sz), strlcpy(nim->nam,rhs,sz) /*---------------------------------------------------------------------------*/ /*! Take an XML-ish ASCII string and create a NIFTI image header to match. diff --git a/niftilib/nifti1_test.c b/niftilib/nifti1_test.c index 46808353..00fd9bd3 100644 --- a/niftilib/nifti1_test.c +++ b/niftilib/nifti1_test.c @@ -19,7 +19,6 @@ int main( int argc , const char *argv[] ) nifti_image *nim ; int iarg=1 , outmode=1 , argn, usegzip=0; char *tmpstr; - size_t ll; if( argc < 2 || strcmp(argv[1],"-help") == 0 ){ printf("Usage: nifti1_test [-n2|-n1|-na|-a2] infile [prefix]\n" @@ -79,24 +78,24 @@ int main( int argc , const char *argv[] ) free(nim->fname) ; free(nim->iname) ; - ll = strlen(argv[iarg]) ; + size_t ll = strlen(argv[iarg]) + 8 ; tmpstr = nifti_makebasename(argv[iarg]); - nim->fname = (char *)calloc(1,ll+8) ; strcpy(nim->fname,tmpstr) ; - nim->iname = (char *)calloc(1,ll+8) ; strcpy(nim->iname,tmpstr) ; + nim->fname = (char *)calloc(1,ll) ; strlcpy(nim->fname, tmpstr, ll) ; + nim->iname = (char *)calloc(1,ll) ; strlcpy(nim->iname, tmpstr, ll) ; free(tmpstr); if( nim->nifti_type == 1 ){ - strcat(nim->fname,".nii") ; - strcat(nim->iname,".nii") ; + strlcat(nim->fname, ".nii", ll) ; + strlcat(nim->iname, ".nii", ll) ; } else if ( nim->nifti_type == 3 ){ - strcat(nim->fname,".nia") ; - strcat(nim->iname,".nia") ; + strlcat(nim->fname, ".nia", ll) ; + strlcat(nim->iname, ".nia", ll) ; } else { - strcat(nim->fname,".hdr") ; - strcat(nim->iname,".img") ; + strlcat(nim->fname, ".hdr", ll) ; + strlcat(nim->iname, ".img", ll) ; } if (usegzip) { - strcat(nim->fname,".gz"); - strcat(nim->iname,".gz"); + strlcat(nim->fname, ".gz", ll); + strlcat(nim->iname, ".gz", ll); } if( nifti_image_write_status( nim ) ) { fprintf(stderr, "** failed to write nifti_image\n"); diff --git a/niftilib/nifti1_tool.c b/niftilib/nifti1_tool.c index 5d9f89a6..8ae791df 100644 --- a/niftilib/nifti1_tool.c +++ b/niftilib/nifti1_tool.c @@ -3356,8 +3356,7 @@ int fill_field( field_s * fp, int type, int offset, int num, const char * name ) fp->size = 1; /* init before check */ fp->len = num; - strncpy(fp->name, name, sizeof(fp->name)); - fp->name[sizeof(fp->name) - 1] = 0; + strlcpy(fp->name, name, sizeof(fp->name)); switch( type ){ case DT_UNKNOWN: diff --git a/niftilib/nifti_tester001.c b/niftilib/nifti_tester001.c index 126e6bd6..137e7e4e 100644 --- a/niftilib/nifti_tester001.c +++ b/niftilib/nifti_tester001.c @@ -80,11 +80,9 @@ static nifti_image * generate_reference_image( const char * write_image_filename reference_header.magic[1]='+'; reference_header.magic[2]='1'; reference_header.magic[3]='\0'; - /* String is purposfully too long */ - strncpy(reference_header.intent_name,"PHANTOM_DATA to be used for regression testing the nifti reader/writer",sizeof(reference_header.intent_name)); - reference_header.intent_name[sizeof(reference_header.intent_name) - 1] = 0; - strncpy(reference_header.descrip,"This is a very long dialog here to use up more than 80 characters of space to test to see if the code is robust enough to deal appropriately with very long and obnoxious lines.",sizeof(reference_header.descrip)); - reference_header.descrip[sizeof(reference_header.descrip) - 1] = 0; + /* String is purposefully too long */ + strlcpy(reference_header.intent_name,"PHANTOM_DATA to be used for regression testing the nifti reader/writer",sizeof(reference_header.intent_name)); + strlcpy(reference_header.descrip,"This is a very long dialog here to use up more than 80 characters of space to test to see if the code is robust enough to deal appropriately with very long and obnoxious lines.",sizeof(reference_header.descrip)); { int nbyper; From 129a6378ef2e03fe5be2d1e83444b56948c5626b Mon Sep 17 00:00:00 2001 From: Sean McBride Date: Fri, 2 Jan 2026 00:01:56 -0500 Subject: [PATCH 77/77] ENH: Add strlcpy and strlcat for platforms whose libc lacks them strlcpy and strlcat originate in OpenBSD and are now in the other BSDs, macOS, and glibc from 2.38. Supply implementations for the platforms that still have neither. --- string_helper/string_helper.c | 58 +++++++++++++++++++++++++++++++++++ string_helper/string_helper.h | 14 +++++++++ 2 files changed, 72 insertions(+) create mode 100644 string_helper/string_helper.c create mode 100644 string_helper/string_helper.h diff --git a/string_helper/string_helper.c b/string_helper/string_helper.c new file mode 100644 index 00000000..94d7c42e --- /dev/null +++ b/string_helper/string_helper.c @@ -0,0 +1,58 @@ +#include +#include +#include +#include +#include + +// Apple provides strlcpy and strlcat, so don't redefine them. +#ifndef __APPLE__ + +// libc 2.38 and above provides strlcpy and strlcat, so don't redefine them. +#if defined (__GLIBC__) && ((__GLIBC__ < 2) || ((__GLIBC__ == 2) && (__GLIBC_MINOR__ < 38))) + +// --------------------------------------------------------------------------------------------------------------- +size_t strlcpy(char* restrict ioDestination, const char* restrict inSource, size_t inDestinationSize) +{ + assert(inSource); + + size_t sourceLength = strlen(inSource); + + if (inDestinationSize) + { + size_t length = (sourceLength >= inDestinationSize) ? inDestinationSize - 1 : sourceLength; + memcpy(ioDestination, inSource, length); + ioDestination[length] = '\0'; + } + + return sourceLength; +} + + +// --------------------------------------------------------------------------------------------------------------- +size_t strlcat(char* restrict ioDestination, const char* restrict inSource, size_t inDestinationSize) +{ + assert(ioDestination); + assert(inSource); + + size_t destinationLength = strlen(ioDestination); + size_t sourceLength = strlen(inSource); + size_t totalLength = destinationLength + sourceLength; + + assert(destinationLength < inDestinationSize); + + ioDestination += destinationLength; + inDestinationSize -= destinationLength; + if (sourceLength >= inDestinationSize) + { + sourceLength = inDestinationSize - 1; + } + memcpy(ioDestination, inSource, sourceLength); + ioDestination[sourceLength] = '\0'; + + return totalLength; +} + +#endif +#endif + + diff --git a/string_helper/string_helper.h b/string_helper/string_helper.h new file mode 100644 index 00000000..b5ca068f --- /dev/null +++ b/string_helper/string_helper.h @@ -0,0 +1,14 @@ +// Apple provides strlcpy and strlcat, so don't redefine them. +#ifndef __APPLE__ + +// libc 2.38 and above provides strlcpy and strlcat, so don't redefine them. +#if defined (__GLIBC__) && ((__GLIBC__ < 2) || ((__GLIBC__ == 2) && (__GLIBC_MINOR__ < 38))) + +#include + +size_t strlcpy(char* restrict ioDestination, const char* restrict inSource, size_t inDestinationSize); + +size_t strlcat(char* restrict ioDestination, const char* restrict inSource, size_t inDestinationSize); + +#endif +#endif