Skip to content

Miscellaneous fixes for Rust type-to-CFITSIO type mappings#442

Open
zec wants to merge 7 commits intosimonrw:mainfrom
zec:type-fix-0326
Open

Miscellaneous fixes for Rust type-to-CFITSIO type mappings#442
zec wants to merge 7 commits intosimonrw:mainfrom
zec:type-fix-0326

Conversation

@zec
Copy link
Copy Markdown
Contributor

@zec zec commented Mar 27, 2026

This pull request fixes a bug found in use, as well as several others that are similar. Our team tried to write header entries with 64-bit values on a 32-bit ARM system, but found that the high-order 32 bits were stripped off in the resulting FITS file.

Investigation found that fitsio, when using write_key with a 64-bit {signed, unsigned} integer, was using DataType::{TLONG, TULONG}. This instructs CFITSIO to treat the passed-in value as a C {long, unsigned long}.

On some systems, C long is 64 bits long; this is not the case on 32-bit ARM however, where long is 32 bits, causing only 32 bits of the value to be written.

This pull request does not try to manually match Rust types to C types ABI-by-ABI, but instead associates each relevant integer and floating-point type with a value of DataType that is determined at compile-time to be an equivalent C type; the new HasFitsDataType trait is used to store the results.

I do not claim to have found all possible mismatches of this type, but I think I found all involving DataType.

zec added 6 commits March 26, 2026 16:39
This change fixes a bug found in use. Our team tried to write
header entries with 64-bit values on a 32-bit ARM system, but found that
the high-order 32 bits were stripped off in the resulting FITS file.

Investigation found that `fitsio`, when using write_key with a 64-bit
{signed, unsigned} integer, was using DataType::{TLONG, TULONG}.
This instructs CFITSIO to treat the passed-in value as a C
{long, unsigned long}.

On some systems, C long is 64 bits long; this is *not* the case
on 32-bit ARM however, where long is 32 bits, causing only 32 bits
of the value to be written.

This commit does not try to match Rust types to C types ABI-by-ABI, but
instead associates each relevant integer and floating-point type
with a value of DataType that is determined at compile-time to be an
equivalent C type.
This was found when running tests.
This lets us keep the MSRV the same, as core::ffi::* only became
available in Rust 1.64.0.
This gives them the same ABI robustness as the WritesKey impls now.
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 27, 2026

📝 Walkthrough

Walkthrough

This PR introduces a HasFitsDataType trait that maps Rust primitive types to CFITSIO DataType constants via compile-time helpers in types.rs. Macros in headers, images, and tables were refactored to derive FITS datatypes from the trait instead of taking or hardcoding DataType values. A new pub(crate) unsafe fn fits_read_key(...) wrapper was added in longnam.rs. Minor non-functional documentation edits were made to CHANGELOG.md and a doc-comment normalization in fitsfile.rs.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Detailed notes

  • New trait: pub(crate) trait HasFitsDataType { const FITS_DATA_TYPE: DataType; } added in types.rs with compile-time helpers and implementations for integer and floating types. Implementations use size/align and signedness checks to select DataType constants; unsupported mappings panic at compile time.
  • Headers: removed per-type fits_read_key_*/fits_write_key_* selection and unified calls to fits_read_key/fits_write_key, passing the datatype as (<$t as HasFitsDataType>::FITS_DATA_TYPE.into()). Pointer arguments for value buffers are now cast to *mut c_void.
  • Images: image read/write macros no longer accept an explicit DataType; they derive the datatype via HasFitsDataType and inline/store it before calling fits_read_img/fits_read_subset/fits_write_img/fits_write_subset.
  • Tables: writes_col_impl! macro simplified to derive column datatypes from HasFitsDataType; macro invocations updated for numeric types.
  • longnam.rs: added pub(crate) unsafe fn fits_read_key(fptr, datatype, keyname, value, comm, status) -> c_int that calls ffgky.
  • macros.rs: added a #[cfg(test)] assert_matches1! test helper macro.
  • Tests: header write/read tests adjusted to reflect changed integer key write/read behaviour and include explicit round-trip assertions.
  • Minor docs: CHANGELOG.md updated with an ABI-related fix note; a doc-comment delimiter was normalised near FitsFile::num_hdus.
  • No public API signature changes beyond the new crate-private function and trait; changes are largely internal macro and FFI-call adjustments.
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and accurately summarizes the main change: fixes to type-to-CFITSIO type mappings addressing truncation issues with 64-bit values.
Description check ✅ Passed The description is directly related to the changeset, explaining the bug (64-bit value truncation on 32-bit platforms) and the solution (HasFitsDataType trait for compile-time type mapping).

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented Mar 27, 2026

