/////////////////////////////////////////////////////////////////////////////
// Name:        src/common/tarstrm.cpp
// Purpose:     Streams for Tar files
// Author:      Mike Wetherell
// Copyright:   (c) 2004 Mike Wetherell
// Licence:     wxWindows licence
/////////////////////////////////////////////////////////////////////////////

// For compilers that support precompilation, includes "wx.h".
#include "wx/wxprec.h"


#if wxUSE_TARSTREAM

#include "wx/tarstrm.h"

#ifndef WX_PRECOMP
    #include "wx/intl.h"
    #include "wx/log.h"
    #include "wx/utils.h"
#endif

#include "wx/buffer.h"
#include "wx/datetime.h"
#include "wx/filename.h"
#include "wx/thread.h"

#include <ctype.h>

#ifdef __UNIX__
#include <pwd.h>
#include <grp.h>
#endif

#include <memory>

/////////////////////////////////////////////////////////////////////////////
// constants

enum {
    TAR_NAME,
    TAR_MODE,
    TAR_UID,
    TAR_GID,
    TAR_SIZE,
    TAR_MTIME,
    TAR_CHKSUM,
    TAR_TYPEFLAG,
    TAR_LINKNAME,
    TAR_MAGIC,
    TAR_VERSION,
    TAR_UNAME,
    TAR_GNAME,
    TAR_DEVMAJOR,
    TAR_DEVMINOR,
    TAR_PREFIX,
    TAR_UNUSED,
    TAR_NUMFIELDS
};

static const int TAR_BLOCKSIZE = 512;

// checksum type
enum {
    SUM_UNKNOWN,
    SUM_UNSIGNED,
    SUM_SIGNED
};

// type of input tar
enum {
    TYPE_OLDTAR,    // fields after TAR_LINKNAME are invalid
    TYPE_GNUTAR,    // all fields except TAR_PREFIX are valid
    TYPE_USTAR      // all fields are valid
};

// signatures
static const char *USTAR_MAGIC   = "ustar";
static const char *USTAR_VERSION = "00";
static const char *GNU_MAGIC     = "ustar ";
static const char *GNU_VERION    = " ";

wxIMPLEMENT_DYNAMIC_CLASS(wxTarEntry, wxArchiveEntry);
wxIMPLEMENT_DYNAMIC_CLASS(wxTarClassFactory, wxArchiveClassFactory);


/////////////////////////////////////////////////////////////////////////////
// Class factory

static wxTarClassFactory g_wxTarClassFactory;

wxTarClassFactory::wxTarClassFactory()
{
    if (this == &g_wxTarClassFactory)
        PushFront();
}

const wxChar * const *
wxTarClassFactory::GetProtocols(wxStreamProtocolType type) const
{
    static const wxChar *protocols[] = { wxT("tar"), nullptr };
    static const wxChar *mimetypes[] = { wxT("application/x-tar"), nullptr };
    static const wxChar *fileexts[]  = { wxT(".tar"), nullptr };
    static const wxChar *empty[]     = { nullptr };

    switch (type) {
        case wxSTREAM_PROTOCOL: return protocols;
        case wxSTREAM_MIMETYPE: return mimetypes;
        case wxSTREAM_FILEEXT:  return fileexts;
        default:                return empty;
    }
}


/////////////////////////////////////////////////////////////////////////////
// tar header block

typedef wxFileOffset wxTarNumber;

struct wxTarField { const wxChar *name; int pos; };

class wxTarHeaderBlock
{
public:
    wxTarHeaderBlock()
        { Clear(); }

    void Clear(size_t len = 0) { memset(data, 0, len ? len : sizeof(data)); }

    bool Read(wxInputStream& in);
    bool Write(wxOutputStream& out);
    inline bool WriteField(wxOutputStream& out, int id);

    bool IsAllZeros() const;
    wxUint32 Sum(bool SignedSum = false);
    wxUint32 SumField(int id);

    char *Get(int id) { return data + fields[id].pos + id; }
    static size_t Len(int id) { return fields[id + 1].pos - fields[id].pos; }
    static const wxChar *Name(int id) { return fields[id].name; }
    static size_t Offset(int id) { return fields[id].pos; }

    bool SetOctal(int id, wxTarNumber n);
    wxTarNumber GetOctal(int id);
    bool SetPath(const wxString& name, wxMBConv& conv);

private:
    char data[TAR_BLOCKSIZE + TAR_NUMFIELDS];
    static const wxTarField fields[];
    static void check();
};

// A table giving the field names and offsets in a tar header block
const wxTarField wxTarHeaderBlock::fields[] =
{
    { wxT("name"), 0 },       // 100
    { wxT("mode"), 100 },     // 8
    { wxT("uid"), 108 },      // 8
    { wxT("gid"), 116 },      // 8
    { wxT("size"), 124 },     // 12
    { wxT("mtime"), 136 },    // 12
    { wxT("chksum"), 148 },   // 8
    { wxT("typeflag"), 156 }, // 1
    { wxT("linkname"), 157 }, // 100
    { wxT("magic"), 257 },    // 6
    { wxT("version"), 263 },  // 2
    { wxT("uname"), 265 },    // 32
    { wxT("gname"), 297 },    // 32
    { wxT("devmajor"), 329 }, // 8
    { wxT("devminor"), 337 }, // 8
    { wxT("prefix"), 345 },   // 155
    { wxT("unused"), 500 },   // 12
    { nullptr, TAR_BLOCKSIZE }
};

void wxTarHeaderBlock::check()
{
#if 0
    wxCOMPILE_TIME_ASSERT(
        WXSIZEOF(fields) == TAR_NUMFIELDS + 1,
        Wrong_number_of_elements_in_fields_table
    );
#endif
}

bool wxTarHeaderBlock::IsAllZeros() const
{
    const char *p = data;
    for (size_t i = 0; i < sizeof(data); i++)
        if (p[i])
            return false;
    return true;
}

