BUG: Fix destructive write and silent short reads in GiplImageIO#6611
Conversation
|
| Filename | Overview |
|---|---|
| Modules/IO/GIPL/src/itkGiplImageIO.cxx | Moves write validation before file opening, rejects multi-component writes, adds INT/UINT swapping, and checks read byte counts. |
| Modules/IO/GIPL/test/CMakeLists.txt | Adds the GIPL GoogleTest driver with the module output directory definition. |
| Modules/IO/GIPL/test/itkGiplImageIOGTest.cxx | Adds tests for failed writes preserving files, truncated reads throwing, multi-component rejection, and INT/UINT round trips. |
| Modules/IO/GIPL/itk-module.cmake | Adds ITKGoogleTest as a test dependency. |
Reviews (1): Last reviewed commit: "ENH: Support INT and UINT component type..." | Re-trigger Greptile
GiplImageIO::Write truncated and partially wrote the header before determining whether the component type had a Gipl encoding, destroying any pre-existing file when the type turned out to be unsupported. Move the component-type validation before the file is opened so an unsupported type is rejected without touching the target file. Addresses item B53 of InsightSoftwareConsortium#6575.
GiplImageIO::Read treated any non-bad ifstream as success, but a short read only sets failbit/eofbit, so a truncated file returned an uninitialized tail as if it were valid pixel data. Use the ImageIOBase::ReadBufferAsBinary helper, which compares gcount() against the expected byte count, checks fail(), and range-checks the streamsize cast. Addresses item B54 of InsightSoftwareConsortium#6575.
GiplImageIO::Read used the caller's output buffer pointer, which is never null, as the success flag for the compressed path, so gzread's actual byte count was never inspected. A truncated .gipl.gz file reported success over a partially filled buffer. Read in chunks of at most 1 GiB, since gzread rejects requests over INT_MAX and a single 32-bit count also truncates for images of 4 GiB or more, and require the 64-bit total to reach the expected byte count. Addresses item B55 of InsightSoftwareConsortium#6575.
GiplImageIO::Write derived the header image_type from the component type alone but sized the pixel payload by component count, so a vector image wrote a scalar header with N times the expected bytes. The Gipl header has no field for component count, so reject multi-component images instead of writing an unreadable file. Behavior change: pipelines that previously wrote RGB or vector images to .gipl produced a malformed-but-present file; they now throw before any file is written. Addresses item B56 of InsightSoftwareConsortium#6575.
Gipl has real type codes for INT (32) and UINT (31), but SwapBytesIfNecessary had no cases for them, so a write hit an unrelated "Pixel Type Unknown" throw after the file was already truncated. Prior commits closed that hole by rejecting INT/UINT before any file I/O. Add the missing swap cases and accept both types again now that they round-trip correctly. Addresses item B53 of InsightSoftwareConsortium#6575.
Read() accumulated the pixel count in a uint32_t. Header dimensions are unsigned short, so a volume can exceed 2^32 pixels, wrapping the count passed to SwapBytesIfNecessary and leaving the buffer tail un-swapped on opposite-endian machines.
SwapBytesIfNecessary repeated the LittleEndian/BigEndian if-else block once per component type; a templated helper reduces each case to one line.
The Gipl GTests repeated the ImageFileWriter/ImageFileReader setup in six places and the truncate-then-read block in two; local helper templates collapse each to one call.
17188b6 to
c8a96f0
Compare
|
Maintainer pass (with @physwkim's commits preserved and two of them rewritten in place): the compressed-read check is now 64-bit safe, plus one additional pre-existing bug fix and two STYLE dedups. Local: 5/5 GTests, 5/5 module ctests, pre-commit clean. What changed and why
Open question, not addressed: |
A bare "Error reading image data." hides the 8:1 mismatch that identifies a packed GIPL_BINARY file; the counts make any short read self-explanatory.
|
Follow-up to the GIPL_BINARY note above: short-read errors now report the byte counts ( |
82050a3
into
InsightSoftwareConsortium:main
GiplImageIOtruncates the target file before validating the write, and reports truncated reads as success. Addresses B53–B56 of #6575, plus the INT/UINT support the B53 investigation turned up.The five commits
Write()opened (and truncated) the output file and wrote the 256-byte header before theswitch (m_ComponentType)that decidesimage_type. An unsupported component type threw only after the target was already destroyed, leaving a 256-byte stub where a valid image used to be. The switch now runs before the file is opened.Read()testedsuccess = !m_Ifstream.bad(). A short read setsfailbit/eofbit, neverbadbit, so a truncated file "succeeded" over an uninitialized buffer tail. Now comparesgcount()againstGetImageSizeInBytes().success = (p != nullptr)on the caller's always-non-null output buffer, withgzread's byte count discarded. Now checks the returned count.image_typecame from the component type alone while the byte count multiplied by the component count, so an N-component image wrote a scalar header followed by N times the pixel bytes. GIPL's header has no component-count field andReadImageInformation()hardcodesSCALAR, so the format cannot represent a vector pixel at all — a multi-component write is now rejected before any file I/O rather than encoded lossily.Write()'s switch already emittedGIPL_INT(32) andGIPL_U_INT(31), butSwapBytesIfNecessary()had no arms for them, so anintimage threw "Pixel Type Unknown" from deep insideWrite(). The two gates disagreed, and that disagreement is what let INT/UINT reach the destructive late throw. The codes exist in the format, so the missing swap arms are added rather than the types declared unsupported. Kept as a separateENH:commit so the fourBUG:commits stay honest — with only the B53 commit applied, anintwrite still throws, but without destroying the file.Local validation
New GoogleTest
itkGiplImageIOGTest.cxx(the module had no GTest driver). Fixtures are written by the test itself — no ExternalData added.WriteOfUnsupportedComponentTypePreservesExistingFileReadOfTruncatedUncompressedFileThrowsReadOfTruncatedCompressedFileThrowsWriteOfMultiComponentImageThrowsRoundTripsIntAndUnsignedIntComponentTypesctest -R Giplwith ExternalData fetched, atmainand at the branch tip:itkGiplImageIOTest,itkGiplImageIOGzTest,itkGiplImageIOTest2all Passed at both states. Those three exercise the normal (non-truncated) compressed and uncompressed round-trip against the realramp.giplfixture, which is exactly what the new byte-count checks could have broken — they do not misfire on valid data.Adjacent gap found, not fixed here
ReadImageInformation()reads all 15 header fields through uncheckedgzread/ifstream::readpairs, andCanReadFile()'s magic-number probe is unchecked too. That is the same silent-corruption class as B54/B55 but a different shape — no success flag is computed at all, so there is nothing to correct locally; it wants its own change (read the 256-byte header in one call and check the count once). Flagged on #6575 rather than folded in.AI assistance