/**
 * Orthanc - A Lightweight, RESTful DICOM Store
 * Copyright (C) 2012-2016 Sebastien Jodogne, Medical Physics
 * Department, University Hospital of Liege, Belgium
 * Copyright (C) 2017-2023 Osimis S.A., Belgium
 * Copyright (C) 2024-2026 Orthanc Team SRL, Belgium
 * Copyright (C) 2021-2026 Sebastien Jodogne, ICTEAM UCLouvain, Belgium
 *
 * This program is free software: you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public License
 * as published by the Free Software Foundation, either version 3 of
 * the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this program. If not, see
 * <http://www.gnu.org/licenses/>.
 **/


#include "../PrecompiledHeaders.h"

#ifndef NOMINMAX
#define NOMINMAX
#endif

#if !defined(ORTHANC_USE_SYSTEM_MINIZIP)
#  error The macro ORTHANC_USE_SYSTEM_MINIZIP must be defined
#endif

#include "ZipReader.h"

#if ORTHANC_USE_SYSTEM_MINIZIP == 1
#  include <minizip/unzip.h>
#else
#  include "../../Resources/ThirdParty/minizip/unzip.h"
#endif


#include "../ChunkedBuffer.h"
#include "../OrthancException.h"
#include "../MultiThreading/ReaderWriterLock.h"

#if ORTHANC_SANDBOXED != 1
#  include "../SystemToolbox.h"
#endif


/**
 * I have not been able to correctly define "ssize_t" on all versions
 * of Visual Studio. As a consequence, I preferred to switch "ssize_t"
 * to "SSIZE_T", that is properly defined on both MSVC 2008 and 2015.
 * I define the macro "SSIZE_T" as an alias to "ssize_t" on
 * POSIX-compliant platforms that wouldn't have "SSIZE_T" defined.
 **/
#if defined(_MSC_VER)
#  include <BaseTsd.h>   // Definition of SSIZE_T
#else
#  if !defined(SSIZE_T)
typedef ssize_t SSIZE_T;
#  endif
#endif

#include <string.h>


static Orthanc::ReaderWriterLock maximumUncompressedFileSizeMutex_;
static bool hasMaximumUncompressedFileSize_ = false;
static size_t maximumUncompressedFileSize_ = 0;


namespace Orthanc
{
  // ZPOS64_T corresponds to "uint64_t"
  
  class ZipReader::MemoryBuffer : public boost::noncopyable
  {
  private:
    const uint8_t*  content_;
    size_t          size_;
    size_t          pos_;

  public:
    MemoryBuffer(const void* p,
                 size_t size) :
      content_(reinterpret_cast<const uint8_t*>(p)),
      size_(size),
      pos_(0)
    {
    }
  
    explicit MemoryBuffer(const std::string& s) :
      content_(s.empty() ? NULL : reinterpret_cast<const uint8_t*>(s.c_str())),
      size_(s.size()),
      pos_(0)
    {
    }

    // Returns the number of bytes actually read
    uLong Read(void *target,
               uLong size)
    {
      if (size <= 0)
      {
        return 0;
      }
      else
      {
        size_t s = static_cast<size_t>(size);
        if (s + pos_ > size_)
        {
          s = size_ - pos_;
        }

        if (s != 0)
        {
          memcpy(target, content_ + pos_, s);
        }
      
        pos_ += s;
        return static_cast<uLong>(s);
      }             
    }

    ZPOS64_T Tell() const
    {
      return static_cast<ZPOS64_T>(pos_);
    }

    long Seek(ZPOS64_T offset,
              int origin)
    {
      SSIZE_T next;
    
      switch (origin)
      {
        case ZLIB_FILEFUNC_SEEK_CUR:
          next = static_cast<SSIZE_T>(offset) + static_cast<SSIZE_T>(pos_);
          break;

        case ZLIB_FILEFUNC_SEEK_SET:
          next = static_cast<SSIZE_T>(offset);
          break;

        case ZLIB_FILEFUNC_SEEK_END:
          next = static_cast<SSIZE_T>(offset) + static_cast<SSIZE_T>(size_);
          break;

        default:  // Should never occur
          return 1;  // Error
      }

      if (next < 0)
      {
        pos_ = 0;
      }
      else if (next >= static_cast<long>(size_))
      {
        pos_ = size_;
      }
      else
      {
        pos_ = static_cast<long>(next);
      }

      return 0;
    }


    static voidpf OpenWrapper(voidpf opaque,
                              const void* filename,
                              int mode)
    {
      // Don't return NULL to make "unzip.c" happy
      return opaque;
    }

    static uLong ReadWrapper(voidpf opaque,
                             voidpf stream,
                             void* buf,
                             uLong size)
    {
      assert(opaque != NULL);
      return reinterpret_cast<MemoryBuffer*>(opaque)->Read(buf, size);
    }

