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