/*
 * This file is part of the Amalthea library.
 * Copyright (C) 2018-2026 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 unix_fcntl = core.sys.posix.fcntl;
import core.sys.posix.fcntl :
    O_RDONLY, O_WRONLY, O_CREAT, O_TRUNC, O_CLOEXEC, O_APPEND;
import core.sys.posix.dirent;
import core.stdc.errno;
import core.stdc.stdio : SEEK_SET;
import core.sys.linux.sys.xattr :
    lgetxattr, lsetxattr, llistxattr, lremovexattr;
import unix_unistd = core.sys.posix.unistd;
import unix_stat = core.sys.posix.sys.stat;
import core.sys.posix.time : timespec;

import std.file;
public import std.typecons;

import std.algorithm;
import std.array;
import std.concurrency;
import std.conv : oct = octal, to;
import std.datetime.systime;
import std.stdio : File, stderr;
import std.string;
import std.exception : collectException, enforce;
import std.regex : regex, matchFirst;
import std.algorithm.iteration : splitter;
static import std.traits;

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

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


enum size_t eloopLimit = 128;

enum AT_FDCWD = -100;
enum AT_SYMLINK_NOFOLLOW = 0x100;
enum F_OK = 0;


/*******************************************************************************
 * Opens the file specified by path.
 *
 * Params:
 *     path   = Path to the file.
 *     flags  = Bit mask of flags for the 'open' system call.
 *              Flags are located in 'core.sys.posix.fcntl'.
 *     mode   = Specifies the file mode bits to be applied when
 *              a new file is created.
 *
 * Returns: the file descriptor.
 */
int openFileDescriptor(string path, int flags, int mode = oct!666) {
    const char* fpz = path.toStringz;
    int fd = unix_fcntl.open(fpz, flags, mode);
    enforce(fd != -1, new FileException("open"));
    return fd;
}


/*******************************************************************************
 * Closes the specified file descriptor.
 */
void closeFileDescriptor(int fd) {
    int ret = unix_unistd.close(fd);
    enforce(ret != -1 || errno == EINTR, new FileException("close"));
}


/*******************************************************************************
 * Reads the contents of the specified file as a byte array.
 *
 * Params:
 *     path   = Path to the file.
 *     size   = Maximum number of bytes to read.
 *              If size < 0, the entire file is read.
 *     offset = Byte offset from which to start reading.
 *
 * Returns: the file bytes.
 */
ubyte[] read(string filepath, ssize_t size = -1, ssize_t offset = 0) {
    int fd = openFileDescriptor(filepath, O_RDONLY | O_CLOEXEC);
    scope(exit) closeFileDescriptor(fd);
    if (-1 == unix_unistd.lseek(fd, offset, SEEK_SET)) {
        throw new FileException("lseek");
    }
    return readFromFileDescriptor(fd, size);
}


private ubyte[] readAllBytes(int fd) {
    enum chunkSize = 65_536;
    ubyte[] result;

    stat_t st;
    if (-1 == unix_stat.fstat(fd, &st)) {
        throw new FileException("fstat");
    }
    FileType ft = getFileTypeFromFileMode(st.st_mode);
    if (ft == FileType.regularFile) {
        result.reserve(st.st_size);
    }
    ubyte[] buffer = new ubyte[chunkSize];
    while (true) {
        ssize_t ret = unix_unistd.read(fd, buffer.ptr, chunkSize);
        if (ret == -1) {
            if (errno == EINTR) {
                continue;
            }
            throw new FileException("read");
        } else if (ret > 0) {
            result ~= buffer[0 .. ret];
        } else if (ret == 0) {
            break;
        }
    }
    return result;
}


/*******************************************************************************
 * Reads the contents by the specified file descriptor.
 *
 * Params:
 *     fd     = File descriptor.
 *     size   = Maximum number of bytes to read.
 *              If size < 0, the entire file is read.
 *
 * Returns: the read bytes.
 */
