/*
 * This file is part of the Amalthea library.
 * Copyright (C) 2018-2025 Eugene 'Vindex' Stulin
 * Distributed under the BSL 1.0 or the GNU LGPL 3.0 or later.
 */

/**
 * The module provides many function related to files.
 */
module amalthea.fs;

import core.sys.posix.sys.stat;
import core.sys.posix.fcntl;
import core.sys.posix.dirent;
import core.stdc.errno;
import core.sys.linux.sys.xattr : getxattr, setxattr, listxattr, removexattr;
import unix_unistd = core.sys.posix.unistd;
import unix_stat = core.sys.posix.sys.stat;

import std.conv : oct = octal;
import std.stdio : File;
import std.file, std.path;
public import std.typecons;

import std.algorithm, std.array, std.datetime.systime, std.string;
import std.regex : regex, matchFirst;
import std.algorithm.iteration : splitter;

public import amalthea.libcore;
import amalthea.sys, amalthea.time;
import amalthea.dataprocessing : isAmong;

alias mv  = std.file.rename;
alias cd  = std.file.chdir;
alias pwd = getcwd;
alias rm  = std.file.remove;


immutable size_t eloopLimit = 128;


/*******************************************************************************
 * Checks if the path is an empty directory.
 */
bool isEmptyDir(string dirPath) {
    if (!isDir(dirPath)) {
        return false;
    }
    foreach(FileEntry ent; DirReader(dirPath)) {
        string name = baseName(ent.path);
        if (name != "." && name != "..") {
            return false;
        }
    }
    return true;
}


/*******************************************************************************
 * Implementation of file list reader.
 * Can be used in foreach as input range, the generator returns FileEntry.
 */
struct DirReader {
    this(string dir) {
        this.directory = dir;
        errno = 0;
        this.dirstream = opendir(dir.toStringz);
        enforce(errno == 0, new FileException(
            dir, "Failed to open directory: " ~ getInfoAboutError(errno))
        );
        this.popFront();
    }

    ~this() {
        closedir(dirstream);
    }

    @property empty() const {
        return this.currentDirent is null;
    }

    FileEntry popFront() {
        auto prevFileEntry = this.currentFileEntry;
        bool readingProblem = true;
        while (readingProblem) {
            errno = 0;
            this.currentDirent = readdir(dirstream);
            enforce(
                !errno,
                new FileException(this.directory, getInfoAboutError(errno))
            );
            if (this.currentDirent !is null) {
                string name = (*currentDirent).d_name.ptr.fromStringz.idup;
                string path = buildPath(this.directory, name);
                try {
                    this.currentFileEntry = FileEntry(path);
                } catch(FileException fe) {
                    continue;  // stat problem
                }
            }
            readingProblem = false;
        }
        return prevFileEntry;
    }

    FileEntry front() const {
        assert(this.currentDirent !is null);
        return this.currentFileEntry;
    }

private:

    dirent* currentDirent;
    DIR* dirstream;
    string directory;
    FileEntry currentFileEntry;
    bool end = false;
}


/*******************************************************************************
 * Reads entries of the directory.
 * Params:
 *     dir              = Directory for listing files.
 *     hidden           = Whether to include hidden files. Yes, by default.
 *     recursively      = Whether to search in subdirectories. No, by default.
 *     saveAbsolutePath = Whether to store an absolute path or not. If Yes,
 *                        a relative path of dir is converted to absolute.
 *                        The parameter has no effect if dir is an absolute
 *                        path. Has value Yes, by default.
 *     enableFilter     = Whether to filter by type. No, by default.
 *     includedTypes = Types for including to final result.
 *                     Used in conjunction with the previous parameter.
 * See_Also:
 *     $(FS_REF getRegularFiles),
 *     $(FS_REF getDirectories),
 *     $(FS_REF getSymlinks).
 */
