// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only

#include "qlocalsocket_p.h"
#include <qscopedvaluerollback.h>
#include <qdeadlinetimer.h>

QT_BEGIN_NAMESPACE

using namespace Qt::StringLiterals;

namespace {
struct QSocketPoller
{
    QSocketPoller(const QLocalSocketPrivate &socket);

    qint64 getRemainingTime(const QDeadlineTimer &deadline) const;
    bool poll(const QDeadlineTimer &deadline);

    enum { maxHandles = 2 };
    HANDLE handles[maxHandles];
    DWORD handleCount = 0;
    bool waitForClose = false;
    bool writePending = false;
};

QSocketPoller::QSocketPoller(const QLocalSocketPrivate &socket)
{
    if (socket.pipeWriter && socket.pipeWriter->isWriteOperationActive()) {
        handles[handleCount++] = socket.pipeWriter->syncEvent();
        writePending = true;
    }
    if (socket.pipeReader->isReadOperationActive())
        handles[handleCount++] = socket.pipeReader->syncEvent();
    else
        waitForClose = true;
}

qint64 QSocketPoller::getRemainingTime(const QDeadlineTimer &deadline) const
{
    const qint64 sleepTime = 10;
    qint64 remainingTime = deadline.remainingTime();
    if (waitForClose && (remainingTime > sleepTime || remainingTime == -1))
        return sleepTime;

    return remainingTime;
}

/*!
    \internal

    Waits until new data is available for reading or write operation
    completes. Returns \c true, if we need to check pipe workers;
    otherwise it returns \c false (if an error occurred or the operation
    timed out).

    \note If the read operation is inactive, it succeeds after
    a short wait, allowing the caller to check the state of the socket.
*/
bool QSocketPoller::poll(const QDeadlineTimer &deadline)
{
    Q_ASSERT(handleCount != 0);
    QDeadlineTimer timer(getRemainingTime(deadline));
    DWORD waitRet;

    do {
        waitRet = WaitForMultipleObjectsEx(handleCount, handles, FALSE,
                                           timer.remainingTime(), TRUE);
    } while (waitRet == WAIT_IO_COMPLETION);

    if (waitRet == WAIT_TIMEOUT)
        return waitForClose || !deadline.hasExpired();

    return waitRet - WAIT_OBJECT_0 < handleCount;
}
} // anonymous namespace

void QLocalSocketPrivate::init()
{
    Q_Q(QLocalSocket);
    pipeReader = new QWindowsPipeReader(q);
    QObjectPrivate::connect(pipeReader, &QWindowsPipeReader::readyRead,
                            this, &QLocalSocketPrivate::_q_canRead);
    q->connect(pipeReader, SIGNAL(pipeClosed()), SLOT(_q_pipeClosed()), Qt::QueuedConnection);
    q->connect(pipeReader, SIGNAL(winError(ulong,QString)), SLOT(_q_winError(ulong,QString)));
}

void QLocalSocketPrivate::_q_winError(ulong windowsError, const QString &function)
{
    Q_Q(QLocalSocket);
    QLocalSocket::LocalSocketState currentState = state;

    // If the connectToServer fails due to WaitNamedPipe() time-out, assume ConnectionError
    if (state == QLocalSocket::ConnectingState && windowsError == ERROR_SEM_TIMEOUT)
        windowsError = ERROR_NO_DATA;

    switch (windowsError) {
    case ERROR_PIPE_NOT_CONNECTED:
    case ERROR_BROKEN_PIPE:
    case ERROR_NO_DATA:
        error = QLocalSocket::ConnectionError;
        errorString = QLocalSocket::tr("%1: Connection error").arg(function);
        state = QLocalSocket::UnconnectedState;
        break;
    case ERROR_FILE_NOT_FOUND:
        error = QLocalSocket::ServerNotFoundError;
        errorString = QLocalSocket::tr("%1: Invalid name").arg(function);
        state = QLocalSocket::UnconnectedState;
        break;
    case ERROR_ACCESS_DENIED:
        error = QLocalSocket::SocketAccessError;
        errorString = QLocalSocket::tr("%1: Access denied").arg(function);
        state = QLocalSocket::UnconnectedState;
        break;
    default:
        error = QLocalSocket::UnknownSocketError;
        errorString = QLocalSocket::tr("%1: Unknown error %2").arg(function).arg(windowsError);
#if defined QLOCALSOCKET_DEBUG
        qWarning() << "QLocalSocket error not handled:" << errorString;
#endif
        state = QLocalSocket::UnconnectedState;
    }

    if (currentState != state) {
        emit q->stateChanged(state);
        if (state == QLocalSocket::UnconnectedState && currentState != QLocalSocket::ConnectingState)
            emit q->disconnected();
    }
    emit q->errorOccurred(error);
}

QLocalSocketPrivate::QLocalSocketPrivate() : QIODevicePrivate(),
       handle(INVALID_HANDLE_VALUE),
       pipeWriter(0),
       pipeReader(0),
       error(QLocalSocket::UnknownSocketError),
       state(QLocalSocket::UnconnectedState),
       emittedReadyRead(false),
       emittedBytesWritten(false)
{
}

QLocalSocketPrivate::~QLocalSocketPrivate()
{
    Q_ASSERT(state == QLocalSocket::UnconnectedState);
    Q_ASSERT(handle == INVALID_HANDLE_VALUE);
    Q_ASSERT(pipeWriter == nullptr);
}

void QLocalSocket::connectToServer(OpenMode openMode)
{
    Q_D(QLocalSocket);
    if (state() == ConnectedState || state() == ConnectingState) {
        d->error = OperationError;
        d->errorString = tr("Trying to connect while connection is in progress");
        emit errorOccurred(QLocalSocket::OperationError);
        return;
    }

    d->error = QLocalSocket::UnknownSocketError;
    d->errorString = QString();
    d->state = ConnectingState;
    emit stateChanged(d->state);
    if (d->serverName.isEmpty()) {
        d->error = ServerNotFoundError;
        d->errorString = tr("%1: Invalid name").arg("QLocalSocket::connectToServer"_L1);
        d->state = UnconnectedState;
        emit errorOccurred(d->error);
        emit stateChanged(d->state);
        return;
    }

    const auto pipePath = "\\\\.\\pipe\\"_L1;
    if (d->serverName.startsWith(pipePath))
        d->fullServerName = d->serverName;
    else
        d->fullServerName = pipePath + d->serverName;
    // Try to open a named pipe
    HANDLE localSocket;
    forever {
        DWORD permissions = (openMode & QIODevice::ReadOnly) ? GENERIC_READ : 0;
        permissions |= (openMode & QIODevice::WriteOnly) ? GENERIC_WRITE : 0;
        localSocket = CreateFile(reinterpret_cast<const wchar_t *>(d->fullServerName.utf16()), // pipe name
                                 permissions,
                                 0,              // no sharing
                                 NULL,           // default security attributes
                                 OPEN_EXISTING,  // opens existing pipe
                                 FILE_FLAG_OVERLAPPED,
                                 NULL);          // no template file

        if (localSocket != INVALID_HANDLE_VALUE)
            break;
        DWORD error = GetLastError();
        // It is really an error only if it is not ERROR_PIPE_BUSY
        if (ERROR_PIPE_BUSY != error) {
            break;
        }

        // All pipe instances are busy, so wait until connected or up to 5 seconds.
        if (!WaitNamedPipe((const wchar_t *)d->fullServerName.utf16(), 5000))
            break;
    }

    if (localSocket == INVALID_HANDLE_VALUE) {
        const DWORD winError = GetLastError();
        d->_q_winError(winError, "QLocalSocket::connectToServer"_L1);
        d->fullServerName = QString();
        return;
    }

    // we have a valid handle
    if (setSocketDescriptor(reinterpret_cast<qintptr>(localSocket), ConnectedState, openMode))
        emit connected();
}

static qint64 transformPipeReaderResult(qint64 res)
{
    // QWindowsPipeReader's reading functions return error codes
    // that don't match what we need.
    switch (res) {
    case 0:     // EOF -> transform to error
        return -1;
    case -2:    // EWOULDBLOCK -> no error, just no bytes
        return 0;
    default:
        return res;
    }
}

// This is reading from the buffer
qint64 QLocalSocket::readData(char *data, qint64 maxSize)
{
    Q_D(QLocalSocket);

    if (!maxSize)
        return 0;

    return transformPipeReaderResult(d->pipeReader->read(data, maxSize));
}

qint64 QLocalSocket::readLineData(char *data, qint64 maxSize)
{
    Q_D(QLocalSocket);

    if (!maxSize)
        return 0;

    // QIODevice::readLine() reserves space for the trailing '\0' byte,
    // so we must read 'maxSize + 1' bytes.
    return transformPipeReaderResult(d->pipeReader->readLine(data, maxSize + 1));
}

