// Copyright (c) 2009 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/common/zip.h"

#include "base/file_util.h"
#include "base/logging.h"
#include "base/string_split.h"
#include "base/string_util.h"
#include "base/utf_string_conversions.h"
#include "net/base/file_stream.h"
#include "third_party/zlib/contrib/minizip/unzip.h"
#include "third_party/zlib/contrib/minizip/zip.h"
#if defined(OS_WIN)
#include "third_party/zlib/contrib/minizip/iowin32.h"
#endif

static const int kZipMaxPath = 256;
static const int kZipBufSize = 8192;

// Extract the 'current' selected file from the zip into dest_dir.
// Output filename is stored in out_file.  Returns true on success.
static bool ExtractCurrentFile(unzFile zip_file,
                               const FilePath& dest_dir) {
  char filename_inzip[kZipMaxPath] = {0};
  unz_file_info file_info;
  int err = unzGetCurrentFileInfo(zip_file, &file_info, filename_inzip,
                                  sizeof(filename_inzip) - 1, NULL, 0, NULL, 0);
  if (err != UNZ_OK)
    return false;
  if (filename_inzip[0] == '\0')
    return false;

  err = unzOpenCurrentFile(zip_file);
  if (err != UNZ_OK)
    return false;

  FilePath::StringType filename;
  std::vector<FilePath::StringType> filename_parts;
#if defined(OS_WIN)
  filename = UTF8ToWide(filename_inzip);
#elif defined(OS_POSIX)
  filename = filename_inzip;
#endif

  // Check the filename here for directory traversal issues. In the name of
  // simplicity and security, we might reject a valid filename such as "a..b".
  if (filename.find(FILE_PATH_LITERAL("..")) != FilePath::StringType::npos)
    return false;

  base::SplitString(filename, '/', &filename_parts);

  FilePath dest_file(dest_dir);
  std::vector<FilePath::StringType>::iterator iter;
  for (iter = filename_parts.begin(); iter != filename_parts.end(); ++iter)
    dest_file = dest_file.Append(*iter);

  // If this is a directory, just create it and return.
  if (filename_inzip[strlen(filename_inzip) - 1] == '/') {
    if (!file_util::CreateDirectory(dest_file))
      return false;
    return true;
  }

  // We can't rely on parent directory entries being specified in the zip, so we
  // make sure they are created.
  FilePath dir = dest_file.DirName();
  if (!file_util::CreateDirectory(dir))
    return false;

  net::FileStream stream;
  int flags = base::PLATFORM_FILE_CREATE_ALWAYS | base::PLATFORM_FILE_WRITE;
  if (stream.Open(dest_file, flags) != 0)
    return false;

  bool ret = true;
  int num_bytes = 0;
  char buf[kZipBufSize];
  do {
    num_bytes = unzReadCurrentFile(zip_file, buf, kZipBufSize);
    if (num_bytes < 0) {
      // If num_bytes < 0, then it's a specific UNZ_* error code.
      // While we're not currently handling these codes specifically, save
      // it away in case we want to in the future.
      err = num_bytes;
      break;
    }
    if (num_bytes > 0) {
      if (num_bytes != stream.Write(buf, num_bytes, NULL)) {
        ret = false;
        break;
      }
    }
  } while (num_bytes > 0);

  stream.Close();
  if (err == UNZ_OK)
    err = unzCloseCurrentFile(zip_file);
  else
    unzCloseCurrentFile(zip_file);  // Don't lose the original error code.
  if (err != UNZ_OK)
    ret = false;
  return ret;
}

#if defined(OS_WIN)
typedef struct {
  HANDLE hf;
  int error;
} WIN32FILE_IOWIN;

// This function is derived from third_party/minizip/iowin32.c.
// Its only difference is that it treats the char* as UTF8 and
// uses the Unicode version of CreateFile.
static void* ZipOpenFunc(void *opaque, const char* filename, int mode) {
  DWORD desired_access, creation_disposition;
  DWORD share_mode, flags_and_attributes;
  HANDLE file = 0;
  void* ret = NULL;

  desired_access = share_mode = flags_and_attributes = 0;

  if ((mode & ZLIB_FILEFUNC_MODE_READWRITEFILTER) == ZLIB_FILEFUNC_MODE_READ) {
    desired_access = GENERIC_READ;
    creation_disposition = OPEN_EXISTING;
    share_mode = FILE_SHARE_READ;
  } else if (mode & ZLIB_FILEFUNC_MODE_EXISTING) {
    desired_access = GENERIC_WRITE | GENERIC_READ;
    creation_disposition = OPEN_EXISTING;
  } else if (mode & ZLIB_FILEFUNC_MODE_CREATE) {
    desired_access = GENERIC_WRITE | GENERIC_READ;
    creation_disposition = CREATE_ALWAYS;
  }

  std::wstring filename_wstr = UTF8ToWide(filename);
  if ((filename != NULL) && (desired_access != 0)) {
    file = CreateFile(filename_wstr.c_str(), desired_access, share_mode,
        NULL, creation_disposition, flags_and_attributes, NULL);
  }

  if (file == INVALID_HANDLE_VALUE)
    file = NULL;

  if (file != NULL) {
    WIN32FILE_IOWIN file_ret;
    file_ret.hf = file;
    file_ret.error = 0;
    ret = malloc(sizeof(WIN32FILE_IOWIN));
    if (ret == NULL)
      CloseHandle(file);
    else
      *(static_cast<WIN32FILE_IOWIN*>(ret)) = file_ret;
  }
  return ret;
}
#endif