FileEntry[] getFiles(string dir,
                     Flag!"hidden" hidden = Yes.hidden,
                     Flag!"recursively" recursively = No.recursively,
                     Flag!"absPath" saveAbsolutePath = Yes.absPath,
                     Flag!"enableFilter" enableFilter = No.enableFilter,
                     FileType[] includedTypes = []) {
    dir = saveAbsolutePath ? getAbsolutePath(dir) : dir;
    errno = 0;
    DIR* dirstream = opendir(dir.toStringz);
    string errInfo = "Failed to open directory: " ~ getInfoAboutError(errno);
    enforce(!errno, new FileException(dir, errInfo));
    scope(exit) closedir(dirstream);
    dirent* dirEntry = readdir(dirstream);
    enforce(!errno, new FileException(dir, getInfoAboutError(errno)));
    FileEntry[] entries;
    for (; dirEntry; dirEntry = readdir(dirstream)) {
        string name = (*dirEntry).d_name.ptr.fromStringz.idup;
        if (hidden && name.isAmong(".", "..")) continue;
        if (!hidden && name.startsWith('.')) continue;
        string path = buildPath(dir, name);
        FileEntry entry;
        try {
            entry = FileEntry(path);
        } catch(FileException fe) {
            // skip: decision to issue "Transport is not connected"
            errno = 0;
            continue;
        }
        if (recursively && entry.type == FileType.directory) {
            entries ~= getFiles(
                path,
                hidden,
                recursively,
                saveAbsolutePath,
                enableFilter,
                includedTypes
            );
        }

        if (!enableFilter || entry.type.isAmong(includedTypes)) {
            entries ~= entry;
        }
    }
    return entries;
}


/*******************************************************************************
 * Gets regular files from the directory.
 * Params:
 *     dir              = Directory for listing regular files.
 *     hidden           = Whether to include hidden files. Yes, by default.
 *     recursively      = Whether to search in subdirectories. No, by default.
 *     saveAbsolutePath = Whether to write the full path. Yes, by default.
 */
FileEntry[] getRegularFiles(string dir,
                            Flag!"hidden" hidden = Yes.hidden,
                            Flag!"recursively" recursively = No.recursively,
                            Flag!"absPath" saveAbsolutePath = Yes.absPath) {
    FileType[] includedTypes = [FileType.regularFile];
    return getFiles(dir, hidden, recursively, saveAbsolutePath,
                    Yes.enableFilter, includedTypes);
}


/*******************************************************************************
 * Gets directories from the directory.
 * Params:
 *     dir         = Directory for listing directories.
 *     hidden      = Whether to include hidden files. Yes, by default.
 *     recursively = Whether to search in subdirectories. No, by default.
 *     saveAbsolutePath = Whether to write the full path. Yes, by default.
 */
FileEntry[] getDirectories(string dir,
                           Flag!"hidden" hidden = Yes.hidden,
                           Flag!"recursively" recursively = No.recursively,
                           Flag!"absPath" saveAbsolutePath = Yes.absPath) {
    FileType[] includedTypes = [FileType.directory];
    return getFiles(dir, hidden, recursively, saveAbsolutePath,
                    Yes.enableFilter, includedTypes);
}


/*******************************************************************************
 * Gets symlinks from the directory.
 * Params:
 *     dir           = Directory for listing symlinks.
 *     hidden        = Whether to include hidden files. Yes, by default.
 *     recursively   = Whether to search in subdirectories. No, by default.
 *     saveAbsolutePath = Whether to write the full path. Yes, by default.
 */
FileEntry[] getSymlinks(string dir,
                        Flag!"hidden" hidden = Yes.hidden,
                        Flag!"recursively" recursively = No.recursively,
                        Flag!"absPath" saveAbsolutePath = Yes.absPath) {
    FileType[] includedTypes = [FileType.symlink];
    return getFiles(dir, hidden, recursively, saveAbsolutePath,
                    Yes.enableFilter, includedTypes);
}


/******************************************************************************
 * Special type for files as entries in directories.
 * Analogue of the 'dirent' structure type.
 */
struct FileEntry {
    /// Path to file.
    string path;
    /// Inode serial number.
    ulong fileno;
    /// UNIX file type. See: enum FileType.
    FileType type;

    /// Creates an instance of a class from an existing file by its path.
    this(string filepath) {
        auto st = FileStat(filepath);
        this.path = filepath;
        this.fileno = st.ino;
        this.type = st.type;
    }