ubyte[] readFromFileDescriptor(int fd, ssize_t size = -1) {
    if (size < 0) {
        return readAllBytes(fd);
    } else if (size == 0) {
        return [];
    }
    ubyte[] result = new ubyte[size];
    size_t readAcc;
    ssize_t ret;
    while (true) {
        ret = unix_unistd.read(fd, result.ptr + readAcc, size - readAcc);
        if (ret == -1) {
            if (errno == EINTR) {
                continue;
            }
            throw new FileException("read");
        } else if (ret > 0) {
            readAcc += ret;
            continue;
        } else if (ret == 0) {
            break;
        }
    }
    return result[0 .. readAcc];
}


/*******************************************************************************
 * Writes data to the specified file descriptor.
 * Well suited for writing strings, arrays of numeric types, and scalar values.
 * For complex data structures, use a cusom serialization approach.
 *
 * Params:
 *     fd     = File descriptor open for writing.
 *     data   = Data for writing.
 */
void writeToFileDescriptor(T)(int fd, const ref T data) {
    static if (std.traits.isArray!T) {
        writeToFileDescriptor(fd, cast(ubyte[]) data);
    } else {
        ubyte* ptr = cast(ubyte*) &data;
        ubyte[] temp = ptr[0 .. T.sizeof];
        writeToFileDescriptor(fd, temp);
    }
}


/*******************************************************************************
 * Writes byte array to the specified file descriptor.
 *
 * Params:
 *     fd     = File descriptor open for writing.
 *     data   = Data for writing.
 */
void writeToFileDescriptor(int fd, const ubyte[] data) {
    enum chunkSize = 65_536;
    auto ptr = data.ptr;
    size_t written;
    while (true) {
        auto rest = data.length - written;
        auto sizeToWrite = rest < chunkSize ? rest : chunkSize;
        ssize_t ret = unix_unistd.write(fd, ptr + written, sizeToWrite);
        if (ret == -1) {
            if (errno == EINTR) {
                continue;
            }
            throw new FileException("write");
        }
        written += ret;
        if (written == data.length) {
            break;
        }
    }
}


/*******************************************************************************
 * Writes data to the specified file.
 *
 * Params:
 *     path   = Path to the file.
 *     data   = Data for writing.
 */
void write(T)(string filepath, const ref T data) {
    int flags = O_CREAT | O_WRONLY | O_TRUNC | O_CLOEXEC;
    int fd = openFileDescriptor(filepath, flags);
    scope(exit) closeFileDescriptor(fd);
    writeToFileDescriptor(fd, data);
}


/*******************************************************************************
 * Appends data to the file end.
 *
 * Params:
 *     filepath = Path to the file.
 *     data     = Data for writing.
 */
void append(T)(string filepath, const ref T data) {
    int flags = O_CREAT | O_WRONLY | O_APPEND | O_CLOEXEC;
    int fd = openFileDescriptor(filepath, flags);
    scope(exit) closeFileDescriptor(fd);
    writeToFileDescriptor(fd, data);
}


extern(C) nothrow @nogc
ssize_t sendfile(int out_fd, int in_fd, off_t* offset, size_t count);


/*******************************************************************************
 * Copies a file to a new location.
 *
 * Params:
 *     src = Source file path.
 *     dst = Destination path.
 *     sync = Synchronize (or not) a state with storage device.
 *     keepMetaData = Whether to copy permissions, timestamps, x-attributes.
 */