    static ZPOS64_T TellWrapper(voidpf opaque,
                                voidpf stream)
    {
      assert(opaque != NULL);
      return reinterpret_cast<MemoryBuffer*>(opaque)->Tell();
    }

    static long SeekWrapper(voidpf opaque,
                            voidpf stream,
                            ZPOS64_T offset,
                            int origin)
    {
      assert(opaque != NULL);
      return reinterpret_cast<MemoryBuffer*>(opaque)->Seek(offset, origin);
    }

    static int CloseWrapper(voidpf opaque,
                            voidpf stream)
    {
      return 0;
    }

    static int TestErrorWrapper(voidpf opaque,
                                voidpf stream)
    {
      return 0;  // ??
    }
  };



  ZipReader* ZipReader::CreateFromMemory(const std::string& buffer)
  {
    if (buffer.empty())
    {
      return CreateFromMemory(NULL, 0);
    }
    else
    {
      return CreateFromMemory(buffer.c_str(), buffer.size());
    }
  }


  bool ZipReader::IsZipMemoryBuffer(const void* buffer,
                                    size_t size)
  {
    if (size < 4)
    {
      return false;
    }
    else
    {
      const uint8_t* c = reinterpret_cast<const uint8_t*>(buffer);
      return (c[0] == 0x50 &&  // 'P'
              c[1] == 0x4b &&  // 'K'
              ((c[2] == 0x03 && c[3] == 0x04) ||
               (c[2] == 0x05 && c[3] == 0x06) ||
               (c[2] == 0x07 && c[3] == 0x08)));
    }
  }
  

  bool ZipReader::IsZipMemoryBuffer(const std::string& content)
  {
    if (content.empty())
    {
      return false;
    }
    else
    {
      return IsZipMemoryBuffer(content.c_str(), content.size());
    }
  }


#if ORTHANC_SANDBOXED != 1
  bool ZipReader::IsZipFile(const boost::filesystem::path& path)
  {
    std::string content;
    SystemToolbox::ReadFileRange(content, path, 0, 4,
                                 false /* don't throw if file is too small */);

    return IsZipMemoryBuffer(content);
  }
#endif


  struct ZipReader::PImpl
  {
    unzFile                       unzip_;
    std::unique_ptr<MemoryBuffer> reader_;
    bool                          done_;

    PImpl() :
      unzip_(NULL),
      done_(true)
    {
    }
  };


  ZipReader::ZipReader() :
    pimpl_(new PImpl)
  {
  }

  
  ZipReader::~ZipReader()
  {
    if (pimpl_->unzip_ != NULL)
    {
      unzClose(pimpl_->unzip_);
      pimpl_->unzip_ = NULL;
    }
  }

  
  uint64_t ZipReader::GetFilesCount() const
  {
    assert(pimpl_->unzip_ != NULL);
    
    unz_global_info64_s info;
    
    if (unzGetGlobalInfo64(pimpl_->unzip_, &info) == 0)
    {
      return info.number_entry;
    }
    else
    {
      throw OrthancException(ErrorCode_BadFileFormat);
    }
  }

  
  void ZipReader::SeekFirst()
  {
    assert(pimpl_->unzip_ != NULL);    
    pimpl_->done_ = (unzGoToFirstFile(pimpl_->unzip_) != 0);
  }


  static bool ReadInternal(std::string& content,
                           const unzFile& unzip,
                           uint64_t size) ORTHANC_NOEXCEPT
  {
#if 0
    /**
     * This was the code using in Orthanc <= 1.12.10
     **/
    content.resize(static_cast<size_t>(size));
    return (unzReadCurrentFile(unzip, &content[0], static_cast<uLong>(content.size())) != 0);

#else
    /**
     * Read chunk by chunk to disarm ZIP bombs
     **/
    ChunkedBuffer buffer;

    std::string chunk;
    chunk.resize(static_cast<size_t>(10) * 1024 * 1024); // Read by chunks of 10MB

    for (;;)
    {
      int r = unzReadCurrentFile(unzip, &chunk[0], chunk.size());

      if (r == 0)
      {
        // We're done
        if (buffer.GetNumBytes() == size)
        {
          buffer.Flatten(content);
          return true;
        }
        else
        {
          return false;  // Presumably a malicious ZIP file
        }
      }
      else if (r < 0)
      {
        // Error (might come from a malicious ZIP file)
        return false;
      }
      else if (static_cast<uint64_t>(buffer.GetNumBytes()) + r > size)
      {
        // Presumably a ZIP bomb
        return false;
      }
      else
      {
        buffer.AddChunk(&chunk[0], r);
      }
    }
#endif
  }