    /// Returns information about the properties of the file.
    FileStat getFileStat() const {
        return FileStat(this.path);
    }

    /***************************************************************************
     * Checks if the file is a symbolic link to a directory.
     * The check is recursive (if the link points to a link).
     */
    bool isLinkToDir() {
        if (type != FileType.symlink) {
            return false;
        }
        string p;
        try {
            p = readLinkRecurse(this.path);
        } catch (FileException e) {
            return false;
        }
        return FileStat(p).type == FileType.directory;
    }
}


/******************************************************************************
 * Returns read symlink path. If the symlink points to other symlink,
 * the function returns path of the last symlink of the link chain.
 */
string readLinkRecurse(string link) {
    size_t counter = 1;
    auto path = link.readLink;
    if (!path.exists) {
        return path;
    }
    while(path.isSymlink) {
        path = path.readLink;
        counter++;
        if (counter > eloopLimit) {
            errno = ELOOP;
            throw new FileException(path, errno);
        }
        if (!path.exists) {
            // broken link
            return path;
        }
    }
    return path;
}



/*******************************************************************************
 * Checks if a file is a broken symbolic link is broken
 * (whether indicates a non-existent location).
 * Params:
 *     link = Path to link.
 *     recurse = Whether to check all nested symbolic links in a chain.
 */
bool isBrokenSymlink(string link, Flag!"recurse" recurse = No.recurse)
do {
    if (!link.isSymlink) {
        return false;
    }
    if (recurse) {
        string path = link.readLink;
        size_t counter = 1;
        while (path.exists) {
            if (!path.isSymlink) {
                return false;
            }
            path = path.readLink;
            counter++;
            if (counter > eloopLimit) {
                errno = ELOOP;
                throw new FileException(path, errno);
            }
        }
        return true;
    }
    return !link.readLink.exists;
}


/// Checks if a path is a pipe file (FIFO).
bool isPipe(in string path) {
    return FileStat(path).type == FileType.pipe;
}


/// Checks if a path is a socket.
bool isSocket(in string path) {
    return FileStat(path).type == FileType.socket;
}


/// Checks if a path is a symbolic link.
bool isSymlink(in string path) {
    return FileStat(path).type == FileType.symlink;
}


/// Checks if a path is a regular file.
bool isRegularFile(in string path) {
    return FileStat(path).type == FileType.regularFile;
}
/// Checks if a FileEntry object related to a regular file.
bool isRegularFile(const FileEntry entry) {
    return entry.type == FileType.directory;
}
bool isRegularFile(ref const FileEntry entry) {
    return entry.type == FileType.directory;
}


/// Checks if a path is a block device.
bool isBlockDevice(in string path) {
    return FileStat(path).type == FileType.blockDevice;
}


/// Checks if a path is a directory.
bool isDir(in string path) {
    return FileStat(path).type == FileType.directory;
}
/// Checks if a FileEntry object related to a directory.
bool isDir(const FileEntry entry) {
    return entry.type == FileType.directory;
}
bool isDir(ref const FileEntry entry) {
    return entry.type == FileType.directory;
}



/***************************************************************************
 * Checks if the file is a symbolic link to a directory.
 * The check is recursive (if the link points to a link).
 * Params:
 *     path = path fo file
 * Returns: whether a symbolic link is a link to a directory.
 */
bool isSymlinkToDir(in string path) {
    return FileEntry(path).isLinkToDir();
}


/// Checks if a path is a character device.
bool isCharDevice(in string path) {
    return FileStat(path).type == FileType.charDevice;
}


/*******************************************************************************
 * Special set of functions for work with ini-like .desktop files.
 */
class DesktopEntry {

    /***************************************************************************
     * This function adds new field to ini-like .desktop file.
     * Params:
     *     filepath = path to .desktop file
     *     field = field name
     *     value = value for new field
     */
    static void addField(string filepath, string field, string value) {
        string content = std.file.readText(filepath);
        auto newLine = field ~ "=" ~ value ~ "\n";
        if (content.endsWith("\n")) {
            content ~= newLine;
        } else {
            content ~= "\n" ~ newLine;
        }
        std.file.write(filepath, content);
    }

