/******************************************************************************
 *
 * Project:  VSI Virtual File System
 * Purpose:  Implementation of Memory Buffer virtual IO functions.
 * Author:   Frank Warmerdam, warmerdam@pobox.com
 *
 ******************************************************************************
 * Copyright (c) 2005, Frank Warmerdam <warmerdam@pobox.com>
 * Copyright (c) 2007-2014, Even Rouault <even dot rouault at spatialys.com>
 *
 * SPDX-License-Identifier: MIT
 ****************************************************************************/

#include "cpl_port.h"
#include "cpl_vsi.h"
#include "cpl_vsi_virtual.h"

#include <cerrno>
#include <cstddef>
#include <cstring>
#include <ctime>
#if HAVE_FCNTL_H
#include <fcntl.h>
#endif
#if HAVE_SYS_STAT_H
#include <sys/stat.h>
#endif

#include <algorithm>
#include <atomic>
#include <map>
#include <string>
#include <utility>
#include <memory>
#include <set>

#include <mutex>
// c++17 or VS2017
#if defined(HAVE_SHARED_MUTEX) || _MSC_VER >= 1910
#include <shared_mutex>
#define CPL_SHARED_MUTEX_TYPE std::shared_mutex
#define CPL_SHARED_LOCK std::shared_lock<std::shared_mutex>
#define CPL_EXCLUSIVE_LOCK std::unique_lock<std::shared_mutex>
#else
// Poor-man implementation of std::shared_mutex with an exclusive mutex
#define CPL_SHARED_MUTEX_TYPE std::mutex
#define CPL_SHARED_LOCK std::lock_guard<std::mutex>
#define CPL_EXCLUSIVE_LOCK std::lock_guard<std::mutex>
#endif

#include "cpl_atomic_ops.h"
#include "cpl_conv.h"
#include "cpl_error.h"
#include "cpl_multiproc.h"
#include "cpl_string.h"

//! @cond Doxygen_Suppress

// szHIDDEN_DIRNAME is for files created by VSIMemGenerateHiddenFilename(pszFilename).
// Such files are of the form "/vsimem/.#!HIDDEN!#./{counter}/{pszFilename}"
//
// The high-level design constraint is that "/vsimem/.#!HIDDEN!#." acts as a
// "side" hierarchy, but still under the "/vsimem/" namespace, so that code
// having special processing of filenames starting with /vsimem/ can still work.
// The structure of the returned filename is also such that those files form
// independent hierarchies, i.e. the tree generated by a
// VSIMemGenerateHiddenFilename() is "invisible" from the one returned by
// another call to it.
//
// As a consequence:
// - we don't want ".#!HIDDEN!#." to be listed in VSIReadDir("/vsimem/")
// - we don't want content under ""/vsimem/.#!HIDDEN!#" to be deleted by
//   VSIRmdirRecursive("/vsimem/")
// - we don't want the creation of a file (or directory) called
//   "/vsimem/.#!HIDDEN!#./{counter}/{pszFilename}"
//   to cause the implicit creation of "/vsimem/.#!HIDDEN!#./{counter}" and
//   "/vsimem/.#!HIDDEN!#". This is done so that users don't have to care about
//   cleaning such implicit directories that are upper in the hierarchy w.r.t.
//   to what we return to them.
// - But we want the creation of file or directory
//   "/vsimem/.#!HIDDEN!#./{counter}/{pszFilename}/something_added_by_user"
//   to cause "/vsimem/.#!HIDDEN!#./{counter}/{pszFilename}" to be implicitly
//   created as a directory, so they can list it, or recursively delete it.
// - we want VSIReadDirRecursive("/vsimem/.#!HIDDEN!#.") to list everything
//   under it (for debugging purposes)
// - we want VSIRmdirRecursive("/vsimem/.#!HIDDEN!#.") to remove everything
//   under it (for debugging purposes)
//

constexpr const char *szHIDDEN_DIRNAME = "/vsimem/.#!HIDDEN!#.";

/*
** Notes on Multithreading:
**
** VSIMemFilesystemHandler: This class maintains a mutex to protect
** access and update of the oFileList array which has all the "files" in
** the memory filesystem area.  It is expected that multiple threads would
** want to create and read different files at the same time and so might
** collide access oFileList without the mutex.
**
** VSIMemFile: A mutex protects accesses to the file
**
** VSIMemHandle: This is essentially a "current location" representing
** on accessor to a file, and is inherently intended only to be used in
** a single thread.
**
** In General:
**
** Multiple threads accessing the memory filesystem are ok as long as
** a given VSIMemHandle (i.e. FILE * at app level) isn't used by multiple
** threads at once.
*/

/************************************************************************/
/* ==================================================================== */
/*                              VSIMemFile                              */
/* ==================================================================== */
/************************************************************************/

class VSIMemFile
{
    CPL_DISALLOW_COPY_ASSIGN(VSIMemFile)

  public:
    CPLString osFilename{};

    bool bIsDirectory = false;

    bool bOwnData = true;
    GByte *pabyData = nullptr;
    vsi_l_offset nLength = 0;
    vsi_l_offset nAllocLength = 0;
    vsi_l_offset nMaxLength = GUINTBIG_MAX;

    time_t mTime = 0;
    CPL_SHARED_MUTEX_TYPE m_oMutex{};

    VSIMemFile();
    virtual ~VSIMemFile();

    bool SetLength(vsi_l_offset nNewSize);
};

