| // Copyright 2020 Joe Drago. All rights reserved. | |
| // SPDX-License-Identifier: BSD-2-Clause | |
| // #define WIN32_MEMORY_LEAK_DETECTION | |
| #ifdef WIN32_MEMORY_LEAK_DETECTION | |
| #define _CRTDBG_MAP_ALLOC | |
| #include <crtdbg.h> | |
| #endif | |
| #include "avif/avif.h" | |
| #include <inttypes.h> | |
| #include <stdio.h> | |
| #include <stdlib.h> | |
| #include <string.h> | |
| #if defined(_WIN32) | |
| #include <windows.h> | |
| typedef struct NextFilenameData | |
| { | |
| int didFirstFile; | |
| HANDLE handle; | |
| WIN32_FIND_DATA wfd; | |
| } NextFilenameData; | |
| static const char * nextFilename(const char * parentDir, const char * extension, NextFilenameData * nfd) | |
| { | |
| for (;;) { | |
| if (nfd->didFirstFile) { | |
| if (FindNextFile(nfd->handle, &nfd->wfd) == 0) { | |
| // No more files | |
| break; | |
| } | |
| } else { | |
| char filenameBuffer[2048]; | |
| snprintf(filenameBuffer, sizeof(filenameBuffer), "%s\\*", parentDir); | |
| filenameBuffer[sizeof(filenameBuffer) - 1] = 0; | |
| nfd->handle = FindFirstFile(filenameBuffer, &nfd->wfd); | |
| if (nfd->handle == INVALID_HANDLE_VALUE) { | |
| return NULL; | |
| } | |
| nfd->didFirstFile = 1; | |
| } | |
| // If we get here, we should have a valid wfd | |
| const char * dot = strrchr(nfd->wfd.cFileName, '.'); | |
| if (dot) { | |
| ++dot; | |
| if (!strcmp(dot, extension)) { | |
| return nfd->wfd.cFileName; | |
| } | |
| } | |
| } | |
| FindClose(nfd->handle); | |
| nfd->handle = INVALID_HANDLE_VALUE; | |
| nfd->didFirstFile = 0; | |
| return NULL; | |
| } | |
| #else | |
| #include <dirent.h> | |
| typedef struct NextFilenameData | |
| { | |
| DIR * dir; | |
| } NextFilenameData; | |
| static const char * nextFilename(const char * parentDir, const char * extension, NextFilenameData * nfd) | |
| { | |
| if (!nfd->dir) { | |
| nfd->dir = opendir(parentDir); | |
| if (!nfd->dir) { | |
| return NULL; | |
| } | |
| } | |
| struct dirent * entry; | |
| while ((entry = readdir(nfd->dir)) != NULL) { | |
| const char * dot = strrchr(entry->d_name, '.'); | |
| if (dot) { | |
| ++dot; | |
| if (!strcmp(dot, extension)) { | |
| return entry->d_name; | |
| } | |
| } | |
| } | |
| closedir(nfd->dir); | |
| nfd->dir = NULL; | |
| return NULL; | |
| } | |
| #endif | |
| typedef struct avifIOTestReader | |
| { | |
| avifIO io; | |
| avifROData rodata; | |
| size_t availableBytes; | |
| } avifIOTestReader; | |
| static avifResult avifIOTestReaderRead(struct avifIO * io, uint32_t readFlags, uint64_t offset, size_t size, avifROData * out) | |
| { | |
| // printf("avifIOTestReaderRead offset %" PRIu64 " size %zu\n", offset, size); | |
| if (readFlags != 0) { | |
| // Unsupported readFlags | |
| return AVIF_RESULT_IO_ERROR; | |
| } | |
| avifIOTestReader * reader = (avifIOTestReader *)io; | |
| // Sanitize/clamp incoming request | |
| if (offset > reader->rodata.size) { | |
| // The offset is past the end of the buffer. | |
| return AVIF_RESULT_IO_ERROR; | |
| } | |
| if (offset == reader->rodata.size) { | |
| // The parser is *exactly* at EOF: return a 0-size pointer to any valid buffer | |
| offset = 0; | |
| size = 0; | |
| } | |
| uint64_t availableSize = reader->rodata.size - offset; | |
| if (size > availableSize) { | |
| size = (size_t)availableSize; | |
| } | |
| if (offset > reader->availableBytes) { | |
| return AVIF_RESULT_WAITING_ON_IO; | |
| } | |
| if (size > (reader->availableBytes - offset)) { | |
| return AVIF_RESULT_WAITING_ON_IO; | |
| } | |
| out->data = reader->rodata.data + offset; | |
| out->size = size; | |
| return AVIF_RESULT_OK; | |
| } | |
| static void avifIOTestReaderDestroy(struct avifIO * io) | |
| { | |
| avifFree(io); | |
| } | |
| static avifIOTestReader * avifIOCreateTestReader(const uint8_t * data, size_t size) | |
| { | |
| avifIOTestReader * reader = avifAlloc(sizeof(avifIOTestReader)); | |
| memset(reader, 0, sizeof(avifIOTestReader)); | |
| reader->io.destroy = avifIOTestReaderDestroy; | |
| reader->io.read = avifIOTestReaderRead; | |
| reader->io.sizeHint = size; | |
| reader->io.persistent = AVIF_TRUE; | |
| reader->rodata.data = data; | |
| reader->rodata.size = size; | |
| return reader; | |
| } | |
| #define FILENAME_MAX_LENGTH 2047 | |
| static int runIOTests(const char * dataDir) | |
| { | |
| printf("AVIF Test Suite: Running IO Tests...\n"); | |
| static const char * ioSuffix = "/io/"; | |
| char ioDir[FILENAME_MAX_LENGTH + 1]; | |
| const size_t dataDirLen = strlen(dataDir); | |
| const size_t ioSuffixLen = strlen(ioSuffix); | |
| const size_t ioDirLen = dataDirLen + ioSuffixLen; | |
| if (ioDirLen > FILENAME_MAX_LENGTH) { | |
| printf("Path too long: %s\n", dataDir); | |
| return 1; | |
| } | |
| memcpy(ioDir, dataDir, dataDirLen); | |
| memcpy(ioDir + dataDirLen, ioSuffix, ioSuffixLen + 1); // includes the NUL terminator | |
| int retCode = 0; | |
| NextFilenameData nfd; | |
| memset(&nfd, 0, sizeof(nfd)); | |
| avifRWData fileBuffer = AVIF_DATA_EMPTY; | |
| const char * filename = nextFilename(ioDir, "avif", &nfd); | |
| for (; filename != NULL; filename = nextFilename(ioDir, "avif", &nfd)) { | |
| char fullFilename[FILENAME_MAX_LENGTH + 1]; | |
| const size_t filenameLen = strlen(filename); | |
| if ((ioDirLen + filenameLen) > FILENAME_MAX_LENGTH) { | |
| printf("Path too long: %s\n", filename); | |
| retCode = 1; | |
| break; | |
| } | |
| memcpy(fullFilename, ioDir, ioDirLen); | |
| memcpy(fullFilename + ioDirLen, filename, filenameLen + 1); // includes the NUL terminator | |
| FILE * f = fopen(fullFilename, "rb"); | |
| if (!f) { | |
| printf("Can't open for read: %s\n", filename); | |
| retCode = 1; | |
| break; | |
| } | |
| fseek(f, 0, SEEK_END); | |
| size_t fileSize = ftell(f); | |
| fseek(f, 0, SEEK_SET); | |
| if (avifRWDataRealloc(&fileBuffer, fileSize) != AVIF_RESULT_OK) { | |
| printf("Out of memory when allocating buffer to read file: %s\n", filename); | |
| fclose(f); | |
| retCode = 1; | |
| break; | |
| } | |
| if (fread(fileBuffer.data, 1, fileSize, f) != fileSize) { | |
| printf("Can't read entire file: %s\n", filename); | |
| fclose(f); | |
| retCode = 1; | |
| break; | |
| } | |
| fclose(f); | |
| avifDecoder * decoder = avifDecoderCreate(); | |
| if (decoder == NULL) { | |
| printf("Memory allocation failure\n"); | |
| retCode = 1; | |
| break; | |
| } | |
| avifIOTestReader * io = avifIOCreateTestReader(fileBuffer.data, fileBuffer.size); | |
| avifDecoderSetIO(decoder, (avifIO *)io); | |
| for (int pass = 0; pass < 4; ++pass) { | |
| io->io.persistent = ((pass % 2) == 0); | |
| decoder->ignoreExif = decoder->ignoreXMP = (pass < 2); | |
| // Slowly pretend to have streamed-in / downloaded more and more bytes | |
| avifResult parseResult = AVIF_RESULT_UNKNOWN_ERROR; | |
| for (io->availableBytes = 0; io->availableBytes <= io->io.sizeHint; ++io->availableBytes) { | |
| parseResult = avifDecoderParse(decoder); | |
| if (parseResult == AVIF_RESULT_WAITING_ON_IO) { | |
| continue; | |
| } | |
| if (parseResult != AVIF_RESULT_OK) { | |
| retCode = 1; | |
| } | |
| printf("File: [%s @ %zu / %" PRIu64 " bytes, %s, %s] parse returned: %s\n", | |
| filename, | |
| io->availableBytes, | |
| io->io.sizeHint, | |
| io->io.persistent ? "Persistent" : "NonPersistent", | |
| decoder->ignoreExif ? "IgnoreMetadata" : "Metadata", | |
| avifResultToString(parseResult)); | |
| break; | |
| } | |
| if (parseResult == AVIF_RESULT_OK) { | |
| for (; io->availableBytes <= io->io.sizeHint; ++io->availableBytes) { | |
| avifExtent extent; | |
| avifResult extentResult = avifDecoderNthImageMaxExtent(decoder, 0, &extent); | |
| if (extentResult != AVIF_RESULT_OK) { | |
| retCode = 1; | |
| printf("File: [%s @ %zu / %" PRIu64 " bytes, %s, %s] maxExtent returned: %s\n", | |
| filename, | |
| io->availableBytes, | |
| io->io.sizeHint, | |
| io->io.persistent ? "Persistent" : "NonPersistent", | |
| decoder->ignoreExif ? "IgnoreMetadata" : "Metadata", | |
| avifResultToString(extentResult)); | |
| } else { | |
| avifResult nextImageResult = avifDecoderNextImage(decoder); | |
| if (nextImageResult == AVIF_RESULT_WAITING_ON_IO) { | |
| continue; | |
| } | |
| if (nextImageResult != AVIF_RESULT_OK) { | |
| retCode = 1; | |
| } | |
| printf("File: [%s @ %zu / %" PRIu64 " bytes, %s, %s] nextImage [MaxExtent off %" PRIu64 ", size %zu] returned: %s\n", | |
| filename, | |
| io->availableBytes, | |
| io->io.sizeHint, | |
| io->io.persistent ? "Persistent" : "NonPersistent", | |
| decoder->ignoreExif ? "IgnoreMetadata" : "Metadata", | |
| extent.offset, | |
| extent.size, | |
| avifResultToString(nextImageResult)); | |
| } | |
| break; | |
| } | |
| } | |
| } | |
| avifDecoderDestroy(decoder); | |
| } | |
| avifRWDataFree(&fileBuffer); | |
| return retCode; | |
| } | |
| static void syntax(void) | |
| { | |
| fprintf(stderr, "Syntax: aviftest dataDir\n"); | |
| } | |
| int main(int argc, char * argv[]) | |
| { | |
| const char * dataDir = NULL; | |
| #ifdef WIN32_MEMORY_LEAK_DETECTION | |
| _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF); | |
| // _CrtSetBreakAlloc(2906); | |
| #endif | |
| // Parse cmdline | |
| for (int i = 1; i < argc; ++i) { | |
| char * arg = argv[i]; | |
| if (!strcmp(arg, "--io-only")) { | |
| fprintf(stderr, "WARNING: --io-only is deprecated; ignoring.\n"); | |
| } else if (dataDir == NULL) { | |
| dataDir = arg; | |
| } else { | |
| fprintf(stderr, "Too many positional arguments: %s\n", arg); | |
| syntax(); | |
| return 1; | |
| } | |
| } | |
| // Verify all required args were set | |
| if (dataDir == NULL) { | |
| fprintf(stderr, "dataDir is required, bailing out.\n"); | |
| syntax(); | |
| return 1; | |
| } | |
| setbuf(stdout, NULL); | |
| char codecVersions[256]; | |
| avifCodecVersions(codecVersions); | |
| printf("Codec Versions: %s\n", codecVersions); | |
| printf("Test Data Dir : %s\n", dataDir); | |
| int retCode = runIOTests(dataDir); | |
| if (retCode == 0) { | |
| printf("AVIF Test Suite: Complete.\n"); | |
| } else { | |
| printf("AVIF Test Suite: Failed.\n"); | |
| } | |
| return retCode; | |
| } |