/* common/kmdirfd.cpp
 *
 * Copyright (C) 2023 OpenKylin, Inc
 */

#include "KMDirFd.h"

#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <glib.h>
#include <assert.h>

#include "KMFileGuard.h"

/**
 * @brief : 规范化fd
 */
int KMDirFd::canonicalize(int dfd)
{
    if (dfd == -1)
    {
        return AT_FDCWD;
    }

    return dfd;
}

/**
 * @brief : 总结起来，如果path是绝对路径，则dfd参数没用。如果path是相对路径，并且dfd的值不是AT_FDCWD，则pathname的参照物是相对于dfd指向的目录，而不是进程的当前工作目录；
 *          反之，如果dfd的值是AT_FDCWD，path则是相对于进程当前工作目录的相对路径
 * @param : [in] dfd, 目录文件描述符，可以是AT_FDCWD或-1
 * @param : [in] path，相对于@dfd的路径；如果@dfd是AT_FDCWD或-1，则@path需要是绝对路径
 * @param : [in] follow，如果%TRUE并且@path的最后一个组件是符号链接，follow symbol
 * @return: 返回文件描述符；或如果打开错误返回-1,可以根据errorno获取错误信息::strerror(errno)
 */
int KMDirFd::opendirat(int dfd, const char *path, bool follow)
{
    int flags = O_RDONLY | O_NONBLOCK | O_DIRECTORY | O_CLOEXEC | O_NOCTTY;
    if (!follow)
    {
        flags |= O_NOFOLLOW;
    }

    dfd = KMDirFd::canonicalize(dfd);

    return ::openat(dfd, path, flags);
}

/**
 * @brief : 将fd相对路径转换为可用于期望绝对路径的遗留API的对象。这是特定于Linux的，并且仅在此进程内有效（除非您将子进程设置为具有完全相同的fd编号，但不要尝试
 * @param : [in] dfd, 目录文件描述符，可以是AT_FDCWD或-1
 * @param : [in] path，相对于@dfd的路径；如果@dfd是AT_FDCWD或-1，则@path需要是绝对路径
 * @return: 转换后的路径
 */
std::string KMDirFd::fdRelToAbsPath(int dfd, const char *path)
{
    dfd = KMDirFd::canonicalize(dfd);
    if (dfd == AT_FDCWD)
    {
        return path;
    }

    return std::string("/proc/self/fd/") + std::to_string(dfd) + "/" + path;
}

/**
 * @brief : 创建相对于dfd的子目录path
 * @param : [in] dfd, 目录文件描述符，可以是AT_FDCWD或-1
 * @param : [in] path，相对于@dfd的路径；如果@dfd是AT_FDCWD或-1，则@path需要是绝对路径
 * @param : [in] mode, 路径权限
 * @return: 是否成功, false时可以根据errorno获取错误信息::strerror(errno)
 */
bool KMDirFd::ensureDir(int dfd, const char *path, mode_t mode)
{
    if (TEMP_FAILURE_RETRY(::mkdirat(dfd, path, mode)) != 0)
    {
        if (G_UNLIKELY(errno != EEXIST))
        {
            return false;
        }
    }

    return true;
}

static bool mkdirPAtInternal(int dfd, const std::string &path, int mode)
{
    bool didRecurse = false;
    do
    {
        if (::mkdirat(dfd, path.c_str(), mode) == -1)
        {
            if (errno == ENOENT)
            {
                assert(!didRecurse);

                size_t lastslash = path.rfind("/");
                if (std::string::npos == lastslash)
                {
                    /* 如果@dfd在打开和传递给mkdir_p_at_internal（）之间被删除，就会发生这种情况。*/
                    return false;
                }

                std::string parentPath = path.substr(0, lastslash);

                if (!KMDirFd::mkdirPAt(dfd, parentPath, mode))
                {
                    return false;
                }

                didRecurse = true;
            }
            else if (errno == EEXIST)
            {
                /* 已经存在，可能不是目录 */
                didRecurse = false;
            }
            else
            {
                return false;
            }
        }
        else
        {
            didRecurse = false;
        }
    } while (didRecurse);

    return true;
}

/**
 * @brief : 递归创建@dfd和@path目录组合引用的目录
 * @param : [in] dfd, 目录文件描述符，可以是AT_FDCWD或-1
 * @param : [in] path，相对于@dfd的路径；如果@dfd是AT_FDCWD或-1，则@path需要是绝对路径
 * @param : [in] mode, 路径权限
 * @return: 是否成功, false时可以根据errorno获取错误信息::strerror(errno)
 */