/************************************************************************/
/* ==================================================================== */
/*                             VSIMemHandle                             */
/* ==================================================================== */
/************************************************************************/

class VSIMemHandle final : public VSIVirtualHandle
{
    CPL_DISALLOW_COPY_ASSIGN(VSIMemHandle)

  public:
    std::shared_ptr<VSIMemFile> poFile = nullptr;
    vsi_l_offset m_nOffset = 0;
    bool m_bReadAllowed = false;
    bool bUpdate = false;
    bool bEOF = false;
    bool m_bError = false;

    VSIMemHandle() = default;
    ~VSIMemHandle() override;

    int Seek(vsi_l_offset nOffset, int nWhence) override;
    vsi_l_offset Tell() override;
    size_t Read(void *pBuffer, size_t nSize, size_t nMemb) override;
    size_t Write(const void *pBuffer, size_t nSize, size_t nMemb) override;
    void ClearErr() override;
    int Error() override;
    int Eof() override;
    int Close() override;
    int Truncate(vsi_l_offset nNewSize) override;

    bool HasPRead() const override
    {
        return true;
    }

    size_t PRead(void * /*pBuffer*/, size_t /* nSize */,
                 vsi_l_offset /*nOffset*/) const override;
};

/************************************************************************/
/* ==================================================================== */
/*                       VSIMemFilesystemHandler                        */
/* ==================================================================== */
/************************************************************************/

class VSIMemFilesystemHandler final : public VSIFilesystemHandler
{
    const std::string m_osPrefix;
    CPL_DISALLOW_COPY_ASSIGN(VSIMemFilesystemHandler)

  public:
    std::map<CPLString, std::shared_ptr<VSIMemFile>> oFileList{};
    CPLMutex *hMutex = nullptr;

    explicit VSIMemFilesystemHandler(const char *pszPrefix)
        : m_osPrefix(pszPrefix)
    {
    }

    ~VSIMemFilesystemHandler() override;

    // TODO(schwehr): Fix VSIFileFromMemBuffer so that using is not needed.
    using VSIFilesystemHandler::Open;

    VSIVirtualHandle *Open(const char *pszFilename, const char *pszAccess,
                           bool bSetError,
                           CSLConstList /* papszOptions */) override;
    int Stat(const char *pszFilename, VSIStatBufL *pStatBuf,
             int nFlags) override;
    int Unlink(const char *pszFilename) override;
    int Mkdir(const char *pszDirname, long nMode) override;
    int Rmdir(const char *pszDirname) override;
    int RmdirRecursive(const char *pszDirname) override;
    char **ReadDirEx(const char *pszDirname, int nMaxFiles) override;
    int Rename(const char *oldpath, const char *newpath) override;
    GIntBig GetDiskFreeSpace(const char *pszDirname) override;

    static std::string NormalizePath(const std::string &in);

    int Unlink_unlocked(const char *pszFilename);

    VSIFilesystemHandler *Duplicate(const char *pszPrefix) override
    {
        return new VSIMemFilesystemHandler(pszPrefix);
    }
};

/************************************************************************/
/* ==================================================================== */
/*                              VSIMemFile                              */
/* ==================================================================== */
/************************************************************************/

/************************************************************************/
/*                             VSIMemFile()                             */
/************************************************************************/

VSIMemFile::VSIMemFile()
{
    time(&mTime);
}

/************************************************************************/
/*                            ~VSIMemFile()                             */
/************************************************************************/

VSIMemFile::~VSIMemFile()
{
    if (bOwnData && pabyData)
        CPLFree(pabyData);
}

/************************************************************************/
/*                             SetLength()                              */
/************************************************************************/

// Must be called under exclusive lock
bool VSIMemFile::SetLength(vsi_l_offset nNewLength)

{
    if (nNewLength > nMaxLength)
    {
        CPLError(CE_Failure, CPLE_NotSupported, "Maximum file size reached!");
        return false;
    }

    /* -------------------------------------------------------------------- */
    /*      Grow underlying array if needed.                                */
    /* -------------------------------------------------------------------- */
    if (nNewLength > nAllocLength)
    {
        // If we don't own the buffer, we cannot reallocate it because
        // the return address might be different from the one passed by
        // the caller. Hence, the caller would not be able to free
        // the buffer.
        if (!bOwnData)
        {
            CPLError(CE_Failure, CPLE_NotSupported,
                     "Cannot extended in-memory file whose ownership was not "
                     "transferred");
            return false;
        }

        // If the first allocation is 1 MB or above, just take that value
        // as the one to allocate
        // Otherwise slightly reserve more to avoid too frequent reallocations.
        const vsi_l_offset nNewAlloc =
            (nAllocLength == 0 && nNewLength >= 1024 * 1024)
                ? nNewLength
                : nNewLength + nNewLength / 10 + 5000;
        GByte *pabyNewData = nullptr;
        if (static_cast<vsi_l_offset>(static_cast<size_t>(nNewAlloc)) ==
            nNewAlloc)
        {
            pabyNewData = static_cast<GByte *>(
                nAllocLength == 0
                    ? VSICalloc(1, static_cast<size_t>(nNewAlloc))
                    : VSIRealloc(pabyData, static_cast<size_t>(nNewAlloc)));
        }
        if (pabyNewData == nullptr)
        {
            CPLError(CE_Failure, CPLE_OutOfMemory,
                     "Cannot extend in-memory file to " CPL_FRMT_GUIB
                     " bytes due to out-of-memory situation",
                     nNewAlloc);
            return false;
        }

        if (nAllocLength > 0)
        {
            // Clear the new allocated part of the buffer (only needed if
            // there was already reserved memory, otherwise VSICalloc() has
            // zeroized it already)
            memset(pabyNewData + nAllocLength, 0,
                   static_cast<size_t>(nNewAlloc - nAllocLength));
        }

        pabyData = pabyNewData;
        nAllocLength = nNewAlloc;
    }
    else if (nNewLength < nLength)
    {
        memset(pabyData + nNewLength, 0,
               static_cast<size_t>(nLength - nNewLength));
    }

    nLength = nNewLength;
    time(&mTime);

    return true;
}