wxUint32 wxTarHeaderBlock::Sum(bool SignedSum /*=false*/)
{
    // the chksum field itself should be blanks during the calculation
    memset(Get(TAR_CHKSUM), ' ', Len(TAR_CHKSUM));
    const char *p = data;
    wxUint32 n = 0;

    if (SignedSum)
        for (size_t i = 0; i < sizeof(data); i++)
            n += (signed char)p[i];
    else
        for (size_t i = 0; i < sizeof(data); i++)
            n += (unsigned char)p[i];

    return n;
}

wxUint32 wxTarHeaderBlock::SumField(int id)
{
    unsigned char *p = (unsigned char*)Get(id);
    unsigned char *q = p + Len(id);
    wxUint32 n = 0;

    while (p < q)
        n += *p++;

    return n;
}

bool wxTarHeaderBlock::Read(wxInputStream& in)
{
    bool ok = true;

    for (int id = 0; id < TAR_NUMFIELDS && ok; id++)
        ok = in.Read(Get(id), Len(id)).LastRead() == Len(id);

    return ok;
}

bool wxTarHeaderBlock::Write(wxOutputStream& out)
{
    bool ok = true;

    for (int id = 0; id < TAR_NUMFIELDS && ok; id++)
        ok = WriteField(out, id);

    return ok;
}

inline bool wxTarHeaderBlock::WriteField(wxOutputStream& out, int id)
{
    return out.Write(Get(id), Len(id)).LastWrite() == Len(id);
}

wxTarNumber wxTarHeaderBlock::GetOctal(int id)
{
    wxTarNumber n = 0;
    const char *p = Get(id);
    while (*p == ' ')
        p++;
    while (*p >= '0' && *p < '8')
        n = (n << 3) | (*p++ - '0');
    return n;
}

bool wxTarHeaderBlock::SetOctal(int id, wxTarNumber n)
{
    // set an octal field, return true if the number fits
    char *field = Get(id);
    char *p = field + Len(id);
    *--p = 0;
    while (p > field) {
        *--p = char('0' + (n & 7));
        n >>= 3;
    }
    return n == 0;
}

bool wxTarHeaderBlock::SetPath(const wxString& name, wxMBConv& conv)
{
    bool badconv = false;

    wxCharBuffer nameBuf = name.mb_str(conv);

    // if the conversion fails make an approximation
    if (!nameBuf) {
        badconv = true;
        nameBuf = name.ToAscii();
    }

    const char *mbName = nameBuf;

    bool fits;
    bool notGoingToFit = false;
    size_t len = strlen(mbName);
    size_t maxname = Len(TAR_NAME);
    size_t maxprefix = Len(TAR_PREFIX);
    size_t i = 0;
    size_t nexti = 0;

    for (;;) {
        fits = i < maxprefix && len - i <= maxname;

        if (!fits) {
            const char *p = strchr(mbName + i, '/');
            if (p)
                nexti = p - mbName + 1;
            if (!p || nexti - 1 > maxprefix)
                notGoingToFit = true;
        }

        if (fits || notGoingToFit) {
            strncpy(Get(TAR_NAME), mbName + i, maxname);
            if (i > 0)
                strncpy(Get(TAR_PREFIX), mbName, i - 1);
            break;
        }

        i = nexti;
    }

    return fits && !badconv;
}


/////////////////////////////////////////////////////////////////////////////
// Some helpers

static wxFileOffset RoundUpSize(wxFileOffset size, int factor = 1)
{
    wxFileOffset chunk = TAR_BLOCKSIZE * factor;
    return ((size + chunk - 1) / chunk) * chunk;
}

#ifdef __UNIX__

static wxString wxTarUserName(int uid)
{
    struct passwd *ppw;

#ifdef HAVE_GETPWUID_R
#if defined HAVE_SYSCONF && defined _SC_GETPW_R_SIZE_MAX
    long pwsize = sysconf(_SC_GETPW_R_SIZE_MAX);
    size_t bufsize(wxMin(wxMax(1024l, pwsize), 32768l));
#else
    size_t bufsize = 1024;
#endif
    wxCharBuffer buf(bufsize);
    struct passwd pw;

    memset(&pw, 0, sizeof(pw));
    if (getpwuid_r(uid, &pw, buf.data(), bufsize, &ppw) == 0 && pw.pw_name)
        return wxString(pw.pw_name, wxConvLibc);
#else
    if ((ppw = getpwuid(uid)) != nullptr)
        return wxString(ppw->pw_name, wxConvLibc);
#endif
    return _("unknown");
}

static wxString wxTarGroupName(int gid)
{
    struct group *pgr;
#ifdef HAVE_GETGRGID_R
#if defined HAVE_SYSCONF && defined _SC_GETGR_R_SIZE_MAX
    long grsize = sysconf(_SC_GETGR_R_SIZE_MAX);
    size_t bufsize(wxMin(wxMax(1024l, grsize), 32768l));
#else
    size_t bufsize = 1024;
#endif
    wxCharBuffer buf(bufsize);
    struct group gr;

    memset(&gr, 0, sizeof(gr));
    if (getgrgid_r(gid, &gr, buf.data(), bufsize, &pgr) == 0 && gr.gr_name)
        return wxString(gr.gr_name, wxConvLibc);
#else
    if ((pgr = getgrgid(gid)) != nullptr)
        return wxString(pgr->gr_name, wxConvLibc);
#endif
    return _("unknown");
}

#endif  // __UNIX__

// Cache the user and group names since getting them can be expensive,
// get both names and ids at the same time.
//
struct wxTarUser
{
    wxTarUser();
    ~wxTarUser() { delete [] uname; delete [] gname; }

    int uid;
    int gid;

    wxChar *uname;
    wxChar *gname;
};

wxTarUser::wxTarUser()
{
#ifdef __UNIX__
    uid = getuid();
    gid = getgid();
    wxString usr = wxTarUserName(uid);
    wxString grp = wxTarGroupName(gid);
#else
    uid = 0;
    gid = 0;
    wxString usr = wxGetUserId();
    wxString grp = _("unknown");
#endif

    uname = new wxChar[usr.length() + 1];
    wxStrcpy(uname, usr.c_str());

    gname = new wxChar[grp.length() + 1];
    wxStrcpy(gname, grp.c_str());
}

static const wxTarUser& wxGetTarUser()
{
#if wxUSE_THREADS
    static wxCriticalSection cs;
    wxCriticalSectionLocker lock(cs);
#endif
    static wxTarUser tu;
    return tu;
}

// ignore the size field for entry types 3, 4, 5 and 6
//
static inline wxFileOffset GetDataSize(const wxTarEntry& entry)
{
    switch (entry.GetTypeFlag()) {
        case wxTAR_CHRTYPE:
        case wxTAR_BLKTYPE:
        case wxTAR_DIRTYPE:
        case wxTAR_FIFOTYPE:
            return 0;
        default:
            return entry.GetSize();
    }
}


/////////////////////////////////////////////////////////////////////////////
// Tar Entry
// Holds all the meta-data for a file in the tar

wxTarEntry::wxTarEntry(const wxString& name /*=wxEmptyString*/,
                       const wxDateTime& dt /*=wxDateTime::Now()*/,
                       wxFileOffset size    /*=0*/)
  : m_Mode(0644),
    m_IsModeSet(false),
    m_UserId(wxGetTarUser().uid),
    m_GroupId(wxGetTarUser().gid),
    m_Size(size),
    m_Offset(wxInvalidOffset),
    m_ModifyTime(dt),
    m_TypeFlag(wxTAR_REGTYPE),
    m_UserName(wxGetTarUser().uname),
    m_GroupName(wxGetTarUser().gname),
    m_DevMajor(~0),
    m_DevMinor(~0)
{
    if (!name.empty())
        SetName(name);
}

wxTarEntry::~wxTarEntry()
{
}

wxTarEntry::wxTarEntry(const wxTarEntry& e)
  : wxArchiveEntry(),
    m_Name(e.m_Name),
    m_Mode(e.m_Mode),
    m_IsModeSet(e.m_IsModeSet),
    m_UserId(e.m_UserId),
    m_GroupId(e.m_GroupId),
    m_Size(e.m_Size),
    m_Offset(e.m_Offset),
    m_ModifyTime(e.m_ModifyTime),
    m_AccessTime(e.m_AccessTime),
    m_CreateTime(e.m_CreateTime),
    m_TypeFlag(e.m_TypeFlag),
    m_LinkName(e.m_LinkName),
    m_UserName(e.m_UserName),
    m_GroupName(e.m_GroupName),
    m_DevMajor(e.m_DevMajor),
    m_DevMinor(e.m_DevMinor)
{
}

wxTarEntry& wxTarEntry::operator=(const wxTarEntry& e)
{
    if (&e != this) {
        m_Name = e.m_Name;
        m_Mode = e.m_Mode;
        m_IsModeSet = e.m_IsModeSet;
        m_UserId = e.m_UserId;
        m_GroupId = e.m_GroupId;
        m_Size = e.m_Size;
        m_Offset = e.m_Offset;
        m_ModifyTime = e.m_ModifyTime;
        m_AccessTime = e.m_AccessTime;
        m_CreateTime = e.m_CreateTime;
        m_TypeFlag = e.m_TypeFlag;
        m_LinkName = e.m_LinkName;
        m_UserName = e.m_UserName;
        m_GroupName = e.m_GroupName;
        m_DevMajor = e.m_DevMajor;
        m_DevMinor = e.m_DevMinor;
    }
    return *this;
}

wxString wxTarEntry::GetName(wxPathFormat format /*=wxPATH_NATIVE*/) const
{
    bool isDir = IsDir() && !m_Name.empty();

    // optimisations for common (and easy) cases
    switch (wxFileName::GetFormat(format)) {
        case wxPATH_DOS:
        {
            wxString name(isDir ? m_Name + wxT("\\") : m_Name);
            for (size_t i = 0; i < name.length(); i++)
                if (name[i] == wxT('/'))
                    name[i] = wxT('\\');
            return name;
        }

        case wxPATH_UNIX:
            return isDir ? m_Name + wxT("/") : m_Name;

        default:
            ;
    }

    wxFileName fn;

    if (isDir)
        fn.AssignDir(m_Name, wxPATH_UNIX);
    else
        fn.Assign(m_Name, wxPATH_UNIX);

    return fn.GetFullPath(format);
}

void wxTarEntry::SetName(const wxString& name, wxPathFormat format)
{
    bool isDir;
    m_Name = GetInternalName(name, format, &isDir);
    SetIsDir(isDir);
}

// Static - Internally tars and zips use forward slashes for the path
// separator, absolute paths aren't allowed, and directory names have a
// trailing slash.  This function converts a path into this internal format,
// but without a trailing slash for a directory.
//
wxString wxTarEntry::GetInternalName(const wxString& name,
                                     wxPathFormat format /*=wxPATH_NATIVE*/,
                                     bool *pIsDir        /*=nullptr*/)
{
    wxString internal;

    if (wxFileName::GetFormat(format) != wxPATH_UNIX)
        internal = wxFileName(name, format).GetFullPath(wxPATH_UNIX);
    else
        internal = name;

    bool isDir = !internal.empty() && internal.Last() == '/';
    if (pIsDir)
        *pIsDir = isDir;
    if (isDir)
        internal.erase(internal.length() - 1);

    while (!internal.empty() && *internal.begin() == '/')
        internal.erase(0, 1);
    while (!internal.empty() && internal.compare(0, 2, wxT("./")) == 0)
        internal.erase(0, 2);
    if (internal == wxT(".") || internal == wxT(".."))
        internal.clear();

    return internal;
}

bool wxTarEntry::IsDir() const
{
    return m_TypeFlag == wxTAR_DIRTYPE;
}

void wxTarEntry::SetIsDir(bool isDir)
{
    if (isDir)
        m_TypeFlag = wxTAR_DIRTYPE;
    else if (m_TypeFlag == wxTAR_DIRTYPE)
        m_TypeFlag = wxTAR_REGTYPE;
}

void wxTarEntry::SetIsReadOnly(bool isReadOnly)
{
    if (isReadOnly)
        m_Mode &= ~0222;
    else
        m_Mode |= 0200;
}

int wxTarEntry::GetMode() const
{
    if (m_IsModeSet || !IsDir())
        return m_Mode;
    else
        return m_Mode | 0111;

}

void wxTarEntry::SetMode(int mode)
{
    m_Mode = mode & 07777;
    m_IsModeSet = true;
}


/////////////////////////////////////////////////////////////////////////////
// Input stream

wxTarInputStream::wxTarInputStream(wxInputStream& stream,
                                   wxMBConv& conv /*=wxConvLocal*/)
 :  wxArchiveInputStream(stream, conv)
{
    Init();
}

wxTarInputStream::wxTarInputStream(wxInputStream *stream,
                                   wxMBConv& conv /*=wxConvLocal*/)
 :  wxArchiveInputStream(stream, conv)
{
    Init();
}

void wxTarInputStream::Init()
{
    m_pos = wxInvalidOffset;
    m_offset = 0;
    m_size = wxInvalidOffset;
    m_sumType = SUM_UNKNOWN;
    m_tarType = TYPE_USTAR;
    m_hdr = new wxTarHeaderBlock;
    m_HeaderRecs = nullptr;
    m_GlobalHeaderRecs = nullptr;
    m_lasterror = m_parent_i_stream->GetLastError();
}

wxTarInputStream::~wxTarInputStream()
{
    delete m_hdr;
    delete m_HeaderRecs;
    delete m_GlobalHeaderRecs;
}

wxTarEntry *wxTarInputStream::GetNextEntry()
{
    m_lasterror = ReadHeaders();

    if (!IsOk())
        return nullptr;

    std::unique_ptr<wxTarEntry> entry(new wxTarEntry);

    entry->SetMode(GetHeaderNumber(TAR_MODE));
    entry->SetUserId(GetHeaderNumber(TAR_UID));
    entry->SetGroupId(GetHeaderNumber(TAR_UID));
    entry->SetSize(GetHeaderNumber(TAR_SIZE));

    entry->SetOffset(m_offset);

    entry->SetDateTime(GetHeaderDate(wxT("mtime")));
    entry->SetAccessTime(GetHeaderDate(wxT("atime")));
    entry->SetCreateTime(GetHeaderDate(wxT("ctime")));

    entry->SetTypeFlag(*m_hdr->Get(TAR_TYPEFLAG));
    bool isDir = entry->IsDir();

    entry->SetLinkName(GetHeaderString(TAR_LINKNAME));

    if (m_tarType != TYPE_OLDTAR) {
        entry->SetUserName(GetHeaderString(TAR_UNAME));
        entry->SetGroupName(GetHeaderString(TAR_GNAME));

        entry->SetDevMajor(GetHeaderNumber(TAR_DEVMAJOR));
        entry->SetDevMinor(GetHeaderNumber(TAR_DEVMINOR));
    }

    entry->SetName(GetHeaderPath(), wxPATH_UNIX);
    if (isDir)
        entry->SetIsDir();

    if (m_HeaderRecs)
        m_HeaderRecs->clear();

    m_size = GetDataSize(*entry);
    m_pos = 0;

    return entry.release();
}

bool wxTarInputStream::OpenEntry(wxTarEntry& entry)
{
    wxFileOffset offset = entry.GetOffset();

    if (GetLastError() != wxSTREAM_READ_ERROR
            && m_parent_i_stream->IsSeekable()
            && m_parent_i_stream->SeekI(offset) == offset)
    {
        m_offset = offset;
        m_size = GetDataSize(entry);
        m_pos = 0;
        m_lasterror = wxSTREAM_NO_ERROR;
        return true;
    } else {
        m_lasterror = wxSTREAM_READ_ERROR;
        return false;
    }
}

bool wxTarInputStream::OpenEntry(wxArchiveEntry& entry)
{
    wxTarEntry *tarEntry = wxStaticCast(&entry, wxTarEntry);
    return tarEntry ? OpenEntry(*tarEntry) : false;
}

bool wxTarInputStream::CloseEntry()
{
    if (m_lasterror == wxSTREAM_READ_ERROR)
        return false;
    if (!IsOpened())
        return true;

    wxFileOffset size = RoundUpSize(m_size);
    wxFileOffset remainder = size - m_pos;

    if (remainder && m_parent_i_stream->IsSeekable()) {
        wxLogNull nolog;
        if (m_parent_i_stream->SeekI(remainder, wxFromCurrent)
                != wxInvalidOffset)
            remainder = 0;
    }

    if (remainder) {
        const int BUFSIZE = 8192;
        wxCharBuffer buf(BUFSIZE);

        while (remainder > 0 && m_parent_i_stream->IsOk())
            remainder -= m_parent_i_stream->Read(
                    buf.data(), wxMin(BUFSIZE, remainder)).LastRead();
    }

    m_pos = wxInvalidOffset;
    m_offset += size;
    m_lasterror = m_parent_i_stream->GetLastError();

    return IsOk();
}

wxStreamError wxTarInputStream::ReadHeaders()
{
    if (!CloseEntry())
        return wxSTREAM_READ_ERROR;

    bool done = false;

    while (!done) {
        m_hdr->Read(*m_parent_i_stream);
        if (m_parent_i_stream->Eof())
        {
            wxLogError(_("incomplete header block in tar"));
        }
        if (!*m_parent_i_stream)
            return wxSTREAM_READ_ERROR;
        m_offset += TAR_BLOCKSIZE;

        // an all-zero header marks the end of the tar
        if (m_hdr->IsAllZeros())
            return wxSTREAM_EOF;

        // the checksum is supposed to be the unsigned sum of the header bytes,
        // but there have been versions of tar that used the signed sum, so
        // accept that too, but only if used throughout.
        wxUint32 chksum = m_hdr->GetOctal(TAR_CHKSUM);
        bool ok = false;

        if (m_sumType != SUM_SIGNED) {
            ok = chksum == m_hdr->Sum();
            if (m_sumType == SUM_UNKNOWN)
                m_sumType = ok ? SUM_UNSIGNED : SUM_SIGNED;
        }
        if (m_sumType == SUM_SIGNED)
            ok = chksum == m_hdr->Sum(true);
        if (!ok) {
            wxLogError(_("checksum failure reading tar header block"));
            return wxSTREAM_READ_ERROR;
        }

        if (strcmp(m_hdr->Get(TAR_MAGIC), USTAR_MAGIC) == 0)
            m_tarType = TYPE_USTAR;
        else if (strcmp(m_hdr->Get(TAR_MAGIC), GNU_MAGIC) == 0 &&
                 strcmp(m_hdr->Get(TAR_VERSION), GNU_VERION) == 0)
            m_tarType = TYPE_GNUTAR;
        else
            m_tarType = TYPE_OLDTAR;

        if (m_tarType != TYPE_USTAR)
            break;

        switch (*m_hdr->Get(TAR_TYPEFLAG)) {
            case 'g': ReadExtendedHeader(m_GlobalHeaderRecs); break;
            case 'x': ReadExtendedHeader(m_HeaderRecs); break;
            default:  done = true;
        }
    }

    return wxSTREAM_NO_ERROR;
}