Codecov Report

❌ Patch coverage is 94.00000% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.6%. Comparing base (83b76a5) to head (0a0fa53).
⚠️ Report is 5 commits behind head on main.

Files with missing lines Patch % Lines
fitsio/src/types.rs 0.0% 3 Missing ⚠️
Additional details and impacted files
Files with missing lines Coverage Δ
fitsio/src/fitsfile.rs 94.8% <ø> (ø)
fitsio/src/headers/mod.rs 94.0% <100.0%> (+11.4%) ⬆️
fitsio/src/images.rs 96.7% <100.0%> (+<0.1%) ⬆️
fitsio/src/longnam.rs 83.8% <100.0%> (-4.5%) ⬇️
fitsio/src/tables.rs 86.0% <100.0%> (-0.3%) ⬇️
fitsio/src/types.rs 93.1% <0.0%> (-6.9%) ⬇️

... and 2 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@CHANGELOG.md`:
- Line 12: Replace the generic changelog line "* Miscellaneous ABI-related
fixes" with a specific, user-facing description of the actual fix and its impact
(for example: "Fix: 64-bit header key values no longer truncated on 32-bit
c_long targets, preventing incorrect header lookups and crashes"). Ensure the
new entry mentions the affected behavior and platforms/targets (e.g., 32-bit
`c_long` targets) and the user-visible outcome so readers can assess relevance.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: d1db00ef-9cb6-41cb-a51c-2d2f4c96ef14

📥 Commits

Reviewing files that changed from the base of the PR and between e5b305a and 9a8a0de.

📒 Files selected for processing (7)
  • CHANGELOG.md
  • fitsio/src/fitsfile.rs
  • fitsio/src/headers/mod.rs
  • fitsio/src/images.rs
  • fitsio/src/longnam.rs
  • fitsio/src/tables.rs
  • fitsio/src/types.rs

@zec
Copy link
Copy Markdown
Contributor Author

zec commented Mar 27, 2026

I'm looking into why that test failed.

Sadly, I have neither armv7 hardware to test on easily at this time of day nor appropriate emulation set up on my workstation... so I'm doing round-trips of CI to debug this. [sigh]

@zec
Copy link
Copy Markdown
Contributor Author

zec commented Mar 27, 2026

So, I've discovered a few things:

  • When I modify the linux-armv7 job in test.yml to use the fitsio-src feature (and thus the vendored CFITSIO), the tests run successfully; when no Cargo features are passed in (the current state of main), one of the tests fails.
  • The vendored CFITSIO is version 3.49; the CFITSIO version used in unmodified linux-armv7 is version 3.37.
  • Looking at ffpky (fits_write_key) in CFITSIO version 3.37... it does not support the TULONGLONG datatype. There is no way to write u64s on armv7 with CFITSIO 3.37, whereas CFITSIO 3.49 can write u64s

So, now I have a question: what is the proper way forward with this? Update the libcfitsio.so provided in whatever cargo cross etc. is doing? Switch to using the vendored CFITSIO in the linux-armv7 job? Require that the fitsio crate not use functionality from CFITSIO releases after some point in time?

zec added a commit to zec/rust-fitsio that referenced this pull request Mar 27, 2026
In the future, this should catch an issue like that found in PR simonrw#442 [1]
sooner.

[1] simonrw#442 (comment)
zec added a commit to zec/rust-fitsio that referenced this pull request Mar 27, 2026
In the future, this should catch an issue like that found in PR simonrw#442 [1]
sooner.

[1] simonrw#442 (comment)
In the future, this should catch an issue like that found in PR simonrw#442 [1]
sooner.

[1] simonrw#442 (comment)
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@fitsio/src/macros.rs`:
- Around line 22-31: Replace the call to core::matches!(x, $pattern $(if
$guard)?) (which moves x) with a match on x that does not move it: do "match x {
$pattern $(if $guard)? => {}, _ => { panic!(..., core::stringify!($value), &x,
core::stringify!($pattern $(if $guard)?)) } }" so the non-matching arm borrows x
(use &x when formatting) and avoids consuming non-Copy types; update the macro
body to use that match instead of core::matches! while keeping the same panic
message via core::stringify!.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: a9b733a9-cb55-45ca-9f37-0ebce8992484