/************************************************************************/
/* ==================================================================== */
/*                             VSIMemHandle                             */
/* ==================================================================== */
/************************************************************************/

/************************************************************************/
/*                            ~VSIMemHandle()                           */
/************************************************************************/

VSIMemHandle::~VSIMemHandle()
{
    VSIMemHandle::Close();
}

/************************************************************************/
/*                               Close()                                */
/************************************************************************/

int VSIMemHandle::Close()

{
    if (poFile)
    {
#ifdef DEBUG_VERBOSE
        CPLDebug("VSIMEM", "Closing handle %p on %s: ref_count=%d (before)",
                 this, poFile->osFilename.c_str(),
                 static_cast<int>(poFile.use_count()));
#endif
        poFile = nullptr;
    }

    return 0;
}

/************************************************************************/
/*                                Seek()                                */
/************************************************************************/

int VSIMemHandle::Seek(vsi_l_offset nOffset, int nWhence)

{
    vsi_l_offset nLength;
    {
        CPL_SHARED_LOCK oLock(poFile->m_oMutex);
        nLength = poFile->nLength;
    }

    if (nWhence == SEEK_CUR)
    {
        if (nOffset > INT_MAX)
        {
            // printf("likely negative offset intended\n");
        }
        m_nOffset += nOffset;
    }
    else if (nWhence == SEEK_SET)
    {
        m_nOffset = nOffset;
    }
    else if (nWhence == SEEK_END)
    {
        m_nOffset = nLength + nOffset;
    }
    else
    {
        errno = EINVAL;
        return -1;
    }

    bEOF = false;

    return 0;
}

/************************************************************************/
/*                                Tell()                                */
/************************************************************************/

vsi_l_offset VSIMemHandle::Tell()

{
    return m_nOffset;
}

/************************************************************************/
/*                                Read()                                */
/************************************************************************/

size_t VSIMemHandle::Read(void *pBuffer, size_t nSize, size_t nCount)

{
    const vsi_l_offset nOffset = m_nOffset;

    size_t nBytesToRead = nSize * nCount;
    if (nBytesToRead == 0)
        return 0;

    if (nCount > 0 && nBytesToRead / nCount != nSize)
    {
        bEOF = true;
        return 0;
    }

    if (!m_bReadAllowed)
    {
        m_bError = true;
        return 0;
    }

    bool bEOFTmp = bEOF;
    // Do not access/modify bEOF under the lock to avoid confusing Coverity
    // Scan since we access it in other methods outside of the lock.
    const auto DoUnderLock =
        [this, nOffset, pBuffer, nSize, &nBytesToRead, &nCount, &bEOFTmp]
    {
        CPL_SHARED_LOCK oLock(poFile->m_oMutex);

        if (poFile->nLength <= nOffset || nBytesToRead + nOffset < nBytesToRead)
        {
            bEOFTmp = true;
            return false;
        }
        if (nBytesToRead + nOffset > poFile->nLength)
        {
            nBytesToRead = static_cast<size_t>(poFile->nLength - nOffset);
            nCount = nBytesToRead / nSize;
            bEOFTmp = true;
        }

        if (nBytesToRead)
            memcpy(pBuffer, poFile->pabyData + nOffset,
                   static_cast<size_t>(nBytesToRead));
        return true;
    };

    bool bRet = DoUnderLock();
    bEOF = bEOFTmp;
    if (!bRet)
        return 0;

    m_nOffset += nBytesToRead;

    return nCount;
}

/************************************************************************/
/*                              PRead()                                 */
/************************************************************************/

size_t VSIMemHandle::PRead(void *pBuffer, size_t nSize,
                           vsi_l_offset nOffset) const
{
    CPL_SHARED_LOCK oLock(poFile->m_oMutex);

    if (nOffset < poFile->nLength)
    {
        const size_t nToCopy = static_cast<size_t>(
            std::min(static_cast<vsi_l_offset>(poFile->nLength - nOffset),
                     static_cast<vsi_l_offset>(nSize)));
        memcpy(pBuffer, poFile->pabyData + static_cast<size_t>(nOffset),
               nToCopy);
        return nToCopy;
    }
    return 0;
}

/************************************************************************/
/*                               Write()                                */
/************************************************************************/

size_t VSIMemHandle::Write(const void *pBuffer, size_t nSize, size_t nCount)

{
    const vsi_l_offset nOffset = m_nOffset;

    if (!bUpdate)
    {
        errno = EACCES;
        return 0;
    }

    const size_t nBytesToWrite = nSize * nCount;

    {
        CPL_EXCLUSIVE_LOCK oLock(poFile->m_oMutex);

        if (nCount > 0 && nBytesToWrite / nCount != nSize)
        {
            return 0;
        }
        if (nBytesToWrite + nOffset < nBytesToWrite)
        {
            return 0;
        }

        if (nBytesToWrite + nOffset > poFile->nLength)
        {
            if (!poFile->SetLength(nBytesToWrite + nOffset))
                return 0;
        }

        if (nBytesToWrite)
            memcpy(poFile->pabyData + nOffset, pBuffer, nBytesToWrite);

        time(&poFile->mTime);
    }

    m_nOffset += nBytesToWrite;

    return nCount;
}

/************************************************************************/
/*                             ClearErr()                               */
/************************************************************************/

void VSIMemHandle::ClearErr()

{
    CPL_SHARED_LOCK oLock(poFile->m_oMutex);
    bEOF = false;
    m_bError = false;
}

/************************************************************************/
/*                              Error()                                 */
/************************************************************************/

int VSIMemHandle::Error()

{
    CPL_SHARED_LOCK oLock(poFile->m_oMutex);
    return m_bError ? TRUE : FALSE;
}

/************************************************************************/
/*                                Eof()                                 */
/************************************************************************/

int VSIMemHandle::Eof()

{
    CPL_SHARED_LOCK oLock(poFile->m_oMutex);
    return bEOF ? TRUE : FALSE;
}

/************************************************************************/
/*                             Truncate()                               */
/************************************************************************/

int VSIMemHandle::Truncate(vsi_l_offset nNewSize)
{
    if (!bUpdate)
    {
        errno = EACCES;
        return -1;
    }

    CPL_EXCLUSIVE_LOCK oLock(poFile->m_oMutex);
    if (poFile->SetLength(nNewSize))
        return 0;

    return -1;
}

/************************************************************************/
/* ==================================================================== */
/*                       VSIMemFilesystemHandler                        */
/* ==================================================================== */
/************************************************************************/

/************************************************************************/
/*                      ~VSIMemFilesystemHandler()                      */
/************************************************************************/

VSIMemFilesystemHandler::~VSIMemFilesystemHandler()

{
    oFileList.clear();

    if (hMutex != nullptr)
        CPLDestroyMutex(hMutex);
    hMutex = nullptr;
}

/************************************************************************/
/*                                Open()                                */
/************************************************************************/

VSIVirtualHandle *VSIMemFilesystemHandler::Open(const char *pszFilename,
                                                const char *pszAccess,
                                                bool bSetError,
                                                CSLConstList /* papszOptions */)

{
    CPLMutexHolder oHolder(&hMutex);
    const CPLString osFilename = NormalizePath(pszFilename);
    if (osFilename.empty())
        return nullptr;

    vsi_l_offset nMaxLength = GUINTBIG_MAX;
    const size_t iPos = osFilename.find("||maxlength=");
    if (iPos != std::string::npos)
    {
        nMaxLength = static_cast<vsi_l_offset>(CPLAtoGIntBig(
            osFilename.substr(iPos + strlen("||maxlength=")).c_str()));
    }

    /* -------------------------------------------------------------------- */
    /*      Get the filename we are opening, create if needed.              */
    /* -------------------------------------------------------------------- */
    std::shared_ptr<VSIMemFile> poFile = nullptr;
    if (oFileList.find(osFilename) != oFileList.end())
    {
        poFile = oFileList[osFilename];
    }

    // If no file and opening in read, error out.
    if (strstr(pszAccess, "w") == nullptr &&
        strstr(pszAccess, "a") == nullptr && poFile == nullptr)
    {
        if (bSetError)
        {
            VSIError(VSIE_FileError, "No such file or directory");
        }
        errno = ENOENT;
        return nullptr;
    }

    // Create.
    if (poFile == nullptr)
    {
        const char *pszFileDir = CPLGetPath(osFilename.c_str());
        if (VSIMkdirRecursive(pszFileDir, 0755) == -1)
        {
            if (bSetError)
            {
                VSIError(VSIE_FileError,
                         "Could not create directory %s for writing",
                         pszFileDir);
            }
            errno = ENOENT;
            return nullptr;
        }

        poFile = std::make_shared<VSIMemFile>();
        poFile->osFilename = osFilename;
        oFileList[poFile->osFilename] = poFile;
#ifdef DEBUG_VERBOSE
        CPLDebug("VSIMEM", "Creating file %s: ref_count=%d", pszFilename,
                 static_cast<int>(poFile.use_count()));
#endif
        poFile->nMaxLength = nMaxLength;
    }
    // Overwrite
    else if (strstr(pszAccess, "w"))
    {
        CPL_EXCLUSIVE_LOCK oLock(poFile->m_oMutex);
        poFile->SetLength(0);
        poFile->nMaxLength = nMaxLength;
    }

    if (poFile->bIsDirectory)
    {
        errno = EISDIR;
        return nullptr;
    }

    /* -------------------------------------------------------------------- */
    /*      Setup the file handle on this file.                             */
    /* -------------------------------------------------------------------- */
    VSIMemHandle *poHandle = new VSIMemHandle;

    poHandle->poFile = poFile;
    poHandle->m_nOffset = 0;
    poHandle->bEOF = false;
    poHandle->bUpdate = strchr(pszAccess, 'w') || strchr(pszAccess, '+') ||
                        strchr(pszAccess, 'a');
    poHandle->m_bReadAllowed = strchr(pszAccess, 'r') || strchr(pszAccess, '+');

#ifdef DEBUG_VERBOSE
    CPLDebug("VSIMEM", "Opening handle %p on %s: ref_count=%d", poHandle,
             pszFilename, static_cast<int>(poFile.use_count()));
#endif
    if (strchr(pszAccess, 'a'))
    {
        vsi_l_offset nOffset;
        {
            CPL_SHARED_LOCK oLock(poFile->m_oMutex);
            nOffset = poFile->nLength;
        }
        poHandle->m_nOffset = nOffset;
    }

    return poHandle;
}

/************************************************************************/
/*                                Stat()                                */
/************************************************************************/

int VSIMemFilesystemHandler::Stat(const char *pszFilename,
                                  VSIStatBufL *pStatBuf, int /* nFlags */)

{
    CPLMutexHolder oHolder(&hMutex);

    const CPLString osFilename = NormalizePath(pszFilename);

    memset(pStatBuf, 0, sizeof(VSIStatBufL));

    if (osFilename + '/' == m_osPrefix || osFilename == m_osPrefix)
    {
        pStatBuf->st_size = 0;
        pStatBuf->st_mode = S_IFDIR;
        return 0;
    }

    auto oIter = oFileList.find(osFilename);
    if (oIter == oFileList.end())
    {
        errno = ENOENT;
        return -1;
    }

    std::shared_ptr<VSIMemFile> poFile = oIter->second;

    memset(pStatBuf, 0, sizeof(VSIStatBufL));

    CPL_SHARED_LOCK oLock(poFile->m_oMutex);
    if (poFile->bIsDirectory)
    {
        pStatBuf->st_size = 0;
        pStatBuf->st_mode = S_IFDIR;
    }
    else
    {
        pStatBuf->st_size = poFile->nLength;
        pStatBuf->st_mode = S_IFREG;
        pStatBuf->st_mtime = poFile->mTime;
    }

    return 0;
}

/************************************************************************/
/*                               Unlink()                               */
/************************************************************************/

int VSIMemFilesystemHandler::Unlink(const char *pszFilename)

{
    CPLMutexHolder oHolder(&hMutex);
    return Unlink_unlocked(pszFilename);
}

/************************************************************************/
/*                           Unlink_unlocked()                          */
/************************************************************************/

int VSIMemFilesystemHandler::Unlink_unlocked(const char *pszFilename)

{
    const CPLString osFilename = NormalizePath(pszFilename);

    auto oIter = oFileList.find(osFilename);
    if (oIter == oFileList.end())
    {
        errno = ENOENT;
        return -1;
    }

#ifdef DEBUG_VERBOSE
    std::shared_ptr<VSIMemFile> poFile = oIter->second;
    CPLDebug("VSIMEM", "Unlink %s: ref_count=%d (before)", pszFilename,
             static_cast<int>(poFile.use_count()));
#endif
    oFileList.erase(oIter);

    return 0;
}

/************************************************************************/
/*                               Mkdir()                                */
/************************************************************************/

int VSIMemFilesystemHandler::Mkdir(const char *pszPathname, long /* nMode */)

{
    CPLMutexHolder oHolder(&hMutex);

    const CPLString osPathname = NormalizePath(pszPathname);
    if (STARTS_WITH(osPathname.c_str(), szHIDDEN_DIRNAME))
    {
        if (osPathname.size() == strlen(szHIDDEN_DIRNAME))
            return 0;
        // "/vsimem/.#!HIDDEN!#./{unique_counter}"
        else if (osPathname.find('/', strlen(szHIDDEN_DIRNAME) + 1) ==
                 std::string::npos)
            return 0;

        // If "/vsimem/.#!HIDDEN!#./{unique_counter}/user_directory", then
        // accept creating an explicit directory
    }

    if (oFileList.find(osPathname) != oFileList.end())
    {
        errno = EEXIST;
        return -1;
    }

    std::shared_ptr<VSIMemFile> poFile = std::make_shared<VSIMemFile>();
    poFile->osFilename = osPathname;
    poFile->bIsDirectory = true;
    oFileList[osPathname] = poFile;
#ifdef DEBUG_VERBOSE
    CPLDebug("VSIMEM", "Mkdir on %s: ref_count=%d", pszPathname,
             static_cast<int>(poFile.use_count()));
#endif
    CPL_IGNORE_RET_VAL(poFile);
    return 0;
}

/************************************************************************/
/*                               Rmdir()                                */
/************************************************************************/

int VSIMemFilesystemHandler::Rmdir(const char *pszPathname)

{
    return Unlink(pszPathname);
}

/************************************************************************/
/*                          RmdirRecursive()                            */
/************************************************************************/

int VSIMemFilesystemHandler::RmdirRecursive(const char *pszDirname)
{
    CPLMutexHolder oHolder(&hMutex);

    const CPLString osPath = NormalizePath(pszDirname);
    const size_t nPathLen = osPath.size();
    int ret = 0;
    if (osPath == "/vsimem")
    {
        // Clean-up all files under pszDirname, except hidden directories
        // if called from "/vsimem"
        for (auto iter = oFileList.begin(); iter != oFileList.end();
             /* no automatic increment */)
        {
            const char *pszFilePath = iter->second->osFilename.c_str();
            const size_t nFileLen = iter->second->osFilename.size();
            if (nFileLen > nPathLen &&
                memcmp(pszFilePath, osPath.c_str(), nPathLen) == 0 &&
                pszFilePath[nPathLen] == '/' &&
                !STARTS_WITH(pszFilePath, szHIDDEN_DIRNAME))
            {
                iter = oFileList.erase(iter);
            }
            else
            {
                ++iter;
            }
        }
    }
    else
    {
        ret = -1;
        for (auto iter = oFileList.begin(); iter != oFileList.end();
             /* no automatic increment */)
        {
            const char *pszFilePath = iter->second->osFilename.c_str();
            const size_t nFileLen = iter->second->osFilename.size();
            if (nFileLen >= nPathLen &&
                memcmp(pszFilePath, osPath.c_str(), nPathLen) == 0 &&
                (nFileLen == nPathLen || pszFilePath[nPathLen] == '/'))
            {
                // If VSIRmdirRecursive() is used correctly, it should at
                // least delete the directory on which it has been called
                ret = 0;
                iter = oFileList.erase(iter);
            }
            else
            {
                ++iter;
            }
        }

        // Make sure that it always succeed on the root hidden directory
        if (osPath == szHIDDEN_DIRNAME)
            ret = 0;
    }
    return ret;
}

/************************************************************************/
/*                             ReadDirEx()                              */
/************************************************************************/

char **VSIMemFilesystemHandler::ReadDirEx(const char *pszPath, int nMaxFiles)

{
    CPLMutexHolder oHolder(&hMutex);

    const CPLString osPath = NormalizePath(pszPath);

    char **papszDir = nullptr;
    const size_t nPathLen = osPath.size();

    // In case of really big number of files in the directory, CSLAddString
    // can be slow (see #2158). We then directly build the list.
    int nItems = 0;
    int nAllocatedItems = 0;

    if (osPath == szHIDDEN_DIRNAME)
    {
        // Special mode for hidden filenames.
        // "/vsimem/.#!HIDDEN!#./{counter}" subdirectories are not explicitly
        // created so they do not appear in oFileList, but their subcontent
        // (e.g "/vsimem/.#!HIDDEN!#./{counter}/foo") does
        std::set<std::string> oSetSubDirs;
        for (const auto &iter : oFileList)
        {
            const char *pszFilePath = iter.second->osFilename.c_str();
            if (iter.second->osFilename.size() > nPathLen &&
                memcmp(pszFilePath, osPath.c_str(), nPathLen) == 0)
            {
                char *pszItem = CPLStrdup(pszFilePath + nPathLen + 1);
                char *pszSlash = strchr(pszItem, '/');
                if (pszSlash)
                    *pszSlash = 0;
                if (cpl::contains(oSetSubDirs, pszItem))
                {
                    CPLFree(pszItem);
                    continue;
                }
                oSetSubDirs.insert(pszItem);

                if (nItems == 0)
                {
                    papszDir =
                        static_cast<char **>(CPLCalloc(2, sizeof(char *)));
                    nAllocatedItems = 1;
                }
                else if (nItems >= nAllocatedItems)
                {
                    nAllocatedItems = nAllocatedItems * 2;
                    papszDir = static_cast<char **>(CPLRealloc(
                        papszDir, (nAllocatedItems + 2) * sizeof(char *)));
                }

                papszDir[nItems] = pszItem;
                papszDir[nItems + 1] = nullptr;

                nItems++;
                if (nMaxFiles > 0 && nItems > nMaxFiles)
                    break;
            }
        }
    }
    else
    {
        for (const auto &iter : oFileList)
        {
            const char *pszFilePath = iter.second->osFilename.c_str();
            if (iter.second->osFilename.size() > nPathLen &&
                memcmp(pszFilePath, osPath.c_str(), nPathLen) == 0 &&
                pszFilePath[nPathLen] == '/' &&
                strstr(pszFilePath + nPathLen + 1, "/") == nullptr)
            {
                if (nItems == 0)
                {
                    papszDir =
                        static_cast<char **>(CPLCalloc(2, sizeof(char *)));
                    nAllocatedItems = 1;
                }
                else if (nItems >= nAllocatedItems)
                {
                    nAllocatedItems = nAllocatedItems * 2;
                    papszDir = static_cast<char **>(CPLRealloc(
                        papszDir, (nAllocatedItems + 2) * sizeof(char *)));
                }

                papszDir[nItems] = CPLStrdup(pszFilePath + nPathLen + 1);
                papszDir[nItems + 1] = nullptr;

                nItems++;
                if (nMaxFiles > 0 && nItems > nMaxFiles)
                    break;
            }
        }
    }

    return papszDir;
}

/************************************************************************/
/*                               Rename()                               */
/************************************************************************/

int VSIMemFilesystemHandler::Rename(const char *pszOldPath,
                                    const char *pszNewPath)

{
    CPLMutexHolder oHolder(&hMutex);

    const CPLString osOldPath = NormalizePath(pszOldPath);
    const CPLString osNewPath = NormalizePath(pszNewPath);
    if (!STARTS_WITH(pszNewPath, m_osPrefix.c_str()))
        return -1;

    if (osOldPath.compare(osNewPath) == 0)
        return 0;

    if (oFileList.find(osOldPath) == oFileList.end())
    {
        errno = ENOENT;
        return -1;
    }

    std::map<CPLString, std::shared_ptr<VSIMemFile>>::iterator it =
        oFileList.find(osOldPath);
    while (it != oFileList.end() && it->first.ifind(osOldPath) == 0)
    {
        const CPLString osRemainder = it->first.substr(osOldPath.size());
        if (osRemainder.empty() || osRemainder[0] == '/')
        {
            const CPLString osNewFullPath = osNewPath + osRemainder;
            Unlink_unlocked(osNewFullPath);
            oFileList[osNewFullPath] = it->second;
            it->second->osFilename = osNewFullPath;
            oFileList.erase(it++);
        }
        else
        {
            ++it;
        }
    }

    return 0;
}