wxString wxTarInputStream::GetExtendedHeader(const wxString& key) const
{
    wxTarHeaderRecords::iterator it;

    // look at normal extended header records first
    if (m_HeaderRecs) {
        it = m_HeaderRecs->find(key);
        if (it != m_HeaderRecs->end())
            return it->second;
    }

    // if not found, look at the global header records
    if (m_GlobalHeaderRecs) {
        it = m_GlobalHeaderRecs->find(key);
        if (it != m_GlobalHeaderRecs->end())
            return it->second;
    }

    return wxEmptyString;
}

wxString wxTarInputStream::GetHeaderPath() const
{
    wxString path(GetExtendedHeader(wxS("path")));

    if (!path.empty())
        return path;

    path = wxString(m_hdr->Get(TAR_NAME), GetConv());
    if (m_tarType != TYPE_USTAR)
        return path;

    const char *prefix = m_hdr->Get(TAR_PREFIX);
    return *prefix ? wxString(prefix, GetConv()) + wxT("/") + path : path;
}

wxDateTime wxTarInputStream::GetHeaderDate(const wxString& key) const
{
    wxString value(GetExtendedHeader(key));

    // try extended header, stored as decimal seconds since the epoch
    if (!value.empty()) {
        wxLongLong ll;
        ll.Assign(wxAtof(value) * 1000.0);
        return ll;
    }

    if (key == wxT("mtime"))
        return wxLongLong(m_hdr->GetOctal(TAR_MTIME)) * 1000L;

    return wxDateTime();
}

wxTarNumber wxTarInputStream::GetHeaderNumber(int id) const
{
    wxString value(GetExtendedHeader(m_hdr->Name(id)));

    if (!value.empty()) {
        wxTarNumber n = 0;
        wxString::const_iterator p = value.begin();
        while (p != value.end() && *p == ' ')
            ++p;
        while (isdigit(*p))
            n = n * 10 + (*p++ - '0');
        return n;
    } else {
        return m_hdr->GetOctal(id);
    }
}

wxString wxTarInputStream::GetHeaderString(int id) const
{
    wxString value(GetExtendedHeader(m_hdr->Name(id)));

    if (value.empty())
        value = wxString(m_hdr->Get(id), GetConv());

    return value;
}

// An extended header consists of one or more records, each constructed:
// "%d %s=%s\n", <length>, <keyword>, <value>
// <length> is the byte length, <keyword> and <value> are UTF-8

bool wxTarInputStream::ReadExtendedHeader(wxTarHeaderRecords*& recs)
{
    if (!recs)
        recs = new wxTarHeaderRecords;

    // round length up to a whole number of blocks
    size_t len = m_hdr->GetOctal(TAR_SIZE);
    size_t size = RoundUpSize(len);

    // read in the whole header since it should be small
    wxCharBuffer buf(size);
    size_t lastread = m_parent_i_stream->Read(buf.data(), size).LastRead();
    if (lastread < len)
        len = lastread;
    buf.data()[len] = 0;
    m_offset += lastread;

    size_t recPos, recSize;
    bool ok = true;

    for (recPos = 0; recPos < len && ok; recPos += recSize) {
        char *pRec = buf.data() + recPos;
        char *p = pRec;

        // read the record size (byte count in ascii decimal)
        recSize = 0;
        while (isdigit((unsigned char) *p))
            recSize = recSize * 10 + *p++ - '0';

        // validity checks
        if (recPos + recSize > len)
            break;
        if (recSize < p - pRec + (size_t)3 || *p != ' '
                || pRec[recSize - 1] != '\012') {
            ok = false;
            continue;
        }

        // replace the final '\n' with a nul, to terminate value
        pRec[recSize - 1] = 0;
        // the key is here, following the space
        char *pKey = ++p;

        // look forward for the '=', the value follows
        while (*p && *p != '=')
            p++;
        if (!*p) {
            ok = false;
            continue;
        }
        // replace the '=' with a nul, to terminate the key
        *p++ = 0;

        wxString key = wxString::FromUTF8(pKey);
        wxString value = wxString::FromUTF8(p);

        // an empty value unsets a previously given value
        if (value.empty())
            recs->erase(key);
        else
            (*recs)[key] = value;
    }

    if (!ok || recPos < len || size != lastread) {
        wxLogWarning(_("invalid data in extended tar header"));
        return false;
    }

    return true;
}

wxFileOffset wxTarInputStream::OnSysSeek(wxFileOffset pos, wxSeekMode mode)
{
    if (!IsOpened()) {
        wxLogError(_("tar entry not open"));
        m_lasterror = wxSTREAM_READ_ERROR;
    }
    if (!IsOk())
        return wxInvalidOffset;

    switch (mode) {
        case wxFromStart:   break;
        case wxFromCurrent: pos += m_pos; break;
        case wxFromEnd:     pos += m_size; break;
    }

    if (pos < 0 || m_parent_i_stream->SeekI(m_offset + pos) == wxInvalidOffset)
        return wxInvalidOffset;

    m_pos = pos;
    return m_pos;
}

size_t wxTarInputStream::OnSysRead(void *buffer, size_t size)
{
    if (!IsOpened()) {
        wxLogError(_("tar entry not open"));
        m_lasterror = wxSTREAM_READ_ERROR;
    }
    if (!IsOk() || !size)
        return 0;

    if (m_pos >= m_size)
        size = 0;
    else if (m_pos + size > m_size + (size_t)0)
        size = m_size - m_pos;

    size_t lastread = m_parent_i_stream->Read(buffer, size).LastRead();
    m_pos += lastread;

    if (m_pos >= m_size) {
        m_lasterror = wxSTREAM_EOF;
    } else if (!m_parent_i_stream->IsOk()) {
        // any other error will have been reported by the underlying stream
        if (m_parent_i_stream->Eof())
        {
            wxLogError(_("unexpected end of file"));
        }
        m_lasterror = wxSTREAM_READ_ERROR;
    }

    return lastread;
}


/////////////////////////////////////////////////////////////////////////////
// Output stream

wxTarOutputStream::wxTarOutputStream(wxOutputStream& stream,
                                     wxTarFormat format /*=wxTAR_PAX*/,
                                     wxMBConv& conv     /*=wxConvLocal*/)
 :  wxArchiveOutputStream(stream, conv)
{
    Init(format);
}

wxTarOutputStream::wxTarOutputStream(wxOutputStream *stream,
                                     wxTarFormat format /*=wxTAR_PAX*/,
                                     wxMBConv& conv     /*=wxConvLocal*/)
 :  wxArchiveOutputStream(stream, conv)
{
    Init(format);
}

void wxTarOutputStream::Init(wxTarFormat format)
{
    m_pos = wxInvalidOffset;
    m_maxpos = wxInvalidOffset;
    m_size = wxInvalidOffset;
    m_headpos = wxInvalidOffset;
    m_datapos = wxInvalidOffset;
    m_tarstart = wxInvalidOffset;
    m_tarsize = 0;
    m_pax = format == wxTAR_PAX;
    m_BlockingFactor = m_pax ? 10 : 20;
    m_chksum = 0;
    m_large = false;
    m_hdr = new wxTarHeaderBlock;
    m_hdr2 = nullptr;
    m_extendedHdr = nullptr;
    m_extendedSize = 0;
    m_lasterror = m_parent_o_stream->GetLastError();
    m_endrecWritten = false;
}

wxTarOutputStream::~wxTarOutputStream()
{
    Close();
    delete m_hdr;
    delete m_hdr2;
    delete [] m_extendedHdr;
}

bool wxTarOutputStream::PutNextEntry(wxTarEntry *entry)
{
    std::unique_ptr<wxTarEntry> e(entry);

    if (!CloseEntry())
        return false;

    if (!m_tarsize) {
        wxLogNull nolog;
        m_tarstart = m_parent_o_stream->TellO();
    }

    if (m_tarstart != wxInvalidOffset)
        m_headpos = m_tarstart + m_tarsize;

    if (WriteHeaders(*e)) {
        m_pos = 0;
        m_maxpos = 0;
        m_size = GetDataSize(*e);
        if (m_tarstart != wxInvalidOffset)
            m_datapos = m_tarstart + m_tarsize;

        // types that are not allowed any data
        const char nodata[] = {
            wxTAR_LNKTYPE, wxTAR_SYMTYPE, wxTAR_CHRTYPE, wxTAR_BLKTYPE,
            wxTAR_DIRTYPE, wxTAR_FIFOTYPE, 0
        };
        int typeflag = e->GetTypeFlag();

        // pax does now allow data for wxTAR_LNKTYPE
        if (!m_pax || typeflag != wxTAR_LNKTYPE)
            if (strchr(nodata, typeflag) != nullptr)
                CloseEntry();
    }

    return IsOk();
}

bool wxTarOutputStream::PutNextEntry(const wxString& name,
                                     const wxDateTime& dt,
                                     wxFileOffset size)
{
    return PutNextEntry(new wxTarEntry(name, dt, size));
}

bool wxTarOutputStream::PutNextDirEntry(const wxString& name,
                                        const wxDateTime& dt)
{
    wxTarEntry *entry = new wxTarEntry(name, dt);
    entry->SetIsDir();
    return PutNextEntry(entry);
}

bool wxTarOutputStream::PutNextEntry(wxArchiveEntry *entry)
{
    wxTarEntry *tarEntry = wxStaticCast(entry, wxTarEntry);
    if (!tarEntry)
        delete entry;
    return PutNextEntry(tarEntry);
}

bool wxTarOutputStream::CopyEntry(wxTarEntry *entry,
                                  wxTarInputStream& inputStream)
{
    if (PutNextEntry(entry))
        Write(inputStream);
    return IsOk() && inputStream.Eof();
}

bool wxTarOutputStream::CopyEntry(wxArchiveEntry *entry,
                                  wxArchiveInputStream& inputStream)
{
    if (PutNextEntry(entry))
        Write(inputStream);
    return IsOk() && inputStream.Eof();
}

bool wxTarOutputStream::CloseEntry()
{
    if (!IsOpened())
        return true;

    if (m_pos < m_maxpos) {
        wxASSERT(m_parent_o_stream->IsSeekable());
        m_parent_o_stream->SeekO(m_datapos + m_maxpos);
        m_lasterror = m_parent_o_stream->GetLastError();
        m_pos = m_maxpos;
    }

    if (IsOk()) {
        wxFileOffset size = RoundUpSize(m_pos);
        if (size > m_pos) {
            m_hdr->Clear(size - m_pos);
            m_parent_o_stream->Write(m_hdr, size - m_pos);
            m_lasterror = m_parent_o_stream->GetLastError();
        }
        m_tarsize += size;
    }

    if (IsOk() && m_pos != m_size)
        ModifyHeader();

    m_pos = wxInvalidOffset;
    m_maxpos = wxInvalidOffset;
    m_size = wxInvalidOffset;
    m_headpos = wxInvalidOffset;
    m_datapos = wxInvalidOffset;

    return IsOk();
}