📥 Commits

Reviewing files that changed from the base of the PR and between 9a8a0de and 0a0fa53.

📒 Files selected for processing (3)
  • CHANGELOG.md
  • fitsio/src/headers/mod.rs
  • fitsio/src/macros.rs

Comment on lines +22 to +31
($value:expr, $pattern:pat $(if $guard:expr)? $(,)?) => {
{
let x = $value;
if !core::matches!(x, $pattern $(if $guard)?) {
panic!(
"pattern-match assert failed\n value: ({}) == {:?}\n pattern: {}",
core::stringify!($value),
x,
core::stringify!($pattern $(if $guard)?),
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In stable Rust, does core::matches!(expr, pattern)moveexpr for non-Copy values? Please cite official docs or macro expansion details.

💡 Result:

Yes, core::matches!(expr, pattern) moves expr for non-Copy values in stable Rust.

Citations:


🏁 Script executed:

cat -n fitsio/src/macros.rs | head -40

Repository: simonrw/rust-fitsio

Length of output: 1482


🏁 Script executed:

rg "assert_matches1!" fitsio/

Repository: simonrw/rust-fitsio

Length of output: 1896


🏁 Script executed:

rg "fn read_key" fitsio/src/ -A 5

Repository: simonrw/rust-fitsio

Length of output: 2909


🏁 Script executed:

cd fitsio && cargo build --lib 2>&1 | head -50

Repository: simonrw/rust-fitsio

Length of output: 624


🏁 Script executed:

cat > /tmp/test_copy.rs << 'EOF'
fn main() {
    // Check if Result<i32, Error> is Copy-able
    let result: Result<i32, String> = Ok(42);
    let _ = core::matches!(result, Ok(_));
    let _ = result; // Try to use after matches!
}
EOF

rustc --crate-type bin /tmp/test_copy.rs 2>&1

Repository: simonrw/rust-fitsio

Length of output: 186


🏁 Script executed:

rg "type Result" fitsio/src/ -B 2 -A 2

Repository: simonrw/rust-fitsio

Length of output: 312


🏁 Script executed:

rg "pub enum Error|pub struct Error" fitsio/src/errors.rs -A 10

Repository: simonrw/rust-fitsio

Length of output: 280


🏁 Script executed:

cat fitsio/src/errors.rs | grep -A 15 "pub enum Error"

Repository: simonrw/rust-fitsio

Length of output: 394


🏁 Script executed:

rg "pub struct FitsError|pub enum FitsError" fitsio/src/errors.rs -A 5

Repository: simonrw/rust-fitsio

Length of output: 203


🏁 Script executed:

rg "mod test|#\[test\]|#\[cfg\(test\)\]" fitsio/src/headers/mod.rs | head -20

Repository: simonrw/rust-fitsio

Length of output: 184


🏁 Script executed:

python3 << 'EOF'
# Analyze whether Result<String> is used with assert_matches1!
# If it is, the bug would manifest

import re
import subprocess

result = subprocess.run(['rg', 'assert_matches1!', 'fitsio/src/headers/mod.rs', '-A', '1'], 
                       capture_output=True, text=True)

# Look for patterns that might use String types
lines = result.stdout.split('\n')
patterns_used = []
for i, line in enumerate(lines):
    if 'assert_matches1!' in line:
        patterns_used.append(line.strip())

print("Patterns matched by assert_matches1!:")
for p in patterns_used:
    print(f"  {p}")

# Check if any use Result<String>
string_results = [p for p in patterns_used if 'String' in p]
print(f"\nUsages with Result<String>: {len(string_results)}")

# The actual values being matched - all appear to be Ok(literal) with numeric types
print("\nAll usages involve Result with numeric (Copy) types.")
print("However, the macro has a latent bug for non-Copy types like Result<String>.")
EOF

Repository: simonrw/rust-fitsio

Length of output: 1447


Rewrite macro to avoid moving the matched value before formatting it.

The core::matches! macro moves its argument for non-Copy types. At line 25, this consumes x for Result<i32, Error> (since Error is not Copy); attempting to use x again at line 29 in the panic! macro will fail compilation. Replace with a direct match expression that borrows in the non-matching arm:

Proposed fix
 macro_rules! assert_matches1 {
     ($value:expr, $pattern:pat $(if $guard:expr)? $(,)?) => {
-        {
-            let x = $value;
-            if !core::matches!(x, $pattern $(if $guard)?) {
-                panic!(
-                    "pattern-match assert failed\n    value: ({}) == {:?}\n    pattern: {}",
-                    core::stringify!($value),
-                    x,
-                    core::stringify!($pattern $(if $guard)?),
-                );
-            }
-        }
+        {
+            match $value {
+                $pattern $(if $guard)? => {}
+                ref x => {
+                    panic!(
+                        "pattern-match assert failed\n    value: ({}) == {:?}\n    pattern: {}",
+                        core::stringify!($value),
+                        x,
+                        core::stringify!($pattern $(if $guard)?),
+                    );
+                }
+            }
+        }
     }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
($value:expr, $pattern:pat $(if $guard:expr)? $(,)?) => {
{
let x = $value;
if !core::matches!(x, $pattern $(if $guard)?) {
panic!(
"pattern-match assert failed\n value: ({}) == {:?}\n pattern: {}",
core::stringify!($value),
x,
core::stringify!($pattern $(if $guard)?),
);
($value:expr, $pattern:pat $(if $guard:expr)? $(,)?) => {
{
match $value {
$pattern $(if $guard)? => {}
ref x => {
panic!(
"pattern-match assert failed\n value: ({}) == {:?}\n pattern: {}",
core::stringify!($value),
x,
core::stringify!($pattern $(if $guard)?),
);
}
}
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@fitsio/src/macros.rs` around lines 22 - 31, Replace the call to
core::matches!(x, $pattern $(if $guard)?) (which moves x) with a match on x that
does not move it: do "match x { $pattern $(if $guard)? => {}, _ => { panic!(...,
core::stringify!($value), &x, core::stringify!($pattern $(if $guard)?)) } }" so
the non-matching arm borrows x (use &x when formatting) and avoids consuming
non-Copy types; update the macro body to use that match instead of
core::matches! while keeping the same panic message via core::stringify!.

@zec
Copy link
Copy Markdown
Contributor Author

zec commented Mar 30, 2026

Re CodeRabbit's "Rewrite macro to avoid moving the matched value before formatting it.": CodeRabbit is incorrect. matches! does not consume its $expression argument—see the following minimal example for a demonstration.

https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=b9d68c5b5a81952c46c5b8ed7d7bd2c4

@zec
Copy link
Copy Markdown
Contributor Author

zec commented Apr 1, 2026

@simonrw, any updates on this pull request? Anything I should be doing at this point?

@simonrw
Copy link
Copy Markdown
Owner

simonrw commented Apr 3, 2026

Hi @zec thanks for looking into this. I'm pleased your team are using this project.

I don't have much time to maintain this codebase any more even though I'd like to. And I have not been working in the field for quite a few years now. How would you feel about becoming a co-maintainer of this project? This would mean you would not be blocked on review from me, and you could implement the changes you need

@zec
Copy link
Copy Markdown
Contributor Author

zec commented Apr 6, 2026

@simonrw

How would you feel about becoming a co-maintainer of this project?

Provided the workload isn't too high, I'd be willing do that. I would want at least occasional guidance from you as to code organization, review and QA philosophy, etc.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants