/*
 * Copyright (C) 2004-2025 ZNC, see the NOTICE file for details.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <znc/FileUtils.h>
#include <znc/ExecSock.h>
#include <znc/Utils.h>
#include <fcntl.h>
#include <pwd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <time.h>

#ifndef HAVE_LSTAT
#define lstat(a, b) stat(a, b)
#endif

#ifndef O_BINARY
#define O_BINARY 0
#endif

CString CFile::m_sHomePath;

CFile::CFile() : CFile("") {}

CFile::CFile(const CString& sLongName)
    : m_sBuffer(""),
      m_iFD(-1),
      m_bHadError(false),
      m_sLongName(""),
      m_sShortName("") {
    SetFileName(sLongName);
}

CFile::~CFile() { Close(); }

void CFile::SetFileName(const CString& sLongName) {
    if (sLongName.StartsWith("~/")) {
        m_sLongName = CFile::GetHomePath() + sLongName.substr(1);
    } else
        m_sLongName = sLongName;

    m_sShortName = sLongName;
    m_sShortName.TrimRight("/");

    CString::size_type uPos = m_sShortName.rfind('/');
    if (uPos != CString::npos) {
        m_sShortName = m_sShortName.substr(uPos + 1);
    }
}

bool CFile::IsDir(const CString& sLongName, bool bUseLstat) {
    if (sLongName.Equals("/"))
        return CFile::FType(sLongName, FT_DIRECTORY, bUseLstat);

    // Some OS don't like trailing slashes for directories
    return CFile::FType(sLongName.TrimRight_n("/"), FT_DIRECTORY, bUseLstat);
}

bool CFile::IsReg(const CString& sLongName, bool bUseLstat) {
    return CFile::FType(sLongName, FT_REGULAR, bUseLstat);
}
bool CFile::IsChr(const CString& sLongName, bool bUseLstat) {
    return CFile::FType(sLongName, FT_CHARACTER, bUseLstat);
}
bool CFile::IsBlk(const CString& sLongName, bool bUseLstat) {
    return CFile::FType(sLongName, FT_BLOCK, bUseLstat);
}
bool CFile::IsFifo(const CString& sLongName, bool bUseLstat) {
    return CFile::FType(sLongName, FT_FIFO, bUseLstat);
}
bool CFile::IsLnk(const CString& sLongName, bool bUseLstat) {
    return CFile::FType(sLongName, FT_LINK, bUseLstat);
}
bool CFile::IsSock(const CString& sLongName, bool bUseLstat) {
    return CFile::FType(sLongName, FT_SOCK, bUseLstat);
}

bool CFile::IsReg(bool bUseLstat) const {
    return CFile::IsReg(m_sLongName, bUseLstat);
}
bool CFile::IsDir(bool bUseLstat) const {
    return CFile::IsDir(m_sLongName, bUseLstat);
}
bool CFile::IsChr(bool bUseLstat) const {
    return CFile::IsChr(m_sLongName, bUseLstat);
}
bool CFile::IsBlk(bool bUseLstat) const {
    return CFile::IsBlk(m_sLongName, bUseLstat);
}
bool CFile::IsFifo(bool bUseLstat) const {
    return CFile::IsFifo(m_sLongName, bUseLstat);
}
bool CFile::IsLnk(bool bUseLstat) const {
    return CFile::IsLnk(m_sLongName, bUseLstat);
}
bool CFile::IsSock(bool bUseLstat) const {
    return CFile::IsSock(m_sLongName, bUseLstat);
}

// for gettin file types, using fstat instead
bool CFile::FType(const CString& sFileName, EFileTypes eType, bool bUseLstat) {
    struct stat st;

    if (!bUseLstat) {
        if (stat(sFileName.c_str(), &st) != 0) {
            return false;
        }
    } else {
        if (lstat(sFileName.c_str(), &st) != 0) {
            return false;
        }
    }

    switch (eType) {
        case FT_REGULAR:
            return S_ISREG(st.st_mode);
        case FT_DIRECTORY:
            return S_ISDIR(st.st_mode);
        case FT_CHARACTER:
            return S_ISCHR(st.st_mode);
        case FT_BLOCK:
            return S_ISBLK(st.st_mode);
        case FT_FIFO:
            return S_ISFIFO(st.st_mode);
        case FT_LINK:
            return S_ISLNK(st.st_mode);
        case FT_SOCK:
            return S_ISSOCK(st.st_mode);
        default:
            break;
    }
    return false;
}

//
// Functions to retrieve file information
//
bool CFile::Exists() const { return CFile::Exists(m_sLongName); }
off_t CFile::GetSize() const { return CFile::GetSize(m_sLongName); }
time_t CFile::GetATime() const { return CFile::GetATime(m_sLongName); }
time_t CFile::GetMTime() const { return CFile::GetMTime(m_sLongName); }
time_t CFile::GetCTime() const { return CFile::GetCTime(m_sLongName); }
uid_t CFile::GetUID() const { return CFile::GetUID(m_sLongName); }
gid_t CFile::GetGID() const { return CFile::GetGID(m_sLongName); }
bool CFile::Exists(const CString& sFile) {
    struct stat st;
    return (stat(sFile.c_str(), &st) == 0);
}

off_t CFile::GetSize(const CString& sFile) {
    struct stat st;
    if (stat(sFile.c_str(), &st) != 0) {
        return 0;
    }

    return (S_ISREG(st.st_mode)) ? st.st_size : 0;
}

time_t CFile::GetATime(const CString& sFile) {
    struct stat st;
    return (stat(sFile.c_str(), &st) != 0) ? 0 : st.st_atime;
}

time_t CFile::GetMTime(const CString& sFile) {
    struct stat st;
    return (stat(sFile.c_str(), &st) != 0) ? 0 : st.st_mtime;
}

time_t CFile::GetCTime(const CString& sFile) {
    struct stat st;
    return (stat(sFile.c_str(), &st) != 0) ? 0 : st.st_ctime;
}

uid_t CFile::GetUID(const CString& sFile) {
    struct stat st;
    return (stat(sFile.c_str(), &st) != 0) ? -1 : (int)st.st_uid;
}

gid_t CFile::GetGID(const CString& sFile) {
    struct stat st;
    return (stat(sFile.c_str(), &st) != 0) ? -1 : (int)st.st_gid;
}
int CFile::GetInfo(const CString& sFile, struct stat& st) {
    return stat(sFile.c_str(), &st);
}

//
// Functions to manipulate the file on the filesystem
//
bool CFile::Delete() {
    if (CFile::Delete(m_sLongName)) return true;
    m_bHadError = true;
    return false;
}

bool CFile::Move(const CString& sNewFileName, bool bOverwrite) {
    if (CFile::Move(m_sLongName, sNewFileName, bOverwrite)) return true;
    m_bHadError = true;
    return false;
}

bool CFile::Copy(const CString& sNewFileName, bool bOverwrite) {
    if (CFile::Copy(m_sLongName, sNewFileName, bOverwrite)) return true;
    m_bHadError = true;
    return false;
}

bool CFile::Delete(const CString& sFileName) {
    return (unlink(sFileName.c_str()) == 0) ? true : false;
}

bool CFile::Move(const CString& sOldFileName, const CString& sNewFileName,
                 bool bOverwrite) {
    if (CFile::Exists(sNewFileName)) {
        if (!bOverwrite) {
            errno = EEXIST;
            return false;
        }
#ifdef _WIN32
        // rename() never overwrites files on Windows.
        DWORD dFlags = MOVEFILE_WRITE_THROUGH | MOVEFILE_COPY_ALLOWED |
                       MOVEFILE_REPLACE_EXISTING;
        return (::MoveFileExA(sOldFileName.c_str(), sNewFileName.c_str(),
                              dFlags) != 0);
#endif
    }

    return (rename(sOldFileName.c_str(), sNewFileName.c_str()) == 0);
}

bool CFile::Copy(const CString& sOldFileName, const CString& sNewFileName,
                 bool bOverwrite) {
    if ((!bOverwrite) && (CFile::Exists(sNewFileName))) {
        errno = EEXIST;
        return false;
    }

    CFile OldFile(sOldFileName);
    CFile NewFile(sNewFileName);

    if (!OldFile.Open()) {
        return false;
    }

    if (!NewFile.Open(O_WRONLY | O_CREAT | O_TRUNC)) {
        return false;
    }

    char szBuf[8192];
    ssize_t len = 0;

    while ((len = OldFile.Read(szBuf, 8192))) {
        if (len < 0) {
            DEBUG("CFile::Copy() failed: " << strerror(errno));
            OldFile.Close();

            // That file is only a partial copy, get rid of it
            NewFile.Close();
            NewFile.Delete();

            return false;
        }
        NewFile.Write(szBuf, len);
    }

    OldFile.Close();
    NewFile.Close();

    struct stat st;
    GetInfo(sOldFileName, st);
    Chmod(sNewFileName, st.st_mode);

    return true;
}

bool CFile::Chmod(mode_t mode) {
    if (m_iFD == -1) {
        errno = EBADF;
        return false;
    }
    if (fchmod(m_iFD, mode) != 0) {
        m_bHadError = true;
        return false;
    }
    return true;
}

bool CFile::Chmod(const CString& sFile, mode_t mode) {
    return (chmod(sFile.c_str(), mode) == 0);
}

bool CFile::Seek(off_t uPos) {
    /* This sets errno in case m_iFD == -1 */
    errno = EBADF;

    if (m_iFD != -1 && lseek(m_iFD, uPos, SEEK_SET) == uPos) {
        ClearBuffer();
        return true;
    }
    m_bHadError = true;

    return false;
}

