Add FuzzTest targets through AVIF_ENABLE_FUZZTEST Experimental Linux support only for now. AVIF_ENABLE_FUZZTEST requires AVIF_LOCAL_FUZZTEST for now. FuzzTest targets are added for transparency and for OSS-Fuzz exposure. The dependency on FuzzTest is added for local reproducibility convenience. The CMake part can be improved in further commits.
diff --git a/.gitignore b/.gitignore index c98df16..1971fe0 100644 --- a/.gitignore +++ b/.gitignore
@@ -3,6 +3,7 @@ /ext/aom /ext/avm /ext/dav1d +/ext/fuzztest /ext/googletest /ext/gpac /ext/libjpeg
diff --git a/CMakeLists.txt b/CMakeLists.txt index 78d8a94..9ed40c5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt
@@ -66,6 +66,9 @@ option(AVIF_LOCAL_GTEST "Build the GoogleTest framework by providing your own copy of the repo in ext/googletest (see Local Builds in README)" OFF ) +option(AVIF_LOCAL_FUZZTEST + "Build the Google FuzzTest framework by providing your own copy of the repo in ext/fuzztest (see Local Builds in README)" OFF +) option(AVIF_LOCAL_AVM "Build the AVM (AV2) codec by providing your own copy of the repo in ext/avm (see Local Builds in README)" OFF ) @@ -646,6 +649,9 @@ option(AVIF_ENABLE_GTEST "Build avif C++ tests, which depend on GoogleTest. Requires GoogleTest. Has no effect unless AVIF_BUILD_TESTS is ON." ON ) +option(AVIF_ENABLE_FUZZTEST + "Build avif fuzztest targets. Requires Google FuzzTest. Has no effect unless AVIF_BUILD_TESTS and AVIF_ENABLE_GTEST are ON." OFF +) if(AVIF_BUILD_APPS OR (AVIF_BUILD_TESTS AND AVIF_ENABLE_GTEST)) if(NOT AVIF_LOCAL_ZLIBPNG)
diff --git a/ext/fuzztest.cmd b/ext/fuzztest.cmd new file mode 100755 index 0000000..bd72b61 --- /dev/null +++ b/ext/fuzztest.cmd
@@ -0,0 +1,19 @@ +: # If you want to use a local build of fuzztest, you must clone the fuzztest repo in this directory. + +: # The odd choice of comment style in this file is to try to share this script between *nix and win32. + +: # cmake must be in your PATH. + +: # If you're running this on Windows, be sure you've already run this (from your VC2019 install dir): +: # "C:\Program Files (x86)\Microsoft Visual Studio\2019\Professional\VC\Auxiliary\Build\vcvars64.bat" + +git clone https://github.com/google/fuzztest.git +cd fuzztest +: # There is no tagged release as of 2023/09/18. Pick the last commit. +git checkout 867d851afd3d77bbc0067d4007360a1d9e826a22 + +mkdir build.libavif +cd build.libavif +cmake -G Ninja -DBUILD_SHARED_LIBS=OFF -DCMAKE_BUILD_TYPE=Release -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ .. +ninja +cd ../..
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index d82fd56..39c2aa5 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt
@@ -43,8 +43,7 @@ enable_language(CXX) set(CMAKE_CXX_STANDARD 14) add_library(aviftest_helpers OBJECT gtest/aviftest_helpers.cc) - target_link_libraries(aviftest_helpers avif_apps) - target_link_libraries(aviftest_helpers avif_internal) + target_link_libraries(aviftest_helpers avif_apps avif_internal) endif() # Fuzz target without any fuzzing engine dependency. For easy reproduction of oss-fuzz issues. @@ -255,6 +254,53 @@ endif() ################################################################################ +# Experimental FuzzTest support (Linux only) + +if(AVIF_ENABLE_GTEST AND AVIF_ENABLE_FUZZTEST) + if(AVIF_LOCAL_FUZZTEST) + # Run ext/fuzztest.cmd first. + # Recommended top-level CMake options: + # -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DAVIF_ENABLE_WERROR=OFF + # Reproducing a failure can be done by setting the environment variable + # FUZZTEST_REPLAY=/path/to/repro_file.test + # and running one of the targets below. + # See https://github.com/google/fuzztest/blob/main/doc/quickstart-cmake.md + add_subdirectory(${CMAKE_SOURCE_DIR}/ext/fuzztest ${CMAKE_SOURCE_DIR}/ext/fuzztest/build.libavif EXCLUDE_FROM_ALL) + # FuzzTest bundles GoogleTest so no need for the targets below to link to GTEST_BOTH_LIBRARIES. + else() + message(FATAL_ERROR "fuzztest: Installed FuzzTest is not supported, please set AVIF_LOCAL_FUZZTEST=ON or AVIF_ENABLE_FUZZTEST=OFF") + endif() + + add_executable(avif_fuzztest_dec_incr gtest/avif_fuzztest_helpers.cc gtest/avif_fuzztest_dec_incr.cc) + target_link_libraries(avif_fuzztest_dec_incr PRIVATE avifincrtest_helpers) + link_fuzztest(avif_fuzztest_dec_incr) + gtest_discover_tests(avif_fuzztest_dec_incr) + add_test(NAME avif_fuzztest_dec_incr COMMAND avif_fuzztest_dec_incr) + + add_executable(avif_fuzztest_enc_dec gtest/avif_fuzztest_helpers.cc gtest/avif_fuzztest_enc_dec.cc) + target_link_libraries(avif_fuzztest_enc_dec PRIVATE aviftest_helpers) + link_fuzztest(avif_fuzztest_enc_dec) + gtest_discover_tests(avif_fuzztest_enc_dec) + add_test(NAME avif_fuzztest_enc_dec COMMAND avif_fuzztest_enc_dec) + + if(AVIF_ENABLE_EXPERIMENTAL_GAIN_MAP) + add_executable(avif_fuzztest_enc_dec_experimental gtest/avif_fuzztest_helpers.cc gtest/avif_fuzztest_enc_dec_experimental.cc) + target_link_libraries(avif_fuzztest_enc_dec_experimental PRIVATE aviftest_helpers) + link_fuzztest(avif_fuzztest_enc_dec_experimental) + gtest_discover_tests(avif_fuzztest_enc_dec_experimental) + add_test(NAME avif_fuzztest_enc_dec_experimental COMMAND avif_fuzztest_enc_dec_experimental) + endif() + + add_executable(avif_fuzztest_enc_dec_incr gtest/avif_fuzztest_helpers.cc gtest/avif_fuzztest_enc_dec_incr.cc) + target_link_libraries(avif_fuzztest_enc_dec_incr PRIVATE aviftest_helpers avifincrtest_helpers) + link_fuzztest(avif_fuzztest_enc_dec_incr) + gtest_discover_tests(avif_fuzztest_enc_dec_incr) + add_test(NAME avif_fuzztest_enc_dec_incr COMMAND avif_fuzztest_enc_dec_incr) +elseif(AVIF_ENABLE_GTEST) + message(STATUS "FuzzTest targets are disabled because AVIF_ENABLE_FUZZTEST is OFF.") +endif() + +################################################################################ # Bash tests if(AVIF_BUILD_APPS)
diff --git a/tests/gtest/avif_fuzztest_dec_incr.cc b/tests/gtest/avif_fuzztest_dec_incr.cc new file mode 100644 index 0000000..5fc2cf3 --- /dev/null +++ b/tests/gtest/avif_fuzztest_dec_incr.cc
@@ -0,0 +1,98 @@ +// Copyright 2022 Google LLC +// SPDX-License-Identifier: BSD-2-Clause +// Compare non-incremental and incremental decode results of an arbitrary byte +// sequence. + +#include <cstdint> +#include <vector> + +#include "avif/avif.h" +#include "avif_fuzztest_helpers.h" +#include "avifincrtest_helpers.h" +#include "fuzztest/fuzztest.h" +#include "gtest/gtest.h" + +using ::fuzztest::Arbitrary; + +namespace libavif { +namespace testutil { +namespace { + +::testing::Environment* const stack_limit_env = + ::testing::AddGlobalTestEnvironment( + new FuzztestStackLimitEnvironment("524288")); // 512 * 1024 + +//------------------------------------------------------------------------------ + +struct DecoderInput { + const uint8_t* available_bytes; + size_t available_size; + size_t read_size; +}; + +// A custom reader is necessary to get the number of bytes read by libavif. +// See avifIOReadFunc() documentation. +avifResult AvifIoRead(struct avifIO* io, uint32_t read_flags, uint64_t offset, + size_t size, avifROData* out) { + DecoderInput* data = reinterpret_cast<DecoderInput*>(io->data); + if (read_flags != 0 || !data || data->available_size < offset) { + return AVIF_RESULT_IO_ERROR; + } + out->data = data->available_bytes + offset; + out->size = std::min(size, data->available_size - offset); + data->read_size = std::max(data->read_size, offset + out->size); + return AVIF_RESULT_OK; +} + +//------------------------------------------------------------------------------ + +void DecodeIncr(const std::vector<uint8_t>& arbitrary_bytes, bool is_persistent, + bool give_size_hint, bool use_nth_image_api) { + AvifImagePtr reference(avifImageCreateEmpty(), avifImageDestroy); + ASSERT_NE(reference.get(), nullptr); + + DecoderInput data = {arbitrary_bytes.data(), arbitrary_bytes.size(), 0}; + avifIO io = {.read = AvifIoRead, + .sizeHint = arbitrary_bytes.size(), + .persistent = AVIF_TRUE, + .data = &data}; + + AvifDecoderPtr decoder(avifDecoderCreate(), avifDecoderDestroy); + ASSERT_NE(decoder.get(), nullptr); + avifDecoderSetIO(decoder.get(), &io); + + if (avifDecoderRead(decoder.get(), reference.get()) == AVIF_RESULT_OK) { + // Avoid timeouts by discarding big images decoded many times. + // TODO(yguyon): Increase this arbitrary threshold but decode incrementally + // fewer times than as many bytes. + if (reference->width * reference->height * data.read_size > + 8 * 1024 * 1024) { + return; + } + // decodeIncrementally() will fail if there are leftover bytes. + const avifRWData encoded_data = {const_cast<uint8_t*>(data.available_bytes), + data.read_size}; + // No clue on whether encoded_data is tiled so use a lower bound of a single + // tile for the whole image. + // Note that an AVIF tile is at most as high as an AV1 frame + // (aomediacodec.github.io/av1-spec says max_frame_height_minus_1 < 65536) + // but libavif successfully decodes AVIF files with dimensions unrelated to + // the underlying AV1 frame (for example a 1x1000000 AVIF for a 1x1 AV1). + // Otherwise we could use the minimum of reference->height and 65536u below. + const uint32_t max_cell_height = reference->height; + DecodeIncrementally(encoded_data, decoder.get(), is_persistent, + give_size_hint, use_nth_image_api, *reference, + max_cell_height); + } +} + +FUZZ_TEST(DecodeAvifTest, DecodeIncr) + .WithDomains(Arbitrary<std::vector<uint8_t>>(), Arbitrary<bool>(), + Arbitrary<bool>(), Arbitrary<bool>()) + .WithSeeds({{GetWhiteSinglePixelAvif(), false, false, false}}); + +//------------------------------------------------------------------------------ + +} // namespace +} // namespace testutil +} // namespace libavif
diff --git a/tests/gtest/avif_fuzztest_enc_dec.cc b/tests/gtest/avif_fuzztest_enc_dec.cc new file mode 100644 index 0000000..013046c --- /dev/null +++ b/tests/gtest/avif_fuzztest_enc_dec.cc
@@ -0,0 +1,59 @@ +// Copyright 2022 Google LLC +// SPDX-License-Identifier: BSD-2-Clause + +#include <vector> + +#include "avif/avif.h" +#include "avif_fuzztest_helpers.h" +#include "aviftest_helpers.h" +#include "fuzztest/fuzztest.h" +#include "gtest/gtest.h" + +namespace libavif { +namespace testutil { +namespace { + +::testing::Environment* const stack_limit_env = + ::testing::AddGlobalTestEnvironment( + new FuzztestStackLimitEnvironment("524288")); // 512 * 1024 + +void EncodeDecodeValid(AvifImagePtr image, AvifEncoderPtr encoder, + AvifDecoderPtr decoder) { + AvifImagePtr decoded_image(avifImageCreateEmpty(), avifImageDestroy); + ASSERT_NE(image.get(), nullptr); + ASSERT_NE(encoder.get(), nullptr); + ASSERT_NE(decoder.get(), nullptr); + ASSERT_NE(decoded_image.get(), nullptr); + + AvifRwData encoded_data; + const avifResult encoder_result = + avifEncoderWrite(encoder.get(), image.get(), &encoded_data); + ASSERT_EQ(encoder_result, AVIF_RESULT_OK) + << avifResultToString(encoder_result); + + const avifResult decoder_result = avifDecoderReadMemory( + decoder.get(), decoded_image.get(), encoded_data.data, encoded_data.size); + ASSERT_EQ(decoder_result, AVIF_RESULT_OK) + << avifResultToString(decoder_result); + + EXPECT_EQ(decoded_image->width, image->width); + EXPECT_EQ(decoded_image->height, image->height); + EXPECT_EQ(decoded_image->depth, image->depth); + EXPECT_EQ(decoded_image->yuvFormat, image->yuvFormat); + + // Verify that an opaque input leads to an opaque output. + if (IsOpaque(image.get())) { + EXPECT_TRUE(IsOpaque(decoded_image.get())); + } + // A transparent image may be heavily compressed to an opaque image. This is + // hard to verify so do not check it. +} + +FUZZ_TEST(EncodeDecodeAvifTest, EncodeDecodeValid) + .WithDomains(ArbitraryAvifImage(), ArbitraryAvifEncoder(), + ArbitraryAvifDecoder({AVIF_CODEC_CHOICE_AUTO, + AVIF_CODEC_CHOICE_DAV1D})); + +} // namespace +} // namespace testutil +} // namespace libavif
diff --git a/tests/gtest/avif_fuzztest_enc_dec_experimental.cc b/tests/gtest/avif_fuzztest_enc_dec_experimental.cc new file mode 100644 index 0000000..aac68fc --- /dev/null +++ b/tests/gtest/avif_fuzztest_enc_dec_experimental.cc
@@ -0,0 +1,127 @@ +// Copyright 2022 Google LLC +// SPDX-License-Identifier: BSD-2-Clause + +#include <array> +#include <cstdint> +#include <cstring> +#include <memory> +#include <vector> + +#include "avif/avif.h" +#include "avif_fuzztest_helpers.h" +#include "aviftest_helpers.h" +#include "fuzztest/fuzztest.h" +#include "gtest/gtest.h" + +namespace libavif { +namespace testutil { +namespace { + +::testing::Environment* const stack_limit_env = + ::testing::AddGlobalTestEnvironment( + new FuzztestStackLimitEnvironment("524288")); // 512 * 1024 + +void CheckGainMapMetadataMatches(const avifGainMapMetadata& actual, + const avifGainMapMetadata& expected) { + // 'expecteed' is the source struct which has arbitrary data and booleans + // values can contain any value, but the decoded ('actual') struct should + // be 0 or 1. + EXPECT_EQ(actual.baseRenditionIsHDR, expected.baseRenditionIsHDR ? 1 : 0); + EXPECT_EQ(actual.hdrCapacityMinN, expected.hdrCapacityMinN); + EXPECT_EQ(actual.hdrCapacityMinD, expected.hdrCapacityMinD); + EXPECT_EQ(actual.hdrCapacityMaxN, expected.hdrCapacityMaxN); + EXPECT_EQ(actual.hdrCapacityMaxD, expected.hdrCapacityMaxD); + for (int c = 0; c < 3; ++c) { + SCOPED_TRACE(c); + EXPECT_EQ(actual.offsetSdrN[c], expected.offsetSdrN[c]); + EXPECT_EQ(actual.offsetSdrD[c], expected.offsetSdrD[c]); + EXPECT_EQ(actual.offsetHdrN[c], expected.offsetHdrN[c]); + EXPECT_EQ(actual.offsetHdrD[c], expected.offsetHdrD[c]); + EXPECT_EQ(actual.gainMapGammaN[c], expected.gainMapGammaN[c]); + EXPECT_EQ(actual.gainMapGammaD[c], expected.gainMapGammaD[c]); + EXPECT_EQ(actual.gainMapMinN[c], expected.gainMapMinN[c]); + EXPECT_EQ(actual.gainMapMinD[c], expected.gainMapMinD[c]); + EXPECT_EQ(actual.gainMapMaxN[c], expected.gainMapMaxN[c]); + EXPECT_EQ(actual.gainMapMaxD[c], expected.gainMapMaxD[c]); + } +} + +void EncodeDecodeValid(AvifImagePtr image, AvifEncoderPtr encoder, + AvifDecoderPtr decoder) { + AvifImagePtr decoded_image(avifImageCreateEmpty(), avifImageDestroy); + ASSERT_NE(image.get(), nullptr); + ASSERT_NE(encoder.get(), nullptr); + ASSERT_NE(decoder.get(), nullptr); + ASSERT_NE(decoded_image.get(), nullptr); + + // TODO(maryla): fuzz with different settings. + decoder->enableDecodingGainMap = true; + decoder->enableParsingGainMapMetadata = true; + + AvifRwData encoded_data; + const avifResult encoder_result = + avifEncoderWrite(encoder.get(), image.get(), &encoded_data); + ASSERT_EQ(encoder_result, AVIF_RESULT_OK) + << avifResultToString(encoder_result); + + const avifResult decoder_result = avifDecoderReadMemory( + decoder.get(), decoded_image.get(), encoded_data.data, encoded_data.size); + ASSERT_EQ(decoder_result, AVIF_RESULT_OK) + << avifResultToString(decoder_result); + + EXPECT_EQ(decoded_image->width, image->width); + EXPECT_EQ(decoded_image->height, image->height); + EXPECT_EQ(decoded_image->depth, image->depth); + EXPECT_EQ(decoded_image->yuvFormat, image->yuvFormat); + + EXPECT_EQ(image->gainMap.image != nullptr, + decoded_image->gainMap.image != nullptr); + if (image->gainMap.image != nullptr) { + EXPECT_EQ(decoded_image->gainMap.image->width, image->gainMap.image->width); + EXPECT_EQ(decoded_image->gainMap.image->height, + image->gainMap.image->height); + EXPECT_EQ(decoded_image->gainMap.image->depth, image->gainMap.image->depth); + EXPECT_EQ(decoded_image->gainMap.image->yuvFormat, + image->gainMap.image->yuvFormat); + EXPECT_EQ(image->gainMap.image->gainMap.image, nullptr); + EXPECT_EQ(decoded_image->gainMap.image->alphaPlane, nullptr); + + CheckGainMapMetadataMatches(decoded_image->gainMap.metadata, + image->gainMap.metadata); + } + + // Verify that an opaque input leads to an opaque output. + if (IsOpaque(image.get())) { + EXPECT_TRUE(IsOpaque(decoded_image.get())); + } + // A transparent image may be heavily compressed to an opaque image. This is + // hard to verify so do not check it. +} + +// Note that the images are passed as raw pointers because unique_ptr didn't +// seem to work. Similarly, avifGainMapMetadata is passed as a byte array +// because the C array fields in the struct seem to prevent fuzztest from +// handling it natively. +AvifImagePtr AddGainMapToImage( + avifImage* image, avifImage* gainMap, + const std::array<uint8_t, sizeof(avifGainMapMetadata)>& metadata) { + image->gainMap.image = gainMap; + std::memcpy(&image->gainMap.metadata, metadata.data(), metadata.size()); + return AvifImagePtr(image, avifImageDestroy); +} + +inline auto ArbitraryAvifImageWithGainMap() { + return fuzztest::Map( + AddGainMapToImage, ArbitraryAvifImageRawPtr(), ArbitraryAvifImageRawPtr(), + fuzztest::Arbitrary<std::array<uint8_t, sizeof(avifGainMapMetadata)>>()); +} + +FUZZ_TEST(EncodeDecodeAvifTest8b, EncodeDecodeValid) + .WithDomains(fuzztest::OneOf(ArbitraryAvifImage(), + ArbitraryAvifImageWithGainMap()), + ArbitraryAvifEncoder(), + ArbitraryAvifDecoder({AVIF_CODEC_CHOICE_AUTO})); + +} // namespace +} // namespace testutil +} // namespace libavif
diff --git a/tests/gtest/avif_fuzztest_enc_dec_incr.cc b/tests/gtest/avif_fuzztest_enc_dec_incr.cc new file mode 100644 index 0000000..c5f9499 --- /dev/null +++ b/tests/gtest/avif_fuzztest_enc_dec_incr.cc
@@ -0,0 +1,132 @@ +// Copyright 2022 Google LLC +// SPDX-License-Identifier: BSD-2-Clause +// Encode a fuzzed image split into a grid and decode it incrementally. +// Compare the output with a non-incremental decode. + +#include <cassert> +#include <cstdint> +#include <vector> + +#include "avif/internal.h" +#include "avif_fuzztest_helpers.h" +#include "avifincrtest_helpers.h" +#include "aviftest_helpers.h" +#include "fuzztest/fuzztest.h" +#include "gtest/gtest.h" + +using ::fuzztest::Arbitrary; +using ::fuzztest::InRange; + +namespace libavif { +namespace testutil { +namespace { + +::testing::Environment* const stack_limit_env = + ::testing::AddGlobalTestEnvironment( + new FuzztestStackLimitEnvironment("524288")); // 512 * 1024 + +// Splits the input image into grid_cols*grid_rows views to be encoded as a +// grid. Returns an empty vector if the input image cannot be split that way. +std::vector<AvifImagePtr> ImageToGrid(const AvifImagePtr& image, + uint32_t grid_cols, uint32_t grid_rows) { + if (image->width < grid_cols || image->height < grid_rows) return {}; + + // Round up, to make sure all samples are used by exactly one cell. + uint32_t cell_width = (image->width + grid_cols - 1) / grid_cols; + uint32_t cell_height = (image->height + grid_rows - 1) / grid_rows; + + if ((grid_cols - 1) * cell_width >= image->width) { + // Some cells are completely outside the image. Fallback to a grid entirely + // contained within the image boundaries. Some samples will be discarded but + // at least the test can go on. + cell_width = image->width / grid_cols; + } + if ((grid_rows - 1) * cell_height >= image->height) { + cell_height = image->height / grid_rows; + } + std::vector<AvifImagePtr> cells; + for (uint32_t row = 0; row < grid_rows; ++row) { + for (uint32_t col = 0; col < grid_cols; ++col) { + avifCropRect rect{col * cell_width, row * cell_height, cell_width, + cell_height}; + assert(rect.x < image->width); + assert(rect.y < image->height); + // The right-most and bottom-most cells may be smaller than others. + // The encoder will pad them. + if (rect.x + rect.width > image->width) { + rect.width = image->width - rect.x; + } + if (rect.y + rect.height > image->height) { + rect.height = image->height - rect.y; + } + cells.emplace_back(avifImageCreateEmpty(), avifImageDestroy); + if (avifImageSetViewRect(cells.back().get(), image.get(), &rect) != + AVIF_RESULT_OK) { + return {}; + } + } + } + return cells; +} + +// Converts a unique_ptr array to a raw pointer array as needed by libavif API. +std::vector<const avifImage*> UniquePtrToRawPtr( + const std::vector<AvifImagePtr>& unique_ptrs) { + std::vector<const avifImage*> rawPtrs; + rawPtrs.reserve(unique_ptrs.size()); + for (const AvifImagePtr& unique_ptr : unique_ptrs) { + rawPtrs.emplace_back(unique_ptr.get()); + } + return rawPtrs; +} + +// Encodes an image into an AVIF grid then decodes it. +void EncodeDecodeGridValid(AvifImagePtr image, AvifEncoderPtr encoder, + AvifDecoderPtr decoder, uint32_t grid_cols, + uint32_t grid_rows, bool is_encoded_data_persistent, + bool give_size_hint_to_decoder) { + ASSERT_NE(image.get(), nullptr); + ASSERT_NE(encoder.get(), nullptr); + + std::vector<AvifImagePtr> cells = ImageToGrid(image, grid_cols, grid_rows); + if (cells.empty()) return; + const uint32_t cell_width = cells.front().get()->width; + const uint32_t cell_height = cells.front().get()->height; + const uint32_t encoded_width = std::min(image->width, grid_cols * cell_width); + const uint32_t encoded_height = + std::min(image->height, grid_rows * cell_height); + + AvifRwData encoded_data; + const avifResult encoder_result = avifEncoderAddImageGrid( + encoder.get(), grid_cols, grid_rows, UniquePtrToRawPtr(cells).data(), + AVIF_ADD_IMAGE_FLAG_SINGLE); + if (((grid_cols > 1 || grid_rows > 1) && + !avifAreGridDimensionsValid(image->yuvFormat, encoded_width, + encoded_height, cell_width, cell_height, + nullptr))) { + ASSERT_TRUE(encoder_result == AVIF_RESULT_INVALID_IMAGE_GRID) + << avifResultToString(encoder_result); + return; + } + ASSERT_EQ(encoder_result, AVIF_RESULT_OK) + << avifResultToString(encoder_result); + + const avifResult finish_result = + avifEncoderFinish(encoder.get(), &encoded_data); + ASSERT_EQ(finish_result, AVIF_RESULT_OK) << avifResultToString(finish_result); + + DecodeNonIncrementallyAndIncrementally(encoded_data, decoder.get(), + is_encoded_data_persistent, + give_size_hint_to_decoder, + /*useNthImageApi=*/true, cell_height); +} + +FUZZ_TEST(EncodeDecodeAvifTest, EncodeDecodeGridValid) + .WithDomains(ArbitraryAvifImage(), ArbitraryAvifEncoder(), + ArbitraryAvifDecoder({AVIF_CODEC_CHOICE_AUTO}), + InRange<uint32_t>(1, 32), InRange<uint32_t>(1, 32), + Arbitrary<bool>(), Arbitrary<bool>()); + +} // namespace +} // namespace testutil +} // namespace libavif
diff --git a/tests/gtest/avif_fuzztest_helpers.cc b/tests/gtest/avif_fuzztest_helpers.cc new file mode 100644 index 0000000..bbd0d93 --- /dev/null +++ b/tests/gtest/avif_fuzztest_helpers.cc
@@ -0,0 +1,204 @@ +// Copyright 2022 Google LLC +// SPDX-License-Identifier: BSD-2-Clause + +#include "avif_fuzztest_helpers.h" + +#include <algorithm> +#include <cassert> + +#include "avif/avif.h" + +namespace libavif { +namespace testutil { +namespace { + +//------------------------------------------------------------------------------ + +AvifImagePtr CreateAvifImage(size_t width, size_t height, int depth, + avifPixelFormat pixel_format, bool has_alpha, + const uint8_t* samples) { + AvifImagePtr image(avifImageCreate(width, height, depth, pixel_format), + avifImageDestroy); + if (image.get() == nullptr) { + return image; + } + avifImageAllocatePlanes(image.get(), + has_alpha ? AVIF_PLANES_ALL : AVIF_PLANES_YUV); + + for (avifChannelIndex c : + {AVIF_CHAN_Y, AVIF_CHAN_U, AVIF_CHAN_V, AVIF_CHAN_A}) { + const size_t plane_height = avifImagePlaneHeight(image.get(), c); + uint8_t* row = avifImagePlane(image.get(), c); + const uint32_t row_bytes = avifImagePlaneRowBytes(image.get(), c); + assert(row_bytes == avifImagePlaneWidth(image.get(), c) * + (avifImageUsesU16(image.get()) ? 2 : 1)); + for (size_t y = 0; y < plane_height; ++y) { + std::copy(samples, samples + row_bytes, row); + row += row_bytes; + samples += row_bytes; + } + } + return image; +} + +} // namespace + +avifImage* CreateAvifImage8bRawPtr(size_t width, size_t height, + avifPixelFormat pixel_format, bool has_alpha, + const std::vector<uint8_t>& samples) { + return CreateAvifImage(width, height, 8, pixel_format, has_alpha, + samples.data()) + .release(); +} + +avifImage* CreateAvifImage16bRawPtr(size_t width, size_t height, int depth, + avifPixelFormat pixel_format, + bool has_alpha, + const std::vector<uint16_t>& samples) { + return CreateAvifImage(width, height, depth, pixel_format, has_alpha, + reinterpret_cast<const uint8_t*>(samples.data())) + .release(); +} + +AvifEncoderPtr CreateAvifEncoder(avifCodecChoice codec_choice, int max_threads, + int min_quantizer, int max_quantizer, + int min_quantizer_alpha, + int max_quantizer_alpha, int tile_rows_log2, + int tile_cols_log2, int speed) { + AvifEncoderPtr encoder(avifEncoderCreate(), avifEncoderDestroy); + if (encoder.get() == nullptr) { + return encoder; + } + encoder->codecChoice = codec_choice; + encoder->maxThreads = max_threads; + // minQuantizer must be at most maxQuantizer. + encoder->minQuantizer = std::min(min_quantizer, max_quantizer); + encoder->maxQuantizer = std::max(min_quantizer, max_quantizer); + encoder->minQuantizerAlpha = + std::min(min_quantizer_alpha, max_quantizer_alpha); + encoder->maxQuantizerAlpha = + std::max(min_quantizer_alpha, max_quantizer_alpha); + encoder->tileRowsLog2 = tile_rows_log2; + encoder->tileColsLog2 = tile_cols_log2; + encoder->speed = speed; + return encoder; +} + +AvifDecoderPtr CreateAvifDecoder(avifCodecChoice codec_choice, int max_threads, + avifDecoderSource requested_source, + bool allow_progressive, bool allow_incremental, + bool ignore_exif, bool ignore_xmp, + uint32_t image_size_limit, + uint32_t image_dimension_limit, + uint32_t image_count_limit, + avifStrictFlags strict_flags) { + AvifDecoderPtr decoder(avifDecoderCreate(), avifDecoderDestroy); + if (decoder.get() == nullptr) { + return decoder; + } + decoder->codecChoice = codec_choice; + decoder->maxThreads = max_threads; + decoder->requestedSource = requested_source; + decoder->allowProgressive = allow_progressive; + decoder->allowIncremental = allow_incremental; + decoder->ignoreExif = ignore_exif; + decoder->ignoreXMP = ignore_xmp; + decoder->imageSizeLimit = image_size_limit; + decoder->imageDimensionLimit = image_dimension_limit; + decoder->imageCountLimit = image_count_limit; + decoder->strictFlags = strict_flags; + return decoder; +} + +AvifImagePtr AvifImageToUniquePtr(avifImage* image) { + return AvifImagePtr(image, avifImageDestroy); +} + +//------------------------------------------------------------------------------ + +size_t GetNumSamples(size_t width, size_t height, avifPixelFormat pixel_format, + bool has_alpha) { + const size_t num_luma_samples = width * height; + + avifPixelFormatInfo pixel_format_info; + avifGetPixelFormatInfo(pixel_format, &pixel_format_info); + size_t num_chroma_samples = 0; + if (!pixel_format_info.monochrome) { + num_chroma_samples = 2 * + ((width + pixel_format_info.chromaShiftX) >> + pixel_format_info.chromaShiftX) * + ((height + pixel_format_info.chromaShiftY) >> + pixel_format_info.chromaShiftY); + } + + size_t num_alpha_samples = 0; + if (has_alpha) { + num_alpha_samples = width * height; + } + + return num_luma_samples + num_chroma_samples + num_alpha_samples; +} + +//------------------------------------------------------------------------------ + +std::vector<uint8_t> GetWhiteSinglePixelAvif() { + return { + 0x0, 0x0, 0x0, 0x20, 0x66, 0x74, 0x79, 0x70, 0x61, 0x76, 0x69, 0x66, + 0x0, 0x0, 0x0, 0x0, 0x61, 0x76, 0x69, 0x66, 0x6d, 0x69, 0x66, 0x31, + 0x6d, 0x69, 0x61, 0x66, 0x4d, 0x41, 0x31, 0x41, 0x0, 0x0, 0x0, 0xf2, + 0x6d, 0x65, 0x74, 0x61, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x28, + 0x68, 0x64, 0x6c, 0x72, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x70, 0x69, 0x63, 0x74, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x0, 0x0, 0x6c, 0x69, 0x62, 0x61, 0x76, 0x69, 0x66, 0x0, + 0x0, 0x0, 0x0, 0xe, 0x70, 0x69, 0x74, 0x6d, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x1, 0x0, 0x0, 0x0, 0x1e, 0x69, 0x6c, 0x6f, 0x63, 0x0, 0x0, + 0x0, 0x0, 0x44, 0x0, 0x0, 0x1, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, + 0x0, 0x0, 0x1, 0x1a, 0x0, 0x0, 0x0, 0x17, 0x0, 0x0, 0x0, 0x28, + 0x69, 0x69, 0x6e, 0x66, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, + 0x0, 0x1a, 0x69, 0x6e, 0x66, 0x65, 0x2, 0x0, 0x0, 0x0, 0x0, 0x1, + 0x0, 0x0, 0x61, 0x76, 0x30, 0x31, 0x43, 0x6f, 0x6c, 0x6f, 0x72, 0x0, + 0x0, 0x0, 0x0, 0x6a, 0x69, 0x70, 0x72, 0x70, 0x0, 0x0, 0x0, 0x4b, + 0x69, 0x70, 0x63, 0x6f, 0x0, 0x0, 0x0, 0x14, 0x69, 0x73, 0x70, 0x65, + 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1, + 0x0, 0x0, 0x0, 0x10, 0x70, 0x69, 0x78, 0x69, 0x0, 0x0, 0x0, 0x0, + 0x3, 0x8, 0x8, 0x8, 0x0, 0x0, 0x0, 0xc, 0x61, 0x76, 0x31, 0x43, + 0x81, 0x20, 0x0, 0x0, 0x0, 0x0, 0x0, 0x13, 0x63, 0x6f, 0x6c, 0x72, + 0x6e, 0x63, 0x6c, 0x78, 0x0, 0x1, 0x0, 0xd, 0x0, 0x6, 0x80, 0x0, + 0x0, 0x0, 0x17, 0x69, 0x70, 0x6d, 0x61, 0x0, 0x0, 0x0, 0x0, 0x0, + 0x0, 0x0, 0x1, 0x0, 0x1, 0x4, 0x1, 0x2, 0x83, 0x4, 0x0, 0x0, + 0x0, 0x1f, 0x6d, 0x64, 0x61, 0x74, 0x12, 0x0, 0xa, 0x7, 0x38, 0x0, + 0x6, 0x10, 0x10, 0xd0, 0x69, 0x32, 0xa, 0x1f, 0xf0, 0x3f, 0xff, 0xff, + 0xc4, 0x0, 0x0, 0xaf, 0x70}; +} + +//------------------------------------------------------------------------------ + +template <typename SampleType> +bool IsOpaque(const avifImage* image) { + if (!image->alphaPlane) { + return true; + } + + const SampleType opaque_alpha = (1 << image->depth) - 1; + for (uint32_t j = 0; j < image->height; ++j) { + const SampleType* const row = reinterpret_cast<SampleType*>( + image->alphaPlane + j * image->alphaRowBytes); + for (uint32_t i = 0; i < image->width; ++i) { + if (row[i] != opaque_alpha) { + assert(row[i] < opaque_alpha); + return false; + } + } + } + return true; +} + +bool IsOpaque(const avifImage* image) { + return avifImageUsesU16(image) ? IsOpaque<uint16_t>(image) + : IsOpaque<uint8_t>(image); +} + +//------------------------------------------------------------------------------ + +} // namespace testutil +} // namespace libavif
diff --git a/tests/gtest/avif_fuzztest_helpers.h b/tests/gtest/avif_fuzztest_helpers.h new file mode 100644 index 0000000..933d23f --- /dev/null +++ b/tests/gtest/avif_fuzztest_helpers.h
@@ -0,0 +1,191 @@ +// Copyright 2022 Google LLC +// SPDX-License-Identifier: BSD-2-Clause + +#ifndef LIBAVIF_TESTS_OSS_FUZZ_AVIF_FUZZTEST_HELPERS_H_ +#define LIBAVIF_TESTS_OSS_FUZZ_AVIF_FUZZTEST_HELPERS_H_ + +#include <cstdlib> +#include <memory> +#include <vector> + +#include "avif/avif.h" +#include "aviftest_helpers.h" +#include "fuzztest/fuzztest.h" +#include "gtest/gtest.h" + +namespace libavif { +namespace testutil { + +//------------------------------------------------------------------------------ +// C++ wrapper for scoped memory management of C API objects. + +// Exposed for convenient fuzztest reproducer output. +avifImage* CreateAvifImage8bRawPtr(size_t width, size_t height, + avifPixelFormat pixel_format, bool has_alpha, + const std::vector<uint8_t>& samples); +avifImage* CreateAvifImage16bRawPtr(size_t width, size_t height, int depth, + avifPixelFormat pixel_format, + bool has_alpha, + const std::vector<uint16_t>& samples); +AvifEncoderPtr CreateAvifEncoder(avifCodecChoice codec_choice, int max_threads, + int min_quantizer, int max_quantizer, + int min_quantizer_alpha, + int max_quantizer_alpha, int tile_rows_log2, + int tile_cols_log2, int speed); +AvifDecoderPtr CreateAvifDecoder(avifCodecChoice codec_choice, int max_threads, + avifDecoderSource requested_source, + bool allow_progressive, bool allow_incremental, + bool ignore_exif, bool ignore_xmp, + uint32_t image_size_limit, + uint32_t image_dimension_limit, + uint32_t image_count_limit, + avifStrictFlags strict_flags); +AvifImagePtr AvifImageToUniquePtr(avifImage* image); + +//------------------------------------------------------------------------------ +// Custom fuzztest generators. +// See https://github.com/google/fuzztest/blob/main/doc/domains-reference.md. + +// Do not generate images wider or taller than this. +inline constexpr size_t kMaxDimension = 512; // In pixels. + +size_t GetNumSamples(size_t width, size_t height, avifPixelFormat pixel_format, + bool has_alpha); + +// To avoid using fuzztest::internal, the return type of the functions below is +// auto. + +inline auto ArbitraryPixelFormat() { + return fuzztest::ElementOf<avifPixelFormat>( + {AVIF_PIXEL_FORMAT_YUV444, AVIF_PIXEL_FORMAT_YUV422, + AVIF_PIXEL_FORMAT_YUV420, AVIF_PIXEL_FORMAT_YUV400}); +} + +// avifImage generator type: Width, height, pixel format and 8-bit samples. +inline auto ArbitraryAvifImage8bRawPtr() { + return fuzztest::FlatMap( + [](size_t width, size_t height, avifPixelFormat pixel_format, + bool has_alpha) { + return fuzztest::Map( + CreateAvifImage8bRawPtr, fuzztest::Just(width), + fuzztest::Just(height), fuzztest::Just(pixel_format), + fuzztest::Just(has_alpha), + fuzztest::Arbitrary<std::vector<uint8_t>>().WithSize( + GetNumSamples(width, height, pixel_format, has_alpha))); + }, + fuzztest::InRange<uint16_t>(1, kMaxDimension), + fuzztest::InRange<uint16_t>(1, kMaxDimension), ArbitraryPixelFormat(), + fuzztest::Arbitrary<bool>()); +} + +// avifImage generator type: Width, height, depth, pixel format and 16-bit +// samples. +inline auto ArbitraryAvifImage16bRawPtr() { + return fuzztest::FlatMap( + [](size_t width, size_t height, int depth, avifPixelFormat pixel_format, + bool has_alpha) { + return fuzztest::Map( + CreateAvifImage16bRawPtr, fuzztest::Just(width), + fuzztest::Just(height), fuzztest::Just(depth), + fuzztest::Just(pixel_format), fuzztest::Just(has_alpha), + fuzztest::ContainerOf<std::vector<uint16_t>>( + fuzztest::InRange<uint16_t>(0, (1 << depth) - 1)) + .WithSize( + GetNumSamples(width, height, pixel_format, has_alpha))); + }, + fuzztest::InRange<uint16_t>(1, kMaxDimension), + fuzztest::InRange<uint16_t>(1, kMaxDimension), + fuzztest::ElementOf({10, 12}), ArbitraryPixelFormat(), + fuzztest::Arbitrary<bool>()); +} + +// Generator for an arbitrary avifImage* (raw pointer). +inline auto ArbitraryAvifImageRawPtr() { + return fuzztest::OneOf(ArbitraryAvifImage8bRawPtr(), + ArbitraryAvifImage16bRawPtr()); +} + +// Generator for an arbitrary AvifImagePtr (unique pointer). +inline auto ArbitraryAvifImage() { + return fuzztest::Map(AvifImageToUniquePtr, ArbitraryAvifImageRawPtr()); +} + +// avifEncoder and avifDecoder generators +inline auto ArbitraryAvifEncoder() { + const auto codec_choice = fuzztest::ElementOf<avifCodecChoice>( + {AVIF_CODEC_CHOICE_AUTO, AVIF_CODEC_CHOICE_AOM}); + // MAX_NUM_THREADS from libaom/aom_util/aom_thread.h + const auto max_threads = fuzztest::InRange(0, 64); + const auto min_quantizer = fuzztest::InRange(AVIF_QUANTIZER_BEST_QUALITY, + AVIF_QUANTIZER_WORST_QUALITY); + const auto max_quantizer = fuzztest::InRange(AVIF_QUANTIZER_BEST_QUALITY, + AVIF_QUANTIZER_WORST_QUALITY); + const auto min_quantizer_alpha = fuzztest::InRange( + AVIF_QUANTIZER_BEST_QUALITY, AVIF_QUANTIZER_WORST_QUALITY); + const auto max_quantizer_alpha = fuzztest::InRange( + AVIF_QUANTIZER_BEST_QUALITY, AVIF_QUANTIZER_WORST_QUALITY); + const auto tile_rows_log2 = fuzztest::InRange(0, 6); + const auto tile_cols_log2 = fuzztest::InRange(0, 6); + // Fuzz only a small range of 'speed' values to avoid slowing down the fuzzer + // too much. The main goal is to fuzz libavif, not the underlying AV1 encoder. + const auto speed = fuzztest::InRange(6, AVIF_SPEED_FASTEST); + return fuzztest::Map(CreateAvifEncoder, codec_choice, max_threads, + min_quantizer, max_quantizer, min_quantizer_alpha, + max_quantizer_alpha, tile_rows_log2, tile_cols_log2, + speed); +} + +inline auto ArbitraryAvifDecoder( + const std::vector<avifCodecChoice>& codecChoices) { + const auto codec_choice = fuzztest::ElementOf<avifCodecChoice>(codecChoices); + // MAX_NUM_THREADS from libaom/aom_util/aom_thread.h + const auto max_threads = fuzztest::InRange(0, 64); + return fuzztest::Map( + CreateAvifDecoder, codec_choice, max_threads, + /*requested_source=*/ + fuzztest::ElementOf( + {AVIF_DECODER_SOURCE_AUTO, AVIF_DECODER_SOURCE_PRIMARY_ITEM}), + /*allow_progressive=*/fuzztest::Arbitrary<bool>(), + /*allow_incremental=*/fuzztest::Arbitrary<bool>(), + /*ignore_exif=*/fuzztest::Arbitrary<bool>(), + /*ignore_xmp=*/fuzztest::Arbitrary<bool>(), + /*image_size_limit=*/fuzztest::Just(kMaxDimension * kMaxDimension), + /*image_dimension_limit=*/fuzztest::Just(kMaxDimension), + /*image_count_limit=*/fuzztest::Just(10), + /*strict_flags=*/ + fuzztest::BitFlagCombinationOf({AVIF_STRICT_PIXI_REQUIRED, + AVIF_STRICT_CLAP_VALID, + AVIF_STRICT_ALPHA_ISPE_REQUIRED})); +} + +//------------------------------------------------------------------------------ + +// Returns a white pixel compressed with AVIF. +std::vector<uint8_t> GetWhiteSinglePixelAvif(); + +// Sets the FUZZTEST_STACK_LIMIT environment variable to the value passed to +// the constructor. +class FuzztestStackLimitEnvironment : public ::testing::Environment { + public: + FuzztestStackLimitEnvironment(const char* stack_limit) + : stack_limit_(stack_limit) {} + ~FuzztestStackLimitEnvironment() override {} + + void SetUp() override { setenv("FUZZTEST_STACK_LIMIT", stack_limit_, 1); } + + private: + const char* stack_limit_; +}; + +//------------------------------------------------------------------------------ + +// Returns true if the given image has a fully opaque alpha plane, or no alpha +// plane at all. +bool IsOpaque(const avifImage* image); + +//------------------------------------------------------------------------------ + +} // namespace testutil +} // namespace libavif + +#endif // LIBAVIF_TESTS_OSS_FUZZ_AVIF_FUZZTEST_HELPERS_H_