bool wxTarOutputStream::Close()
{
    if (!CloseEntry() || (m_tarsize == 0 && m_endrecWritten))
        return false;

    m_hdr->Clear();
    int count = (RoundUpSize(m_tarsize + 2 * TAR_BLOCKSIZE, m_BlockingFactor)
                    - m_tarsize) / TAR_BLOCKSIZE;
    while (count--)
        m_parent_o_stream->Write(m_hdr, TAR_BLOCKSIZE);

    m_tarsize = 0;
    m_tarstart = wxInvalidOffset;
    m_lasterror = m_parent_o_stream->GetLastError();
    m_endrecWritten = true;
    return IsOk();
}

bool wxTarOutputStream::WriteHeaders(wxTarEntry& entry)
{
    m_hdr->Clear();

    SetHeaderPath(entry.GetName(wxPATH_UNIX));

    SetHeaderNumber(TAR_MODE, entry.GetMode());
    SetHeaderNumber(TAR_UID, entry.GetUserId());
    SetHeaderNumber(TAR_GID, entry.GetGroupId());

    if (entry.GetSize() == wxInvalidOffset)
        entry.SetSize(0);
    m_large = !SetHeaderNumber(TAR_SIZE, entry.GetSize());

    SetHeaderDate(wxT("mtime"), entry.GetDateTime());
    if (entry.GetAccessTime().IsValid())
        SetHeaderDate(wxT("atime"), entry.GetAccessTime());
    if (entry.GetCreateTime().IsValid())
        SetHeaderDate(wxT("ctime"), entry.GetCreateTime());

    *m_hdr->Get(TAR_TYPEFLAG) = char(entry.GetTypeFlag());

    strcpy(m_hdr->Get(TAR_MAGIC), USTAR_MAGIC);
    strcpy(m_hdr->Get(TAR_VERSION), USTAR_VERSION);

    SetHeaderString(TAR_LINKNAME, entry.GetLinkName());
    SetHeaderString(TAR_UNAME, entry.GetUserName());
    SetHeaderString(TAR_GNAME, entry.GetGroupName());

    if (~entry.GetDevMajor())
        SetHeaderNumber(TAR_DEVMAJOR, entry.GetDevMajor());
    if (~entry.GetDevMinor())
        SetHeaderNumber(TAR_DEVMINOR, entry.GetDevMinor());

    m_chksum = m_hdr->Sum();
    m_hdr->SetOctal(TAR_CHKSUM, m_chksum);
    if (!m_large)
        m_chksum -= m_hdr->SumField(TAR_SIZE);

    // The main header is now fully prepared so we know what extended headers
    // (if any) will be needed. Output any extended headers before writing
    // the main header.
    if (m_extendedHdr && *m_extendedHdr) {
        wxASSERT(m_pax);
        // the extended headers are written to the tar as a file entry,
        // so prepare a regular header block for the pseudo-file.
        if (!m_hdr2)
            m_hdr2 = new wxTarHeaderBlock;
        m_hdr2->Clear();

        // an old tar that doesn't understand extended headers will
        // extract it as a file, so give these fields reasonable values
        // so that the user will have access to read and remove it.
        m_hdr2->SetPath(PaxHeaderPath(wxT("%d/PaxHeaders.%p/%f"),
                                      entry.GetName(wxPATH_UNIX)), GetConv());
        m_hdr2->SetOctal(TAR_MODE, 0600);
        strcpy(m_hdr2->Get(TAR_UID), m_hdr->Get(TAR_UID));
        strcpy(m_hdr2->Get(TAR_GID), m_hdr->Get(TAR_GID));
        size_t length = strlen(m_extendedHdr);
        m_hdr2->SetOctal(TAR_SIZE, length);
        strcpy(m_hdr2->Get(TAR_MTIME), m_hdr->Get(TAR_MTIME));
        *m_hdr2->Get(TAR_TYPEFLAG) = 'x';
        strcpy(m_hdr2->Get(TAR_MAGIC), USTAR_MAGIC);
        strcpy(m_hdr2->Get(TAR_VERSION), USTAR_VERSION);
        strcpy(m_hdr2->Get(TAR_UNAME), m_hdr->Get(TAR_UNAME));
        strcpy(m_hdr2->Get(TAR_GNAME), m_hdr->Get(TAR_GNAME));

        m_hdr2->SetOctal(TAR_CHKSUM, m_hdr2->Sum());

        m_hdr2->Write(*m_parent_o_stream);
        m_tarsize += TAR_BLOCKSIZE;

        size_t rounded = RoundUpSize(length);
        memset(m_extendedHdr + length, 0, rounded - length);
        m_parent_o_stream->Write(m_extendedHdr, rounded);
        m_tarsize += rounded;

        *m_extendedHdr = 0;

        // update m_headpos which is used to seek back to fix up the file
        // length if it is not known in advance
        if (m_tarstart != wxInvalidOffset)
            m_headpos = m_tarstart + m_tarsize;
    }

    // if don't have extended headers just report error
    if (!m_badfit.empty()) {
        wxASSERT(!m_pax);
        wxLogWarning(_("%s did not fit the tar header for entry '%s'"),
                     m_badfit.c_str(), entry.GetName().c_str());
        m_badfit.clear();
    }

    m_hdr->Write(*m_parent_o_stream);
    m_tarsize += TAR_BLOCKSIZE;
    m_lasterror = m_parent_o_stream->GetLastError();

    return IsOk();
}

wxString wxTarOutputStream::PaxHeaderPath(const wxString& format,
                                          const wxString& path)
{
    wxString d = path.BeforeLast(wxT('/'));
    wxString f = path.AfterLast(wxT('/'));
    wxString ret;

    if (d.empty())
        d = wxT(".");

    ret.reserve(format.length() + path.length() + 16);

    size_t begin = 0;

    for (;;) {
        size_t end;
        end = format.find('%', begin);
        if (end == wxString::npos || end + 1 >= format.length())
            break;
        ret << format.substr(begin, end - begin);
        switch ( format[end + 1].GetValue() ) {
            case 'd': ret << d; break;
            case 'f': ret << f; break;
            case 'p': ret << wxGetProcessId(); break;
            case '%': ret << wxT("%"); break;
        }
        begin = end + 2;
    }

    ret << format.substr(begin);

    return ret;
}