void copy(
    string src, string dst, bool sync = false, bool keepMetaData = false
) {
    int fd1 = openFileDescriptor(src, O_RDONLY);
    scope(exit) closeFileDescriptor(fd1);
    int fd2 = openFileDescriptor(dst, O_CREAT | O_WRONLY | O_TRUNC);
    scope(exit) closeFileDescriptor(fd2);

    applyFileAdvise(fd1, FileAdvice.sequential);
    applyFileAdvise(fd2, FileAdvice.noReuse);

    stat_t st;
    enforce(-1 != fstat(fd1, &st), new FileException("fstat"));
    ulong size = st.st_size;
    ulong copied;

    while (copied < size) {
        ssize_t ret = sendfile(fd2, fd1, null, size - copied);
        if (ret == -1 && errno == EINTR) {
            continue;
        }
        enforce(ret > 0, new FileException("sendfile"));
        copied += ret;
    }
    if (sync) {
        unix_unistd.fsync(fd2);
    }
    if (keepMetaData) {
        enforce(-1 != fchmod(fd2, st.st_mode), new FileException("fchmod"));
        timespec[2] times;
        static if (__traits(compiles, st.st_atimensec)) {
            times[0] = timespec(st.st_atime, st.st_atimensec);
            times[1] = timespec(st.st_mtime, st.st_mtimensec);
        } else {
            times[0] = st.st_atim;
            times[1] = st.st_mtim;
        }
        enforce(-1 != futimens(fd2, times), new FileException("futimens"));
        foreach(k; xattr.getAttrs(dst)) {  // remove old keys
            collectException(xattr.remove(dst, k));
        }
        foreach(k, v; xattr.getAttrsAndValues(src)) {  // add new x-attrs
            collectException(xattr.set(dst, k, v));
        }
    }
}


/*******************************************************************************
 * 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;
    /// File properties.
    FileStat filestat;

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

    /// Updates information about the properties of the file.
    void updateFileStat() {
        this.filestat = FileStat(this.path);
    }

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

    /***************************************************************************
     * 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;
        }
        if (!p.exists) {
            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 FileEntry object related to a pipe file.
bool isPipe(const FileEntry entry) {
    return entry.type == FileType.pipe;
}
/// ditto
bool isPipe(ref const FileEntry entry) {
    return entry.type == FileType.pipe;
}


/// Checks if a path is a socket.
bool isSocket(in string path) {
    return FileStat(path).type == FileType.socket;
}
/// Checks if a FileEntry object related to a socket file.
bool isSocket(const FileEntry entry) {
    return entry.type == FileType.socket;
}
/// ditto
bool isSocket(ref const FileEntry entry) {
    return entry.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 FileEntry object related to a symbolic link.
bool isSymlink(const FileEntry entry) {
    return entry.type == FileType.symlink;
}
/// ditto
bool isSymlink(ref const FileEntry entry) {
    return entry.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.regularFile;
}
/// ditto
bool isRegularFile(ref const FileEntry entry) {
    return entry.type == FileType.regularFile;
}


/// Checks if a path is a block device.
bool isBlockDevice(in string path) {
    return FileStat(path).type == FileType.blockDevice;
}
/// Checks if a FileEntry object related to a block device.
bool isBlockDevice(const FileEntry entry) {
    return entry.type == FileType.blockDevice;
}
/// ditto
bool isBlockDevice(ref const FileEntry entry) {
    return entry.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 working 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\n";
        string finalFileName = name.replace("/", "|");
        if (!endsWith(name, ".desktop")) {
            finalFileName ~= ".desktop";
        }
        string filePath = buildPath(destinationDir, finalFileName);
        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.
 * Files can have multiple hard links, and that is accounted for.
 *
 * Params:
 *     from = source file
 *     to   = destination
 *     followLinks = whether to follow symbolic links
 *     keepMetaData = Whether to copy access mode, timestamps, x-attributes.
 *     passErrors  = if No, the 'copyRecurse()' function can throw exceptions
 *                   else errors are ignored
 *     printErrors = print information about errors or not
 *                   (the parameter works if passErrors is Yes)
 */
