/*
    SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
    SPDX-FileCopyrightText: 2020-2021 Harald Sitter <sitter@kde.org>
*/

#include <QCoreApplication>
#include <QScopeGuard>
#include <QUrlQuery>

#include "smbcdiscoverer.h"
#include "udsentry_compat.h"

static QEvent::Type LoopEvent = QEvent::User;

class SMBCServerDiscovery : public SMBCDiscovery
{
public:
    SMBCServerDiscovery(const UDSEntry &entry)
        : SMBCDiscovery(entry)
    {
        KIOExtras::insertStrings(m_entry,
                                 {
                                     {KIO::UDSEntry::UDS_URL, url()},
                                     {KIO::UDSEntry::UDS_MIME_TYPE, QStringLiteral("application/x-smb-server")},
                                     {KIO::UDSEntry::UDS_ICON_NAME, QStringLiteral("network-server")},
                                 });
        KIOExtras::insertNumbers(m_entry,
                                 {
                                     {KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR},
                                     {KIO::UDSEntry::UDS_ACCESS, (S_IRUSR | S_IXUSR | S_IRGRP | S_IXGRP | S_IROTH | S_IXOTH)},
                                 });
    }

    QString url()
    {
        QUrl u("smb://");
        u.setHost(udsName());
        return u.url();
    }
};

class SMBCShareDiscovery : public SMBCDiscovery
{
public:
    SMBCShareDiscovery(const UDSEntry &entry)
        : SMBCDiscovery(entry)
    {
        KIOExtras::insertNumbers(m_entry,
                                 {
                                     {KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR},
                                     {KIO::UDSEntry::UDS_ACCESS, (S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH)},
                                 });
    }
};

class SMBCWorkgroupDiscovery : public SMBCDiscovery
{
public:
    SMBCWorkgroupDiscovery(const UDSEntry &entry)
        : SMBCDiscovery(entry)
    {
        KIOExtras::insertStrings(m_entry,
                                 {
                                     {KIO::UDSEntry::UDS_MIME_TYPE, QStringLiteral("application/x-smb-workgroup")},
                                     {KIO::UDSEntry::UDS_URL, url()},
                                 });
        KIOExtras::insertNumbers(m_entry,
                                 {
                                     {KIO::UDSEntry::UDS_FILE_TYPE, S_IFDIR},
                                     {KIO::UDSEntry::UDS_ACCESS, (S_IRUSR | S_IRGRP | S_IROTH | S_IXUSR | S_IXGRP | S_IXOTH)},
                                 });
    }

    QString url()
    {
        QUrl u("smb://");
        u.setHost(udsName());
        if (!u.isValid()) {
            // In the event that the workgroup contains bad characters, put it in a query instead.
            // This is transparently handled by SMBUrl when we get this as input again.
            // Also see documentation there.
            // https://bugs.kde.org/show_bug.cgi?id=204423
            u.setHost(QString());
            QUrlQuery q;
            q.addQueryItem("kio-workgroup", udsName());
            u.setQuery(q);
        }
        return u.url();
    }
};

class SMBCPrinterDiscovery : public SMBCDiscovery
{
public:
    SMBCPrinterDiscovery(const UDSEntry &entry)
        : SMBCDiscovery(entry)
    {
        // The URL is relative to parent, but need to set it so we can append a marker. Subsequent stat calls
        // wouldn't know that this is a printer because libsmbc doesn't reflect that in the stat output.
        const QString printerUrl = udsName() + QStringLiteral("?kio-printer=true");
        KIOExtras::insertStrings(m_entry,
                                 {
                                     {KIO::UDSEntry::UDS_MIME_TYPE, QStringLiteral("inode/vnd.kde.kio.smb.printer")},
                                     {KIO::UDSEntry::UDS_URL, printerUrl},
                                 });
        m_entry.fastInsert(KIO::UDSEntry::UDS_ACCESS, 0x0);
    }
};

SMBCDiscovery::SMBCDiscovery(const UDSEntry &entry)
    : m_entry(entry)
    // cache the name, it may get accessed more than once
    , m_name(entry.stringValue(KIO::UDSEntry::UDS_NAME))
{
}

