/******************************************************************************
 *
 * Project:  GDAL
 * Purpose:  Fuzzer
 * Author:   Even Rouault, even.rouault at spatialys.com
 *
 ******************************************************************************
 * Copyright (c) 2017, Even Rouault <even.rouault at spatialys.com>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included
 * in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 ****************************************************************************/

#include <stddef.h>
#include <stdint.h>

#include <algorithm>
#include <vector>

#include "gdal.h"
#include "cpl_conv.h"
#include "cpl_string.h"
#include "cpl_vsi.h"
#include "gdal_alg.h"
#include "gdal_priv.h"
#include "gdal_frmts.h"

#ifndef REGISTER_FUNC
#define REGISTER_FUNC GDALAllRegister
#endif

#ifndef GDAL_SKIP
#define GDAL_SKIP "CAD"
#endif

#ifndef EXTENSION
#define EXTENSION "bin"
#endif

#ifndef MEM_FILENAME
#define MEM_FILENAME "/vsimem/test"
#endif

#ifndef GDAL_FILENAME
#define GDAL_FILENAME MEM_FILENAME
#endif

extern "C" int LLVMFuzzerInitialize(int *argc, char ***argv);
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *buf, size_t len);

int LLVMFuzzerInitialize(int * /*argc*/, char ***argv)
{
    const char *exe_path = (*argv)[0];
    if (CPLGetConfigOption("GDAL_DATA", nullptr) == nullptr)
    {
        CPLSetConfigOption("GDAL_DATA", CPLGetPathSafe(exe_path).c_str());
    }
    CPLSetConfigOption("CPL_TMPDIR", "/tmp");
    CPLSetConfigOption("DISABLE_OPEN_REAL_NETCDF_FILES", "YES");
    // Disable PDF text rendering as fontconfig cannot access its config files
    CPLSetConfigOption("GDAL_PDF_RENDERING_OPTIONS", "RASTER,VECTOR");
    // to avoid timeout in WMS driver
    CPLSetConfigOption("GDAL_WMS_ABORT_CURL_REQUEST", "YES");
    CPLSetConfigOption("GDAL_HTTP_TIMEOUT", "1");
    CPLSetConfigOption("GDAL_HTTP_CONNECTTIMEOUT", "1");
    CPLSetConfigOption("GDAL_CACHEMAX", "1000");  // Limit to 1 GB
#ifdef GTIFF_USE_MMAP
    CPLSetConfigOption("GTIFF_USE_MMAP", "YES");
#endif

#ifdef GDAL_SKIP
    CPLSetConfigOption("GDAL_SKIP", GDAL_SKIP);
#endif
    REGISTER_FUNC();

    return 0;
}

static void ExploreAttributes(const GDALIHasAttribute *attributeHolder)
{
    const auto attributes = attributeHolder->GetAttributes();
    for (const auto &attribute : attributes)
    {
        attribute->ReadAsRaw();
    }

    attributeHolder->GetAttribute("i_do_not_exist");
}

static void ExploreArray(const std::shared_ptr<GDALMDArray> &poArray,
                         const char *pszDriverName)
{
    ExploreAttributes(poArray.get());

    poArray->GetFilename();
    poArray->GetStructuralInfo();
    poArray->GetUnit();
    poArray->GetSpatialRef();
    poArray->GetRawNoDataValue();
    poArray->GetOffset();
    poArray->GetScale();
    poArray->GetCoordinateVariables();

    const auto nDimCount = poArray->GetDimensionCount();
    bool bRead = true;
    constexpr size_t MAX_ALLOC = 1000 * 1000 * 1000U;
    if (pszDriverName && EQUAL(pszDriverName, "GRIB"))
    {
        const auto &poDims = poArray->GetDimensions();
        if (nDimCount >= 2 &&
            poDims[nDimCount - 2]->GetSize() >
                MAX_ALLOC / sizeof(double) / poDims[nDimCount - 1]->GetSize())
        {
            bRead = false;
        }
    }
    else
    {
        const auto anBlockSize = poArray->GetBlockSize();
        size_t nBlockSize = poArray->GetDataType().GetSize();
        for (const auto nDimBlockSize : anBlockSize)
        {
            if (nDimBlockSize == 0)
            {
                break;
            }
            if (nBlockSize > MAX_ALLOC / nDimBlockSize)
            {
                bRead = false;
                break;
            }
            nBlockSize *= static_cast<size_t>(nDimBlockSize);
        }
    }

    if (bRead && poArray->GetDataType().GetClass() == GEDTC_NUMERIC)
    {
        std::vector<GUInt64> anArrayStartIdx(nDimCount);
        std::vector<size_t> anCount(nDimCount, 1);
        std::vector<GInt64> anArrayStep(nDimCount);
        std::vector<GPtrDiff_t> anBufferStride(nDimCount);
        std::vector<GByte> abyData(poArray->GetDataType().GetSize());
        poArray->Read(anArrayStartIdx.data(), anCount.data(),
                      anArrayStep.data(), anBufferStride.data(),
                      poArray->GetDataType(), &abyData[0]);
    }
}