bool Unzip(const FilePath& src_file, const FilePath& dest_dir) {
#if defined(OS_WIN)
  zlib_filefunc_def zip_funcs;
  fill_win32_filefunc(&zip_funcs);
  zip_funcs.zopen_file = ZipOpenFunc;
#endif

#if defined(OS_POSIX)
  std::string src_file_str = src_file.value();
  unzFile zip_file = unzOpen(src_file_str.c_str());
#elif defined(OS_WIN)
  std::string src_file_str = WideToUTF8(src_file.value());
  unzFile zip_file = unzOpen2(src_file_str.c_str(), &zip_funcs);
#endif
  if (!zip_file) {
    LOG(WARNING) << "couldn't create file " << src_file_str;
    return false;
  }
  unz_global_info zip_info;
  int err;
  err = unzGetGlobalInfo(zip_file, &zip_info);
  if (err != UNZ_OK) {
    LOG(WARNING) << "couldn't open zip " << src_file_str;
    return false;
  }
  bool ret = true;
  for (unsigned int i = 0; i < zip_info.number_entry; ++i) {
    if (!ExtractCurrentFile(zip_file, dest_dir)) {
      ret = false;
      break;
    }

    if (i + 1 < zip_info.number_entry) {
      err = unzGoToNextFile(zip_file);
      if (err != UNZ_OK) {
        LOG(WARNING) << "error %d in unzGoToNextFile";
        ret = false;
        break;
      }
    }
  }
  unzClose(zip_file);
  return ret;
}

static bool AddFileToZip(zipFile zip_file, const FilePath& src_dir) {
  net::FileStream stream;
  int flags = base::PLATFORM_FILE_OPEN | base::PLATFORM_FILE_READ;
  if (stream.Open(src_dir, flags) != 0) {
    LOG(ERROR) << "Could not open stream for path "
               << src_dir.value();
    return false;
  }

  int num_bytes;
  char buf[kZipBufSize];
  do {
    num_bytes = stream.Read(buf, kZipBufSize, NULL);
    if (num_bytes > 0) {
      if (ZIP_OK != zipWriteInFileInZip(zip_file, buf, num_bytes)) {
        LOG(ERROR) << "Could not write data to zip for path "
                   << src_dir.value();
        return false;
      }
    }
  } while (num_bytes > 0);

  return true;
}

static bool AddEntryToZip(zipFile zip_file, const FilePath& path,
                          const FilePath& root_path) {
#if defined(OS_WIN)
  std::string str_path =
      WideToUTF8(path.value().substr(root_path.value().length() + 1));
  ReplaceSubstringsAfterOffset(&str_path, 0u, "\\", "/");
#else
  std::string str_path = path.value().substr(root_path.value().length() + 1);
#endif

  bool is_directory = file_util::DirectoryExists(path);
  if (is_directory)
    str_path += "/";

  if (ZIP_OK != zipOpenNewFileInZip(
      zip_file, str_path.c_str(),
      NULL, NULL, 0u, NULL, 0u, NULL,  // file info, extrafield local, length,
                                       // extrafield global, length, comment
      Z_DEFLATED, Z_DEFAULT_COMPRESSION)) {
    LOG(ERROR) << "Could not open zip file entry " << str_path;
    return false;
  }

  bool success = true;
  if (!is_directory) {
    success = AddFileToZip(zip_file, path);
  }

  if (ZIP_OK != zipCloseFileInZip(zip_file)) {
    LOG(ERROR) << "Could not close zip file entry " << str_path;
    return false;
  }

  return success;
}

bool Zip(const FilePath& src_dir, const FilePath& dest_file,
         bool include_hidden_files) {
  DCHECK(file_util::DirectoryExists(src_dir));

#if defined(OS_WIN)
  zlib_filefunc_def zip_funcs;
  fill_win32_filefunc(&zip_funcs);
  zip_funcs.zopen_file = ZipOpenFunc;
#endif

#if defined(OS_POSIX)
  std::string dest_file_str = dest_file.value();
  std::string src_dir_str = src_dir.value();
  zipFile zip_file = zipOpen(dest_file_str.c_str(), APPEND_STATUS_CREATE);
#elif defined(OS_WIN)
  std::string dest_file_str = WideToUTF8(dest_file.value());
  zipFile zip_file = zipOpen2(dest_file_str.c_str(), APPEND_STATUS_CREATE,
                              NULL,  // global comment
                              &zip_funcs);
#endif

  if (!zip_file) {
    LOG(WARNING) << "couldn't create file " << dest_file_str;
    return false;
  }

  bool success = true;
  file_util::FileEnumerator file_enumerator(
      src_dir, true,  // recursive
      static_cast<file_util::FileEnumerator::FILE_TYPE>(
          file_util::FileEnumerator::FILES |
          file_util::FileEnumerator::DIRECTORIES));
  for (FilePath path = file_enumerator.Next(); !path.value().empty();
       path = file_enumerator.Next()) {
    if (!include_hidden_files && path.BaseName().value()[0] == '.')
      continue;

    if (!AddEntryToZip(zip_file, path, src_dir)) {
      success = false;
      return false;
    }
  }

  if (ZIP_OK != zipClose(zip_file, NULL)) {  // global comment
    LOG(ERROR) << "Error closing zip file " << dest_file_str;
    return false;
  }

  return success;
}