Split Get() into Identity and Features Usual clients such as php-src begin by identifying the bitstream's format and only after start to parse it for width, height etc. Splitting the function in two avoids a stream rewind and/or parsing the same data twice. Change-Id: I4fc814e442bebe6f48d94f634c7e86dd07c73867
diff --git a/avifinfo.c b/avifinfo.c index 88a6e11..5b93d19 100644 --- a/avifinfo.c +++ b/avifinfo.c
@@ -24,6 +24,13 @@ kInvalid, // Input incorrectly parsed. } AvifInfoInternalStatus; +static AvifInfoStatus AvifInfoInternalConvertStatus(AvifInfoInternalStatus s) { + return (s == kFound) ? kAvifInfoOk + : (s == kNotFound || s == kTruncated) ? kAvifInfoNotEnoughData + : (s == kAborted) ? kAvifInfoTooComplex + : kAvifInfoInvalidFile; +} + // uint32_t is used everywhere in this file. It is unlikely to be insufficient // to parse AVIF headers. #define AVIFINFO_MAX_SIZE UINT32_MAX @@ -611,8 +618,30 @@ //------------------------------------------------------------------------------ -// Parses a file 'stream'. The file type is checked through the "ftyp" box and -// 'features' are extracted through the "meta" box. +// Parses a file 'stream'. The file type is checked through the "ftyp" box. +static AvifInfoInternalStatus ParseFtyp(AvifInfoInternalStream* stream) { + AvifInfoInternalBox box; + uint32_t num_parsed_boxes = 0; + AVIFINFO_CHECK_FOUND(AvifInfoInternalParseBox(stream, AVIFINFO_MAX_SIZE, + &num_parsed_boxes, &box)); + AVIFINFO_CHECK(!memcmp(box.type, "ftyp", 4), kInvalid); + // Iterate over brands. See ISO/IEC 14496-12:2012(E) 4.3.1 + AVIFINFO_CHECK(box.content_size >= 8, kInvalid); // major_brand,minor_version + for (uint32_t i = 0; i + 4 <= box.content_size; i += 4) { + const uint8_t* data; + AVIFINFO_CHECK_FOUND(AvifInfoInternalRead(stream, 4, &data)); + if (i == 4) continue; // Skip minor_version. + if (!memcmp(data, "avif", 4) || !memcmp(data, "avis", 4)) { + AVIFINFO_CHECK_FOUND( + AvifInfoInternalSkip(stream, box.content_size - (i + 4))); + return kFound; + } + AVIFINFO_CHECK(i <= 32 * 4, kAborted); // Be reasonable. + } + AVIFINFO_RETURN(kInvalid); // No AVIF brand no good. +} + +// Parses a file 'stream'. 'features' are extracted from the "meta" box. static AvifInfoInternalStatus ParseFile(AvifInfoInternalStream* stream, uint32_t* num_parsed_boxes, AvifInfoInternalFeatures* features) { @@ -620,28 +649,7 @@ AvifInfoInternalBox box; AVIFINFO_CHECK_FOUND(AvifInfoInternalParseBox(stream, AVIFINFO_MAX_SIZE, num_parsed_boxes, &box)); - // The first box must be "ftyp" and no other box at root can be "ftyp". - AVIFINFO_CHECK((*num_parsed_boxes == 1) == !memcmp(box.type, "ftyp", 4), - kInvalid); - - if (!memcmp(box.type, "ftyp", 4)) { - // Iterate over brands. See ISO/IEC 14496-12:2012(E) 4.3.1 - AVIFINFO_CHECK(box.content_size >= 8, kInvalid); // major_brand + version - uint8_t is_avif = 0; - for (uint32_t i = 0; i + 4 <= box.content_size; i += 4) { - const uint8_t* data; - AVIFINFO_CHECK_FOUND(AvifInfoInternalRead(stream, 4, &data)); - if (i == 4) continue; // Skip minor_version. - if (!memcmp(data, "avif", 4) || !memcmp(data, "avis", 4)) { - is_avif = 1; - AVIFINFO_CHECK_FOUND( - AvifInfoInternalSkip(stream, box.content_size - (i + 4))); - break; - } - AVIFINFO_CHECK(i <= 32 * 4, kAborted); // Be reasonable. - } - AVIFINFO_CHECK(is_avif, kInvalid); - } else if (!memcmp(box.type, "meta", 4)) { + if (!memcmp(box.type, "meta", 4)) { return ParseMeta(stream, box.content_size, num_parsed_boxes, features); } else { AVIFINFO_CHECK_FOUND(AvifInfoInternalSkip(stream, box.content_size)); @@ -678,22 +686,43 @@ //------------------------------------------------------------------------------ // Fixed-size input public API -AvifInfoStatus AvifInfoGet(const uint8_t* data, size_t data_size, - AvifInfoFeatures* features) { +AvifInfoStatus AvifInfoIdentify(const uint8_t* data, size_t data_size) { AvifInfoInternalForward stream; stream.data = data; stream.data_size = data_size; // Forward null 'data' as a null 'stream' to handle it the same way. - return AvifInfoRead((void*)&stream, - (data == NULL) ? NULL : AvifInfoInternalForwardRead, - AvifInfoInternalForwardSkip, features); + return AvifInfoIdentifyStream( + (void*)&stream, (data == NULL) ? NULL : AvifInfoInternalForwardRead, + AvifInfoInternalForwardSkip); +} + +AvifInfoStatus AvifInfoGetFeatures(const uint8_t* data, size_t data_size, + AvifInfoFeatures* features) { + AvifInfoInternalForward stream; + stream.data = data; + stream.data_size = data_size; + return AvifInfoGetFeaturesStream( + (void*)&stream, (data == NULL) ? NULL : AvifInfoInternalForwardRead, + AvifInfoInternalForwardSkip, features); } //------------------------------------------------------------------------------ // Streamed input API -AvifInfoStatus AvifInfoRead(void* stream, read_stream_t read, - skip_stream_t skip, AvifInfoFeatures* features) { +AvifInfoStatus AvifInfoIdentifyStream(void* stream, read_stream_t read, + skip_stream_t skip) { + if (read == NULL) return kAvifInfoNotEnoughData; + + AvifInfoInternalStream internal_stream; + internal_stream.stream = stream; + internal_stream.read = read; + internal_stream.skip = skip; // Fallbacks to 'read' if null. + return AvifInfoInternalConvertStatus(ParseFtyp(&internal_stream)); +} + +AvifInfoStatus AvifInfoGetFeaturesStream(void* stream, read_stream_t read, + skip_stream_t skip, + AvifInfoFeatures* features) { if (features != NULL) memset(features, 0, sizeof(*features)); if (read == NULL) return kAvifInfoNotEnoughData; @@ -708,14 +737,9 @@ // Go through all relevant boxes sequentially. const AvifInfoInternalStatus status = ParseFile(&internal_stream, &num_parsed_boxes, &internal_features); - - if (status == kNotFound) return kAvifInfoNotEnoughData; - if (status == kTruncated) return kAvifInfoNotEnoughData; - if (status == kInvalid) return kAvifInfoInvalidFile; - if (status == kAborted) return kAvifInfoTooComplex; - if (features != NULL) { + if (status == kFound && features != NULL) { memcpy(features, &internal_features.primary_item_features, sizeof(*features)); } - return kAvifInfoOk; + return AvifInfoInternalConvertStatus(status); }
diff --git a/avifinfo.h b/avifinfo.h index a940ea7..4c898c2 100644 --- a/avifinfo.h +++ b/avifinfo.h
@@ -45,12 +45,16 @@ // Fixed-size input API // Use this API if a raw byte array of fixed size is available as input. -// Parses the AVIF 'data' and extracts its 'features'. +// Parses the 'data' and returns kAvifInfoOk if it is identified as an AVIF. +// The file type can be identified in the first 12 bytes of most AVIF files. +AvifInfoStatus AvifInfoIdentify(const uint8_t* data, size_t data_size); + +// Parses the identified AVIF 'data' and extracts its 'features'. // 'data' can be partial but must point to the beginning of the AVIF file. // The 'features' can be parsed in the first 450 bytes of most AVIF files. // 'features' are set to 0 unless kAvifInfoOk is returned. -AvifInfoStatus AvifInfoGet(const uint8_t* data, size_t data_size, - AvifInfoFeatures* features); +AvifInfoStatus AvifInfoGetFeatures(const uint8_t* data, size_t data_size, + AvifInfoFeatures* features); //------------------------------------------------------------------------------ // Streamed input API @@ -69,11 +73,15 @@ // Maximum number of bytes requested per read. There is no limit per skip. #define AVIFINFO_MAX_NUM_READ_BYTES 64 -// Same as AvifInfoGet() but takes a 'stream' as input. AvifInfoRead() does not -// access the 'stream' directly but passes it as is to 'read' and 'skip'. +// Same as AvifInfo*() but takes a 'stream' as input. AvifInfo*Stream() does +// not access the 'stream' directly but passes it as is to 'read' and 'skip'. // 'read' cannot be null. If 'skip' is null, 'read' is called instead. -AvifInfoStatus AvifInfoRead(void* stream, read_stream_t read, - skip_stream_t skip, AvifInfoFeatures* features); +AvifInfoStatus AvifInfoIdentifyStream(void* stream, read_stream_t read, + skip_stream_t skip); +// Can be called right after AvifInfoIdentifyStream() with the same 'stream'. +AvifInfoStatus AvifInfoGetFeaturesStream(void* stream, read_stream_t read, + skip_stream_t skip, + AvifInfoFeatures* features); //------------------------------------------------------------------------------
diff --git a/tests/avifinfo_fuzz.cc b/tests/avifinfo_fuzz.cc index d4a6526..be39869 100644 --- a/tests/avifinfo_fuzz.cc +++ b/tests/avifinfo_fuzz.cc
@@ -47,7 +47,8 @@ // Test a random bitstream of random size, whether it is valid or not. extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t data_size) { - AvifInfoStatus previous_status = kAvifInfoNotEnoughData; + AvifInfoStatus previous_status_identity = kAvifInfoNotEnoughData; + AvifInfoStatus previous_status_features = kAvifInfoNotEnoughData; AvifInfoFeatures previous_features = {0}; // Check the consistency of the returned status and features: @@ -56,22 +57,26 @@ for (size_t size = 0; size < data_size; ++size) { StreamData stream = {data, size}; AvifInfoFeatures features; - const AvifInfoStatus status = - AvifInfoRead(&stream, StreamRead, StreamSkip, &features); + const AvifInfoStatus status_identity = + AvifInfoIdentifyStream(&stream, StreamRead, StreamSkip); + const AvifInfoStatus status_features = + AvifInfoGetFeaturesStream(&stream, StreamRead, StreamSkip, &features); - if (previous_status != kAvifInfoNotEnoughData && - status != previous_status) { + if ((previous_status_identity != kAvifInfoNotEnoughData && + status_identity != previous_status_identity) || + (previous_status_features != kAvifInfoNotEnoughData && + status_features != previous_status_features)) { std::abort(); } - if (status == previous_status) { + if (status_features == previous_status_features) { if (features.width != previous_features.width || features.height != previous_features.height || features.bit_depth != previous_features.bit_depth || features.num_channels != previous_features.num_channels) { std::abort(); } - } else if (status == kAvifInfoOk) { + } else if (status_features == kAvifInfoOk) { if (features.width == 0u || features.height == 0u || features.bit_depth == 0u || features.num_channels == 0u) { std::abort(); @@ -83,7 +88,8 @@ } } - previous_status = status; + previous_status_identity = status_identity; + previous_status_features = status_features; previous_features = features; } return 0;
diff --git a/tests/avifinfo_test.cc b/tests/avifinfo_test.cc index 0ab2ac7..c0cb7c7 100644 --- a/tests/avifinfo_test.cc +++ b/tests/avifinfo_test.cc
@@ -41,9 +41,10 @@ const Data input = LoadFile("avifinfo_test_1x1.avif"); ASSERT_FALSE(input.empty()); - AvifInfoFeatures features; - EXPECT_EQ(AvifInfoGet(input.data(), input.size(), &features), kAvifInfoOk); - EXPECT_TRUE(AreEqual(features, {1u, 1u, 8u, 3u})); + EXPECT_EQ(AvifInfoIdentify(input.data(), input.size()), kAvifInfoOk); + AvifInfoFeatures f; + EXPECT_EQ(AvifInfoGetFeatures(input.data(), input.size(), &f), kAvifInfoOk); + EXPECT_TRUE(AreEqual(f, {1u, 1u, 8u, 3u})); } TEST(AvifInfoGetTest, NoPixi10b) { @@ -54,9 +55,10 @@ LoadFile("avifinfo_test_1x1_10b_nopixi_metasize64b_mdatsize0.avif"); ASSERT_FALSE(input.empty()); - AvifInfoFeatures features; - EXPECT_EQ(AvifInfoGet(input.data(), input.size(), &features), kAvifInfoOk); - EXPECT_TRUE(AreEqual(features, {1u, 1u, 10u, 3u})); + EXPECT_EQ(AvifInfoIdentify(input.data(), input.size()), kAvifInfoOk); + AvifInfoFeatures f; + EXPECT_EQ(AvifInfoGetFeatures(input.data(), input.size(), &f), kAvifInfoOk); + EXPECT_TRUE(AreEqual(f, {1u, 1u, 10u, 3u})); } TEST(AvifInfoGetTest, EnoughBytes) { @@ -67,39 +69,28 @@ input.resize(std::search(input.begin(), input.end(), kMdatTag, kMdatTag + 4) - input.begin()); - AvifInfoFeatures features; - EXPECT_EQ(AvifInfoGet(input.data(), input.size(), &features), kAvifInfoOk); - EXPECT_TRUE(AreEqual(features, {1u, 1u, 8u, 3u})); -} - -TEST(AvifInfoGetTest, BigMetaBox) { - Data input = LoadFile("avifinfo_test_1x1.avif"); - ASSERT_FALSE(input.empty()); - // Change "meta" box size to the size 2^32-1. - const uint8_t kMetaTag[] = {'m', 'e', 't', 'a'}; - auto meta_tag = - std::search(input.begin(), input.end(), kMetaTag, kMetaTag + 4); - meta_tag[-4] = meta_tag[-3] = meta_tag[-2] = meta_tag[-1] = 255; - - AvifInfoFeatures features; - EXPECT_EQ(AvifInfoGet(input.data(), input.size(), &features), kAvifInfoOk); - EXPECT_TRUE(AreEqual(features, {1u, 1u, 8u, 3u})); + EXPECT_EQ(AvifInfoIdentify(input.data(), input.size()), kAvifInfoOk); + AvifInfoFeatures f; + EXPECT_EQ(AvifInfoGetFeatures(input.data(), input.size(), &f), kAvifInfoOk); + EXPECT_TRUE(AreEqual(f, {1u, 1u, 8u, 3u})); } TEST(AvifInfoGetTest, Null) { const Data input = LoadFile("avifinfo_test_1x1.avif"); ASSERT_FALSE(input.empty()); - EXPECT_EQ(AvifInfoGet(input.data(), input.size(), nullptr), kAvifInfoOk); + EXPECT_EQ(AvifInfoGetFeatures(input.data(), input.size(), nullptr), + kAvifInfoOk); } //------------------------------------------------------------------------------ // Negative tests TEST(AvifInfoGetTest, Empty) { - AvifInfoFeatures features; - EXPECT_EQ(AvifInfoGet(nullptr, 0, &features), kAvifInfoNotEnoughData); - EXPECT_TRUE(AreEqual(features, {0})); + EXPECT_EQ(AvifInfoIdentify(nullptr, 0), kAvifInfoNotEnoughData); + AvifInfoFeatures f; + EXPECT_EQ(AvifInfoGetFeatures(nullptr, 0, &f), kAvifInfoNotEnoughData); + EXPECT_TRUE(AreEqual(f, {0})); } TEST(AvifInfoGetTest, NotEnoughBytes) { @@ -110,8 +101,9 @@ input.resize(std::search(input.begin(), input.end(), kIpmaTag, kIpmaTag + 4) - input.begin()); - AvifInfoFeatures features; - EXPECT_EQ(AvifInfoGet(input.data(), input.size(), &features), + EXPECT_EQ(AvifInfoIdentify(input.data(), input.size()), kAvifInfoOk); + AvifInfoFeatures f; + EXPECT_EQ(AvifInfoGetFeatures(input.data(), input.size(), &f), kAvifInfoNotEnoughData); } @@ -122,10 +114,11 @@ const uint8_t kIspeTag[] = {'i', 's', 'p', 'e'}; std::search(input.begin(), input.end(), kIspeTag, kIspeTag + 4)[0] = 'a'; - AvifInfoFeatures features; - EXPECT_EQ(AvifInfoGet(input.data(), input.size(), &features), + EXPECT_EQ(AvifInfoIdentify(input.data(), input.size()), kAvifInfoOk); + AvifInfoFeatures f; + EXPECT_EQ(AvifInfoGetFeatures(input.data(), input.size(), &f), kAvifInfoInvalidFile); - EXPECT_TRUE(AreEqual(features, {0})); + EXPECT_TRUE(AreEqual(f, {0})); } TEST(AvifInfoGetTest, MetaBoxIsTooBig) { @@ -139,10 +132,11 @@ meta_tag[-1] = 1; // 32-bit "1" then 4-char "meta" then 64-bit size. input.insert(meta_tag + 4, {255, 255, 255, 255, 255, 255, 255, 255}); - AvifInfoFeatures features; - EXPECT_EQ(AvifInfoGet(input.data(), input.size(), &features), + EXPECT_EQ(AvifInfoIdentify(input.data(), input.size()), kAvifInfoOk); + AvifInfoFeatures f; + EXPECT_EQ(AvifInfoGetFeatures(input.data(), input.size(), &f), kAvifInfoTooComplex); - EXPECT_TRUE(AreEqual(features, {0})); + EXPECT_TRUE(AreEqual(f, {0})); } TEST(AvifInfoGetTest, TooManyBoxes) { @@ -156,18 +150,22 @@ input.insert(input.end(), kBox, kBox + kBox[3]); } - AvifInfoFeatures features; - EXPECT_EQ(AvifInfoGet(reinterpret_cast<uint8_t*>(input.data()), - input.size() * 4, &features), + EXPECT_EQ(AvifInfoIdentify(input.data(), input.size()), kAvifInfoOk); + AvifInfoFeatures f; + EXPECT_EQ(AvifInfoGetFeatures(reinterpret_cast<uint8_t*>(input.data()), + input.size() * 4, &f), kAvifInfoTooComplex); } TEST(AvifInfoReadTest, Null) { - AvifInfoFeatures features; - EXPECT_EQ(AvifInfoRead(/*stream=*/nullptr, /*read=*/nullptr, /*skip=*/nullptr, - &features), + EXPECT_EQ(AvifInfoIdentifyStream(/*stream=*/nullptr, /*read=*/nullptr, + /*skip=*/nullptr), kAvifInfoNotEnoughData); - EXPECT_TRUE(AreEqual(features, {0})); + AvifInfoFeatures f; + EXPECT_EQ(AvifInfoGetFeaturesStream(/*stream=*/nullptr, /*read=*/nullptr, + /*skip=*/nullptr, &f), + kAvifInfoNotEnoughData); + EXPECT_TRUE(AreEqual(f, {0})); } //------------------------------------------------------------------------------
diff --git a/tools/avifinfo_tool.cc b/tools/avifinfo_tool.cc index 0d0dc13..4d4e4ae 100644 --- a/tools/avifinfo_tool.cc +++ b/tools/avifinfo_tool.cc
@@ -70,8 +70,9 @@ // Parses the AVIF at 'data' of 'data_size' bytes using libavifinfo. Result ParseAvif(const uint8_t data[], size_t data_size) { Result result; - const AvifInfoStatus status = AvifInfoGet(data, data_size, &result.features); - result.success = (status == kAvifInfoOk); + result.success = + (AvifInfoIdentify(data, data_size) == kAvifInfoOk && + AvifInfoGetFeatures(data, data_size, &result.features) == kAvifInfoOk); return result; } @@ -88,7 +89,7 @@ size_t max_data_size = data_size; while (min_data_size < max_data_size) { const size_t middle = (min_data_size + max_data_size) / 2; - if (AvifInfoGet(data, middle, nullptr) == kAvifInfoOk) { + if (ParseAvif(data, middle).success) { max_data_size = middle; } else { min_data_size = middle + 1;