void copyRecurse(
    string from,
    string to,
    Flag!"followLinks" followLinks = No.followLinks,
    Flag!"keepMetaData" keepMetaData = No.keepMetaData,
    Flag!"passErrors" passErrors = No.passErrors,
    Flag!"printErrors" printErrors = Yes.printErrors
) {
    void copyOneFile(string src, string dest)
    in {
        assert(!src.isSymlink);
    } do {
        try {
            if (dest.exists) {
                rm(dest);
            }
            if (src.isPipe) {
                auto pipeStat = FileStat(src);
                amalthea.sys.createPipe(dest, pipeStat.mode);
            } else {
                copy(src, dest);
            }
            if (keepMetaData) {
                copyMetaData(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) {
            if (entry.isLinkToDir) {
                std.file.mkdirRecurse(dest);
                auto distantDir = entry.path.readLinkRecurse;
                copyRecurse(
                    distantDir, newPath,
                    followLinks, keepMetaData, passErrors, printErrors
                );
            } else if (isBrokenSymlink(entry.path, Yes.recurse)) {
                mkdirRecurse(dirName(newPath));  // create dir over file
                std.file.symlink(entry.path.readLink, newPath);
            } else {
                copyOneFile(entry.path.readLinkRecurse, newPath);
            }
        } else {
            mkdirRecurse(dirName(newPath));  // create dir over file
            std.file.symlink(entry.path.readLink, newPath);
        }
    }

    void copyDirToDir(string src, string dest) {
        immutable errsNotSup = [EPERM, ENOTSUP, EOPNOTSUPP, ENOSYS];
        struct FileId {
            ulong dev;
            ulong ino;
        }
        string[FileId] copiedFiles;
        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) {
                if (!keepMetaData) {
                    auto newDir = buildPath(to, entry.path);
                    if (!newDir.exists) {
                        std.file.mkdirRecurse(newDir);
                    }
                } else {
                    string[] pathElements = entry.path.split("/");
                    if (pathElements.empty) {
                        pathElements = ["."];
                    }
                    string currentSrc = pathElements[0];
                    string currentDest = to;
                    foreach(pathElem; pathElements) {
                        currentSrc = buildPath(currentSrc, pathElem);
                        currentDest = buildPath(currentDest, pathElem);
                        if (!currentDest.exists) {
                            std.file.mkdir(currentDest);
                        } else if (!currentDest.isDir) {
                            std.file.remove(currentDest);
                            std.file.mkdir(currentDest);
                        }
                        copyMetaData(currentSrc, currentDest);
                    }
                }
            } else if (entry.type == FileType.symlink) {
                processSymlink(entry, to);
            } else {  // other files
                auto newPath = buildPath(to, entry.path);
                auto topDir = dirName(newPath);
                if (!exists(topDir)) {
                    mkdirRecurse(topDir);  // create dir over file
                }
                FileId fileId = FileId(entry.filestat.dev, entry.filestat.ino);
                if (fileId !in copiedFiles) {
                    copyOneFile(entry.path, newPath);
                    copiedFiles[fileId] = newPath;
                } else {
                    if (newPath.exists) {
                        if (newPath.isDir) {
                            std.file.rmdirRecurse(newPath);
                        } else {
                            std.file.remove(newPath);
                        }
                    }
                    auto savedPath = copiedFiles[fileId];
                    try {
                        amalthea.fs.hardlink(savedPath, newPath);
                    } catch (FileException e) {
                        if (e.errno.isAmong(errsNotSup)) {
                            copyOneFile(entry.path, newPath);
                        } else {
                            throw e;
                        }
                    }
                }
            }
        }
    }

    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) {  // dir to dir
        copyDirToDir(from, to);
    } else if (!from.isDir && to.exists && to.isDir) {  // file to dir
        if (from.isSymlink) {
            processSymlink(FileEntry(from), buildPath(to, baseName(from)));
        } else {
            copyOneFile(from, buildPath(to, baseName(from)));
        }
    } else {  //file to file
        if (from.isSymlink) {
            processSymlink(FileEntry(from), to);
        } else {
            copyOneFile(from, to);
        }
    }
}
deprecated alias cp = copyRecurse;