bool wxTarOutputStream::ModifyHeader()
{
    wxFileOffset originalPos = wxInvalidOffset;
    wxFileOffset sizePos = wxInvalidOffset;

    if (!m_large && m_headpos != wxInvalidOffset
            && m_parent_o_stream->IsSeekable())
    {
        wxLogNull nolog;
        originalPos = m_parent_o_stream->TellO();
        if (originalPos != wxInvalidOffset)
            sizePos =
                m_parent_o_stream->SeekO(m_headpos + m_hdr->Offset(TAR_SIZE));
    }

    if (sizePos == wxInvalidOffset || !m_hdr->SetOctal(TAR_SIZE, m_pos)) {
        wxLogError(_("incorrect size given for tar entry"));
        m_lasterror = wxSTREAM_WRITE_ERROR;
        return false;
    }

    m_chksum += m_hdr->SumField(TAR_SIZE);
    m_hdr->SetOctal(TAR_CHKSUM, m_chksum);
    wxFileOffset sumPos = m_headpos + m_hdr->Offset(TAR_CHKSUM);

    return
        m_hdr->WriteField(*m_parent_o_stream, TAR_SIZE) &&
        m_parent_o_stream->SeekO(sumPos) == sumPos &&
        m_hdr->WriteField(*m_parent_o_stream, TAR_CHKSUM) &&
        m_parent_o_stream->SeekO(originalPos) == originalPos;
}

void wxTarOutputStream::SetHeaderPath(const wxString& name)
{
    if (!m_hdr->SetPath(name, GetConv()) || (m_pax && !name.IsAscii()))
        SetExtendedHeader(wxT("path"), name);
}

bool wxTarOutputStream::SetHeaderNumber(int id, wxTarNumber n)
{
    if (m_hdr->SetOctal(id, n)) {
        return true;
    } else {
        SetExtendedHeader(m_hdr->Name(id), wxLongLong(n).ToString());
        return false;
    }
}

void wxTarOutputStream::SetHeaderString(int id, const wxString& str)
{
    strncpy(m_hdr->Get(id), str.mb_str(GetConv()), m_hdr->Len(id));
    if (str.length() > m_hdr->Len(id))
        SetExtendedHeader(m_hdr->Name(id), str);
}

void wxTarOutputStream::SetHeaderDate(const wxString& key,
                                      const wxDateTime& datetime)
{
    wxLongLong ll = datetime.IsValid() ? datetime.GetValue() : wxLongLong(0);
    wxLongLong secs = ll / 1000L;

    if (key != wxT("mtime")
        || !m_hdr->SetOctal(TAR_MTIME, wxTarNumber(secs.GetValue()))
        || secs <= 0 || secs >= 0x7fffffff)
    {
        wxString str;
        if (ll >= LONG_MIN && ll <= LONG_MAX) {
            str.Printf(wxT("%g"), ll.ToLong() / 1000.0);
        } else {
            str = ll.ToString();
            str.insert(str.end() - 3, '.');
        }
        SetExtendedHeader(key, str);
    }
}

void wxTarOutputStream::SetExtendedHeader(const wxString& key,
                                          const wxString& value)
{
    if (m_pax) {
        const wxCharBuffer utf_key = key.utf8_str();
        const wxCharBuffer utf_value = value.utf8_str();

        // a small buffer to format the length field in
        char buf[32];
        // length of "99<space><key>=<value>\n"
        unsigned long length = strlen(utf_value) + strlen(utf_key) + 5;
        sprintf(buf, "%lu", length);
        // the length includes itself
        size_t lenlen = strlen(buf);
        if (lenlen != 2) {
            length += lenlen - 2;
            sprintf(buf, "%lu", length);
            if (strlen(buf) > lenlen)
                sprintf(buf, "%lu", ++length);
        }

        // reallocate m_extendedHdr if it's not big enough
        if (m_extendedSize < length) {
            size_t rounded = RoundUpSize(length);
            m_extendedSize <<= 1;
            if (rounded > m_extendedSize)
                m_extendedSize = rounded;
            char *oldHdr = m_extendedHdr;
            m_extendedHdr = new char[m_extendedSize];
            if (oldHdr) {
                strcpy(m_extendedHdr, oldHdr);
                delete [] oldHdr;
            } else {
                *m_extendedHdr = 0;
            }
        }

        // append the new record
        char *append = strchr(m_extendedHdr, 0);
        sprintf(append, "%s %s=%s\012", buf,
                (const char*)utf_key, (const char*)utf_value);
    }
    else {
        // if not pax then make a list of fields to report as errors
        if (!m_badfit.empty())
            m_badfit += wxT(", ");
        m_badfit += key;
    }
}

void wxTarOutputStream::Sync()
{
    m_parent_o_stream->Sync();
}

wxFileOffset wxTarOutputStream::OnSysSeek(wxFileOffset pos, wxSeekMode mode)
{
    if (!IsOpened()) {
        wxLogError(_("tar entry not open"));
        m_lasterror = wxSTREAM_WRITE_ERROR;
    }
    if (!IsOk() || m_datapos == wxInvalidOffset)
        return wxInvalidOffset;

    switch (mode) {
        case wxFromStart:   break;
        case wxFromCurrent: pos += m_pos; break;
        case wxFromEnd:     pos += m_maxpos; break;
    }

    if (pos < 0 || m_parent_o_stream->SeekO(m_datapos + pos) == wxInvalidOffset)
        return wxInvalidOffset;

    m_pos = pos;
    return m_pos;
}

size_t wxTarOutputStream::OnSysWrite(const void *buffer, size_t size)
{
    if (!IsOpened()) {
        wxLogError(_("tar entry not open"));
        m_lasterror = wxSTREAM_WRITE_ERROR;
    }
    if (!IsOk() || !size)
        return 0;

    size_t lastwrite = m_parent_o_stream->Write(buffer, size).LastWrite();
    m_pos += lastwrite;
    if (m_pos > m_maxpos)
        m_maxpos = m_pos;

    if (lastwrite != size)
        m_lasterror = wxSTREAM_WRITE_ERROR;

    return lastwrite;
}

#endif // wxUSE_TARSTREAM