bool CFile::Truncate() {
    /* This sets errno in case m_iFD == -1 */
    errno = EBADF;

    if (m_iFD != -1 && ftruncate(m_iFD, 0) == 0) {
        ClearBuffer();
        return true;
    }

    m_bHadError = true;

    return false;
}

bool CFile::Sync() {
    /* This sets errno in case m_iFD == -1 */
    errno = EBADF;

    if (m_iFD != -1 && fsync(m_iFD) == 0) return true;
    m_bHadError = true;
    return false;
}

bool CFile::Open(const CString& sFileName, int iFlags, mode_t iMode) {
    SetFileName(sFileName);
    return Open(iFlags, iMode);
}

bool CFile::Open(int iFlags, mode_t iMode) {
    if (m_iFD != -1) {
        errno = EEXIST;
        m_bHadError = true;
        return false;
    }

    // We never want to get a controlling TTY through this -> O_NOCTTY
    iFlags |= O_NOCTTY;

    // Some weird OS from MS needs O_BINARY or else it generates fake EOFs
    // when reading ^Z from a file.
    iFlags |= O_BINARY;

    m_iFD = open(m_sLongName.c_str(), iFlags, iMode);
    if (m_iFD < 0) {
        m_bHadError = true;
        return false;
    }

    /* Make sure this FD isn't given to childs */
    SetFdCloseOnExec(m_iFD);

    return true;
}