    /***************************************************************************
     * This function returns value of specified field from .desktop file.
     * Params:
     *     filepath = path to .desktop file
     *     field = field name
     */
    static string getField(string filepath, string field) {
        field = field.replace(`[`, `\[`).replace(`]`, `\]`);
        string content = std.file.readText(filepath);
        auto reg = regex(field ~ `=(.*)`);
        auto res = matchFirst(content, reg);
        return res[1].idup;
    }

    /***************************************************************************
     * This function sets new value for existent field from .desktop file.
     * Params:
     *     filepath = path to .desktop file
     *     field = field name
     *     value = new value of the field
     */
    static void setField(string filepath, string field, string value) {
        string oldValue = DesktopEntry.getField(filepath, field);
        string content = std.file.readText(filepath);
        content = content.replace(field~"="~oldValue, field~"="~value);
        std.file.write(filepath, content);
    }

    /***************************************************************************
     * The function returns title from .desktop file (field "Name").
     */
    static string getTitle(string pathToDesktopFile) {
        string desktopEntry = readText(pathToDesktopFile);
        auto indexOfNameField = desktopEntry.indexOf("Name=");
        auto indexOfTypeField = desktopEntry.indexOf("\nType=");
        return desktopEntry[indexOfNameField + 4 .. indexOfTypeField].idup;
    }

    /***************************************************************************
     * Creates a .desktop file with the link to the network resource.
     * Params:
     *     networkPath = path to the network resource
     *     destinationDir = directory for saving
     *     name = final desktop-file name (if empty, the name is taken from
     *            the title of the HTML page or from network address)
     * Returns: path to created desktop file
     */
    static string createNetworkLink(string networkPath,
                                    string destinationDir,
                                    string name="") {
        import amalthea.net : isLinkToHTML, getHTMLPageTitle;
        if (name.empty) {
            bool HTML = isLinkToHTML(networkPath);
            name = HTML ? getHTMLPageTitle(networkPath)
                        : baseName(networkPath);
            if (name.empty) {
                name = networkPath;
            }
        }
        string desktopEntry = "[Desktop Entry]\n"
                            ~ "Encoding=UTF-8\n"
                            ~ "Name=" ~ name ~ "\n"
                            ~ "Type=Link\n"
                            ~ "URL=" ~ networkPath ~ "\n"
                            ~ "Comment=" ~ "\n"
                            ~ "Icon=text-html";
        string finalFileName = name.replace("/", "|");
        if (!endsWith(name, ".desktop")) {
            finalFileName ~= ".desktop";
        }
        string filePath = chainPath(destinationDir, finalFileName).array;
        std.file.write(filePath, desktopEntry);
        return filePath;
    }

    /***************************************************************************
     * Returns link to network resource from .desktop file (field "URL").
     */
    static string getURL(string pathToDesktopFile) {
        return DesktopEntry.getField(pathToDesktopFile, "URL");
    }
}


/*******************************************************************************
 * Returns absolute path for any raw path. Trailing slashes are removed.
 */
string getAbsolutePath(string path) {
    import std.path : buildNormalizedPath, absolutePath;
    return buildNormalizedPath(absolutePath(path));
}
unittest {
    assert(getAbsolutePath("/usr/bin") == "/usr/bin");
    assert(getAbsolutePath("/usr/bin/") == "/usr/bin");
}


/*******************************************************************************
 * Function for copying files and directories.
 * Params:
 *     from = source file
 *     to   = destination
 *     followLinks = whether to follow symbolic links
 *     passErrors  = if No, the 'cp' function can throw exceptions
 *                   else errors are ignored
 *     printErrors = print information about errors or not
 *                   (the parameter works if passErrors is Yes)
 */