QString SMBCDiscovery::udsName() const
{
    return m_name;
}

KIO::UDSEntry SMBCDiscovery::toEntry() const
{
    return m_entry;
}

SMBCDiscoverer::SMBCDiscoverer(const SMBUrl &url, QEventLoop *loop, SMBWorker *worker)
    : m_url(url)
    , m_loop(loop)
    , m_worker(worker)
{
}

SMBCDiscoverer::~SMBCDiscoverer()
{
    if (m_dirFd > 0) {
        smbc_closedir(m_dirFd);
    }
}

void SMBCDiscoverer::start()
{
    queue();
}

bool SMBCDiscoverer::discoverNextFileInfo()
{
#ifdef HAVE_READDIRPLUS2
    // Readdirplus2 dir/file listing. Becomes noop when at end of data associated with dirfd.
    // If readdirplus2 isn't available the regular dirent listing is done.
    // readdirplus2 improves performance by giving us a stat without separate call (Samba>=4.12)
    struct stat st;
    const struct libsmb_file_info *fileInfo = smbc_readdirplus2(m_dirFd, &st);
    if (fileInfo) {
        const QString name = QString::fromUtf8(fileInfo->name);
        qCDebug(KIO_SMB_LOG) << "fileInfo"
                             << "name:" << name;
        if (name == ".") {
            return true;
        } else if (name == "..") {
            m_dirWasRoot = false;
            return true;
        }
        UDSEntry entry;
        // Minimal size. stat will set at least 4 fields.
        KIOExtras::reserveEntry(entry, 3, 2);
        entry.fastInsert(KIO::UDSEntry::UDS_NAME, name);

        m_url.addPath(name);
        m_worker->statToUDSEntry(m_url, st, entry); // won't produce useful error
        Q_EMIT newDiscovery(Discovery::Ptr(new SMBCDiscovery(entry)));
        m_url.cdUp();
        return true;
    }
#endif // HAVE_READDIRPLUS2
    return false;
}