static void ExploreGroup(const std::shared_ptr<GDALGroup> &poGroup,
                         const char *pszDriverName)
{
    ExploreAttributes(poGroup.get());

    const auto groupNames = poGroup->GetGroupNames();
    poGroup->OpenGroup("i_do_not_exist");
    for (const auto &name : groupNames)
    {
        auto poSubGroup = poGroup->OpenGroup(name);
        if (poSubGroup)
            ExploreGroup(poSubGroup, pszDriverName);
    }

    const auto arrayNames = poGroup->GetMDArrayNames();
    poGroup->OpenMDArray("i_do_not_exist");
    for (const auto &name : arrayNames)
    {
        auto poArray = poGroup->OpenMDArray(name);
        if (poArray)
        {
            ExploreArray(poArray, pszDriverName);
        }
    }
}

int LLVMFuzzerTestOneInput(const uint8_t *buf, size_t len)
{
#ifdef USE_FILESYSTEM
    char szTempFilename[64];
    snprintf(szTempFilename, sizeof(szTempFilename), "/tmp/gdal_fuzzer_%d.%s",
             (int)getpid(), EXTENSION);
    VSILFILE *fp = VSIFOpenL(szTempFilename, "wb");
    if (!fp)
    {
        fprintf(stderr, "Cannot create %s\n", szTempFilename);
        return 1;
    }
    VSIFWriteL(buf, 1, len, fp);
#else
    VSILFILE *fp = VSIFileFromMemBuffer(
        MEM_FILENAME, reinterpret_cast<GByte *>(const_cast<uint8_t *>(buf)),
        len, FALSE);
#endif
    VSIFCloseL(fp);

    CPLPushErrorHandler(CPLQuietErrorHandler);
#ifdef USE_FILESYSTEM
    const char *pszGDALFilename = szTempFilename;
#else
    const char *pszGDALFilename = GDAL_FILENAME;
#endif

#ifdef DRIVER_NAME
    const char *const apszAllowedDrivers[] = {DRIVER_NAME, nullptr};
#else
    const char *const *apszAllowedDrivers = nullptr;
#endif

    GDALDatasetH hDS = GDALOpenEx(pszGDALFilename, GDAL_OF_RASTER,
                                  apszAllowedDrivers, nullptr, nullptr);
    if (hDS)
    {
        const int nTotalBands = GDALGetRasterCount(hDS);
        const int nBands = std::min(10, nTotalBands);
        bool bDoCheckSum = true;
        int nXSizeToRead = std::min(1024, GDALGetRasterXSize(hDS));
        int nYSizeToRead = std::min(1024, GDALGetRasterYSize(hDS));
        if (nBands > 0)
        {
            const char *pszInterleave =
                GDALGetMetadataItem(hDS, "INTERLEAVE", "IMAGE_STRUCTURE");
            int nSimultaneousBands =
                (pszInterleave && EQUAL(pszInterleave, "PIXEL")) ? nTotalBands
                                                                 : 1;

            // When using the RGBA interface in pixel-interleaved mode, take
            // into account the raw number of bands to compute memory
            // requirements
            if (nBands == 4 && nSimultaneousBands != 1 &&
                GDALGetDatasetDriver(hDS) == GDALGetDriverByName("GTiff"))
            {
                GDALDatasetH hRawDS = GDALOpen(
                    (CPLString("GTIFF_RAW:") + pszGDALFilename).c_str(),
                    GA_ReadOnly);
                if (hRawDS)
                {
                    nSimultaneousBands = GDALGetRasterCount(hRawDS);
                    // shouldn't happen, but will make Coverity Scan happy
                    if (nSimultaneousBands == 0)
                        nSimultaneousBands = 1;
                    GDALClose(hRawDS);
                }
            }

            // If we know that we will need to allocate a lot of memory
            // given the block size and interleaving mode, do not read
            // pixels to avoid out of memory conditions by ASAN
            GIntBig nPixels = 0;
            for (int i = 0; i < nBands; i++)
            {
                int nBXSize = 0, nBYSize = 0;
                GDALGetBlockSize(GDALGetRasterBand(hDS, i + 1), &nBXSize,
                                 &nBYSize);
                if (nBXSize == 0 || nBYSize == 0 || nBXSize > INT_MAX / nBYSize)
                {
                    bDoCheckSum = false;
                    break;
                }

                // Limit to 1000 blocks read for each band.
                while ((nXSizeToRead > 1 || nYSizeToRead > 1) &&
                       (DIV_ROUND_UP(nXSizeToRead, nBXSize) *
                            DIV_ROUND_UP(nYSizeToRead, nBYSize) >
                        1000))
                {
                    if (nXSizeToRead > 1 &&
                        DIV_ROUND_UP(nXSizeToRead, nBXSize) >
                            DIV_ROUND_UP(nYSizeToRead, nBYSize))
                        nXSizeToRead /= 2;
                    else if (nYSizeToRead > 1)
                        nYSizeToRead /= 2;
                    else
                        nXSizeToRead /= 2;
                }

                // Currently decoding of PIXARLOG compressed TIFF requires
                // a temporary buffer for the whole strip (if stripped) or
                // image (if tiled), so be careful for a
                // GTiffSplitBand
                // Could probably be fixed for the CHUNKY_STRIP_READ_SUPPORT
                // mode.
                // Workaround
                // https://bugs.chromium.org/p/oss-fuzz/issues/detail?id=2606
                const char *pszCompress =
                    GDALGetMetadataItem(hDS, "COMPRESSION", "IMAGE_STRUCTURE");
                if (pszCompress != nullptr &&
                    ((nBYSize == 1 && GDALGetRasterYSize(hDS) > 1 &&
                      GDALGetMetadataItem(GDALGetRasterBand(hDS, 1),
                                          "BLOCK_OFFSET_0_1",
                                          "TIFF") == nullptr) ||
                     nBXSize != GDALGetRasterXSize(hDS)) &&
                    GDALGetDatasetDriver(hDS) == GDALGetDriverByName("GTiff"))
                {
                    if (EQUAL(pszCompress, "PIXARLOG") &&
                        GDALGetRasterYSize(hDS) >
                            (INT_MAX / 2) / static_cast<int>(sizeof(GUInt16)) /
                                nSimultaneousBands / GDALGetRasterXSize(hDS))
                    {
                        bDoCheckSum = false;
                    }
                    // https://bugs.chromium.org/p/oss-fuzz/issues/detail?id=2874
                    else if (EQUAL(pszCompress, "SGILOG24") &&
                             GDALGetRasterYSize(hDS) >
                                 (INT_MAX / 2) /
                                     static_cast<int>(sizeof(GUInt32)) /
                                     nSimultaneousBands /
                                     GDALGetRasterXSize(hDS))
                    {
                        bDoCheckSum = false;
                    }
                    // https://bugs.chromium.org/p/oss-fuzz/issues/detail?id=38051
                    else if (STARTS_WITH_CI(pszCompress, "LERC") &&
                             (GDALGetRasterYSize(hDS) >
                                  (INT_MAX / 2) / nSimultaneousBands /
                                      GDALGetRasterXSize(hDS) ||
                              static_cast<int64_t>(GDALGetRasterYSize(hDS)) *
                                          nSimultaneousBands *
                                          GDALGetRasterXSize(hDS) * 4 / 3 +
                                      100 >
                                  (INT_MAX / 2)))
                    {
                        bDoCheckSum = false;
                    }
                }

                GIntBig nNewPixels = static_cast<GIntBig>(nBXSize) * nBYSize;
                nNewPixels *= DIV_ROUND_UP(nXSizeToRead, nBXSize);
                nNewPixels *= DIV_ROUND_UP(nYSizeToRead, nBYSize);
                if (nNewPixels > nPixels)
                    nPixels = nNewPixels;
            }
            if (bDoCheckSum)
            {
                const GDALDataType eDT =
                    GDALGetRasterDataType(GDALGetRasterBand(hDS, 1));
                const int nDTSize = GDALGetDataTypeSizeBytes(eDT);
                if (nPixels > 10 * 1024 * 1024 / nDTSize / nSimultaneousBands)
                {
                    bDoCheckSum = false;
                }
            }
        }
        if (bDoCheckSum)
        {
            for (int i = 0; i < nBands; i++)
            {
                GDALRasterBandH hBand = GDALGetRasterBand(hDS, i + 1);
                CPLDebug("FUZZER", "Checksum band %d: %d,%d,%d,%d", i + 1, 0, 0,
                         nXSizeToRead, nYSizeToRead);
                GDALChecksumImage(hBand, 0, 0, nXSizeToRead, nYSizeToRead);
            }
        }

        // Test other API
        GDALGetProjectionRef(hDS);
        double adfGeoTransform[6];
        GDALGetGeoTransform(hDS, adfGeoTransform);
        CSLDestroy(GDALGetFileList(hDS));
        GDALGetGCPCount(hDS);
        GDALGetGCPs(hDS);
        GDALGetGCPProjection(hDS);
        GDALGetMetadata(hDS, nullptr);
        GDALGetMetadataItem(hDS, "foo", nullptr);
        CSLDestroy(GDALGetFileList(hDS));
        if (nBands > 0)
        {
            GDALRasterBandH hBand = GDALGetRasterBand(hDS, 1);

            int bFound = FALSE;
            GDALGetRasterNoDataValue(hBand, &bFound);
            GDALGetRasterOffset(hBand, &bFound);
            GDALGetRasterScale(hBand, &bFound);
            GDALGetRasterUnitType(hBand);
            GDALGetMetadata(hBand, nullptr);
            GDALGetMetadataItem(hBand, "foo", nullptr);

            int nFlags = GDALGetMaskFlags(hBand);
            GDALRasterBandH hMaskBand = GDALGetMaskBand(hBand);
            GDALGetRasterBandXSize(hMaskBand);
            if (bDoCheckSum && nFlags == GMF_PER_DATASET)
            {
                int nBXSize = 0, nBYSize = 0;
                GDALGetBlockSize(hMaskBand, &nBXSize, &nBYSize);
                if (nBXSize == 0 || nBYSize == 0 ||
                    nBXSize > INT_MAX / 2 / nBYSize)
                {
                    // do nothing
                }
                else
                {
                    GDALChecksumImage(hMaskBand, 0, 0, nXSizeToRead,
                                      nYSizeToRead);
                }
            }

            int nOverviewCount = GDALGetOverviewCount(hBand);
            for (int i = 0; i < nOverviewCount; i++)
            {
                GDALGetOverview(hBand, i);
            }
        }

        GDALClose(hDS);
    }

    auto poDS = std::unique_ptr<GDALDataset>(
        GDALDataset::Open(pszGDALFilename, GDAL_OF_MULTIDIM_RASTER));
    if (poDS)
    {
        auto poDriver = poDS->GetDriver();
        const char *pszDriverName = nullptr;
        if (poDriver)
            pszDriverName = poDriver->GetDescription();
        auto poRootGroup = poDS->GetRootGroup();
        poDS.reset();
        if (poRootGroup)
            ExploreGroup(poRootGroup, pszDriverName);
    }

    CPLPopErrorHandler();
#ifdef USE_FILESYSTEM
    VSIUnlink(szTempFilename);
#else
    VSIUnlink(MEM_FILENAME);
#endif
    return 0;
}