void cp(string from,
        string to,
        Flag!"followLinks" followLinks = No.followLinks,
        Flag!"passErrors" passErrors = No.passErrors,
        Flag!"printErrors" printErrors = Yes.printErrors) {

    void copyOneFile(string src, string dest) {
        try {
            if (dest.exists) {
                rm(dest);
            }
            alias isBroken = isBrokenSymlink;
            if (!followLinks && src.isSymlink || isBroken(src, Yes.recurse)) {
                std.file.symlink(src.readLink, dest);
            } else if (src.isPipe) {
                auto pipeStat = FileStat(src);
                amalthea.sys.createPipe(dest, pipeStat.mode);
            } else {
                std.file.copy(src, dest);
            }
        } catch (Exception e) {
            if (!passErrors) {
                throw e;
            }
            if (printErrors) {
                stderr.writeln(e.msg);
            }
        }
    }

    void processSymlink(FileEntry entry, string dest) {
        auto newPath = buildPath(dest, entry.path);
        if (followLinks && entry.isLinkToDir) {
            std.file.mkdirRecurse(newPath);
            auto distantDir = entry.path.readLinkRecurse;
            cp(distantDir, newPath, followLinks, passErrors, printErrors);
        } else {
            mkdirRecurse(dirName(newPath)); // create dir over file
            copyOneFile(entry.path, newPath);
        }
    }

    void copyDirToDir(string src, string dest) {
        to = getAbsolutePath(dest);
        auto startPath = pwd;
        cd(src);
        scope(exit) cd(startPath);
        FileEntry[] entries;
        entries = getFiles(".", Yes.hidden, Yes.recursively, No.absPath);
        foreach(entry; entries) {
            if (entry.type == FileType.directory) {
                std.file.mkdirRecurse(buildPath(to, entry.path));
            } else if (entry.type == FileType.symlink) {
                processSymlink(entry, to);
            } else { // other files
                auto newPath = buildPath(to, entry.path);
                mkdirRecurse(dirName(newPath)); // create dir over file
                copyOneFile(entry.path, newPath);
            }
        }
    }

    enforce(exists(from), new FileException(from, "Not found."));
    if (from.isDir && to.exists && !to.isDir) {
        throw new std.file.FileException(to, "Is not a directory.");
    }

    if (from.isDir && !to.exists) {
        std.file.mkdirRecurse(to);
    }

    if (from.isDir && to.isDir) { // dir to dir
        copyDirToDir(from, to);
    } else if (!from.isDir && to.exists && to.isDir) { // file to dir
        copyOneFile(from, buildPath(to, baseName(from)));
    } else { //file to file
        copyOneFile(from, to);
    }
}


/// A low-level structure to filling with the 'statx' system call.
extern(C)
struct statx_t {
   // 0x00
   uint stx_mask;
   uint stx_blksize;
   ulong stx_attributes;
   // 0x10
   uint stx_nlink;
   uint stx_uid;
   uint stx_gid;
   ushort stx_mode;
   ushort[1] __spare0;
   // 0x20
   ulong stx_ino;
   ulong stx_size;
   ulong stx_blocks;
   ulong stx_attributes_mask;
   // 0x40
   timestamp_t stx_atime;
   timestamp_t stx_btime;
   timestamp_t stx_ctime;
   timestamp_t stx_mtime;
   // 0x80
   uint stx_rdev_major;
   uint stx_rdev_minor;
   uint stx_dev_major;
   uint stx_dev_minor;
   // 0x90
   ulong stx_mnt_id;
   ulong __spare2;
   // 0xA0
   ulong[12] __spare3;	// Spare space for future expansion
}


/*******************************************************************************
 * This function  returns information about a file,
 * storing it in the buffer pointed to by statxbuf.
 * System call of Linux 4.11+, see more: `man statx`
 * This function is used in the high-level FileStat structure type.
 */
extern(C)
int statx(int dfd, const char* filename, int flags, uint mask, statx_t* stx);

/// See: `man statx`
enum STATX_TYPE        = 0x00000001U;
enum STATX_MODE        = 0x00000002U;
enum STATX_NLINK       = 0x00000004U;
enum STATX_UID         = 0x00000008U;
enum STATX_GID         = 0x00000010U;
enum STATX_ATIME       = 0x00000020U;
enum STATX_MTIME       = 0x00000040U;
enum STATX_CTIME       = 0x00000080U;
enum STATX_INO         = 0x00000100U;
enum STATX_SIZE        = 0x00000200U;
enum STATX_BLOCKS      = 0x00000400U;
enum STATX_BASIC_STATS = 0x000007ffU;
enum STATX_BTIME       = 0x00000800U;
enum STATX_MNT_ID      = 0x00001000U;

