1 package nom.tam.fits.utilities;
2
3 /*-
4 * #%L
5 * nom.tam FITS library
6 * %%
7 * Copyright (C) 1996 - 2024 nom-tam-fits
8 * %%
9 * This is free and unencumbered software released into the public domain.
10 *
11 * Anyone is free to copy, modify, publish, use, compile, sell, or
12 * distribute this software, either in source code form or as a compiled
13 * binary, for any purpose, commercial or non-commercial, and by any
14 * means.
15 *
16 * In jurisdictions that recognize copyright laws, the author or authors
17 * of this software dedicate any and all copyright interest in the
18 * software to the public domain. We make this dedication for the benefit
19 * of the public at large and to the detriment of our heirs and
20 * successors. We intend this dedication to be an overt act of
21 * relinquishment in perpetuity of all present and future rights to this
22 * software under copyright law.
23 *
24 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
25 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
26 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
27 * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
28 * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
29 * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
30 * OTHER DEALINGS IN THE SOFTWARE.
31 * #L%
32 */
33
34 import java.io.DataInputStream;
35 import java.io.EOFException;
36 import java.io.IOException;
37 import java.io.InputStream;
38 import java.io.PipedInputStream;
39 import java.io.PipedOutputStream;
40 import java.nio.ByteBuffer;
41 import java.nio.ByteOrder;
42 import java.nio.IntBuffer;
43 import java.nio.charset.StandardCharsets;
44 import java.util.Arrays;
45
46 import nom.tam.fits.BasicHDU;
47 import nom.tam.fits.Data;
48 import nom.tam.fits.FitsDate;
49 import nom.tam.fits.FitsElement;
50 import nom.tam.fits.FitsException;
51 import nom.tam.fits.Header;
52 import nom.tam.fits.HeaderCard;
53 import nom.tam.util.FitsIO;
54 import nom.tam.util.FitsOutputStream;
55 import nom.tam.util.RandomAccess;
56
57 import static nom.tam.fits.header.Checksum.CHECKSUM;
58 import static nom.tam.fits.header.Checksum.DATASUM;
59
60 /**
61 * <p>
62 * Helper class for dealing with FITS checksums. It implements the Seaman-Pence 32-bit 1's complement checksum
63 * calculation. The implementation accumulates in two 64-bit integer values the low and high-order 16-bits of adjacent
64 * 4-byte groups. A carry-over of bits are calculated only at the end of the loop. Given the use of 64-bit accumulators,
65 * overflow would occur approximately at 280 billion short values.
66 * </p>
67 * <p>
68 * This updated version of the class is a little more flexible than the prior incarnation. Specifically it allows
69 * incremental updates to data sums, and provides methods for dealing with partial checksums (e.g. from modified
70 * segments of the data), which may be used e.g. to calculate delta sums from changed blocks of data. The new
71 * implementation provides methods for decoding encoded checksums, and for calculating checksums directly from files
72 * without the need to read potentially huge data into RAM first, and for easily accessing the values stored in FITS
73 * headers.
74 * </p>
75 * <p>
76 * See <a href="http://arxiv.org/abs/1201.1345" target="@top">FITS Checksum Proposal</a>
77 * </p>
78 * <p>
79 * Note this class probably should be in the <code>nom.tam.util</code> package, but somehow it ended up here and now we
80 * are stuck with it.
81 * </p>
82 *
83 * @author R J Mather, Tony Johnson, Attila Kovacs
84 *
85 * @see nom.tam.fits.header.Checksum#CHECKSUM
86 */
87 public final class FitsCheckSum {
88
89 private static final int CHECKSUM_BLOCK_SIZE = 4;
90 private static final int CHECKSUM_BLOCK_MASK = CHECKSUM_BLOCK_SIZE - 1;
91 private static final int CHECKSUM_STRING_SIZE = 16;
92 private static final int SHIFT_2_BYTES = 16;
93 private static final int MASK_2_BYTES = 0xffff;
94 private static final int MASK_4_BYTES = 0xffffffff;
95 private static final int MASK_BYTE = 0xff;
96 private static final int ASCII_ZERO = '0';
97 private static final int BUFFER_SIZE = 0x8000; // 32 kB
98
99 private static final int[] SELECT_BYTE = {24, 16, 8, 0};
100 private static final String EXCLUDE = ":;<=>?@[\\]^_`";
101 private static final String CHECKSUM_DEFAULT = "0000000000000000";
102
103 /** The expected checksum for a HDU that already contains a valid CHECKSUM keyword */
104 public static final long HDU_CHECKSUM = 0xffffffffL;
105
106 private FitsCheckSum() {
107 }
108
109 /**
110 * Internal class for accumulating FITS checksums.
111 */
112 private static class Checksum {
113 private long h, l;
114
115 Checksum(long prior) {
116 h = (prior >>> SHIFT_2_BYTES) & MASK_2_BYTES;
117 l = prior & MASK_2_BYTES;
118 }
119
120 void add(int i) {
121 h += i >>> SHIFT_2_BYTES;
122 l += i & MASK_2_BYTES;
123 }
124
125 long getChecksum() {
126 long hi = h & MASK_4_BYTES; // as unsigned 32-bit integer
127 long lo = l & MASK_4_BYTES;
128
129 for (;;) {
130 long hicarry = hi >>> SHIFT_2_BYTES;
131 long locarry = lo >>> SHIFT_2_BYTES;
132 if ((hicarry | locarry) == 0) {
133 break;
134 }
135 hi = (hi & MASK_2_BYTES) + locarry;
136 lo = (lo & MASK_2_BYTES) + hicarry;
137 }
138 return (hi << SHIFT_2_BYTES) | lo;
139 }
140
141 }
142
143 private static class PipeWriter extends Thread {
144 private Exception exception;
145 private PipedOutputStream out;
146 private FitsElement data;
147
148 PipeWriter(FitsElement data, PipedInputStream in) throws IOException {
149 this.data = data;
150 out = new PipedOutputStream(in);
151 }
152
153 @Override
154 public void run() {
155 exception = null;
156 try (FitsOutputStream fos = new FitsOutputStream(out)) {
157 if (data instanceof Header) {
158 ((Header) data).writeUnchecked(fos);
159 } else {
160 data.write(fos);
161 }
162 } catch (Exception e) {
163 exception = e;
164 }
165 }
166
167 public Exception getException() {
168 return exception;
169 }
170 }
171
172 /**
173 * Computes the checksum for a byte array.
174 *
175 * @param data the byte sequence for which to calculate a chekcsum
176 *
177 * @return the 32bit checksum in the range from 0 to 2^32-1
178 *
179 * @see #checksum(byte[], int, int)
180 */
181 public static long checksum(byte[] data) {
182 return checksum(ByteBuffer.wrap(data));
183 }
184
185 /**
186 * Computes the checksum for a segment of a byte array.
187 *
188 * @param data the byte sequence for which to calculate a chekcsum
189 * @param from Stating index of bytes to include in checksum calculation
190 * @param to Ending index (exclusive) of bytes to include in checksum
191 *
192 * @return the 32-bit checksum in the range from 0 to 2^32-1
193 *
194 * @see #checksum(RandomAccess, long, long)
195 *
196 * @since 1.17
197 */
198 public static long checksum(byte[] data, int from, int to) {
199 return checksum(ByteBuffer.wrap(data, from, to));
200 }
201
202 /**
203 * Computes the checksum from a buffer. This method can be used to calculate partial checksums for any data that can
204 * be wrapped into a buffer one way or another. As such it is suitable for calculating partial sums from segments of
205 * data that can be used to update datasums incrementally (e.g. by incrementing the datasum with the difference of
206 * the checksum of the new data segment vs the old data segment), or for updating the checksum for new data that has
207 * been added to the existing data (e.g. new rows in a binary table) as long as the modified data segment is a
208 * multiple of 4 bytes.
209 *
210 * @param data the buffer for which to calculate a (partial) checksum
211 *
212 * @return the computed 32-bit unsigned checksum as a Java <code>long</code>
213 *
214 * @since 1.17
215 *
216 * @see #checksum(Data)
217 * @see #checksum(Header)
218 * @see #sumOf(long...)
219 * @see #differenceOf(long, long)
220 */
221 public static long checksum(ByteBuffer data) {
222 Checksum sum = new Checksum(0);
223 if (!(data.remaining() % CHECKSUM_BLOCK_SIZE == 0)) {
224 throw new IllegalArgumentException("fits blocks must always be divisible by 4");
225 }
226 data.position(0);
227 data.order(ByteOrder.BIG_ENDIAN);
228 IntBuffer iData = data.asIntBuffer();
229 while (iData.hasRemaining()) {
230 sum.add(iData.get());
231 }
232 return sum.getChecksum();
233 }
234
235 private static long checksum(InputStream in) throws IOException {
236 Checksum sum = new Checksum(0);
237 DataInputStream din = new DataInputStream(in);
238
239 for (;;) {
240 try {
241 sum.add(din.readInt());
242 } catch (EOFException e) {
243 break;
244 }
245 }
246
247 return sum.getChecksum();
248 }
249
250 private static long compute(final FitsElement data) throws FitsException {
251 try (PipedInputStream in = new PipedInputStream()) {
252 PipeWriter writer = new PipeWriter(data, in);
253 writer.start();
254
255 long sum = checksum(in);
256
257 writer.join();
258 if (writer.getException() != null) {
259 throw writer.getException();
260 }
261
262 return sum;
263 } catch (Exception e) {
264 if (e instanceof FitsException) {
265 throw (FitsException) e;
266 }
267 throw new FitsException("Exception while checksumming FITS element: " + e.getMessage(), e);
268 }
269 }
270
271 /**
272 * Computes the checksum for a FITS data object, e.g. to be used with {@link #setDatasum(Header, long)}. This call
273 * will always calculate the checksum for the data in memory, and as such load deferred mode data into RAM as
274 * necessary to perform the calculation. If you rather not load a huge amount of data into RAM, you might consider
275 * using {@link #checksum(RandomAccess, long, long)} instead.
276 *
277 * @deprecated Use {@link BasicHDU#verifyIntegrity()} instead.
278 *
279 * @param data The FITS data object for which to calculate a checksum
280 *
281 * @return The checksum of the data
282 *
283 * @throws FitsException If there was an error serializing the data object
284 *
285 * @see Data#calcChecksum()
286 * @see BasicHDU#verifyDataIntegrity()
287 * @see #checksum(RandomAccess, long, long)
288 * @see #setDatasum(Header, long)
289 * @see #setChecksum(BasicHDU)
290 *
291 * @since 1.17
292 */
293 public static long checksum(Data data) throws FitsException {
294 return compute(data);
295 }
296
297 /**
298 * Computes the checksum for a FITS header. It returns the checksum for the header as is, without attempting to
299 * validate the header, or modifying it in any way.
300 *
301 * @deprecated Use {@link BasicHDU#verifyIntegrity()} instead.
302 *
303 * @param header The FITS header object for which to calculate a checksum
304 *
305 * @return The checksum of the data
306 *
307 * @throws FitsException If there was an error serializing the FITS header
308 *
309 * @see #checksum(Data)
310 *
311 * @since 1.17
312 */
313 public static long checksum(Header header) throws FitsException {
314 return compute(header);
315 }
316
317 /**
318 * Calculates the FITS checksum for a HDU, e.g to compare agains the value stored under the CHECKSUM header keyword.
319 * The
320 *
321 * @param hdu The Fits HDU for which to calculate a checksum, including both the header and data
322 * segments.
323 *
324 * @return The calculated checksum for the given HDU.
325 *
326 * @throws FitsException if there was an error accessing the contents of the HDU.
327 *
328 * @see BasicHDU#verifyIntegrity()
329 * @see #checksum(Data)
330 * @see #sumOf(long...)
331 *
332 * @deprecated Use {@link BasicHDU#verifyIntegrity()} instead to verify checksums.
333 *
334 * @since 1.17
335 */
336 public static long checksum(BasicHDU<?> hdu) throws FitsException {
337 return sumOf(checksum(hdu.getHeader()), checksum(hdu.getData()));
338 }
339
340 /**
341 * Computes the checksum directly from a region of a random access file, by buffering moderately sized chunks from
342 * the file as necessary. The file may be very large, up to the full range of 64-bit addresses.
343 *
344 * @param f the random access file, from which to compute a checksum
345 * @param from the starting position in the file, where to start computing the checksum from.
346 * @param size the number of bytes in the file to include in the checksum calculation.
347 *
348 * @return the checksum for the given segment of the file
349 *
350 * @throws IOException if there was a problem accessing the file during the computation.
351 *
352 * @since 1.17
353 *
354 * @see #checksum(ByteBuffer)
355 * @see #checksum(Data)
356 */
357 public static long checksum(RandomAccess f, long from, long size) throws IOException {
358 if (f == null) {
359 return 0L;
360 }
361
362 int len = (int) Math.min(BUFFER_SIZE, size);
363 byte[] buf = new byte[len];
364 long oldpos = f.position();
365 f.position(from);
366 long sum = 0;
367
368 while (size > 0) {
369 len = (int) Math.min(BUFFER_SIZE, size);
370 len = f.read(buf, 0, len);
371 sum = sumOf(sum, checksum(buf, 0, len));
372 from += len;
373 size -= len;
374 }
375
376 f.position(oldpos);
377 return sum;
378 }
379
380 /**
381 * @deprecated Use {@link #encode(long, boolean)} instead.
382 *
383 * @param c The calculated 32-bit (unsigned) checksum
384 * @param compl Whether to complement the raw checksum (as defined by the convention).
385 *
386 * @return The encoded checksum, suitably encoded for use with the CHECKSUM header
387 */
388 @Deprecated
389 public static String checksumEnc(final long c, final boolean compl) {
390 return encode(c, compl);
391 }
392
393 /**
394 * Encodes the complemented checksum. It is the same as <code>encode(checksum, true)</code>.
395 *
396 * @param checksum The calculated 32-bit (unsigned) checksum
397 *
398 * @return The encoded checksum, suitably encoded for use with the CHECKSUM header
399 *
400 * @see #decode(String)
401 *
402 * @since 1.17
403 */
404 public static String encode(long checksum) {
405 return encode(checksum, true);
406 }
407
408 /**
409 * Encodes the given checksum as is or by its complement.
410 *
411 * @param checksum The calculated 32-bit (unsigned) checksum
412 * @param compl If <code>true</code> the complement of the specified value will be encoded. Otherwise, the value
413 * as is will be encoded. (FITS normally uses the complemenyed value).
414 *
415 * @return The encoded checksum, suitably encoded for use with the CHECKSUM header
416 *
417 * @see #decode(String, boolean)
418 *
419 * @since 1.17
420 */
421 public static String encode(long checksum, boolean compl) {
422 if (compl) {
423 checksum = ~checksum & FitsIO.INTEGER_MASK;
424 }
425
426 final byte[] asc = new byte[CHECKSUM_STRING_SIZE];
427 final byte[] ch = new byte[CHECKSUM_BLOCK_SIZE];
428 final int sum = (int) checksum;
429
430 for (int i = 0; i < CHECKSUM_BLOCK_SIZE; i++) {
431 // each byte becomes four
432 final int byt = MASK_BYTE & (sum >>> SELECT_BYTE[i]);
433
434 Arrays.fill(ch, (byte) ((byt >>> 2) + ASCII_ZERO)); // quotient
435 ch[0] += (byte) (byt & CHECKSUM_BLOCK_MASK); // remainder
436
437 for (int j = 0; j < CHECKSUM_BLOCK_SIZE; j += 2) {
438 while (EXCLUDE.indexOf(ch[j]) >= 0 || EXCLUDE.indexOf(ch[j + 1]) >= 0) {
439 ch[j]++;
440 ch[j + 1]--;
441 }
442 }
443
444 for (int j = 0; j < CHECKSUM_BLOCK_SIZE; j++) {
445 int k = CHECKSUM_BLOCK_SIZE * j + i + 1;
446 k = (k < CHECKSUM_STRING_SIZE) ? k : 0; // rotate right
447 asc[k] = ch[j];
448 }
449 }
450
451 return new String(asc, StandardCharsets.US_ASCII);
452 }
453
454 /**
455 * Decodes an encoded (and complemented) checksum. The same as <code>decode(encoded, true)</code>, and the the
456 * inverse of {@link #encode(long)}.
457 *
458 * @param encoded The encoded checksum (16 character string)
459 *
460 * @return The unsigned 32-bit integer complemeted checksum.
461 *
462 * @throws IllegalArgumentException if the checksum string is invalid (wrong length or contains illegal ASCII
463 * characters)
464 *
465 * @see #encode(long)
466 *
467 * @since 1.17
468 */
469 public static long decode(String encoded) throws IllegalArgumentException {
470 return decode(encoded, true);
471 }
472
473 /**
474 * Decodes an encoded checksum, complementing it as required. It is the inverse of {@link #encode(long, boolean)}.
475 *
476 * @param encoded the encoded checksum (16 character string)
477 * @param compl whether to complement the checksum after decoding. Normally FITS uses
478 * complemented 32-bit checksums, so typically this optional argument should be
479 * <code>true</code>.
480 *
481 * @return The unsigned 32-bit integer checksum.
482 *
483 * @throws IllegalArgumentException if the checksum string is invalid (wrong length or contains illegal ASCII
484 * characters)
485 *
486 * @see #encode(long, boolean)
487 *
488 * @since 1.17
489 */
490 public static long decode(String encoded, boolean compl) throws IllegalArgumentException {
491 byte[] bytes = encoded.getBytes(StandardCharsets.US_ASCII);
492 if (bytes.length != CHECKSUM_STRING_SIZE) {
493 throw new IllegalArgumentException("Bad checksum with " + bytes.length + " chars (expected 16)");
494 }
495 // Rotate the bytes one to the left
496 byte tmp = bytes[0];
497 System.arraycopy(bytes, 1, bytes, 0, CHECKSUM_STRING_SIZE - 1);
498 bytes[CHECKSUM_STRING_SIZE - 1] = tmp;
499
500 for (int i = 0; i < CHECKSUM_STRING_SIZE; i++) {
501 if (bytes[i] < ASCII_ZERO) {
502 throw new IllegalArgumentException("Bad checksum with illegal char " + Integer.toHexString(bytes[i])
503 + " at pos " + i + " (ASCII below 0x30)");
504 }
505 bytes[i] -= ASCII_ZERO;
506 }
507
508 ByteBuffer bb = ByteBuffer.wrap(bytes).order(ByteOrder.BIG_ENDIAN);
509
510 long sum = (bb.getInt() + bb.getInt() + bb.getInt() + bb.getInt());
511 return (compl ? ~sum : sum) & FitsIO.INTEGER_MASK;
512 }
513
514 /**
515 * Calculates the total checksum from partial sums. For example combining checksums from a header and data segment
516 * of a HDU, or for composing a data checksum from image tiles.
517 *
518 * @param parts The partial sums that are to be added together.
519 *
520 * @return The aggregated checksum as a 32-bit unsigned value.
521 *
522 * @see #differenceOf(long, long)
523 *
524 * @since 1.17
525 */
526 public static long sumOf(long... parts) {
527 Checksum sum = new Checksum(0);
528
529 for (long part : parts) {
530 sum.h += part >>> SHIFT_2_BYTES;
531 sum.l += part & MASK_2_BYTES;
532 }
533 return sum.getChecksum();
534 }
535
536 /**
537 * Subtracts a partial checksum from an aggregated total. One may use it, for example to update the datasum of a
538 * large data, when modifying only a small segment of it. Thus, one would first subtract the checksum of the old
539 * segment from tha prior datasum, and then add the checksum of the new data segment -- without hacing to
540 * recalculate the checksum for the entire data again.
541 *
542 * @deprecated Not foolproof, because of the carry over handling in checksums, some additionas are not
543 * reversible. E.g. 0xffffffff + 0xffffffff = 0xffffffff, but 0xffffffff - 0xffffffff = 0;
544 *
545 * @param total The total checksum.
546 * @param part The partial checksum to be subtracted from the total.
547 *
548 * @return The checksum after subtracting the partial sum, as a 32-bit unsigned value.
549 *
550 * @see #sumOf(long...)
551 *
552 * @since 1.17
553 */
554 public static long differenceOf(long total, long part) {
555 Checksum sum = new Checksum(total);
556 sum.h -= part >>> SHIFT_2_BYTES;
557 sum.l -= part & MASK_2_BYTES;
558 return sum.getChecksum();
559 }
560
561 /**
562 * Sets the <code>DATASUM</code> and <code>CHECKSUM</code> keywords in a FITS header, based on the provided checksum
563 * of the data (calculated elsewhere) and the checksum calculated afresh for the header.
564 *
565 * @param header the header in which to store the <code>DATASUM</code> and <code>CHECKSUM</code> values
566 * @param datasum the checksum for the data segment that follows the header in the HDU.
567 *
568 * @throws FitsException if there was an error serializing the header. Note, the method never throws any other type
569 * of exception (including runtime exceptions), which are instead wrapped into a
570 * <code>FitsException</code> when they occur.
571 *
572 * @see #setChecksum(BasicHDU)
573 * @see #getStoredDatasum(Header)
574 *
575 * @since 1.17
576 */
577 public static void setDatasum(Header header, long datasum) throws FitsException {
578 // Add the freshly calculated datasum to the header, before calculating the checksum
579 header.seekTail();
580 header.updateLine(DATASUM,
581 new HeaderCard(DATASUM.key(), Long.toString(datasum), "data checksum at " + FitsDate.getFitsDateString()));
582
583 HeaderCard hc = header.getCard(CHECKSUM);
584
585 if (hc != null) {
586 hc.setValue(CHECKSUM_DEFAULT);
587 hc.setComment("HDU checksum at " + FitsDate.getFitsDateString());
588 } else {
589 header.seekTail();
590 header.addValue(CHECKSUM, CHECKSUM_DEFAULT);
591 }
592
593 long hsum = checksum(header);
594 header.getCard(CHECKSUM).setValue(encode(sumOf(hsum, datasum)));
595 }
596
597 /**
598 * Computes and sets the DATASUM and CHECKSUM keywords for a given HDU. This method calculates the sums from
599 * scratch, and can be computationally expensive. There are less expensive incremental update methods that can be
600 * used if the HDU already had sums recorded earlier, which need to be updated e.g. because there were modifications
601 * to the header, or (parts of) the data.
602 *
603 * @param hdu the HDU to be updated.
604 *
605 * @throws FitsException if there was an error serializing the HDU. Note, the method never throws any other type of
606 * exception (including runtime exceptions), which are instead wrapped into a
607 * <code>FitsException</code> when they occur.
608 *
609 * @see #setDatasum(Header, long)
610 * @see #sumOf(long...)
611 * @see #differenceOf(long, long)
612 *
613 * @author R J Mather, Attila Kovacs
614 */
615 public static void setChecksum(BasicHDU<?> hdu) throws FitsException {
616 try {
617 setDatasum(hdu.getHeader(), checksum(hdu.getData()));
618 } catch (FitsException e) {
619 throw e;
620 } catch (Exception e) {
621 throw new FitsException("Exception while computing data checksum: " + e.getMessage(), e);
622 }
623 }
624
625 /**
626 * Returns the DATASUM value stored in a FITS header.
627 *
628 * @param header the FITS header
629 *
630 * @return The stored datasum value (unsigned 32-bit integer) as a Java <code>long</code>.
631 *
632 * @throws FitsException if the header does not contain a <code>DATASUM</code> entry.
633 *
634 * @since 1.17
635 *
636 * @see #setDatasum(Header, long)
637 * @see BasicHDU#getStoredDatasum()
638 */
639 public static long getStoredDatasum(Header header) throws FitsException {
640 HeaderCard hc = header.getCard(DATASUM);
641
642 if (hc == null) {
643 throw new FitsException("Header does not have a DATASUM value.");
644 }
645
646 return hc.getValue(Long.class, 0L) & FitsIO.INTEGER_MASK;
647 }
648
649 /**
650 * Returns the decoded CHECKSUM value stored in a FITS header.
651 *
652 * @deprecated Not very useful, since it has no meaning other than ensuring that the checksum of the
653 * HDU yields <code>(int) -1</code> (that is <code>0xffffffff</code>) after including
654 * this value for the CHECKSUM keyword in the header. It will be removed in the
655 * future.
656 *
657 * @param header the FITS header
658 *
659 * @return The decoded <code>CHECKSUM</code> value (unsigned 32-bit integer) recorded in the
660 * header as a Java <code>long</code>.
661 *
662 * @throws FitsException if the header does not contain a <code>CHECKSUM</code> entry, or it is invalid.
663 *
664 * @since 1.17
665 *
666 * @see #getStoredDatasum(Header)
667 * @see #setChecksum(BasicHDU)
668 */
669 public static long getStoredChecksum(Header header) throws FitsException {
670 String encoded = header.getStringValue(CHECKSUM);
671
672 if (encoded == null) {
673 throw new FitsException("Header does not have a CHECKUM value.");
674 }
675
676 return decode(encoded);
677 }
678 }