qint64 QLocalSocket::skipData(qint64 maxSize)
{
    Q_D(QLocalSocket);

    if (!maxSize)
        return 0;

    return transformPipeReaderResult(d->pipeReader->skip(maxSize));
}

qint64 QLocalSocket::writeData(const char *data, qint64 len)
{
    Q_D(QLocalSocket);
    if (!isValid()) {
        d->error = OperationError;
        d->errorString = tr("Socket is not connected");
        return -1;
    }

    if (len == 0)
        return 0;

    if (!d->pipeWriter) {
        d->pipeWriter = new QWindowsPipeWriter(d->handle, this);
        QObjectPrivate::connect(d->pipeWriter, &QWindowsPipeWriter::bytesWritten,
                                d, &QLocalSocketPrivate::_q_bytesWritten);
        QObjectPrivate::connect(d->pipeWriter, &QWindowsPipeWriter::writeFailed,
                                d, &QLocalSocketPrivate::_q_writeFailed);
    }

    if (d->isWriteChunkCached(data, len))
        d->pipeWriter->write(*(d->currentWriteChunk));
    else
        d->pipeWriter->write(data, len);

    return len;
}

void QLocalSocket::abort()
{
    Q_D(QLocalSocket);
    if (d->pipeWriter) {
        delete d->pipeWriter;
        d->pipeWriter = 0;
    }
    close();
}

void QLocalSocketPrivate::_q_canRead()
{
    Q_Q(QLocalSocket);
    if (!emittedReadyRead) {
        QScopedValueRollback<bool> guard(emittedReadyRead, true);
        emit q->readyRead();
    }
}

void QLocalSocketPrivate::_q_pipeClosed()
{
    Q_Q(QLocalSocket);
    if (state == QLocalSocket::UnconnectedState)
        return;

    if (state != QLocalSocket::ClosingState) {
        state = QLocalSocket::ClosingState;
        emit q->stateChanged(state);
        if (state != QLocalSocket::ClosingState)
            return;
    }

    serverName.clear();
    fullServerName.clear();
    pipeReader->stop();
    delete pipeWriter;
    pipeWriter = nullptr;
    if (handle != INVALID_HANDLE_VALUE) {
        DisconnectNamedPipe(handle);
        CloseHandle(handle);
        handle = INVALID_HANDLE_VALUE;
    }

    state = QLocalSocket::UnconnectedState;
    emit q->stateChanged(state);
    emit q->readChannelFinished();
    emit q->disconnected();
}

qint64 QLocalSocket::bytesAvailable() const
{
    Q_D(const QLocalSocket);
    qint64 available = QIODevice::bytesAvailable();
    available += d->pipeReader->bytesAvailable();
    return available;
}

qint64 QLocalSocket::bytesToWrite() const
{
    Q_D(const QLocalSocket);
    return d->pipeWriterBytesToWrite();
}

bool QLocalSocket::canReadLine() const
{
    Q_D(const QLocalSocket);
    return QIODevice::canReadLine() || d->pipeReader->canReadLine();
}

void QLocalSocket::close()
{
    Q_D(QLocalSocket);

    QIODevice::close();
    d->pipeReader->stopAndClear();
    d->serverName = QString();
    d->fullServerName = QString();
    disconnectFromServer();
}

bool QLocalSocket::flush()
{
    Q_D(QLocalSocket);

    return d->pipeWriter && d->pipeWriter->checkForWrite();
}

void QLocalSocket::disconnectFromServer()
{
    Q_D(QLocalSocket);

    if (bytesToWrite() == 0) {
        d->_q_pipeClosed();
    } else if (d->state != QLocalSocket::ClosingState) {
        d->state = QLocalSocket::ClosingState;
        emit stateChanged(d->state);
    }
}

QLocalSocket::LocalSocketError QLocalSocket::error() const
{
    Q_D(const QLocalSocket);
    return d->error;
}

bool QLocalSocket::setSocketDescriptor(qintptr socketDescriptor,
              LocalSocketState socketState, OpenMode openMode)
{
    Q_D(QLocalSocket);
    d->pipeReader->stop();
    d->handle = reinterpret_cast<HANDLE>(socketDescriptor);
    d->state = socketState;
    d->pipeReader->setHandle(d->handle);
    QIODevice::open(openMode);
    emit stateChanged(d->state);
    if (d->state == ConnectedState && openMode.testFlag(QIODevice::ReadOnly))
        d->pipeReader->startAsyncRead();
    return true;
}