bool KMDirFd::mkdirPAt(int dfd, const std::string &path, int mode)
{
    struct stat stbuf;
    /* 查询路径是否已经存在 */
    if (::fstatat(dfd, path.c_str(), &stbuf, AT_SYMLINK_NOFOLLOW) == 0)
    {
        /* 已经存在 */
        if (S_ISDIR(stbuf.st_mode))
        {
            return true;
        }
    }

    if (!mkdirPAtInternal(dfd, path, mode))
    {
        return false;
    }

    return true;
}

/**
 * @brief : 递归删除@dfd和@path目录组合引用的文件名；它可以是一个文件或目录。如果@path不存在，则不会引发错误。
 * @param : [in] dfd, 目录文件描述符，可以是AT_FDCWD或-1
 * @param : [in] path，相对于@dfd的路径；如果@dfd是AT_FDCWD或-1，则@path需要是绝对路径
 * @return: 是否成功, false时可以根据errorno获取错误信息::strerror(errno)
 */
bool KMDirFd::rmRfAt(int dfd, const char *path)
{
    dfd = KMDirFd::canonicalize(dfd);

    int target_dfd = ::openat(dfd, path, O_RDONLY | O_NONBLOCK | O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW);
    if (target_dfd == -1)
    {
        int errsv = errno;
        if (errsv == ENOENT)
        {
            ;
        }
        else if (errsv == ENOTDIR || errsv == ELOOP)
        {
            if (TEMP_FAILURE_RETRY(::unlinkat(dfd, path, 0)) != 0)
            {
                return false;
            }
        }
        else
        {
            return false;
        }
    }
    else
    {
        KMDirFdIterator dfd_iter;
        if (!dfd_iter.initTakeFd(target_dfd))
        {
            return false;
        }

        if (!dfd_iter.rm_rf_children())
        {
            return false;
        }

        if (!KMDirFd::unlinkatAllowNoent(dfd, path, AT_REMOVEDIR))
        {
            return false;
        }
    }

    return true;
}

/**
 * @brief : 删除@dfd和@path目录组合引用的文件名；它可以是一个文件或目录，如果@path不存在，则不会引发错误。
 * @param : [in] dfd, 目录文件描述符，可以是AT_FDCWD或-1
 * @param : [in] path，相对于@dfd的路径；如果@dfd是AT_FDCWD或-1，则@path需要是绝对路径
 * @param : [in] flags, 标志，如：AT_REMOVEDIR、AT_SYMLINK_NOFOLLOW、AT_SYMLINK_FOLLOW、AT_EMPTY_PATH、AT_RECURSIVE等等
 * @return: 是否成功, false时可以根据errorno获取错误信息::strerror(errno)
 */
bool KMDirFd::unlinkatAllowNoent(int dfd, const char *path, int flags)
{
    if (::unlinkat(dfd, path, flags) == -1)
    {
        if (errno != ENOENT)
        {
            return false;
        }
    }

    return true;
}

/**
 * @dfd: 目录文件描述符，可以是AT_FDCWD或-1
 * @path: 相对于@dfd的路径；如果@dfd是AT_FDCWD或-1，则@path需要是绝对路径
 * @buf: （外调用者分配）：返回stat详细信息的位置
 * @flags: 要传递给fstatat（）的标志，如：AT_REMOVEDIR、AT_SYMLINK_NOFOLLOW、AT_SYMLINK_FOLLOW、AT_EMPTY_PATH、AT_RECURSIVE等等
 */
bool KMDirFd::fstatat(int dfd, const std::string &path, struct stat *buf, int flags)
{
    if (TEMP_FAILURE_RETRY(::fstatat(dfd, path.c_str(), buf, flags)) != 0)
    {
        return false;
    }
    return true;
}

/**
 * readlinkat:
 * @dfd: 目录文件描述符
 * @subpath: path
 *
 * 读取符号链接的值。
 */
std::string KMDirFd::readlinkat(int dfd, const std::string &subpath)
{
    dfd = KMDirFd::canonicalize(dfd);

    size_t l = 100;
    for (;;)
    {
        char *c = new char[l];
        ssize_t n = TEMP_FAILURE_RETRY(::readlinkat(dfd, subpath.c_str(), c, l - 1));
        if (n < 0)
        {
            delete[] c;
            break;
        }

        if ((size_t)n < l - 1)
        {
            c[n] = 0;
            std::string ret(c);
            delete[] c;
            return ret;
        }

        l *= 2;
    }
    return "";
}

