// XarHandler.cpp

#include "StdAfx.h"

#include "../../../C/CpuArch.h"

#include "../../Common/ComTry.h"
#include "../../Common/MyLinux.h"
#include "../../Common/MyXml.h"
#include "../../Common/StringToInt.h"
#include "../../Common/UTFConvert.h"

#include "../../Windows/PropVariant.h"
#include "../../Windows/TimeUtils.h"

#include "../Common/LimitedStreams.h"
#include "../Common/ProgressUtils.h"
#include "../Common/RegisterArc.h"
#include "../Common/StreamObjects.h"
#include "../Common/StreamUtils.h"

#include "../Compress/BZip2Decoder.h"
#include "../Compress/CopyCoder.h"
#include "../Compress/ZlibDecoder.h"

#include "Common/OutStreamWithSha1.h"

using namespace NWindows;

#define XAR_SHOW_RAW

#define Get16(p) GetBe16(p)
#define Get32(p) GetBe32(p)
#define Get64(p) GetBe64(p)

namespace NArchive {
namespace NXar {

static const size_t kXmlSizeMax = ((size_t )1 << 30) - (1 << 14);
static const size_t kXmlPackSizeMax = kXmlSizeMax;

/*
#define XAR_CKSUM_NONE  0
#define XAR_CKSUM_SHA1  1
#define XAR_CKSUM_MD5   2

static const char * const k_ChecksumAlgos[] =
{
    "None"
  , "SHA-1"
  , "MD5"
};
*/

#define METHOD_NAME_ZLIB "zlib"


struct CFile
{
  AString Name;
  AString Method;
  UInt64 Size;
  UInt64 PackSize;
  UInt64 Offset;
  
  UInt64 CTime;
  UInt64 MTime;
  UInt64 ATime;
  UInt32 Mode;

  AString User;
  AString Group;
  
  bool IsDir;
  bool HasData;
  bool ModeDefined;
  bool Sha1IsDefined;
  // bool packSha1IsDefined;

  Byte Sha1[SHA1_DIGEST_SIZE];
  // Byte packSha1[SHA1_DIGEST_SIZE];

  int Parent;

  CFile(): IsDir(false), HasData(false), ModeDefined(false), Sha1IsDefined(false),
      /* packSha1IsDefined(false), */
      Parent(-1),
      Size(0), PackSize(0), Offset(0),
      CTime(0), MTime(0), ATime(0), Mode(0) {}

  bool IsCopyMethod() const
  {
    return Method.IsEmpty() || Method == "octet-stream";
  }

