/* This file is part of the KDE libraries
   SPDX-FileCopyrightText: 2002 Laurence Anderson <l.d.anderson@warwick.ac.uk>

   SPDX-License-Identifier: LGPL-2.0-or-later
*/

#include "kar.h"
#include "karchive_p.h"
#include "loggingcategory.h"

#include <QDebug>
#include <QFile>

#include <limits>

#include "kcompressiondevice.h"
//#include "klimitediodevice_p.h"

// As documented in QByteArray
static constexpr int kMaxQByteArraySize = std::numeric_limits<int>::max() - 32;

////////////////////////////////////////////////////////////////////////
/////////////////////////// KAr ///////////////////////////////////////
////////////////////////////////////////////////////////////////////////

class Q_DECL_HIDDEN KAr::KArPrivate
{
public:
    KArPrivate()
    {
    }
};

KAr::KAr(const QString &filename)
    : KArchive(filename)
    , d(new KArPrivate)
{
}

KAr::KAr(QIODevice *dev)
    : KArchive(dev)
    , d(new KArPrivate)
{
}

KAr::~KAr()
{
    if (isOpen()) {
        close();
    }
    delete d;
}

bool KAr::doPrepareWriting(const QString &, const QString &, const QString &, qint64, mode_t, const QDateTime &, const QDateTime &, const QDateTime &)
{
    setErrorString(tr("Cannot write to AR file"));
    qCWarning(KArchiveLog) << "doPrepareWriting not implemented for KAr";
    return false;
}

bool KAr::doFinishWriting(qint64)
{
    setErrorString(tr("Cannot write to AR file"));
    qCWarning(KArchiveLog) << "doFinishWriting not implemented for KAr";
    return false;
}

bool KAr::doWriteDir(const QString &, const QString &, const QString &, mode_t, const QDateTime &, const QDateTime &, const QDateTime &)
{
    setErrorString(tr("Cannot write to AR file"));
    qCWarning(KArchiveLog) << "doWriteDir not implemented for KAr";
    return false;
}

bool KAr::doWriteSymLink(const QString &, const QString &, const QString &, const QString &, mode_t, const QDateTime &, const QDateTime &, const QDateTime &)
{
    setErrorString(tr("Cannot write to AR file"));
    qCWarning(KArchiveLog) << "doWriteSymLink not implemented for KAr";
    return false;
}

bool KAr::openArchive(QIODevice::OpenMode mode)
{
    // Open archive

    if (mode == QIODevice::WriteOnly) {
        return true;
    }
    if (mode != QIODevice::ReadOnly && mode != QIODevice::ReadWrite) {
        setErrorString(tr("Unsupported mode %1").arg(mode));
        return false;
    }

    QIODevice *dev = device();
    if (!dev) {
        return false;
    }

    QByteArray magic = dev->read(7);
    if (magic != "!<arch>") {
        setErrorString(tr("Invalid main magic"));
        return false;
    }

    QByteArray ar_longnames;
    while (!dev->atEnd()) {
        QByteArray ar_header;
        ar_header.resize(60);

        dev->seek(dev->pos() + (2 - (dev->pos() % 2)) % 2); // Ar headers are padded to byte boundary

        if (dev->read(ar_header.data(), 60) != 60) { // Read ar header
            qCWarning(KArchiveLog) << "Couldn't read header";
            return true; // Probably EOF / trailing junk
        }

        if (!ar_header.endsWith("`\n")) { // Check header magic // krazy:exclude=strings
            setErrorString(tr("Invalid magic"));
            return false;
        }

        QByteArray name = ar_header.mid(0, 16); // Process header
        const int date = ar_header.mid(16, 12).trimmed().toInt();
        // const int uid = ar_header.mid( 28, 6 ).trimmed().toInt();
        // const int gid = ar_header.mid( 34, 6 ).trimmed().toInt();
        const int mode = ar_header.mid(40, 8).trimmed().toInt(nullptr, 8);
        const qint64 size = ar_header.mid(48, 10).trimmed().toInt();
        if (size < 0 || size > kMaxQByteArraySize) {
            setErrorString(tr("Invalid size"));
            return false;
        }

        bool skip_entry = false; // Deal with special entries
        if (name.mid(0, 1) == "/") {
            if (name.mid(1, 1) == "/") { // Longfilename table entry
                ar_longnames.resize(size);
                // Read the table. Note that the QByteArray will contain NUL characters after each entry.
                dev->read(ar_longnames.data(), size);
                skip_entry = true;
                qCDebug(KArchiveLog) << "Read in longnames entry";
            } else if (name.mid(1, 1) == " ") { // Symbol table entry
                qCDebug(KArchiveLog) << "Skipped symbol entry";
                dev->seek(dev->pos() + size);
                skip_entry = true;
            } else { // Longfilename, look it up in the table
                const int ar_longnamesIndex = name.mid(1, 15).trimmed().toInt();
                qCDebug(KArchiveLog) << "Longfilename #" << ar_longnamesIndex;
                if (ar_longnames.isEmpty()) {
                    setErrorString(tr("Invalid longfilename reference"));
                    return false;
                }
                if (ar_longnamesIndex < 0 || ar_longnamesIndex >= ar_longnames.size()) {
                    setErrorString(tr("Invalid longfilename position reference"));
                    return false;
                }
                name = QByteArray(ar_longnames.constData() + ar_longnamesIndex);
                name.truncate(name.indexOf('/'));
            }
        }
        if (skip_entry) {
            continue;
        }

        // Process filename
        name = name.trimmed();
        name.replace('/', QByteArray());
        qCDebug(KArchiveLog) << "Filename: " << name << " Size: " << size;

        KArchiveEntry *entry = new KArchiveFile(this,
                                                QString::fromLocal8Bit(name.constData()),
                                                mode,
                                                KArchivePrivate::time_tToDateTime(date),
                                                rootDir()->user(),
                                                rootDir()->group(),
                                                /*symlink*/ QString(),
                                                dev->pos(),
                                                size);
        rootDir()->addEntry(entry); // Ar files don't support directories, so everything in root

        dev->seek(dev->pos() + size); // Skip contents
    }

    return true;
}

bool KAr::closeArchive()
{
    // Close the archive
    return true;
}

void KAr::virtual_hook(int id, void *data)
{
    KArchive::virtual_hook(id, data);
}