/************************************************************************/
/*                           NormalizePath()                            */
/************************************************************************/

std::string VSIMemFilesystemHandler::NormalizePath(const std::string &in)
{
    CPLString s(in);
    std::replace(s.begin(), s.end(), '\\', '/');
    s.replaceAll("//", '/');
    if (!s.empty() && s.back() == '/')
        s.pop_back();
#if __GNUC__ >= 13
    // gcc 13 complains about below explicit std::move()
    return s;
#else
    // Android NDK (and probably other compilers) warn about
    // "warning: local variable 's' will be copied despite being returned by name [-Wreturn-std-move]"
    // if not specifying std::move()
    return std::move(s);
#endif
}

/************************************************************************/
/*                        GetDiskFreeSpace()                            */
/************************************************************************/

GIntBig VSIMemFilesystemHandler::GetDiskFreeSpace(const char * /*pszDirname*/)
{
    const GIntBig nRet = CPLGetUsablePhysicalRAM();
    if (nRet <= 0)
        return -1;
    return nRet;
}

//! @endcond

/************************************************************************/
/*                       VSIInstallMemFileHandler()                     */
/************************************************************************/

/**
 * \brief Install "memory" file system handler.
 *
 * A special file handler is installed that allows block of memory to be
 * treated as files.   All portions of the file system underneath the base
 * path "/vsimem/" will be handled by this driver.
 *
 * Normal VSI*L functions can be used freely to create and destroy memory
 * arrays treating them as if they were real file system objects.  Some
 * additional methods exist to efficient create memory file system objects
 * without duplicating original copies of the data or to "steal" the block
 * of memory associated with a memory file.
 *
 * Directory related functions are supported.
 *
 * This code example demonstrates using GDAL to translate from one memory
 * buffer to another.
 *
 * \code
 * GByte *ConvertBufferFormat( GByte *pabyInData, vsi_l_offset nInDataLength,
 *                             vsi_l_offset *pnOutDataLength )
 * {
 *     // create memory file system object from buffer.
 *     VSIFCloseL( VSIFileFromMemBuffer( "/vsimem/work.dat", pabyInData,
 *                                       nInDataLength, FALSE ) );
 *
 *     // Open memory buffer for read.
 *     GDALDatasetH hDS = GDALOpen( "/vsimem/work.dat", GA_ReadOnly );
 *
 *     // Get output format driver.
 *     GDALDriverH hDriver = GDALGetDriverByName( "GTiff" );
 *     GDALDatasetH hOutDS;
 *
 *     hOutDS = GDALCreateCopy( hDriver, "/vsimem/out.tif", hDS, TRUE, NULL,
 *                              NULL, NULL );
 *
 *     // close source file, and "unlink" it.
 *     GDALClose( hDS );
 *     VSIUnlink( "/vsimem/work.dat" );
 *
 *     // seize the buffer associated with the output file.
 *
 *     return VSIGetMemFileBuffer( "/vsimem/out.tif", pnOutDataLength, TRUE );
 * }
 * \endcode
 */

void VSIInstallMemFileHandler()
{
    VSIFileManager::InstallHandler("/vsimem/",
                                   new VSIMemFilesystemHandler("/vsimem/"));
}

/************************************************************************/
/*                        VSIFileFromMemBuffer()                        */
/************************************************************************/

/**
 * \brief Create memory "file" from a buffer.
 *
 * A virtual memory file is created from the passed buffer with the indicated
 * filename.  Under normal conditions the filename would need to be absolute
 * and within the /vsimem/ portion of the filesystem.
 * Starting with GDAL 3.6, nullptr can also be passed as pszFilename to mean
 * an anonymous file, that is destroyed when the handle is closed.
 *
 * If bTakeOwnership is TRUE, then the memory file system handler will take
 * ownership of the buffer, freeing it when the file is deleted.  Otherwise
 * it remains the responsibility of the caller, but should not be freed as
 * long as it might be accessed as a file.  In no circumstances does this
 * function take a copy of the pabyData contents.
 *
 * @param pszFilename the filename to be created, or nullptr
 * @param pabyData the data buffer for the file.
 * @param nDataLength the length of buffer in bytes.
 * @param bTakeOwnership TRUE to transfer "ownership" of buffer or FALSE.
 *
 * @return open file handle on created file (see VSIFOpenL()).
 */

VSILFILE *VSIFileFromMemBuffer(const char *pszFilename, GByte *pabyData,
                               vsi_l_offset nDataLength, int bTakeOwnership)