/*******************************************************************************
 * Copies access mode, timestamps, x-attributes
 * from the 1st file to the 2nd file.
 */
void copyMetaData(string src, string dst) {
    auto dstz = dst.toStringz;
    stat_t st = getStat(src);
    enforce(-1 != chmod(dstz, st.st_mode), new FileException("chmod"));
    timespec[2] times;

    static if (__traits(compiles, st.st_atimensec)) {
        times[0] = timespec(st.st_atime, st.st_atimensec);
        times[1] = timespec(st.st_mtime, st.st_mtimensec);
    } else {
        times[0] = st.st_atim;
        times[1] = st.st_mtim;
    }
    import std.stdio : writeln;
    if (-1 == utimensat(AT_FDCWD, dstz, times, AT_SYMLINK_NOFOLLOW)) {
        throw new FileException("utimensat");
    }
    foreach(k; xattr.getAttrs(dst)) {  // remove old keys
        collectException(xattr.remove(dst, k));
    }
    foreach(k, v; xattr.getAttrsAndValues(src)) {  // add new x-attrs
        collectException(xattr.set(dst, k, v));
    }
}


/// 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);
    }
}


/// 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 ubyte[] read(string path, string key) {
        auto pathPtr = path.toStringz;
        auto keyPtr = key.toStringz;
        auto valueLength = lgetxattr(pathPtr, keyPtr, null, 0);
        enforce(valueLength != -1, new FileException("lgetxattr"));
        ubyte[] buffer = new ubyte[valueLength];
        valueLength = lgetxattr(pathPtr, keyPtr, buffer.ptr, valueLength);
        enforce(valueLength != -1, new FileException("lgetxattr"));
        return buffer;
    }

    /**
     * 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, const ubyte[] value) {
        auto pathPtr = path.toStringz;
        auto keyPtr = key.toStringz;
        int ret = lsetxattr(pathPtr, keyPtr, value.ptr, value.length, 0);
        enforce(ret != -1, new FileException("lsetxattr"));
    }
    /// ditto
    static void set(string path, string key, string value) {
        set(path, key, cast(const ubyte[]) value);
    }

    /**
     * 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 = lremovexattr(path.toStringz, key.toStringz);
        enforce(ret != -1, new FileException("lremovexattr"));
    }

    /**
     * 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 = llistxattr(pathPtr, null, 0);
        enforce(bufferLength != -1, new FileException("llistxattr"));
        ubyte[] buffer = new ubyte[bufferLength];
        bufferLength = llistxattr(pathPtr, cast(char*)buffer.ptr, bufferLength);
        enforce(bufferLength != -1, new FileException("llistxattr"));
        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 ubyte[][string] getAttrsAndValues(string path) {
        string[] names = xattr.getAttrs(path);
        ubyte[][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"));
}



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


extern(C)
private int rename(const char* oldpath, const char* newpath) nothrow @nogc;


/// This function renames the specified file.
void rename(string oldPath, string newPath) {
    enforce(oldPath.exists, new std.file.FileException(oldPath));
    enforce(dirName(newPath).exists, new std.file.FileException(newPath));
    int ret = rename(oldPath.toStringz, newPath.toStringz);
    if (ret == -1) {
        oldPath = escapeString(oldPath);
        newPath = escapeString(newPath);
        string msg = format!`Failed to rename "%s" to "%s".`(oldPath, newPath);
        throw new std.file.FileException(msg);
    }
}


/*******************************************************************************
 * The function for creating a path from components.
 * Its behavior towards components with leading slashes differs from
 * the behavior of `std.path.buildPath` (see unit-tests).
 */