ssize_t CFile::Read(char* pszBuffer, int iBytes) {
    if (m_iFD == -1) {
        errno = EBADF;
        return -1;
    }

    ssize_t res = read(m_iFD, pszBuffer, iBytes);
    if (res != iBytes) m_bHadError = true;
    return res;
}

bool CFile::ReadLine(CString& sData, const CString& sDelimiter) {
    char buff[4096];
    ssize_t iBytes;

    if (m_iFD == -1) {
        errno = EBADF;
        return false;
    }

    do {
        CString::size_type iFind = m_sBuffer.find(sDelimiter);
        if (iFind != CString::npos) {
            // We found a line, return it
            sData = m_sBuffer.substr(0, iFind + sDelimiter.length());
            m_sBuffer.erase(0, iFind + sDelimiter.length());
            return true;
        }

        iBytes = read(m_iFD, buff, sizeof(buff));

        if (iBytes > 0) {
            m_sBuffer.append(buff, iBytes);
        }
    } while (iBytes > 0);

    // We are at the end of the file or an error happened

    if (!m_sBuffer.empty()) {
        // ..but there is still some partial line in the buffer
        sData = m_sBuffer;
        m_sBuffer.clear();
        return true;
    }

    // Nothing left for reading :(
    return false;
}

bool CFile::ReadFile(CString& sData, size_t iMaxSize) {
    char buff[4096];
    size_t iBytesRead = 0;

    sData.clear();

    while (iBytesRead < iMaxSize) {
        ssize_t iBytes = Read(buff, sizeof(buff));

        if (iBytes < 0)
            // Error
            return false;

        if (iBytes == 0)
            // EOF
            return true;

        sData.append(buff, iBytes);
        iBytesRead += iBytes;
    }

    // Buffer limit reached
    return false;
}

