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 }