string buildPath(string[] args...) @safe pure nothrow {
    bool useAbsPath;
    size_t resultLen;
    size_t componentCount;
    bool checkAbsPath = true;

    foreach(ref arg; args) {
        if (checkAbsPath && arg.length != 0) {
            if (arg[0] == '/') {
                useAbsPath = true;
                resultLen++;  // additional slash
            }
            checkAbsPath = false;
        }
        size_t index1 = 0;
        size_t index2 = arg.length;
        while (index1 < index2 && arg[index1] == '/') {
            index1++;
        }
        while (index2 > index1 && arg[index2 - 1] == '/') {
            index2--;
        }
        if (index1 == index2) {
            arg = "";
            continue;
        }
        arg = arg[index1 .. index2];
        resultLen += index2 - index1 + 1;  // plus additional slash
        componentCount++;
    }
    if (componentCount == 0) {
        return useAbsPath ? "/" : "";
    }
    char[] result = new char[resultLen];
    size_t pos = 0;
    if (useAbsPath) {
        result[pos] = '/';
        pos = 1;
    }
    foreach(arg; args) {
        if (arg.length == 0) {
            continue;
        }
        result[pos .. pos + arg.length] = arg[];
        pos += arg.length;
        result[pos] = '/';
        pos++;
    }
    return result[0 .. $-1];
}
unittest {
    assert(buildPath("l1", "l2") == "l1/l2");
    assert(buildPath("l1", "/l2") == "l1/l2");
    assert(buildPath("l1/", "/l2/") == "l1/l2");
    assert(buildPath("/l1/", "/l2/") == "/l1/l2");
    assert(buildPath("", "/") == "/");
    assert(buildPath("/path", "", "/", "") == "/path");
    assert(buildPath("", "/path", "", "/", "") == "/path");
}


/*******************************************************************************
 * Returns the parent directory of the specified path.
 */
string dirName(string path) @trusted nothrow pure {
    if (path.length == 0) {
        return ".";
    }
    auto ptr = path.ptr + path.length - 1;
    while (ptr > path.ptr && *ptr == '/') {  // strip right slashes
        ptr--;
    }
    while (ptr > path.ptr) {
        ptr--;
        if (*ptr == '/') {
            immutable len = ptr - path.ptr;
            return path.ptr == ptr ? "/" : path[0 .. len];
        }
    }
    return path[0] == '/' ? "/" : ".";
}
unittest {
    assert(dirName("dir/subdir/file.txt") == "dir/subdir");
    assert(dirName("file.txt") == ".");
    assert(dirName("/file.txt") == "/");
    assert(dirName("/dir/") == "/");
    assert(dirName("///") == "/");
    assert(dirName("/") == "/");
    assert(dirName("/a") == "/");
    assert(dirName("a/") == ".");
    assert(dirName("") == ".");
    assert(dirName(".") == ".");
    assert(dirName("./file") == ".");
}


/*******************************************************************************
 * Gets the file name from the specified path, without any leading directory.
 */
string baseName(string path) @trusted nothrow pure {
    if (path.length == 0) {
        return "";
    }
    auto ptr2 = path.ptr + path.length - 1;
    while (ptr2 > path.ptr && *ptr2 == '/') {
        ptr2--;
    }
    if (ptr2 == path.ptr) {
        return path[0 .. 1];
    }
    auto ptr1 = ptr2;
    while (ptr1 > path.ptr) {
        ptr1--;
        if (*ptr1 == '/') {
            immutable len = ptr2 - ptr1 + 1;
            return ptr1[1 .. len];
        }
    }
    immutable len = ptr2 - ptr1 + 1;
    return ptr1[0 .. len];
}
unittest {
    assert(baseName("dir/subdir/file.txt") == "file.txt");
    assert(baseName("file.txt") == "file.txt");
    assert(baseName("/file.txt") == "file.txt");
    assert(baseName("///file.txt") == "file.txt");
    assert(baseName("/dir/") == "dir");
    assert(baseName("///") == "/");
    assert(baseName("/") == "/");
    assert(baseName("a/") == "a");
    assert(baseName("/a") == "a");
    assert(baseName("") == "");
}