/// Mask for stat field by default.
enum STATX_DEFAULT = STATX_BASIC_STATS | STATX_BTIME;


/*******************************************************************************
 * Returns file information as statx_t.
 * Wrapper for low level statx call.
 */
statx_t getStatX(string filepath) {
    int atflag = AT_SYMLINK_NOFOLLOW;
    statx_t stx;
    char* p = cast(char*)toStringz(filepath);
    auto ret = statx(AT_FDCWD, p, atflag, STATX_DEFAULT, &stx);
    if (ret < 0) {
        throw new FileException(filepath, getInfoAboutError(errno));
    }
    return stx;
}


/// High-level function for getting stat_t from file path.
stat_t getStat(string filepath) {
    stat_t st;
    int ret = lstat(filepath.toStringz, &st);
    enforce(ret != -1, new FileException(filepath, errno));
    return st;
}


/*******************************************************************************
 * A structure for storing and retrieving file information on Linux.
 */
struct FileStat {
    /// Mask of bits indicating filled fields.
    uint mask;
    /// Block size for filesystem I/O.
    uint blksize;
    /// Extra file attribute indicators.
    ulong attributes;
    /// Number of hard links.
    uint nlink;
    /// User ID of owner.
    uint uid;
    /// Group ID of owner.
    uint gid;
    /// File type and mode.
    ushort mode;
    /// Inode number.
    ulong ino;
    /// Total size in bytes.
    ulong size;
    /// Number of 512B blocks allocated.
    ulong blocks;
    /// Mask to show what's supported in stx_attributes.
    ulong attributes_mask;
    /// Time of last access.
    timestamp_t atime;
    /// Time of creation.
    timestamp_t btime;
    /// Time of last modification.
    timestamp_t mtime;
    /// Time of last status change.
    timestamp_t ctime;

    /// Device ID (if this file is a device).
    ulong rdev;
    /// Device major id (if this file is a device).
    uint rdev_major;
    /// Device minor id (if this file is a device).
    uint rdev_minor;

    /// ID of device containing file.
    ulong dev;
    /// Major ID of the filesystem where the file resides.
    uint dev_major;
    /// Minor ID of the filesystem where the file resides.
    uint dev_minor;

    /// File path.
    string filepath;

    /// Stored mask for required structure fields.
    uint fieldMask;

    /***************************************************************************
     * The constructor creates object with file information by file path.
     * Params:
     *     filepath  = The path to the file being examined.
     *     fieldMask = Used to tell the kernel which fields
     *                 the caller is interested in.
     */
    this(string filepath, uint fieldMask = STATX_DEFAULT) {
        this.filepath = filepath;
        this.fieldMask = fieldMask;
        update();
    }

    /***************************************************************************
     * This method re-reads and updates file information.
     */
    void update() {
        statx_t stx = getStatX(filepath);
        this.mask = stx.stx_mask;
        this.blksize = stx.stx_blksize;
        this.attributes = stx.stx_attributes;
        this.nlink = stx.stx_nlink;
        this.uid = stx.stx_uid;
        this.gid = stx.stx_gid;
        this.mode = stx.stx_mode;
        this.ino = stx.stx_ino;
        this.size = stx.stx_size;
        this.blocks = stx.stx_blocks;
        this.attributes_mask = stx.stx_attributes_mask;
        this.atime = stx.stx_atime;
        this.btime = stx.stx_btime;
        this.mtime = stx.stx_mtime;
        this.ctime = stx.stx_ctime;
        this.rdev_major = stx.stx_rdev_major;
        this.rdev_minor = stx.stx_rdev_minor;
        this.rdev = (cast(ulong)rdev_major << 8) | rdev_minor;
        this.dev_major = stx.stx_dev_major;
        this.dev_minor = stx.stx_dev_minor;
        this.dev = (cast(ulong)dev_major << 8) | dev_minor;
    }