ssize_t CFile::Write(const char* pszBuffer, size_t iBytes) {
    if (m_iFD == -1) {
        errno = EBADF;
        return -1;
    }

    ssize_t res = write(m_iFD, pszBuffer, iBytes);
    if (-1 == res) m_bHadError = true;
    return res;
}

ssize_t CFile::Write(const CString& sData) {
    return Write(sData.data(), sData.size());
}
void CFile::Close() {
    if (m_iFD >= 0) {
        if (close(m_iFD) < 0) {
            m_bHadError = true;
            DEBUG("CFile::Close(): close() failed with [" << strerror(errno)
                                                          << "]");
        }
    }
    m_iFD = -1;
    ClearBuffer();
}
void CFile::ClearBuffer() { m_sBuffer.clear(); }

bool CFile::TryExLock(const CString& sLockFile, int iFlags) {
    return Open(sLockFile, iFlags) && TryExLock();
}

bool CFile::TryExLock() { return Lock(F_WRLCK, false); }

bool CFile::ExLock() { return Lock(F_WRLCK, true); }

bool CFile::UnLock() { return Lock(F_UNLCK, true); }

bool CFile::Lock(short iType, bool bBlocking) {
    struct flock fl;

    if (m_iFD == -1) {
        return false;
    }

    fl.l_type = iType;
    fl.l_whence = SEEK_SET;
    fl.l_start = 0;
    fl.l_len = 0;
    return (fcntl(m_iFD, (bBlocking ? F_SETLKW : F_SETLK), &fl) != -1);
}

bool CFile::IsOpen() const { return (m_iFD != -1); }
CString CFile::GetLongName() const { return m_sLongName; }
CString CFile::GetShortName() const { return m_sShortName; }
CString CFile::GetDir() const {
    CString sDir(m_sLongName);

    while (!sDir.empty() && !sDir.EndsWith("/") && !sDir.EndsWith("\\")) {
        sDir.RightChomp();
    }

    return sDir;
}

void CFile::InitHomePath(const CString& sFallback) {
    const char* home = getenv("HOME");

    m_sHomePath.clear();
    if (home) {
        m_sHomePath = home;
    }

    if (m_sHomePath.empty()) {
        const struct passwd* pUserInfo = getpwuid(getuid());

        if (pUserInfo) {
            m_sHomePath = pUserInfo->pw_dir;
        }
    }

    if (m_sHomePath.empty()) {
        m_sHomePath = sFallback;
    }
}