{
    if (VSIFileManager::GetHandler("") ==
        VSIFileManager::GetHandler("/vsimem/"))
        VSIInstallMemFileHandler();

    VSIMemFilesystemHandler *poHandler = static_cast<VSIMemFilesystemHandler *>(
        VSIFileManager::GetHandler("/vsimem/"));

    const CPLString osFilename =
        pszFilename ? VSIMemFilesystemHandler::NormalizePath(pszFilename)
                    : std::string();
    if (osFilename == "/vsimem/")
    {
        CPLDebug("VSIMEM", "VSIFileFromMemBuffer(): illegal filename: %s",
                 pszFilename);
        return nullptr;
    }

    // Try to create the parent directory, if needed, before taking
    // ownership of pabyData.
    if (!osFilename.empty())
    {
        const char *pszFileDir = CPLGetPath(osFilename.c_str());
        if (VSIMkdirRecursive(pszFileDir, 0755) == -1)
        {
            VSIError(VSIE_FileError,
                     "Could not create directory %s for writing", pszFileDir);
            errno = ENOENT;
            return nullptr;
        }
    }

    std::shared_ptr<VSIMemFile> poFile = std::make_shared<VSIMemFile>();

    poFile->osFilename = osFilename;
    poFile->bOwnData = CPL_TO_BOOL(bTakeOwnership);
    poFile->pabyData = pabyData;
    poFile->nLength = nDataLength;
    poFile->nAllocLength = nDataLength;

    if (!osFilename.empty())
    {
        CPLMutexHolder oHolder(&poHandler->hMutex);
        poHandler->Unlink_unlocked(osFilename);
        poHandler->oFileList[poFile->osFilename] = poFile;
#ifdef DEBUG_VERBOSE
        CPLDebug("VSIMEM", "VSIFileFromMemBuffer() %s: ref_count=%d (after)",
                 poFile->osFilename.c_str(),
                 static_cast<int>(poFile.use_count()));
#endif
    }

    /* -------------------------------------------------------------------- */
    /*      Setup the file handle on this file.                             */
    /* -------------------------------------------------------------------- */
    VSIMemHandle *poHandle = new VSIMemHandle;

    poHandle->poFile = std::move(poFile);
    poHandle->bUpdate = true;
    poHandle->m_bReadAllowed = true;
    return poHandle;
}

/************************************************************************/
/*                        VSIGetMemFileBuffer()                         */
/************************************************************************/

/**
 * \brief Fetch buffer underlying memory file.
 *
 * This function returns a pointer to the memory buffer underlying a
 * virtual "in memory" file.  If bUnlinkAndSeize is TRUE the filesystem
 * object will be deleted, and ownership of the buffer will pass to the
 * caller otherwise the underlying file will remain in existence.
 *
 * @param pszFilename the name of the file to grab the buffer of.
 * @param pnDataLength (file) length returned in this variable.
 * @param bUnlinkAndSeize TRUE to remove the file, or FALSE to leave unaltered.
 *
 * @return pointer to memory buffer or NULL on failure.
 */

GByte *VSIGetMemFileBuffer(const char *pszFilename, vsi_l_offset *pnDataLength,
                           int bUnlinkAndSeize)

{
    VSIMemFilesystemHandler *poHandler = static_cast<VSIMemFilesystemHandler *>(
        VSIFileManager::GetHandler("/vsimem/"));

    if (pszFilename == nullptr)
        return nullptr;

    const CPLString osFilename =
        VSIMemFilesystemHandler::NormalizePath(pszFilename);

    CPLMutexHolder oHolder(&poHandler->hMutex);

    if (poHandler->oFileList.find(osFilename) == poHandler->oFileList.end())
        return nullptr;

    std::shared_ptr<VSIMemFile> poFile = poHandler->oFileList[osFilename];
    GByte *pabyData = poFile->pabyData;
    if (pnDataLength != nullptr)
        *pnDataLength = poFile->nLength;

    if (bUnlinkAndSeize)
    {
        if (!poFile->bOwnData)
            CPLDebug("VSIMemFile",
                     "File doesn't own data in VSIGetMemFileBuffer!");
        else
            poFile->bOwnData = false;

        poHandler->oFileList.erase(poHandler->oFileList.find(osFilename));
#ifdef DEBUG_VERBOSE
        CPLDebug("VSIMEM", "VSIGetMemFileBuffer() %s: ref_count=%d (before)",
                 poFile->osFilename.c_str(),
                 static_cast<int>(poFile.use_count()));
#endif
        poFile->pabyData = nullptr;
        poFile->nLength = 0;
        poFile->nAllocLength = 0;
    }

    return pabyData;
}

/************************************************************************/
/*                    VSIMemGenerateHiddenFilename()                    */
/************************************************************************/

/**
 * \brief Generates a unique filename that can be used with the /vsimem/
 * virtual file system.
 *
 * This function returns a (short-lived) string containing a unique filename,
 * (using an atomic counter), designed for temporary files that must remain
 * invisible for other users working at the "/vsimem/" top-level, i.e.
 * such files are not returned by VSIReadDir("/vsimem/") or
 * VSIReadDirRecursive("/vsimem/)".
 *
 * The function does not create the file per se. Such filename can be used to
 * create a regular file with VSIFOpenL() or VSIFileFromMemBuffer(), or create
 * a directory with VSIMkdir()
 *
 * Once created, deletion of those files using VSIUnlink(), VSIRmdirRecursive(),
 * etc. is of the responsibility of the user. The user should not attempt to
 * work with the "parent" directory returned by CPLGetPath() / CPLGetDirname()
 * on the returned filename, and work only with files at the same level or
 * in subdirectories of what is returned by this function.
 *
 * @param pszFilename the filename to be appended at the end of the returned
 *                    filename. If not specified, defaults to "unnamed".
 *
 * @return pointer to a short-lived string (rotating buffer of strings in
 * thread-local storage). It is recommended to use CPLStrdup() or std::string()
 * immediately on it.
 *
 * @since GDAL 3.10
 */
const char *VSIMemGenerateHiddenFilename(const char *pszFilename)
{
    static std::atomic<uint32_t> nCounter{0};
    return CPLSPrintf("%s/%u/%s", szHIDDEN_DIRNAME, ++nCounter,
                      pszFilename ? pszFilename : "unnamed");
}