  void UpdateTotalPackSize(UInt64 &totalSize) const
  {
    UInt64 t = Offset + PackSize;
    if (totalSize < t)
      totalSize = t;
  }
};

class CHandler:
  public IInArchive,
  public IInArchiveGetStream,
  public CMyUnknownImp
{
  UInt64 _dataStartPos;
  CMyComPtr<IInStream> _inStream;
  CByteArr _xml;
  size_t _xmlLen;
  CObjectVector<CFile> _files;
  // UInt32 _checkSumAlgo;
  UInt64 _phySize;
  Int32 _mainSubfile;
  bool _is_pkg;

  HRESULT Open2(IInStream *stream);
  HRESULT Extract(IInStream *stream);
public:
  MY_UNKNOWN_IMP2(IInArchive, IInArchiveGetStream)
  INTERFACE_IInArchive(;)
  STDMETHOD(GetStream)(UInt32 index, ISequentialInStream **stream);
};

static const Byte kArcProps[] =
{
  kpidSubType,
  kpidHeadersSize
};

static const Byte kProps[] =
{
  kpidPath,
  kpidSize,
  kpidPackSize,
  kpidMTime,
  kpidCTime,
  kpidATime,
  kpidPosixAttrib,
  kpidUser,
  kpidGroup,
  kpidMethod
};

IMP_IInArchive_Props
IMP_IInArchive_ArcProps

#define PARSE_NUM(_num_, _dest_) \
    { const char *end; _dest_ = ConvertStringToUInt32(p, &end); \
    if ((unsigned)(end - p) != _num_) return 0; p += _num_ + 1; }

static bool ParseUInt64(const CXmlItem &item, const char *name, UInt64 &res)
{
  const AString s = item.GetSubStringForTag(name);
  if (s.IsEmpty())
    return false;
  const char *end;
  res = ConvertStringToUInt64(s, &end);
  return *end == 0;
}

static UInt64 ParseTime(const CXmlItem &item, const char *name)
{
  const AString s = item.GetSubStringForTag(name);
  if (s.Len() < 20)
    return 0;
  const char *p = s;
  if (p[ 4] != '-' || p[ 7] != '-' || p[10] != 'T' ||
      p[13] != ':' || p[16] != ':' || p[19] != 'Z')
    return 0;
  UInt32 year, month, day, hour, min, sec;
  PARSE_NUM(4, year)
  PARSE_NUM(2, month)
  PARSE_NUM(2, day)
  PARSE_NUM(2, hour)
  PARSE_NUM(2, min)
  PARSE_NUM(2, sec)
  
  UInt64 numSecs;
  if (!NTime::GetSecondsSince1601(year, month, day, hour, min, sec, numSecs))
    return 0;
  return numSecs * 10000000;
}

static int HexToByte(unsigned char c)
{
  if (c >= '0' && c <= '9') return c - '0';
  if (c >= 'A' && c <= 'F') return c - 'A' + 10;
  if (c >= 'a' && c <= 'f') return c - 'a' + 10;
  return -1;
}

static bool ParseSha1(const CXmlItem &item, const char *name, Byte *digest)
{
  int index = item.FindSubTag(name);
  if (index < 0)
    return false;
  const CXmlItem &checkItem = item.SubItems[index];
  const AString style = checkItem.GetPropVal("style");
  if (style == "SHA1")
  {
    const AString s = checkItem.GetSubString();
    if (s.Len() != SHA1_DIGEST_SIZE * 2)
      return false;
    for (unsigned i = 0; i < s.Len(); i += 2)
    {
      int b0 = HexToByte(s[i]);
      int b1 = HexToByte(s[i + 1]);
      if (b0 < 0 || b1 < 0)
        return false;
      digest[i / 2] = (Byte)((b0 << 4) | b1);
    }
    return true;
  }
  return false;
}

static bool AddItem(const CXmlItem &item, CObjectVector<CFile> &files, int parent)
{
  if (!item.IsTag)
    return true;
  if (item.Name == "file")
  {
    CFile file;
    file.Parent = parent;
    parent = files.Size();
    file.Name = item.GetSubStringForTag("name");
    AString type = item.GetSubStringForTag("type");
    if (type == "directory")
      file.IsDir = true;
    else if (type == "file")
      file.IsDir = false;
    else
      return false;

    int dataIndex = item.FindSubTag("data");
    if (dataIndex >= 0 && !file.IsDir)
    {
      file.HasData = true;
      const CXmlItem &dataItem = item.SubItems[dataIndex];
      if (!ParseUInt64(dataItem, "size", file.Size))
        return false;
      if (!ParseUInt64(dataItem, "length", file.PackSize))
        return false;
      if (!ParseUInt64(dataItem, "offset", file.Offset))
        return false;
      file.Sha1IsDefined = ParseSha1(dataItem, "extracted-checksum", file.Sha1);
      // file.packSha1IsDefined = ParseSha1(dataItem, "archived-checksum",  file.packSha1);
      int encodingIndex = dataItem.FindSubTag("encoding");
      if (encodingIndex >= 0)
      {
        const CXmlItem &encodingItem = dataItem.SubItems[encodingIndex];
        if (encodingItem.IsTag)
        {
          AString s = encodingItem.GetPropVal("style");
          if (s.Len() >= 0)
          {
            AString appl = "application/";
            if (s.IsPrefixedBy(appl))
            {
              s.DeleteFrontal(appl.Len());
              AString xx = "x-";
              if (s.IsPrefixedBy(xx))
              {
                s.DeleteFrontal(xx.Len());
                if (s == "gzip")
                  s = METHOD_NAME_ZLIB;
              }
            }
            file.Method = s;
          }
        }
      }
    }

    file.CTime = ParseTime(item, "ctime");
    file.MTime = ParseTime(item, "mtime");
    file.ATime = ParseTime(item, "atime");

    {
      const AString s = item.GetSubStringForTag("mode");
      if (s[0] == '0')
      {
        const char *end;
        file.Mode = ConvertOctStringToUInt32(s, &end);
        file.ModeDefined = (*end == 0);
      }
    }

    file.User = item.GetSubStringForTag("user");
    file.Group = item.GetSubStringForTag("group");

    files.Add(file);
  }
  FOR_VECTOR (i, item.SubItems)
    if (!AddItem(item.SubItems[i], files, parent))
      return false;
  return true;
}

HRESULT CHandler::Open2(IInStream *stream)
{
  const UInt32 kHeaderSize = 0x1C;
  Byte buf[kHeaderSize];
  RINOK(ReadStream_FALSE(stream, buf, kHeaderSize));

  UInt32 size = Get16(buf + 4);
  // UInt32 ver = Get16(buf + 6); // == 1
  if (Get32(buf) != 0x78617221 || size != kHeaderSize)
    return S_FALSE;

  UInt64 packSize = Get64(buf + 8);
  UInt64 unpackSize = Get64(buf + 0x10);

  // _checkSumAlgo = Get32(buf + 0x18);

  if (packSize >= kXmlPackSizeMax ||
      unpackSize >= kXmlSizeMax)
    return S_FALSE;

  _dataStartPos = kHeaderSize + packSize;
  _phySize = _dataStartPos;

  _xml.Alloc((size_t)unpackSize + 1);
  _xmlLen = (size_t)unpackSize;
  
  NCompress::NZlib::CDecoder *zlibCoderSpec = new NCompress::NZlib::CDecoder();
  CMyComPtr<ICompressCoder> zlibCoder = zlibCoderSpec;

  CLimitedSequentialInStream *inStreamLimSpec = new CLimitedSequentialInStream;
  CMyComPtr<ISequentialInStream> inStreamLim(inStreamLimSpec);
  inStreamLimSpec->SetStream(stream);
  inStreamLimSpec->Init(packSize);

  CBufPtrSeqOutStream *outStreamLimSpec = new CBufPtrSeqOutStream;
  CMyComPtr<ISequentialOutStream> outStreamLim(outStreamLimSpec);
  outStreamLimSpec->Init(_xml, (size_t)unpackSize);

  RINOK(zlibCoder->Code(inStreamLim, outStreamLim, NULL, NULL, NULL));

  if (outStreamLimSpec->GetPos() != (size_t)unpackSize)
    return S_FALSE;

  _xml[(size_t)unpackSize] = 0;
  if (strlen((const char *)(const Byte *)_xml) != unpackSize) return S_FALSE;

  CXml xml;
  if (!xml.Parse((const char *)(const Byte *)_xml))
    return S_FALSE;
  
  if (!xml.Root.IsTagged("xar") || xml.Root.SubItems.Size() != 1)
    return S_FALSE;
  const CXmlItem &toc = xml.Root.SubItems[0];
  if (!toc.IsTagged("toc"))
    return S_FALSE;
  if (!AddItem(toc, _files, -1))
    return S_FALSE;

  UInt64 totalPackSize = 0;
  unsigned numMainFiles = 0;
  
  FOR_VECTOR (i, _files)
  {
    const CFile &file = _files[i];
    file.UpdateTotalPackSize(totalPackSize);
    if (file.Name == "Payload")
    {
      _mainSubfile = i;
      numMainFiles++;
    }
    if (file.Name == "PackageInfo")
      _is_pkg = true;
  }

  if (numMainFiles > 1)
    _mainSubfile = -1;
  
  _phySize = _dataStartPos + totalPackSize;

  return S_OK;
}

STDMETHODIMP CHandler::Open(IInStream *stream,
    const UInt64 * /* maxCheckStartPosition */,
    IArchiveOpenCallback * /* openArchiveCallback */)
{
  COM_TRY_BEGIN
  {
    Close();
    if (Open2(stream) != S_OK)
      return S_FALSE;
    _inStream = stream;
  }
  return S_OK;
  COM_TRY_END
}

STDMETHODIMP CHandler::Close()
{
  _phySize = 0;
  _inStream.Release();
  _files.Clear();
  _xmlLen = 0;
  _xml.Free();
  _mainSubfile = -1;
  _is_pkg = false;
  return S_OK;
}

STDMETHODIMP CHandler::GetNumberOfItems(UInt32 *numItems)
{
  *numItems = _files.Size()
    #ifdef XAR_SHOW_RAW
    + 1
    #endif
  ;
  return S_OK;
}

static void TimeToProp(UInt64 t, NCOM::CPropVariant &prop)
{
  if (t != 0)
  {
    FILETIME ft;
    ft.dwLowDateTime = (UInt32)(t);
    ft.dwHighDateTime = (UInt32)(t >> 32);
    prop = ft;
  }
}

static void Utf8StringToProp(const AString &s, NCOM::CPropVariant &prop)
{
  if (!s.IsEmpty())
  {
    UString us;
    if (ConvertUTF8ToUnicode(s, us))
      prop = us;
  }
}

STDMETHODIMP CHandler::GetArchiveProperty(PROPID propID, PROPVARIANT *value)
{
  COM_TRY_BEGIN
  NCOM::CPropVariant prop;
  switch (propID)
  {
    case kpidHeadersSize: prop = _dataStartPos; break;
    case kpidPhySize: prop = _phySize; break;
    case kpidMainSubfile: if (_mainSubfile >= 0) prop = (UInt32)_mainSubfile; break;
    case kpidSubType: if (_is_pkg) prop = "pkg"; break;
    case kpidExtension: prop = _is_pkg ? "pkg" : "xar"; break;
  }
  prop.Detach(value);
  return S_OK;
  COM_TRY_END
}

STDMETHODIMP CHandler::GetProperty(UInt32 index, PROPID propID, PROPVARIANT *value)
{
  COM_TRY_BEGIN
  NCOM::CPropVariant prop;
  
  #ifdef XAR_SHOW_RAW
  if (index == _files.Size())
  {
    switch (propID)
    {
      case kpidPath: prop = "[TOC].xml"; break;
      case kpidSize:
      case kpidPackSize: prop = (UInt64)_xmlLen; break;
    }
  }
  else
  #endif
  {
    const CFile &item = _files[index];
    switch (propID)
    {
      case kpidMethod: Utf8StringToProp(item.Method, prop); break;

      case kpidPath:
      {
        AString path;
        int cur = index;
        do
        {
          const CFile &item2 = _files[cur];
          if (!path.IsEmpty())
            path.InsertAtFront(CHAR_PATH_SEPARATOR);
          if (item2.Name.IsEmpty())
            path.Insert(0, "unknown");
          else
            path.Insert(0, item2.Name);
          cur = item2.Parent;
        }
        while (cur >= 0);

        Utf8StringToProp(path, prop);
        break;
      }
      
      case kpidIsDir: prop = item.IsDir; break;
      case kpidSize: if (!item.IsDir) prop = item.Size; break;
      case kpidPackSize: if (!item.IsDir) prop = item.PackSize; break;
      
      case kpidMTime: TimeToProp(item.MTime, prop); break;
      case kpidCTime: TimeToProp(item.CTime, prop); break;
      case kpidATime: TimeToProp(item.ATime, prop); break;
      case kpidPosixAttrib:
        if (item.ModeDefined)
        {
          UInt32 mode = item.Mode;
          if ((mode & MY_LIN_S_IFMT) == 0)
            mode |= (item.IsDir ? MY_LIN_S_IFDIR : MY_LIN_S_IFREG);
          prop = mode;
        }
        break;
      case kpidUser: Utf8StringToProp(item.User, prop); break;
      case kpidGroup: Utf8StringToProp(item.Group, prop); break;
    }
  }
  prop.Detach(value);
  return S_OK;
  COM_TRY_END
}

STDMETHODIMP CHandler::Extract(const UInt32 *indices, UInt32 numItems,
    Int32 testMode, IArchiveExtractCallback *extractCallback)
{
  COM_TRY_BEGIN
  bool allFilesMode = (numItems == (UInt32)(Int32)-1);
  if (allFilesMode)
    numItems = _files.Size();
  if (numItems == 0)
    return S_OK;
  UInt64 totalSize = 0;
  UInt32 i;
  for (i = 0; i < numItems; i++)
  {
    UInt32 index = (allFilesMode ? i : indices[i]);
    #ifdef XAR_SHOW_RAW
    if (index == _files.Size())
      totalSize += _xmlLen;
    else
    #endif
      totalSize += _files[index].Size;
  }
  extractCallback->SetTotal(totalSize);

  UInt64 currentPackTotal = 0;
  UInt64 currentUnpTotal = 0;
  UInt64 currentPackSize = 0;
  UInt64 currentUnpSize = 0;

  const UInt32 kZeroBufSize = (1 << 14);
  CByteBuffer zeroBuf(kZeroBufSize);
  memset(zeroBuf, 0, kZeroBufSize);
  
  NCompress::CCopyCoder *copyCoderSpec = new NCompress::CCopyCoder();
  CMyComPtr<ICompressCoder> copyCoder = copyCoderSpec;

  NCompress::NZlib::CDecoder *zlibCoderSpec = new NCompress::NZlib::CDecoder();
  CMyComPtr<ICompressCoder> zlibCoder = zlibCoderSpec;
  
  NCompress::NBZip2::CDecoder *bzip2CoderSpec = new NCompress::NBZip2::CDecoder();
  CMyComPtr<ICompressCoder> bzip2Coder = bzip2CoderSpec;

  NCompress::NDeflate::NDecoder::CCOMCoder *deflateCoderSpec = new NCompress::NDeflate::NDecoder::CCOMCoder();
  CMyComPtr<ICompressCoder> deflateCoder = deflateCoderSpec;

  CLocalProgress *lps = new CLocalProgress;
  CMyComPtr<ICompressProgressInfo> progress = lps;
  lps->Init(extractCallback, false);

  CLimitedSequentialInStream *inStreamSpec = new CLimitedSequentialInStream;
  CMyComPtr<ISequentialInStream> inStream(inStreamSpec);
  inStreamSpec->SetStream(_inStream);

  
  CLimitedSequentialOutStream *outStreamLimSpec = new CLimitedSequentialOutStream;
  CMyComPtr<ISequentialOutStream> outStream(outStreamLimSpec);

  COutStreamWithSha1 *outStreamSha1Spec = new COutStreamWithSha1;
  {
    CMyComPtr<ISequentialOutStream> outStreamSha1(outStreamSha1Spec);
    outStreamLimSpec->SetStream(outStreamSha1);
  }

  for (i = 0; i < numItems; i++, currentPackTotal += currentPackSize, currentUnpTotal += currentUnpSize)
  {
    lps->InSize = currentPackTotal;
    lps->OutSize = currentUnpTotal;
    currentPackSize = 0;
    currentUnpSize = 0;
    RINOK(lps->SetCur());
    CMyComPtr<ISequentialOutStream> realOutStream;
    Int32 askMode = testMode ?
        NExtract::NAskMode::kTest :
        NExtract::NAskMode::kExtract;
    UInt32 index = allFilesMode ? i : indices[i];
    RINOK(extractCallback->GetStream(index, &realOutStream, askMode));
    
    if (index < _files.Size())
    {
      const CFile &item = _files[index];
      if (item.IsDir)
      {
        RINOK(extractCallback->PrepareOperation(askMode));
        RINOK(extractCallback->SetOperationResult(NExtract::NOperationResult::kOK));
        continue;
      }
    }

    if (!testMode && !realOutStream)
      continue;
    RINOK(extractCallback->PrepareOperation(askMode));

    outStreamSha1Spec->SetStream(realOutStream);
    realOutStream.Release();

    Int32 opRes = NExtract::NOperationResult::kOK;
    #ifdef XAR_SHOW_RAW
    if (index == _files.Size())
    {
      outStreamSha1Spec->Init(false);
      outStreamLimSpec->Init(_xmlLen);
      RINOK(WriteStream(outStream, _xml, _xmlLen));
      currentPackSize = currentUnpSize = _xmlLen;
    }
    else
    #endif
    {
      const CFile &item = _files[index];
      if (item.HasData)
      {
        currentPackSize = item.PackSize;
        currentUnpSize = item.Size;
        
        RINOK(_inStream->Seek(_dataStartPos + item.Offset, STREAM_SEEK_SET, NULL));
        inStreamSpec->Init(item.PackSize);
        outStreamSha1Spec->Init(item.Sha1IsDefined);
        outStreamLimSpec->Init(item.Size);
        HRESULT res = S_OK;
        
        ICompressCoder *coder = NULL;
        if (item.IsCopyMethod())
          if (item.PackSize == item.Size)
            coder = copyCoder;
          else
            opRes = NExtract::NOperationResult::kUnsupportedMethod;
        else if (item.Method == METHOD_NAME_ZLIB)
          coder = zlibCoder;
        else if (item.Method == "bzip2")
          coder = bzip2Coder;
        else
          opRes = NExtract::NOperationResult::kUnsupportedMethod;
        
        if (coder)
          res = coder->Code(inStream, outStream, NULL, NULL, progress);
        
        if (res != S_OK)
        {
          if (!outStreamLimSpec->IsFinishedOK())
            opRes = NExtract::NOperationResult::kDataError;
          else if (res != S_FALSE)
            return res;
          if (opRes == NExtract::NOperationResult::kOK)
            opRes = NExtract::NOperationResult::kDataError;
        }

        if (opRes == NExtract::NOperationResult::kOK)
        {
          if (outStreamLimSpec->IsFinishedOK() &&
              outStreamSha1Spec->GetSize() == item.Size)
          {
            if (!outStreamLimSpec->IsFinishedOK())
            {
              opRes = NExtract::NOperationResult::kDataError;
            }
            else if (item.Sha1IsDefined)
            {
              Byte digest[SHA1_DIGEST_SIZE];
              outStreamSha1Spec->Final(digest);
              if (memcmp(digest, item.Sha1, SHA1_DIGEST_SIZE) != 0)
                opRes = NExtract::NOperationResult::kCRCError;
            }
          }
          else
            opRes = NExtract::NOperationResult::kDataError;
        }
      }
    }
    outStreamSha1Spec->ReleaseStream();
    RINOK(extractCallback->SetOperationResult(opRes));
  }
  return S_OK;
  COM_TRY_END
}

STDMETHODIMP CHandler::GetStream(UInt32 index, ISequentialInStream **stream)
{
  *stream = NULL;
  COM_TRY_BEGIN
  #ifdef XAR_SHOW_RAW
  if (index == _files.Size())
  {
    Create_BufInStream_WithNewBuffer(_xml, _xmlLen, stream);
    return S_OK;
  }
  else
  #endif
  {
    const CFile &item = _files[index];
    if (item.HasData && item.IsCopyMethod() && item.PackSize == item.Size)
      return CreateLimitedInStream(_inStream, _dataStartPos + item.Offset, item.Size, stream);
  }
  return S_FALSE;
  COM_TRY_END
}

static const Byte k_Signature[] = { 'x', 'a', 'r', '!', 0, 0x1C };

REGISTER_ARC_I(
  "Xar", "xar pkg", 0, 0xE1,
  k_Signature,
  0,
  0,
  NULL)

}}