CString CDir::ChangeDir(const CString& sPath, const CString& sAdd,
                        const CString& sHome) {
    CString sHomeDir(sHome);

    if (sHomeDir.empty()) {
        sHomeDir = CFile::GetHomePath();
    }

    if (sAdd == "~") {
        return sHomeDir;
    }

    CString sAddDir(sAdd);

    if (sAddDir.StartsWith("~/")) {
        sAddDir.LeftChomp();
        sAddDir = sHomeDir + sAddDir;
    }

    CString sRet = ((sAddDir.size()) && (sAddDir[0] == '/')) ? "" : sPath;
    sAddDir += "/";
    CString sCurDir;

    sRet.TrimSuffix("/");

    for (unsigned int a = 0; a < sAddDir.size(); a++) {
        switch (sAddDir[a]) {
            case '/':
                if (sCurDir == "..") {
                    sRet = sRet.substr(0, sRet.rfind('/'));
                } else if ((sCurDir != "") && (sCurDir != ".")) {
                    sRet += "/" + sCurDir;
                }

                sCurDir = "";
                break;
            default:
                sCurDir += sAddDir[a];
                break;
        }
    }

    return (sRet.empty()) ? "/" : sRet;
}

CString CDir::CheckPathPrefix(const CString& sPath, const CString& sAdd,
                              const CString& sHomeDir) {
    CString sPrefix = sPath.Replace_n("//", "/").TrimRight_n("/") + "/";
    CString sAbsolutePath = ChangeDir(sPrefix, sAdd, sHomeDir);

    if (!sAbsolutePath.StartsWith(sPrefix)) return "";
    return sAbsolutePath;
}

bool CDir::MakeDir(const CString& sPath, mode_t iMode) {
    CString sDir;
    VCString dirs;
    VCString::iterator it;

    // Just in case someone tries this...
    if (sPath.empty()) {
        errno = ENOENT;
        return false;
    }

    // If this is an absolute path, we need to handle this now!
    if (sPath.StartsWith("/")) sDir = "/";

    // For every single subpath, do...
    sPath.Split("/", dirs, false);
    for (it = dirs.begin(); it != dirs.end(); ++it) {
        // Add this to the path we already created
        sDir += *it;

        int i = mkdir(sDir.c_str(), iMode);

        if (i != 0) {
            // All errors except EEXIST are fatal
            if (errno != EEXIST) return false;

            // If it's EEXIST we have to make sure it's a dir
            if (!CFile::IsDir(sDir)) return false;
        }

        sDir += "/";
    }

    // All went well
    return true;
}

int CExecSock::popen2(int& iReadFD, int& iWriteFD, const CString& sCommand) {
    int rpipes[2] = {-1, -1};
    int wpipes[2] = {-1, -1};
    iReadFD = -1;
    iWriteFD = -1;

    if (pipe(rpipes) < 0) return -1;

    if (pipe(wpipes) < 0) {
        close(rpipes[0]);
        close(rpipes[1]);
        return -1;
    }

    int iPid = fork();

    if (iPid == -1) {
        close(rpipes[0]);
        close(rpipes[1]);
        close(wpipes[0]);
        close(wpipes[1]);
        return -1;
    }

    if (iPid == 0) {
        sigset_t signals;
        sigemptyset(&signals);
        sigprocmask(SIG_SETMASK, &signals, nullptr);
        close(wpipes[1]);
        close(rpipes[0]);
        dup2(wpipes[0], 0);
        dup2(rpipes[1], 1);
        dup2(rpipes[1], 2);
        close(wpipes[0]);
        close(rpipes[1]);
        const char* pArgv[] = {"sh", "-c", sCommand.c_str(), nullptr};
        execvp("sh", (char* const*)pArgv);
        // if execvp returns, there was an error
        perror("execvp");
        exit(1);
    }

    close(wpipes[0]);
    close(rpipes[1]);

    iWriteFD = wpipes[1];
    iReadFD = rpipes[0];

    return iPid;
}

void CExecSock::close2(int iPid, int iReadFD, int iWriteFD) {
    close(iReadFD);
    close(iWriteFD);
    time_t iNow = time(nullptr);
    while (waitpid(iPid, nullptr, WNOHANG) == 0) {
        if ((time(nullptr) - iNow) > 5) break;  // giveup
        usleep(100);
    }
    return;
}