KMDirFdIterator::KMDirFdIterator()
{
}

KMDirFdIterator::~KMDirFdIterator()
{
    if (!m_initialized)
    {
        return;
    }

    if (m_dir)
    {
        ::closedir(m_dir);
    }
    m_fd = -1;

    m_initialized = false;
}

/**
 * @brief : 根据dfd和path初始化子目录访问迭代器
 * @param : [in] dfd, 目录文件描述符，可以是AT_FDCWD或-1
 * @param : [in] path，相对于@dfd的路径
 * @param : [in] follow，如果%TRUE并且@path的最后一个组件是符号链接，follow symbol
 * @return: 是否成功, false时可以根据errorno获取错误信息::strerror(errno)
 */
bool KMDirFdIterator::initAt(int dfd, const char *path, bool follow)
{
    int fd = KMDirFd::opendirat(dfd, path, follow);
    if (fd < 0)
    {
        return false;
    }

    return initTakeFd(fd);
}

/**
 * @brief : 根据dfd初始化目录访问迭代器
 * @param : [in | out] dfd, 目录文件描述符，可以是AT_FDCWD或-1；拥有权被取走，dfd会被修改为-1，调用者不能再close(dfd)
 * @return: 是否成功, false时可以根据errorno获取错误信息::strerror(errno)
 */
bool KMDirFdIterator::initTakeFd(int &dfd)
{
    m_dir = ::fdopendir(dfd);
    if (!m_dir)
    {
        return false;
    }

    m_fd = dfd;
    dfd = -1;
    m_initialized = true;

    return true;
}

/**
 * @brief : 获取下一个条目信息
 * @param : [out] dent, 遍历到的目录信息，遍历完后返回的值为NULL
 * @return: false-发生了错误，可以根据errno获取错误信息
 */
bool KMDirFdIterator::nextDent(struct dirent **dent)
{
    if (!m_initialized || !dent)
    {
        return false;
    }

    do
    {
        errno = 0;
        *dent = ::readdir(m_dir);
        if (*dent == NULL && errno != 0)
        {
            return false;
        }
    } while (*dent && (strcmp((*dent)->d_name, ".") == 0 || strcmp((*dent)->d_name, "..") == 0));

    return true;
}

/**
 * @brief : 获取下一个条目信息，dent->d_type被填充为fstatat获取的信息
 * @param : [out] dent, 遍历到的目录信息，遍历完后返回的值为NULL
 * @return: false-发生了错误，可以根据errno获取错误信息
 */
bool KMDirFdIterator::nextDentEnsureDtype(struct dirent **dent)
{
    if (!nextDent(dent))
    {
        return false;
    }

    struct dirent *retDent = *dent;
    if (retDent)
    {
        if (retDent->d_type == DT_UNKNOWN)
        {
            struct stat stbuf;
            if (!KMDirFd::fstatat(m_fd, retDent->d_name, &stbuf, AT_SYMLINK_NOFOLLOW))
            {
                return false;
            }

            retDent->d_type = IFTODT(stbuf.st_mode);
        }
    }

    return true;
}

/**
 * @brief : 回放到@dfd_iter的开头。下一次调用nextDent（）将提供目录中的第一个条目。
 */
void KMDirFdIterator::rewind()
{
    if (!m_initialized)
    {
        return;
    }

    ::rewinddir(m_dir);
}

/**
 * @brief : 递归删除目录子项
 */
bool KMDirFdIterator::rm_rf_children()
{
    struct dirent *dent;

    while (true)
    {
        if (!nextDentEnsureDtype(&dent))
        {
            return false;
        }

        if (dent == nullptr)
        {
            break;
        }

        if (dent->d_type == DT_DIR)
        {
            KMDirFdIterator childDfdIter;
            if (childDfdIter.initAt(m_fd, dent->d_name, false))
            {
                return false;
            }

            if (!childDfdIter.rm_rf_children())
            {
                return false;
            }

            if (TEMP_FAILURE_RETRY(::unlinkat(m_fd, dent->d_name, AT_REMOVEDIR)) != 0)
            {
                return false;
            }
        }
        else
        {
            if (!KMDirFd::unlinkatAllowNoent(m_fd, dent->d_name, 0))
            {
                return false;
            }
        }
    }

    return true;
}