    /***************************************************************************
     * Gets user name of owner.
     */
    @property string user() {
        return amalthea.sys.getNameByUID(uid);
    }

    /***************************************************************************
     * Gets group name of owner.
     */
    @property string group() {
        return amalthea.sys.getNameByGID(gid);
    }

    /***************************************************************************
     * Returns file type.
     */
    @property const FileType type() {
        return getFileTypeFromFileMode(this.mode);
    }
}


/*******************************************************************************
 * Gets FileType by stat field 'mode'.
 */
FileType getFileTypeFromFileMode(uint mode) nothrow {
    return getFileTypeFromFileMask(mode & S_IFMT);
}


/*******************************************************************************
 * Gets FileType by the file mask obtained from stat field 'mode'.
 */
FileType getFileTypeFromFileMask(uint mask) nothrow {
    switch(mask) {
        case S_IFSOCK: return FileType.socket;
        case S_IFLNK:  return FileType.symlink;
        case S_IFREG:  return FileType.regularFile;
        case S_IFBLK:  return FileType.blockDevice;
        case S_IFDIR:  return FileType.directory;
        case S_IFCHR:  return FileType.charDevice;
        case S_IFIFO:  return FileType.pipe;
        default:       return FileType.unknown;
    }
}

/*******************************************************************************
 * Enumeration of UNIX file types.
 */
enum FileType {
    socket = 's',
    symlink = 'l',
    regularFile = '-',
    blockDevice = 'b',
    directory = 'd',
    charDevice = 'c',
    pipe = 'p',
    unknown = 'z'
}


/*******************************************************************************
 * The function returns file permissions in a string form (like -rwxrwxrwx)
 */
string makeUnixFileModeLine(in uint mode) {
    char[10] permissions;
    permissions[0] = cast(char)getFileTypeFromFileMode(mode);
    foreach(shift, rwxMask; [0: S_IRWXU, 3: S_IRWXG, 6: S_IRWXO]) {
        const rwxMode = (mode & rwxMask) >> (6-shift);
        permissions[shift+1] = (rwxMode & 4) ? 'r' : '-';
        permissions[shift+2] = (rwxMode & 2) ? 'w' : '-';
        permissions[shift+3] = (rwxMode & 1) ? 'x' : '-';
    }
    if (mode & S_ISUID) {
        permissions[3] = 's';
    }
    if (mode & S_ISGID) {
        permissions[6] = 's';
    }
    if (mode & S_ISVTX) {
        permissions[9] = 't';
    }
    return permissions.idup;
}
/// ditto
string makeUnixFileModeLine(const FileStat st) {
    return makeUnixFileModeLine(st.mode);
}
string makeUnixFileModeLine(ref const FileStat st) {
    return makeUnixFileModeLine(st.mode);
}


/*******************************************************************************
 * Creates a new hard link (make a new name for a file, see `man 2 link`).
 * Params:
 *     original = Original file path.
 *     link     = New link location path.
 */
void hardlink(string original, string link) {
    int ret = unix_unistd.link(original.toStringz, link.toStringz);
    if (-1 == ret) {
        string info = format!"Link from %s to %s"(original, link);
        throw new FileException(info);
    }
}

private extern(C) {
    int faccessat(
        int dirfd, const char* pathname, int mode, int flags
    ) nothrow @nogc;
    enum AT_FDCWD = -100;
    enum AT_SYMLINK_NOFOLLOW = 0x100;
    enum F_OK = 0;
}

/// Checks if the given file exists.
bool exists(string filepath) nothrow {
    stat_t st = void;
    return lstat(filepath.toStringz, &st) == 0;
}


/*******************************************************************************
 * Function set for working with file extended attributes.
 * See_Also: `man xattr`
 */
class xattr {
    /**
     * Gets value of the required extended attribute.
     * Params:
     *     path = Path to explored file.
     *            If it's a symlink, the target will be used.
     *     key = Attribute name.
     * Returns: read value.
     */
    static string read(string path, string key) {
        auto pathPtr = path.toStringz;
        auto keyPtr = key.toStringz;
        auto valueLength = getxattr(pathPtr, keyPtr, null, 0);
        enforce(valueLength != -1, new FileException("getxattr"));
        char[] buffer = new char[valueLength];
        valueLength = getxattr(pathPtr, keyPtr, buffer.ptr, valueLength);
        enforce(valueLength != -1, new FileException("getxattr"));
        return buffer.to!string;
    }

