/***************************************************************************
**
**  This file is part of QGpCoreTools.
**
**  This library 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 2.1 of the License, or (at your option) any later version.
**
**  This file 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 library; if not, write to the Free Software
**  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
**
**  See http://www.geopsy.org for more information.
**
**  Created: 2006-11-10
**  Copyright: 2006-2019
**    Marc Wathelet
**    Marc Wathelet (LGIT, Grenoble, France)
**
***************************************************************************/


#include "Tar.h"
#include "CoreApplication.h"

namespace QGpCoreTools {

/*
  POSIX tar format
*/

struct TarHeader
{                              /* byte offset */
  char name[100];               /*   0 */
  char mode[8];                 /* 100 */
  char uid[8];                  /* 108 */
  char gid[8];                  /* 116 */
  char size[12];                /* 124 */
  char mtime[12];               /* 136 */
  char chksum[8];               /* 148 */
  char typeflag;                /* 156 */
  char linkname[100];           /* 157 */
  char magic[8];                /* 257 */
  char uname[32];               /* 265 */
  char gname[32];               /* 297 */
  char devmajor[8];             /* 329 */
  char devminor[8];             /* 337 */
  char prefix[167];             /* 345 */
                                /* 512 */
};

/*!
  \class Tar Tar.h
  \brief Brief description of class still missing

  Full description of class still missing

  https://en.wikipedia.org/wiki/Tar_(computing)
*/

/*!
  Description of constructor still missing
*/
Tar::Tar()
{
  TRACE;
  _f=0;
  _cachedFiles.setMaxCost(256*1024*1024); // a 256 Mb cache
}

/*!
  Description of destructor still missing
*/
Tar::~Tar()
{
  TRACE;
  close();
}

/*!
  \fn bool Tar::writeBlock(char * buffer)
  Write a block of 512 bytes to the compressed stream.
*/

/*!
  \fn bool Tar::readBlock(char * buffer)
  Read a block of 512 bytes from the compressed stream.
*/

/*!
  Open a compressed gz file or uncompressed (automatically detected by zlib)

  Accepted modes are QIODevice::ReadOnly and QIODevice::WriteOnly.
  When creating the file, it is always compressed, but on reading, it may be compressed or not.
*/
bool Tar::open(QString file, QIODevice::OpenMode m)
{
  TRACE;
  _fileName=file;
  _mode=m;
  switch (_mode) {
  case QIODevice::ReadOnly:
    _f=gzopen(_fileName.toLocal8Bit().data(), "rb");
    return _f;
  case QIODevice::WriteOnly:
    _f=gzopen(_fileName.toLocal8Bit().data(), "wb");
    return _f;
  default:
    App::log(tr("Tar: Unsupported file mode access\n") );
    return false;
  }
}

/*!
  Description of destructor still missing
*/
void Tar::close()
{
  TRACE;
  if(_f) {
    // Terminate the tar file with two blank blocks
    if(_mode==QIODevice::WriteOnly) {
      char buffer[512];
      memset(buffer, 0, 512);  // initialize a blank block
      writeBlock(buffer);
      writeBlock(buffer);
    }
    gzclose(_f);
    _f=0;
  }
}

/*!
  Reset file pointer to the first file
*/
bool Tar::rewind()
{
  TRACE;
  return (gzrewind(_f)==0);
}

/*!
  Extract \a data with \a fileName in tar archive.
  Files are read sequentially without using cache if \a cache is false
*/
bool Tar::nextFile(QString& fileName, QByteArray& data, bool cache)
{
  TRACE;
  z_off_t offset=gztell (_f);
  // read header block
  TarHeader fh;
  if(!readBlock((char *)&fh)) {
    return false;
  }
  fileName=QByteArray(fh.name, 100);
  if(fileName.isEmpty()) { // probably end of tar archive
    return false;
  }
  // Read blocks of data
  if(readFile(fh, data)) {
    if(cache) {
      _cachedFiles.insert(fileName, new QByteArray(data), data.size());
      _fileOffsets.insert(fileName, offset);
    }
    return true;
  } else {
    return false;
  }
}

/*!
  Random access to file in an efficient way with cache to avoid useless and expensive unzip operations
*/
bool Tar::file(const QString fileName, QByteArray& data)
{
  TRACE;
  // Check if file available in cache
  if(_cachedFiles.contains(fileName)) {
    data= *_cachedFiles.object(fileName);
    return true;
  }
  QString readFileName;
  if(_fileOffsets.contains(fileName)) { /* file already read but removed from cache
                                           go directly to right position
                                           this might happen only for very big archives
                                        */
    gzseek(_f, _fileOffsets.value(fileName), SEEK_SET);
    if(nextFile(readFileName, data, true)) {
      return (readFileName==fileName);
    } else {
      return false;
    }
  }
  // search for correct header block, matching fileName
  while(fileName!= readFileName) {
    if(!nextFile(readFileName, data, true)) return false;
  }
  return true;
}

/*!
  Add \a data with \a fileName in tar archive
*/
bool Tar::addFile(QString fileName, const QByteArray& data)
{
  TRACE;
  int nameLen=fileName.count();
  if(nameLen>100) {
    App::log(tr("File names greater than 99 charaters are currently not supported\n") );
    return false;
  }
  if(nameLen<100) nameLen++;
  TarHeader fh;
  memset((char *) &fh, 0, 512);  // initialize a blank header block

  memcpy(fh.name, fileName.toLatin1().data(), nameLen);
  memcpy(fh.mode,"0000644", 8);// default permissions -rw-r--r--
  memcpy(fh.uid,"0001750", 8);  // default user uid 1000
  memcpy(fh.gid,"0000144", 8);  // default group gid 100
  // Maximum size of a QByteArray is less than maximum size of file in tar file 777 7777 7777 in octal > 0x7FFFFFFF
  memcpy(fh.size, int2octal(data.size()), 12);
  // Modification time
  memcpy(fh.mtime, int2octal(QDateTime::currentDateTime().toTime_t()), 12);
  memcpy(fh.chksum,"        ", 8); // blank checksum
  fh.typeflag='0'; // regular file
  // ignore link name
  memcpy(fh.magic,"ustar  ",8); // magic tag
  memcpy(fh.uname,"username",9);
  memcpy(fh.gname,"users",6);
  // Computing checksum
  checksum(fh);
  // max checksum is 0x1FFFF, hence 377777 in octal
  memcpy(fh.chksum, int2octal(checksum(fh))+5, 7);

  // header block is ready to send to stream
  if(!writeBlock((char *) &fh)) return false;

  const char * dataPtr=data.data();
  const char * endDataPtr=dataPtr+data.size()-512;
  while(dataPtr<endDataPtr) {
    if(!writeBlock(dataPtr)) return false;
    dataPtr+=512;
  }
  endDataPtr+=512;
  static char buffer[512];
  memset(buffer, 0, 512);  // initialize a blank block
  memcpy(buffer, dataPtr, endDataPtr-dataPtr);
  if(!writeBlock(buffer)) return false;
  return true;
}

/*!
  Read blocks of file described by \a fh.
*/
bool Tar::readFile(TarHeader& fh, QByteArray& data)
{
  TRACE;
  bool ok;
  // Check header block
  if(strncmp(fh.magic,"ustar",5)!=0) {
    App::log(1, tr("Cannot read file in archive: bad header block, magic!=ustar\n"));
    return false;
  }
  if(fh.typeflag!='0') {
    App::log(1, tr("Cannot read file in archive: bad header block, typeflag!=0\n"));
    return false;
  }
  uint headerChecksum=QByteArray(fh.chksum, 8).trimmed().toUInt(&ok, 8);
  memcpy(fh.chksum,"        ", 8); // blank checksum
  if(!ok || checksum(fh)!=headerChecksum) {
    App::log(1, tr("Cannot read file in archive: bad header block, computed checksum (%1)!=saved checksum (%2)")
                     .arg(checksum(fh)).arg(fh.chksum));
    return false;
  }
  // All checks passed, now read data blocks
  int size=QByteArray(fh.size, 12).trimmed().toInt(&ok, 8);
  if(!ok || size<0) {
    App::log(1, tr("Negative file size in tar archive.\n"));
    return false;
  }
  if(size>256*1024*1024) { // Our software never produce very large files
                           // Prevents corruption while reading file in a web service
    App::log(1, tr("File size must less than 256Mb.\n"));
    return false;
  }
  data.resize(size);
  char * dataPtr=data.data();
  char * endDataPtr=dataPtr+size;
  for(int i=size/512; i>0; i--) {
    if(!readBlock(dataPtr)) return false;
    dataPtr+=512;
  }
  // read last block if any
  if(dataPtr<endDataPtr) {
    char buffer[512];
    if(!readBlock(buffer)) return false;
    memcpy(dataPtr, buffer, endDataPtr-dataPtr);
  }
  return true;
}

uint Tar::checksum(TarHeader& fh)
{
  TRACE;
  uint chksum=0;
  for(int i=0; i<512; i++ ) {
    chksum+=(uint)((uchar *)&fh)[i];
  }
  return chksum;
}

/*!
  Convert \a val into an octal string with 0 left padding on 11 characters.
  The returned pointer must not be deleted.
*/
char * Tar::int2octal(qint64 val)
{
  TRACE;
  static char octStr [12];
  if(val>=0 && val<8LL*1024LL*1024LL*1024LL) {
    sprintf(octStr, "%011llo", val);
  } else {
    App::log(1, tr("Cannot convert %1 to octal with 11 digit\n").arg(val));
    sprintf(octStr, "%011o", 0);
  }
  return octStr;
}

/*!
  Convert \a octStr into an integer
*/
qint64 Tar::octal2int(const char * octStr)
{
  TRACE;
  qint64 val;
  if(octStr[11]=='\0') {
    sscanf(octStr, "%llo", &val);
  } else {
    val=0;
  }
  return val;
}

} // namespace QGpCoreTools
