Files
OrcaSlicer/tests/libslic3r/test_png_read_write.cpp
T
ExPikaPaka 4c74d84b94 Fail cleanly on a truncated or corrupt PNG (#16014)
libpng reports a bad file by longjmp()ing back to the buffer set with
setjmp(), and the frame it lands in must own nothing that needs
destroying: with exceptions enabled MSVC unwinds the stack as part of
longjmp, and returning from a frame unwound that way crashes. It did on
Windows while working everywhere else.

The read callback also returned quietly on a short read, leaving libpng
to decode whatever happened to be in the output buffer.

The calls that can fail now sit in two helpers that own nothing but
pointers, so every C++ object the decoders need stays in their own
frames, and a short read is reported through png_error().
2026-10-09 11:52:55 +08:00

84 lines
2.8 KiB
C++

#include <catch2/catch_all.hpp>
#include <cstdint>
#include <fstream>
#include <iterator>
#include <vector>
#include <boost/filesystem.hpp>
#include "libslic3r/PNGReadWrite.hpp"
using namespace Slic3r;
// libpng reports a corrupt or truncated file by longjmp()ing out of the decoder, so the decoders have
// to come back with false rather than crash or hand back a half filled image.
namespace {
// A real PNG, produced by the writer next door, so the bytes are a file libpng accepts.
std::vector<uint8_t> encoded_png(size_t w, size_t h)
{
std::vector<uint8_t> pixels(w * h);
for (size_t i = 0; i < pixels.size(); ++ i)
pixels[i] = uint8_t((i * 7) % 256);
const boost::filesystem::path path = boost::filesystem::temp_directory_path() /
boost::filesystem::unique_path("png_rw_%%%%%%%%.png");
REQUIRE(png::write_gray_to_file(path.string(), w, h, pixels));
std::vector<uint8_t> bytes;
{
std::ifstream ifs(path.string(), std::ios::binary);
bytes.assign(std::istreambuf_iterator<char>(ifs), std::istreambuf_iterator<char>());
}
boost::system::error_code ec;
boost::filesystem::remove(path, ec);
REQUIRE(bytes.size() > 64);
return bytes;
}
png::ReadBuf buf_of(const std::vector<uint8_t> &bytes, size_t size)
{
return png::ReadBuf{ bytes.data(), size };
}
} // namespace
TEST_CASE("A whole PNG decodes", "[PNG]") {
const std::vector<uint8_t> bytes = encoded_png(24, 16);
png::ImageGreyscale grey;
REQUIRE(png::decode_png(buf_of(bytes, bytes.size()), grey));
CHECK(grey.cols == 24);
CHECK(grey.rows == 16);
CHECK(grey.buf.size() == 24 * 16);
}
TEST_CASE("A truncated PNG is refused instead of crashing", "[PNG]") {
const std::vector<uint8_t> bytes = encoded_png(64, 64);
// Cut past the signature: inside the header, and inside the pixel data. Not in the trailing
// chunks - decode_png() does not read those, so a file missing only its IEND still decodes, and
// that is the pre-existing contract rather than anything this change touches.
const size_t size = GENERATE_COPY(size_t(16), size_t(40), bytes.size() / 2, bytes.size() * 3 / 4);
REQUIRE(size < bytes.size());
png::ImageGreyscale grey;
CHECK_FALSE(png::decode_png(buf_of(bytes, size), grey));
png::ImageColorscale colour;
CHECK_FALSE(png::decode_colored_png(buf_of(bytes, size), colour));
}
TEST_CASE("A PNG whose body is garbage is refused", "[PNG]") {
std::vector<uint8_t> bytes = encoded_png(32, 32);
// Keep the signature, scribble over everything after it.
for (size_t i = 8; i < bytes.size(); ++ i)
bytes[i] = uint8_t(0xA5);
png::ImageGreyscale grey;
CHECK_FALSE(png::decode_png(buf_of(bytes, bytes.size()), grey));
png::ImageColorscale colour;
CHECK_FALSE(png::decode_colored_png(buf_of(bytes, bytes.size()), colour));
}