  bool ZipReader::ReadNextFile(std::string& filename,
                               std::string& content)
  {
    assert(pimpl_->unzip_ != NULL);

    if (pimpl_->done_)
    {
      return false;
    }
    else
    {
      unz_file_info64_s info;
      if (unzGetCurrentFileInfo64(pimpl_->unzip_, &info, NULL, 0, NULL, 0, NULL, 0) != 0)
      {
        throw OrthancException(ErrorCode_BadFileFormat);
      }

      if (static_cast<uint64_t>(static_cast<size_t>(info.uncompressed_size)) != info.uncompressed_size)
      {
        // Too large file for a 32bit architecture
        throw OrthancException(ErrorCode_NotEnoughMemory);
      }

      filename.resize(info.size_filename);
      if (!filename.empty() &&
          unzGetCurrentFileInfo64(pimpl_->unzip_, &info, &filename[0],
                                  static_cast<uLong>(filename.size()), NULL, 0, NULL, 0) != 0)
      {
        throw OrthancException(ErrorCode_BadFileFormat);
      }

      {
        // Prevent ZIP bombs
        ReaderWriterLock::ReadLock lock(maximumUncompressedFileSizeMutex_);

        if (hasMaximumUncompressedFileSize_ &&
            info.uncompressed_size > maximumUncompressedFileSize_)
        {
          char s[32];
          sprintf(s, "%0.1f", static_cast<float>(maximumUncompressedFileSize_) / (1024.0f * 1024.0f));
          throw OrthancException(ErrorCode_BadFileFormat, "Uncompressed size exceeds limit (" + std::string(s) + "MB)");
        }
      }

      if (info.uncompressed_size == 0)
      {
        content.clear();
      }
      else
      {
        if (unzOpenCurrentFile(pimpl_->unzip_) == 0)
        {
          bool success = ReadInternal(content, pimpl_->unzip_, info.uncompressed_size);
                          
          if (unzCloseCurrentFile(pimpl_->unzip_) != 0 ||
              !success)
          {
            throw OrthancException(ErrorCode_BadFileFormat);
          }
        }
        else
        {
          throw OrthancException(ErrorCode_BadFileFormat, "Invalid file or unsupported compression method (e.g. Deflate64)");
        }
      }
      
      pimpl_->done_ = (unzGoToNextFile(pimpl_->unzip_) != 0);
 
      return true;
    }
  }    

  
  ZipReader* ZipReader::CreateFromMemory(const void* buffer,
                                         size_t size)
  {
    if (!IsZipMemoryBuffer(buffer, size))
    {
      throw OrthancException(ErrorCode_BadFileFormat, "The memory buffer doesn't contain a ZIP archive");
    }
    else
    {
      std::unique_ptr<ZipReader> reader(new ZipReader);

      reader->pimpl_->reader_.reset(new MemoryBuffer(buffer, size));
      if (reader->pimpl_->reader_.get() == NULL)
      {
        THROW_WITH_FILE_AND_LINE_INFO(ErrorCode_InternalError);
      }
    
      zlib_filefunc64_def funcs;
      memset(&funcs, 0, sizeof(funcs));

      funcs.opaque = reader->pimpl_->reader_.get();
      funcs.zopen64_file = MemoryBuffer::OpenWrapper;
      funcs.zread_file = MemoryBuffer::ReadWrapper;
      funcs.ztell64_file = MemoryBuffer::TellWrapper;
      funcs.zseek64_file = MemoryBuffer::SeekWrapper;
      funcs.zclose_file = MemoryBuffer::CloseWrapper;
      funcs.zerror_file = MemoryBuffer::TestErrorWrapper;

      reader->pimpl_->unzip_ = unzOpen2_64(NULL, &funcs);
      if (reader->pimpl_->unzip_ == NULL)
      {
        throw OrthancException(ErrorCode_BadFileFormat, "Cannot open ZIP archive from memory buffer");
      }
      else
      {
        reader->SeekFirst();
        return reader.release();
      }
    }
  }
  

#if ORTHANC_SANDBOXED != 1
  ZipReader* ZipReader::CreateFromFile(const boost::filesystem::path& path)
  {
    if (!IsZipFile(path))
    {
      throw OrthancException(ErrorCode_BadFileFormat, "The file doesn't contain a ZIP archive: " + SystemToolbox::PathToUtf8(path));
    }
    else
    {
      std::unique_ptr<ZipReader> reader(new ZipReader);

      reader->pimpl_->unzip_ = unzOpen64(SystemToolbox::PathToUtf8(path).c_str());
      if (reader->pimpl_->unzip_ == NULL)
      {
        throw OrthancException(ErrorCode_BadFileFormat, "Cannot open ZIP archive from file: " + SystemToolbox::PathToUtf8(path));
      }
      else
      {
        reader->SeekFirst();
        return reader.release();
      }
    }
  }
#endif


  void ZipReader::SetMaximumUncompressedFileSize(uint64_t size)
  {
    if (static_cast<uint64_t>(static_cast<size_t>(size)) != size)
    {
      throw OrthancException(ErrorCode_NotEnoughMemory);
    }
    else
    {
      ReaderWriterLock::WriteLock lock(maximumUncompressedFileSizeMutex_);
      hasMaximumUncompressedFileSize_ = true;
      maximumUncompressedFileSize_ = size;
    }
  }
}