void SMBCDiscoverer::discoverNext()
{
    // Poor man's concurrency. smbc isn't thread safe so we'd hold up other
    // discoverers until we are done. While that will likely happen anyway
    // because smbc_opendir (usually?) blocks until it actually has all
    // the data to loop on, meaning the actual looping after open is fairly
    // fast. Even so, there's benefit in letting other discoverers do
    // their work in the meantime because they may do more atomic
    // requests that are async and can take a while due to network latency.
    // To get somewhat reasonable behavior we simulate an async smbc discovery
    // by posting loop events to the eventloop and each loop run we process
    // a single dirent.
    // This effectively unblocks the eventloop between iterations.
    // Once we are out of entries this discoverer is considered finished.

    // Always queue a new iteration when returning so we don't forget to.
    auto autoQueue = qScopeGuard([this] {
        queue();
    });

    if (m_dirFd == -1) {
        init();
        Q_ASSERT(m_dirFd || m_finished);
        return;
    }

    static bool printersOnly = qEnvironmentVariableIntValue("KIO_SMB_PRINTERS") > 0;

    if (!printersOnly && discoverNextFileInfo()) {
        return;
    }

    qCDebug(KIO_SMB_LOG) << "smbc_readdir ";
    struct smbc_dirent *dirp = smbc_readdir(m_dirFd);
    if (dirp == nullptr) {
        qCDebug(KIO_SMB_LOG) << "done with smbc";
        stop();
        return;
    }

    // We cannot trust dirp->commentlen as it might be with or without the NUL character
    // See KDE bug #111430 and Samba bug #3030.
    // This further complicates things because name is really declared as name[1]
    // which is insofar a problem as in Qt6 QByteArrayView knows to handle this properly
    // for the fromUtf8 conversion resulting in only a single character getting extracted.
    // To mitigate both problems we a) always rely on the terminal NUL detection and
    // b) explicitly decay name to a pointer such that QString has to obey a).
    const QString name = QString::fromUtf8(std::addressof(dirp->name[0]));
    const QString comment = QString::fromUtf8(dirp->comment);

    qCDebug(KIO_SMB_LOG) << "dirent "
                         << "name:" << name << "comment:" << comment << "type:" << dirp->smbc_type;

    UDSEntry entry;
    // Minimal potential size. The actual size depends on this function,
    // possibly the stat function, and lastly the Discovery objects themselves.
    // The smallest will be a ShareDiscovery with 5 fields.
    KIOExtras::reserveEntry(entry, 3, 2);
    KIOExtras::insertStrings(entry,
                             {
                                 {KIO::UDSEntry::UDS_NAME, name},
                                 {KIO::UDSEntry::UDS_COMMENT, comment},
                             });
    // Ensure system shares are marked hidden.
    if (name.endsWith(QLatin1Char('$'))) {
        entry.fastInsert(KIO::UDSEntry::UDS_HIDDEN, 1);
    }

    if (printersOnly) {
        if (dirp->smbc_type == SMBC_PRINTER_SHARE) {
            Q_EMIT newDiscovery(Discovery::Ptr(new SMBCPrinterDiscovery(entry)));
        }
        return;
    }

#if !defined(HAVE_READDIRPLUS2)
    // . and .. are always of the dir type so they are of no consequence outside
    // actual dir listing and that'd be done by readdirplus2 already
    if (name == ".") {
        // Skip the "." entry
        // Mind the way m_currentUrl is handled in the loop
    } else if (name == "..") {
        m_dirWasRoot = false;
    } else if (dirp->smbc_type == SMBC_FILE || dirp->smbc_type == SMBC_DIR) {
        // Set stat information
        m_url.addPath(name);
        const int statErr = m_worker->browse_stat_path(m_url, entry);
        if (statErr != 0) {
            // The entry can disappear in the time span between
            // listing and the stat call. There's nothing we or the user
            // can do about it. Log the incident and move on with listing.
            qCWarning(KIO_SMB_LOG) << "Failed to stat" << m_url << statErr;
        } else {
            Q_EMIT newDiscovery(Discovery::Ptr(new SMBCDiscovery(entry)));
        }
        m_url.cdUp();
    }
#endif // HAVE_READDIRPLUS2

    if (dirp->smbc_type == SMBC_SERVER) {
        Q_EMIT newDiscovery(Discovery::Ptr(new SMBCServerDiscovery(entry)));
    } else if (dirp->smbc_type == SMBC_FILE_SHARE) {
        Q_EMIT newDiscovery(Discovery::Ptr(new SMBCShareDiscovery(entry)));
    } else if (dirp->smbc_type == SMBC_WORKGROUP) {
        Q_EMIT newDiscovery(Discovery::Ptr(new SMBCWorkgroupDiscovery(entry)));
    } else {
        qCDebug(KIO_SMB_LOG) << "SMBC_UNKNOWN :" << name;
    }
}

void SMBCDiscoverer::customEvent(QEvent *event)
{
    if (event->type() == LoopEvent) {
        if (!m_finished) {
            discoverNext();
        }
        return;
    }
    QObject::customEvent(event);
}

void SMBCDiscoverer::stop()
{
    m_finished = true;
    Q_EMIT finished();
}

bool SMBCDiscoverer::isFinished() const
{
    return m_finished;
}

bool SMBCDiscoverer::dirWasRoot() const
{
    return m_dirWasRoot;
}

int SMBCDiscoverer::error() const
{
    return m_error;
}

void SMBCDiscoverer::init()
{
    Q_ASSERT(m_dirFd < 0);

    m_dirFd = smbc_opendir(m_url.toSmbcUrl());
    if (m_dirFd >= 0) {
        m_error = 0;
    } else {
        m_error = errno;
        stop();
    }

    qCDebug(KIO_SMB_LOG) << "open" << m_url.toSmbcUrl() << "url-type:" << m_url.getType() << "dirfd:" << m_dirFd << "errNum:" << m_error;

    return;
}

void SMBCDiscoverer::queue()
{
    if (m_finished) {
        return;
    }

    // Queue low priority events. For server discovery (that is: other discoverers run as well)
    // we want the modern discoverers to be peferred. For other discoveries only
    // SMBC is running on the loop and so the priority has no negative impact.
    QCoreApplication::postEvent(this, new QEvent(LoopEvent), Qt::LowEventPriority);
}

#include "moc_smbcdiscoverer.cpp"
