Files
tensor/io/fits_hostile_pins_test.go
T
petrbalvin af4ee19703
Release / gates (push) Successful in 4m38s
Test / test (push) Successful in 5m16s
Release / release (push) Successful in 35s
feat: initial release
Assisted-by: GLM 5.3 Flash
2026-09-03 10:00:00 +02:00

273 lines
9.2 KiB
Go

// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: MIT
package io
import (
"encoding/binary"
"strings"
"testing"
)
// Regression pins for hostile FITS input: negative and missing axis
// cards, TBCOL and row-byte arithmetic that wraps, repeat counts past
// the int range, unbounded HDU skips and garbage PCOUNT, all refused by
// name before any payload is read.
// tableHostile builds a FITS table file from card bodies plus one
// 2880-byte data block of the given row content.
func tableHostile(cards []string, rows int) []byte {
var b []byte
for _, body := range cards {
b = append(b, card(body)...)
}
b = append(b, card("END")...)
if pad := len(b) % 2880; pad != 0 {
b = append(b, make([]byte, 2880-pad)...)
}
b = append(b, make([]byte, 2880)...)
_ = rows
return b
}
// TestFITSNegativeAxisCard pins the refusal of a negative or
// missing NAXIS before the axis slice is allocated, and the image skip
// past a zero axis in the table reader.
func TestFITSNegativeAxisCard(t *testing.T) {
build := func(bodies []string) []byte {
var b []byte
for _, body := range bodies {
b = append(b, card(body)...)
}
b = append(b, card("END")...)
if pad := len(b) % 2880; pad != 0 {
b = append(b, make([]byte, 2880-pad)...)
}
return b
}
noNaxis := writeHostile(t, "no-naxis.fits", build([]string{
"SIMPLE = T",
"BITPIX = -64",
}))
if _, _, err := LoadFITS(noNaxis); err == nil {
t.Fatal("expected an error for a missing NAXIS card")
}
negative := writeHostile(t, "neg-naxis.fits", build([]string{
"SIMPLE = T",
"BITPIX = -64",
"NAXIS = -3",
}))
if _, _, err := LoadFITS(negative); err == nil {
t.Fatal("expected an error for a negative NAXIS")
}
// A zero axis followed by another divides the running product in
// the table reader's image skip; the skip must pass it without
// dividing by the zeroed product and report that no table follows.
zeroAxis := writeHostile(t, "zero-axis.fits", build([]string{
"SIMPLE = T",
"BITPIX = -64",
"NAXIS = 2",
"NAXIS1 = 0",
"NAXIS2 = 1",
}))
if _, err := LoadFITSTable(zeroAxis); err == nil || !strings.Contains(err.Error(), "no table extension") {
t.Fatalf("LoadFITSTable past a zero axis: err = %v, want the no-table report", err)
}
if _, _, err := LoadFITS(zeroAxis); err == nil {
t.Fatal("LoadFITS: expected an error for a zero axis under positive NAXIS")
}
}
// TestFITSHugeNaxisRefused pins that a NAXIS beyond the NAXISn cards
// the file carries is refused before the axis slice is allocated: a
// NAXIS of MaxInt64 used to reach make([]int, naxis) and panic with
// makeslice instead of answering the card-count error.
func TestFITSHugeNaxisRefused(t *testing.T) {
build := func(bodies []string) []byte {
var b []byte
for _, body := range bodies {
b = append(b, card(body)...)
}
b = append(b, card("END")...)
if pad := len(b) % 2880; pad != 0 {
b = append(b, make([]byte, 2880-pad)...)
}
return b
}
huge := writeHostile(t, "huge-naxis.fits", build([]string{
"SIMPLE = T",
"BITPIX = -64",
"NAXIS = 9223372036854775807",
}))
if _, _, err := LoadFITS(huge); err == nil || !strings.Contains(err.Error(), "NAXISn cards") {
t.Fatalf("LoadFITS with NAXIS = MaxInt64: err = %v, want the card-count refusal", err)
}
}
// TestASCIITableTBCOLWrap pins the overflow-free bound on
// TBCOL + width: a TBCOL near MaxInt64 used to wrap the sum negative,
// pass the guard and panic on the slice.
func TestASCIITableTBCOLWrap(t *testing.T) {
data := tableHostile([]string{
"XTENSION= 'TABLE '",
"BITPIX = 8",
"NAXIS = 2",
"NAXIS1 = 20",
"NAXIS2 = 1",
"TFIELDS = 1",
"TFORM1 = 'I10 '",
"TBCOL1 = '9223372036854775807'",
}, 1)
path := writeHostile(t, "tbcol-wrap.fits", data)
if _, err := LoadFITSTable(path); err == nil {
t.Fatal("expected an error for a TBCOL whose sum with the width wraps")
}
}
// TestBINTABLERowBytesWrap pins the per-column bound on the
// row prefix sum: two A columns of 2^62 repeat each used to wrap
// rowBytes negative, skip the truncation guard and read past the file.
func TestBINTABLERowBytesWrap(t *testing.T) {
data := tableHostile([]string{
"XTENSION= 'BINTABLE'",
"BITPIX = 8",
"NAXIS = 2",
"NAXIS1 = 16",
"NAXIS2 = 1",
"TFIELDS = 2",
"TFORM1 = '4611686018427387904A'",
"TFORM2 = '4611686018427387904A'",
}, 1)
path := writeHostile(t, "rowbytes-wrap.fits", data)
if _, err := LoadFITSTable(path); err == nil {
t.Fatal("expected an error for column widths whose sum wraps")
}
}
// TestTFORMRepeatOverflowRefused pins the named refusal for a
// repeat count that does not fit an int, which used to fall back to a
// silent scalar of width 1.
func TestTFORMRepeatOverflowRefused(t *testing.T) {
if _, _, err := parseTFORM("99999999999999999999E"); err == nil {
t.Fatal("parseTFORM: expected an error for an overflowing repeat")
}
if r, code, err := parseTFORM("16A"); err != nil || r != 16 || code != "A" {
t.Fatalf("parseTFORM(16A) = %d, %q, %v", r, code, err)
}
}
// TestImageHDUSkipBounded pins the image-HDU skip arithmetic:
// axis products that would wrap the int cannot skip into attacker
// bytes; the file is refused as truncated instead.
func TestImageHDUSkipBounded(t *testing.T) {
var b []byte
for _, body := range []string{
"SIMPLE = T",
"BITPIX = -64",
"NAXIS = 2",
"NAXIS1 = 2147483647",
"NAXIS2 = 4",
"PCOUNT = 0",
"GCOUNT = 2",
} {
b = append(b, card(body)...)
}
b = append(b, card("END")...)
if pad := len(b) % 2880; pad != 0 {
b = append(b, make([]byte, 2880-pad)...)
}
b = append(b, make([]byte, 2880)...)
path := writeHostile(t, "skip-wrap.fits", b)
// The table reader skips the image HDU to look for a table after
// it; the wrapped product used to land the skip inside the image
// data. Now the skip is bounded and the file has no table.
if _, err := LoadFITSTable(path); err == nil {
t.Fatal("expected an error: no table HDU follows the bounded skip")
}
}
// TestPCOUNTGarbageRefused pins that a non-integer PCOUNT is
// an error, not a silent zero.
func TestPCOUNTGarbageRefused(t *testing.T) {
var b []byte
for _, body := range []string{
"SIMPLE = T",
"BITPIX = -64",
"NAXIS = 2",
"NAXIS1 = 2",
"NAXIS2 = 2",
"PCOUNT = '1e3'",
} {
b = append(b, card(body)...)
}
b = append(b, card("END")...)
if pad := len(b) % 2880; pad != 0 {
b = append(b, make([]byte, 2880-pad)...)
}
b = append(b, make([]byte, 2880)...)
path := writeHostile(t, "pcount.fits", b)
if _, err := LoadFITSTable(path); err == nil {
t.Fatal("expected an error for a non-integer PCOUNT")
}
}
// TestFITSNAXISnOrder pins that NAXISn cards are bound by
// their axis number: a header writing NAXIS2 before NAXIS1 under
// NAXIS = 2 with matching extents stays the image it declares, and a
// missing axis is an error.
func TestFITSNAXISnOrder(t *testing.T) {
build := func(bodies []string) []byte {
var b []byte
for _, body := range bodies {
b = append(b, card(body)...)
}
b = append(b, card("END")...)
if pad := len(b) % 2880; pad != 0 {
b = append(b, make([]byte, 2880-pad)...)
}
// 2x2 float64 image data.
for range 4 {
b = binary.BigEndian.AppendUint64(b, 1)
}
return b
}
// Reversed card order: the image is still 3 by 2.
path := writeHostile(t, "order.fits", build([]string{
"SIMPLE = T",
"BITPIX = -64",
"NAXIS = 2",
"NAXIS2 = 2",
"NAXIS1 = 3",
}))
// 3x2 = 6 doubles but only 4 present: truncated either way, so the
// claim is checked by the value the reader reports. Give it enough
// data instead: rebuild with full payload.
full := build([]string{
"SIMPLE = T",
"BITPIX = -64",
"NAXIS = 2",
"NAXIS2 = 2",
"NAXIS1 = 3",
})
full = append(full, make([]byte, 2880)...)
path = writeHostile(t, "order-full.fits", full)
a, _, err := LoadFITS(path)
if err != nil {
t.Fatalf("LoadFITS with reversed NAXISn cards: %v", err)
}
if a.Shape()[0] != 2 || a.Shape()[1] != 3 {
t.Fatalf("shape = %v, want [2 3] (Fortran order, NAXIS1 = 3)", a.Shape())
}
// A missing axis is refused by name.
missing := build([]string{
"SIMPLE = T",
"BITPIX = -64",
"NAXIS = 2",
"NAXIS1 = 2",
})
mpath := writeHostile(t, "missing.fits", missing)
if _, _, err := LoadFITS(mpath); err == nil || !strings.Contains(err.Error(), "NAXIS") {
t.Fatalf("err = %v, want the missing NAXISn card named", err)
}
}