// TarOut.cpp

#include "StdAfx.h"

#include "../../../../C/7zCrc.h"

#include "../../../Common/IntToString.h"

#include "../../Common/StreamUtils.h"

#include "TarOut.h"

namespace NArchive {
namespace NTar {

using namespace NFileHeader;

// it's path prefix assigned by 7-Zip to show that file path was cut
#define K_PREFIX_PATH_CUT "@PathCut"

static const UInt32 k_7_oct_digits_Val_Max = ((UInt32)1 << (7 * 3)) - 1;

static void WriteOctal_8(char *s, UInt32 val)
{
  const unsigned kNumDigits = 8 - 1;
  if (val >= ((UInt32)1 << (kNumDigits * 3)))
  {
    val = 0;
    // return false;
  }
  for (unsigned i = 0; i < kNumDigits; i++)
  {
    s[kNumDigits - 1 - i] = (char)('0' + (val & 7));
    val >>= 3;
  }
  // return true;
}

static void WriteBin_64bit(char *s, UInt64 val)
{
  for (unsigned i = 0; i < 8; i++, val <<= 8)
    s[i] = (char)(val >> 56);
}

static void WriteOctal_12(char *s, UInt64 val)
{
  const unsigned kNumDigits = 12 - 1;
  if (val >= ((UInt64)1 << (kNumDigits * 3)))
  {
    // GNU extension;
    s[0] = (char)(Byte)0x80;
    s[1] = s[2] = s[3] = 0;
    WriteBin_64bit(s + 4, val);
    return;
  }
  for (unsigned i = 0; i < kNumDigits; i++)
  {
    s[kNumDigits - 1 - i] = (char)('0' + (val & 7));
    val >>= 3;
  }
}

static void WriteOctal_12_Signed(char *s, Int64 val)
{
  if (val >= 0)
  {
    WriteOctal_12(s, (UInt64)val);
    return;
  }
  s[0] = s[1] = s[2] = s[3] = (char)(Byte)0xFF;
  WriteBin_64bit(s + 4, val);
}

static void CopyString(char *dest, const AString &src, unsigned maxSize)
{
  unsigned len = src.Len();
  if (len == 0)
    return;
  // 21.07: new gnu : we don't require additional 0 character at the end
  // if (len >= maxSize)
  if (len > maxSize)
  {
    len = maxSize;
    /*
    // oldgnu needs 0 character at the end
    len = maxSize - 1;
    dest[len] = 0;
    */
  }
  memcpy(dest, src.Ptr(), len);
}

// #define RETURN_IF_NOT_TRUE(x) { if (!(x)) return E_INVALIDARG; }
#define RETURN_IF_NOT_TRUE(x) { x; }

#define COPY_STRING_CHECK(dest, src, size) \
    CopyString(dest, src, size);   dest += (size);

#define WRITE_OCTAL_8_CHECK(dest, src) \
    RETURN_IF_NOT_TRUE(WriteOctal_8(dest, src))
    

HRESULT COutArchive::WriteHeaderReal(const CItem &item, bool isPax
    // , bool zero_PackSize
    // , bool zero_MTime
    )
{
  /*
    if (isPax) { we don't use Glob_Name and Prefix }
    if (!isPax)
    {
      we use Glob_Name if it's not empty
      we use Prefix    if it's not empty
    }
  */
  char record[kRecordSize];
  memset(record, 0, kRecordSize);
  char *cur = record;

  COPY_STRING_CHECK (cur,
      (!isPax && !Glob_Name.IsEmpty()) ? Glob_Name : item.Name,
      kNameSize);

  WRITE_OCTAL_8_CHECK (cur, item.Mode);  cur += 8; // & k_7_oct_digits_Val_Max
  WRITE_OCTAL_8_CHECK (cur, item.UID);   cur += 8;
  WRITE_OCTAL_8_CHECK (cur, item.GID);   cur += 8;

  WriteOctal_12 (cur, /* zero_PackSize ? 0 : */ item.PackSize); cur += 12;
  WriteOctal_12_Signed (cur, /* zero_MTime ? 0 : */ item.MTime); cur += 12;
  
  // we will use binary init for checksum instead of memset
  // checksum field:
  // memset(cur, ' ', 8);
  cur += 8;

  *cur++ = item.LinkFlag;

  COPY_STRING_CHECK (cur, item.LinkName, kNameSize);

  memcpy(cur, item.Magic, 8);
  cur += 8;

  COPY_STRING_CHECK (cur, item.User, kUserNameSize);
  COPY_STRING_CHECK (cur, item.Group, kGroupNameSize);

  const bool needDevice = (IsPosixMode && !isPax);
  
  if (item.DeviceMajor_Defined)
    WRITE_OCTAL_8_CHECK (cur, item.DeviceMajor)
  else if (needDevice)
    WRITE_OCTAL_8_CHECK (cur, 0)
  cur += 8;
  
  if (item.DeviceMinor_Defined)
    WRITE_OCTAL_8_CHECK (cur, item.DeviceMinor)
  else if (needDevice)
    WRITE_OCTAL_8_CHECK (cur, 0)
  cur += 8;

  if (!isPax && !Prefix.IsEmpty())
  {
    COPY_STRING_CHECK (cur, Prefix, kPrefixSize);
  }

  if (item.Is_Sparse())
  {
    record[482] = (char)(item.SparseBlocks.Size() > 4 ? 1 : 0);
    WriteOctal_12(record + 483, item.Size);
    for (unsigned i = 0; i < item.SparseBlocks.Size() && i < 4; i++)
    {
      const CSparseBlock &sb = item.SparseBlocks[i];
      char *p = record + 386 + 24 * i;
      WriteOctal_12(p, sb.Offset);
      WriteOctal_12(p + 12, sb.Size);
    }
  }

  {
    UInt32 sum = (unsigned)(' ') * 8; // we use binary init
    {
      for (unsigned i = 0; i < kRecordSize; i++)
        sum += (Byte)record[i];
    }
    /* checksum field is formatted differently from the
       other fields: it has [6] digits, a null, then a space. */
    // WRITE_OCTAL_8_CHECK(record + 148, sum);
    const unsigned kNumDigits = 6;
    for (unsigned i = 0; i < kNumDigits; i++)
    {
      record[148 + kNumDigits - 1 - i] = (char)('0' + (sum & 7));
      sum >>= 3;
    }
    // record[148 + 6] = 0; // we need it, if we use memset(' ') init
    record[148 + 7] = ' '; // we need it, if we use binary init
  }

  RINOK(Write_Data(record, kRecordSize));

  if (item.Is_Sparse())
  {
    for (unsigned i = 4; i < item.SparseBlocks.Size();)
    {
      memset(record, 0, kRecordSize);
      for (unsigned t = 0; t < 21 && i < item.SparseBlocks.Size(); t++, i++)
      {
        const CSparseBlock &sb = item.SparseBlocks[i];
        char *p = record + 24 * t;
        WriteOctal_12(p, sb.Offset);
        WriteOctal_12(p + 12, sb.Size);
      }
      record[21 * 24] = (char)(i < item.SparseBlocks.Size() ? 1 : 0);
      RINOK(Write_Data(record, kRecordSize));
    }
  }

  return S_OK;
}


static void AddPaxLine(AString &s, const char *name, const AString &val)
{
  // s.Add_LF(); // for debug
  const unsigned len = 3 + (unsigned)strlen(name) + val.Len();
  AString n;
  for (unsigned numDigits = 1;; numDigits++)
  {
    n.Empty();
    n.Add_UInt32(numDigits + len);
    if (numDigits == n.Len())
      break;
  }
  s += n;
  s.Add_Space();
  s += name;
  s += '=';
  s += val;
  s.Add_LF();
}


static void AddPaxTime(AString &s, const char *name, const CPaxTime &pt,
    const CTimeOptions &options)
{
  unsigned numDigits = pt.NumDigits;
  if (numDigits > options.NumDigitsMax)
    numDigits = options.NumDigitsMax;

  bool needNs = false;
  UInt32 ns = 0;
  if (numDigits != 0)
  {
    ns = pt.Ns;
    // if (ns != 0) before reduction, we show all digits after digits reduction
    needNs = (ns != 0 || options.RemoveZeroMode == k_PaxTimeMode_DontRemoveZero);
    UInt32 d = 1;
    for (unsigned k = numDigits; k < 9; k++)
      d *= 10;
    ns /= d;
    ns *= d;
  }

  AString v;
  {
    Int64 sec = pt.Sec;
    if (pt.Sec < 0)
    {
      sec = -sec;
      v += '-';
      if (ns != 0)
      {
        ns = 1000*1000*1000 - ns;
        sec--;
      }
    }
    v.Add_UInt64(sec);
  }
  
  if (needNs)
  {
    AString d;
    d.Add_UInt32(ns);
    while (d.Len() < 9)
      d.InsertAtFront('0');
    // here we have precision
    while (d.Len() > (unsigned)numDigits)
      d.DeleteBack();
    // GNU TAR reduces '0' digits.
    if (options.RemoveZeroMode == k_PaxTimeMode_RemoveZero_Always)
    while (!d.IsEmpty() && d.Back() == '0')
      d.DeleteBack();

    if (!d.IsEmpty())
    {
      v += '.';
      v += d;
      // v += "1234567009999"; // for debug
      // for (int y = 0; y < 1000; y++) v += '8'; // for debug
    }
  }

  AddPaxLine(s, name, v);
}


static void AddPax_UInt32_ifBig(AString &s, const char *name, const UInt32 &v)
{
  if (v > k_7_oct_digits_Val_Max)
  {
    AString s2;
    s2.Add_UInt32(v);
    AddPaxLine(s, name, s2);
  }
}
  

/* OLD_GNU_TAR: writes name with zero at the end
   NEW_GNU_TAR: can write name filled with all kNameSize characters */

static const unsigned kNameSize_Max =
    kNameSize;     // NEW_GNU_TAR / 7-Zip 21.07
    // kNameSize - 1; // OLD_GNU_TAR / old 7-Zip

#define DOES_NAME_FIT_IN_FIELD(name) ((name).Len() <= kNameSize_Max)


HRESULT COutArchive::WriteHeader(const CItem &item)
{
  Glob_Name.Empty();
  Prefix.Empty();

  unsigned namePos = 0;
  bool needPathCut = false;
  bool allowPrefix = false;

  if (!DOES_NAME_FIT_IN_FIELD(item.Name))
  {
    const char *s = item.Name;
    const char *p = s + item.Name.Len() - 1;
    for (; *p == '/' && p != s; p--)
      {}
    for (; p != s && p[-1] != '/'; p--)
      {}
    namePos = (unsigned)(p - s);
    needPathCut = true;
  }

  if (IsPosixMode)
  {
    AString s;
    
    if (needPathCut)
    {
      const unsigned nameLen = item.Name.Len() - namePos;
      if (   item.LinkFlag >= NLinkFlag::kNormal
          && item.LinkFlag <= NLinkFlag::kDirectory
          && namePos > 1
          && nameLen != 0
          // && IsPrefixAllowed
          && item.IsMagic_Posix_ustar_00())
      {
        /* GNU TAR decoder supports prefix field, only if (magic)
           signature matches 6-bytes "ustar\0".
           so here we use prefix field only in posix mode with posix signature */

        allowPrefix = true;
        // allowPrefix = false; // for debug
        if (namePos <= kPrefixSize + 1 && nameLen <= kNameSize_Max)
        {
          needPathCut = false;
          /* we will set Prefix and Glob_Name later, for such conditions:
             if (!DOES_NAME_FIT_IN_FIELD(item.Name) && !needPathCut) */
        }
      }

      if (needPathCut)
        AddPaxLine(s, "path", item.Name);
    }
    
    // AddPaxLine(s, "testname", AString("testval")); // for debug
    
    if (item.LinkName.Len() > kNameSize_Max)
      AddPaxLine(s, "linkpath", item.LinkName);
    
    const UInt64 kPaxSize_Limit = ((UInt64)1 << 33);
    // const UInt64 kPaxSize_Limit = ((UInt64)1 << 1); // for debug
    // bool zero_PackSize = false;
    if (item.PackSize >= kPaxSize_Limit)
    {
      /* GNU TAR in pax mode sets PackSize = 0 in main record, if pack_size >= 8 GiB
         But old 7-Zip doesn't detect "size" property from pax header.
         So we write real size (>= 8 GiB) to main record in binary format,
         and old 7-Zip can decode size correctly */
      // zero_PackSize = true;
      AString v;
      v.Add_UInt64(item.PackSize);
      AddPaxLine(s, "size", v);
    }

    /* GNU TAR encoder can set "devmajor" / "devminor" attributes,
       but GNU TAR decoder doesn't parse "devmajor" / "devminor" */
    if (item.DeviceMajor_Defined)
      AddPax_UInt32_ifBig(s, "devmajor", item.DeviceMajor);
    if (item.DeviceMinor_Defined)
      AddPax_UInt32_ifBig(s, "devminor", item.DeviceMinor);

    AddPax_UInt32_ifBig(s, "uid", item.UID);
    AddPax_UInt32_ifBig(s, "gid", item.GID);
    
    const UInt64 kPax_MTime_Limit = ((UInt64)1 << 33);
    const bool zero_MTime = (
        item.MTime < 0 ||
        item.MTime >= (Int64)kPax_MTime_Limit);

    const CPaxTime &mtime = item.PaxTimes.MTime;
    if (mtime.IsDefined())
    {
      bool needPax = false;
      if (zero_MTime)
        needPax = true;
      else if (TimeOptions.NumDigitsMax > 0)
        if (mtime.Ns != 0 ||
            (mtime.NumDigits != 0 &&
            TimeOptions.RemoveZeroMode == k_PaxTimeMode_DontRemoveZero))
          needPax = true;
      if (needPax)
        AddPaxTime(s, "mtime", mtime, TimeOptions);
    }
    
    if (item.PaxTimes.ATime.IsDefined())
      AddPaxTime(s, "atime", item.PaxTimes.ATime, TimeOptions);
    if (item.PaxTimes.CTime.IsDefined())
      AddPaxTime(s, "ctime", item.PaxTimes.CTime, TimeOptions);

    if (item.User.Len() > kUserNameSize)
      AddPaxLine(s, "uname", item.User);
    if (item.Group.Len() > kGroupNameSize)
      AddPaxLine(s, "gname", item.Group);

    /*
    // for debug
    AString a ("11"); for (int y = 0; y < (1 << 24); y++) AddPaxLine(s, "temp", a);
    */

    const unsigned paxSize = s.Len();
    if (paxSize != 0)
    {
      CItem mi = item;
      mi.LinkName.Empty();
      // SparseBlocks will be ignored by Is_Sparse()
      // mi.SparseBlocks.Clear();
      //  we use "PaxHeader/*" for compatibility with previous 7-Zip decoder

      // GNU TAR writes empty for these fields;
      mi.User.Empty();
      mi.Group.Empty();
      mi.UID = 0;
      mi.GID = 0;

      mi.DeviceMajor_Defined = false;
      mi.DeviceMinor_Defined = false;

      mi.Name = "PaxHeader/@PaxHeader";
      mi.Mode = 0644; // octal
      if (zero_MTime)
        mi.MTime = 0;
      mi.LinkFlag = NLinkFlag::kPax;
      // mi.LinkFlag = 'Z'; // for debug
      mi.PackSize = paxSize;
      // for (unsigned y = 0; y < 1; y++) { // for debug
      RINOK(WriteHeaderReal(mi, true)); // isPax
      RINOK(Write_Data_And_Residual(s, paxSize));
      // } // for debug
      /*
        we can send (zero_MTime) for compatibility with gnu tar output.
        we can send (zero_MTime = false) for better compatibility with old 7-Zip
      */
      // return WriteHeaderReal(item);
      /*
      false, // isPax
      false, // zero_PackSize
      false); // zero_MTime
      */
    }
  }
  else // !PosixMode
  if (!DOES_NAME_FIT_IN_FIELD(item.Name) ||
      !DOES_NAME_FIT_IN_FIELD(item.LinkName))
  {
    // here we can get all fields from main (item) or create new empty item
    /*
    CItem mi;
    mi.SetDefaultWriteFields();
    */
    CItem mi = item;
    mi.LinkName.Empty();
    // SparseBlocks will be ignored by Is_Sparse()
    // mi.SparseBlocks.Clear();
    mi.Name = kLongLink;
    // mi.Name = "././@BAD_LONG_LINK_TEST"; // for debug
    // 21.07 : we set Mode and MTime props as in GNU TAR:
    mi.Mode = 0644; // octal
    mi.MTime = 0;

    mi.User.Empty();
    mi.Group.Empty();
    /*
      gnu tar sets "root" for such items:
        uid_to_uname (0, &uname);
        gid_to_gname (0, &gname);
    */
    /*
    mi.User = "root";
    mi.Group = "root";
    */
    mi.UID = 0;
    mi.GID = 0;
    mi.DeviceMajor_Defined = false;
    mi.DeviceMinor_Defined = false;

    
    for (unsigned i = 0; i < 2; i++)
    {
      const AString *name;
      // We suppose that GNU TAR also writes item for long link before item for LongName?
      if (i == 0)
      {
        mi.LinkFlag = NLinkFlag::kGnu_LongLink;
        name = &item.LinkName;
      }
      else
      {
        mi.LinkFlag = NLinkFlag::kGnu_LongName;
        name = &item.Name;
      }
      if (DOES_NAME_FIT_IN_FIELD(*name))
        continue;
      // GNU TAR writes null character after NAME to file. We do same here:
      const unsigned nameStreamSize = name->Len() + 1;
      mi.PackSize = nameStreamSize;
      // for (unsigned y = 0; y < 3; y++) { // for debug
      RINOK(WriteHeaderReal(mi));
      RINOK(Write_Data_And_Residual(name->Ptr(), nameStreamSize));
      // }
      
      // for debug
      /*
      const unsigned kSize = (1 << 29) + 16;
      CByteBuffer buf;
      buf.Alloc(kSize);
      memset(buf, 0, kSize);
      memcpy(buf, name->Ptr(), name->Len());
      const unsigned nameStreamSize = kSize;
      mi.PackSize = nameStreamSize;
      // for (unsigned y = 0; y < 3; y++) { // for debug
      RINOK(WriteHeaderReal(mi));
      RINOK(WriteBytes(buf, nameStreamSize));
      RINOK(FillDataResidual(nameStreamSize));
      */
    }
  }

  // bool fals = false; if (fals) // for debug: for bit-to-bit output compatibility with GNU TAR

  if (!DOES_NAME_FIT_IN_FIELD(item.Name))
  {
    const unsigned nameLen = item.Name.Len() - namePos;
    if (!needPathCut)
      Prefix.SetFrom(item.Name, namePos - 1);
    else
    {
      Glob_Name = K_PREFIX_PATH_CUT "/_pc_";
      
      if (namePos == 0)
        Glob_Name += "root";
      else
      {
        Glob_Name += "crc32/";
        char temp[12];
        ConvertUInt32ToHex8Digits(CrcCalc(item.Name, namePos - 1), temp);
        Glob_Name += temp;
      }
      
      if (!allowPrefix || Glob_Name.Len() + 1 + nameLen <= kNameSize_Max)
        Glob_Name.Add_Slash();
      else
      {
        Prefix = Glob_Name;
        Glob_Name.Empty();
      }
    }
    Glob_Name.AddFrom(item.Name.Ptr(namePos), nameLen);
  }

  return WriteHeaderReal(item);
}


HRESULT COutArchive::Write_Data(const void *data, unsigned size)
{
  Pos += size;
  return WriteStream(Stream, data, size);
}

HRESULT COutArchive::Write_AfterDataResidual(UInt64 dataSize)
{
  const unsigned v = ((unsigned)dataSize & (kRecordSize - 1));
  if (v == 0)
    return S_OK;
  const unsigned rem = kRecordSize - v;
  Byte buf[kRecordSize];
  memset(buf, 0, rem);
  return Write_Data(buf, rem);
}


HRESULT COutArchive::Write_Data_And_Residual(const void *data, unsigned size)
{
  RINOK(Write_Data(data, size));
  return Write_AfterDataResidual(size);
}


HRESULT COutArchive::WriteFinishHeader()
{
  Byte record[kRecordSize];
  memset(record, 0, kRecordSize);

  const unsigned kNumFinishRecords = 2;

  /* GNU TAR by default uses --blocking-factor=20 (512 * 20 = 10 KiB)
     we also can use cluster alignment:
  const unsigned numBlocks = (unsigned)(Pos / kRecordSize) + kNumFinishRecords;
  const unsigned kNumClusterBlocks = (1 << 3); // 8 blocks = 4 KiB
  const unsigned numFinishRecords = kNumFinishRecords + ((kNumClusterBlocks - numBlocks) & (kNumClusterBlocks - 1));
  */
  
  for (unsigned i = 0; i < kNumFinishRecords; i++)
  {
    RINOK(Write_Data(record, kRecordSize));
  }
  return S_OK;
}

}}