qint64 QLocalSocketPrivate::pipeWriterBytesToWrite() const
{
    return pipeWriter ? pipeWriter->bytesToWrite() : qint64(0);
}

void QLocalSocketPrivate::_q_bytesWritten(qint64 bytes)
{
    Q_Q(QLocalSocket);
    if (!emittedBytesWritten) {
        QScopedValueRollback<bool> guard(emittedBytesWritten, true);
        emit q->bytesWritten(bytes);
    }
    if (state == QLocalSocket::ClosingState)
        q->disconnectFromServer();
}

void QLocalSocketPrivate::_q_writeFailed()
{
    Q_Q(QLocalSocket);
    error = QLocalSocket::PeerClosedError;
    errorString = QLocalSocket::tr("Remote closed");
    emit q->errorOccurred(error);

    _q_pipeClosed();
}

qintptr QLocalSocket::socketDescriptor() const
{
    Q_D(const QLocalSocket);
    return reinterpret_cast<qintptr>(d->handle);
}

qint64 QLocalSocket::readBufferSize() const
{
    Q_D(const QLocalSocket);
    return d->pipeReader->maxReadBufferSize();
}

void QLocalSocket::setReadBufferSize(qint64 size)
{
    Q_D(QLocalSocket);
    d->pipeReader->setMaxReadBufferSize(size);
}

bool QLocalSocket::waitForConnected(int msecs)
{
    Q_UNUSED(msecs);
    return (state() == ConnectedState);
}

bool QLocalSocket::waitForDisconnected(int msecs)
{
    Q_D(QLocalSocket);
    if (state() == UnconnectedState) {
        qWarning("QLocalSocket::waitForDisconnected() is not allowed in UnconnectedState");
        return false;
    }

    QDeadlineTimer deadline(msecs);
    bool wasChecked = false;
    while (!d->pipeReader->isPipeClosed()) {
        if (wasChecked && deadline.hasExpired())
            return false;

        QSocketPoller poller(*d);
        // The first parameter of the WaitForMultipleObjectsEx() call cannot
        // be zero. So we have to call SleepEx() here.
        if (!poller.writePending && poller.waitForClose) {
            // Prevent waiting on the first pass, if both the pipe reader
            // and the pipe writer are inactive.
            if (wasChecked)
                SleepEx(poller.getRemainingTime(deadline), TRUE);
        } else if (!poller.poll(deadline)) {
            return false;
        }

        if (d->pipeWriter)
            d->pipeWriter->checkForWrite();

        // When the read buffer is full, the read sequence is not running,
        // so we need to peek the pipe to detect disconnection.
        if (poller.waitForClose && isValid())
            d->pipeReader->checkPipeState();

        d->pipeReader->checkForReadyRead();
        wasChecked = true;
    }
    d->_q_pipeClosed();
    return true;
}

bool QLocalSocket::isValid() const
{
    Q_D(const QLocalSocket);
    return d->handle != INVALID_HANDLE_VALUE;
}

bool QLocalSocket::waitForReadyRead(int msecs)
{
    Q_D(QLocalSocket);

    if (d->state != QLocalSocket::ConnectedState)
        return false;

    QDeadlineTimer deadline(msecs);
    while (!d->pipeReader->isPipeClosed()) {
        QSocketPoller poller(*d);
        if (poller.waitForClose || !poller.poll(deadline))
            return false;

        if (d->pipeWriter)
            d->pipeWriter->checkForWrite();

        if (d->pipeReader->checkForReadyRead())
            return true;
    }
    d->_q_pipeClosed();
    return false;
}

bool QLocalSocket::waitForBytesWritten(int msecs)
{
    Q_D(QLocalSocket);

    if (d->state == QLocalSocket::UnconnectedState)
        return false;

    QDeadlineTimer deadline(msecs);
    bool wasChecked = false;
    while (!d->pipeReader->isPipeClosed()) {
        if (wasChecked && deadline.hasExpired())
            return false;

        QSocketPoller poller(*d);
        if (!poller.writePending || !poller.poll(deadline))
            return false;

        Q_ASSERT(d->pipeWriter);
        if (d->pipeWriter->checkForWrite())
            return true;

        if (poller.waitForClose && isValid())
            d->pipeReader->checkPipeState();

        d->pipeReader->checkForReadyRead();
        wasChecked = true;
    }
    d->_q_pipeClosed();
    return false;
}

QT_END_NAMESPACE