    /**
     * Sets new value of the required extended attribute.
     * Params:
     *     path = Path to explored file.
     *            If it's a symlink, the target will be used.
     *     key = Attribute name.
     *           If the attribute doesn't exist, it will be created.
     *     value = New value of the attribute.
     */
    static void set(string path, string key, string value) {
        auto pathPtr = path.toStringz;
        auto keyPtr = key.toStringz;
        auto valuePtr = cast(void*)(value.toStringz);
        int ret = setxattr(pathPtr, keyPtr, valuePtr, value.length, 0);
        enforce(ret != -1, new FileException("setxattr"));
    }

    /**
     * Removes the requested attribute.
     * Params:
     *     path = Path to file.
     *            If it's a symlink, the target will be used.
     *     key = Attribute name.
     */
    static void remove(string path, string key) {
        int ret = removexattr(path.toStringz, key.toStringz);
        enforce(ret != -1, new FileException("removexattr"));
    }

    /**
     * Reads list of all attributes.
     * Params:
     *     path = Path to explored file.
     *            If it's a symlink, the target will be used.
     * Returns: extended attributes of the specified file.
     */
    static string[] getAttrs(string path) {
        auto pathPtr = path.toStringz;
        auto bufferLength = listxattr(pathPtr, null, 0);
        enforce(bufferLength != -1, new FileException("listxattr"));
        ubyte[] buffer = new ubyte[bufferLength];
        bufferLength = listxattr(pathPtr, cast(char*)buffer.ptr, bufferLength);
        enforce(bufferLength != -1, new FileException("listxattr"));
        if (bufferLength == 0) {
            return [];
        }
        buffer = buffer[0 .. $-1];  // without last zero
        string[] list;
        foreach(ubyte[] elem; buffer.splitter(0)) {
            list ~= to!string(cast(char[])elem);
        }
        return list;
    }

    /**
     * Creates associative array with attribute names and attribute values.
     * Params:
     *     path = Path to explored file.
     *            If it's a symlink, the target will be used.
     * Returns: all extended attributes and their values of the specified file.
     */
    static string[string] getAttrsAndValues(string path) {
        string[] names = xattr.getAttrs(path);
        string[string] result;
        foreach(attr; names) {
            result[attr] = xattr.read(path, attr);
        }
        return result;
    }
}


/*******************************************************************************
 * Truncate a file to a specified length.
 * Params:
 *     filepath = File to be resized.
 *     size = New length in bytes.
 */
void truncate(string filepath, size_t size) {
    int ret = unix_unistd.truncate(filepath.toStringz, size);
    enforce(ret != -1, new FileException(filepath, errno));
}


/******************************************************************************
 * Create a directory only if it doesn't exist.
 */
void safeMkdir(string path) {
    errno = 0;
    int ret = unix_stat.mkdir(path.toStringz, oct!777);
    if (ret == -1) {
        enforce(errno == EEXIST, new FileException("safeMkdir"));
    }
}


private extern(C)
int posix_fadvise(int fd, off_t offset, off_t len, int advice) nothrow @nogc;


/// Access patterns for file data.
enum FileAdvice {
    normal     = 0,  /// No further special treatment.
    random     = 1,  /// Expect random page references.
    sequential = 2,  /// Expect sequential page references.
    willNeed   = 3,  /// Will need these pages.
    dontNeed   = 4,  /// Don't need these pages.
    noReuse    = 5   /// Data will be accessed once.
}


/*******************************************************************************
 * Predeclare an access pattern for file data.
 * Params:
 *     f = File object.
 *     advice = Access patern.
 *
 * See_Also: `man 2 posix_fadvise`
 */
void applyFileAdvise(File f, FileAdvice advice) {
    int ret = posix_fadvise(f.fileno, 0, 0, cast(int)advice);
    enforce(ret == 0, new FileException("applyFileAdvise"));
}
