root/src/tools/fs_shell/fssh.cpp
/*
 * Copyright 2007-2008, Ingo Weinhold, ingo_weinhold@gmx.de.
 * Distributed under the terms of the MIT License.
 */


#include "compatibility.h"

#include "fssh.h"

#include <stdarg.h>
#include <stdio.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <stdlib.h>

#include <vector>

#include "command_cp.h"
#include "driver_settings.h"
#include "external_commands.h"
#include "fd.h"
#include "fssh_dirent.h"
#include "fssh_errno.h"
#include "fssh_errors.h"
#include "fssh_fs_info.h"
#include "fssh_fcntl.h"
#include "fssh_module.h"
#include "fssh_node_monitor.h"
#include "fssh_stat.h"
#include "fssh_string.h"
#include "fssh_type_constants.h"
#include "module.h"
#include "partition_support.h"
#include "path_util.h"
#include "syscalls.h"
#include "vfs.h"


extern fssh_module_info *modules[];


extern fssh_file_system_module_info gRootFileSystem;

namespace FSShell {

const char* kMountPoint = "/myfs";

// command line args
static  int                                     sArgc;
static  const char* const*      sArgv;

static mode_t sUmask = 0022;


static fssh_status_t
init_kernel()
{
        fssh_status_t error;

        // init module subsystem
        error = module_init(NULL);
        if (error != FSSH_B_OK) {
                fprintf(stderr, "module_init() failed: %s\n", fssh_strerror(error));
                return error;
        }

        // init driver settings
        error = driver_settings_init();
        if (error != FSSH_B_OK) {
                fprintf(stderr, "initializing driver settings failed: %s\n",
                        fssh_strerror(error));
                return error;
        }

        // register built-in modules, i.e. the rootfs and the client FS
        register_builtin_module(&gRootFileSystem.info);
        for (int i = 0; modules[i]; i++)
                register_builtin_module(modules[i]);

        // init VFS
        error = vfs_init(NULL);
        if (error != FSSH_B_OK) {
                fprintf(stderr, "initializing VFS failed: %s\n", fssh_strerror(error));
                return error;
        }

        // init kernel IO context
        gKernelIOContext = (io_context*)vfs_new_io_context(NULL);
        if (!gKernelIOContext) {
                fprintf(stderr, "creating IO context failed!\n");
                return FSSH_B_NO_MEMORY;
        }

        // mount root FS
        fssh_dev_t rootDev = _kern_mount("/", NULL, "rootfs", 0, NULL, 0);
        if (rootDev < 0) {
                fprintf(stderr, "mounting rootfs failed: %s\n", fssh_strerror(rootDev));
                return rootDev;
        }

        // set cwd to "/"
        error = _kern_setcwd(-1, "/");
        if (error != FSSH_B_OK) {
                fprintf(stderr, "setting cwd failed: %s\n", fssh_strerror(error));
                return error;
        }

        // create mount point for the client FS
        error = _kern_create_dir(-1, kMountPoint, 0775);
        if (error != FSSH_B_OK) {
                fprintf(stderr, "creating mount point failed: %s\n",
                        fssh_strerror(error));
                return error;
        }

        return FSSH_B_OK;
}


// #pragma mark - Command

Command::Command(const char* name, const char* description)
        : fName(name),
          fDescription(description)
{
}


Command::Command(command_function* function, const char* name,
        const char* description)
        : fName(name),
          fDescription(description),
          fFunction(function)
{
}


Command::~Command()
{
}


const char*
Command::Name() const
{
        return fName.c_str();
}


const char*
Command::Description() const
{
        return fDescription.c_str();
}


fssh_status_t
Command::Do(int argc, const char* const* argv)
{
        if (!fFunction) {
                fprintf(stderr, "No function given for command \"%s\"\n", Name());
                return FSSH_B_BAD_VALUE;
        }

        return (*fFunction)(argc, argv);
}


// #pragma mark - CommandManager

CommandManager::CommandManager()
{
}


CommandManager*
CommandManager::Default()
{
        if (!sManager)
                sManager = new CommandManager;
        return sManager;
}


void
CommandManager::AddCommand(Command* command)
{
        // The command name may consist of several aliases. Split them and
        // register the command for each of them.
        char _names[1024];
        char* names = _names;
        strcpy(names, command->Name());

        char* cookie;
        while (char* name = strtok_r(names, " /", &cookie)) {
                fCommands[name] = command;
                names = NULL;
        }
}


void
CommandManager::AddCommand(command_function* function, const char* name,
        const char* description)
{
        AddCommand(new Command(function, name, description));
}


void
CommandManager::AddCommands(command_function* function, const char* name,
        const char* description, ...)
{
        va_list args;
        va_start(args, description);

        while (function) {
                AddCommand(function, name, description);

                function = va_arg(args, command_function*);
                if (function) {
                        name = va_arg(args, const char*);
                        description = va_arg(args, const char*);
                }
        }

        va_end(args);
}


Command*
CommandManager::FindCommand(const char* name) const
{
        CommandMap::const_iterator it = fCommands.find(name);
        if (it == fCommands.end())
                return NULL;

        return it->second;
}


void
CommandManager::ListCommands() const
{
        for (CommandMap::const_iterator it = fCommands.begin();
                        it != fCommands.end(); ++it) {
                const char* name = it->first.c_str();
                Command* command = it->second;
                printf("%-16s - %s\n", name, command->Description());
        }
}


CommandManager* CommandManager::sManager = NULL;


// #pragma mark - Command support functions


static bool
get_permissions(const char* modeString, fssh_mode_t& _permissions)
{
        // currently only octal mode is supported
        if (strlen(modeString) != 3)
                return false;

        fssh_mode_t permissions = 0;
        for (int i = 0; i < 3; i++) {
                char c = modeString[i];
                if (c < '0' || c > '7')
                        return false;
                permissions = (permissions << 3) | (c - '0');
        }

        _permissions = permissions;
        return true;
}


static fssh_dev_t
get_volume_id()
{
        struct fssh_stat st;
        fssh_status_t error = _kern_read_stat(-1, kMountPoint, false, &st,
                sizeof(st));
        if (error != FSSH_B_OK) {
                fprintf(stderr, "Error: Failed to stat() mount point: %s\n",
                        fssh_strerror(error));
                return error;
        }

        return st.fssh_st_dev;
}


static const char *
byte_string(int64_t numBlocks, int64_t blockSize)
{
        double blocks = 1. * numBlocks * blockSize;
        static char string[64];

        if (blocks < 1024)
                sprintf(string, "%" FSSH_B_PRId64, numBlocks * blockSize);
        else {
                const char* units[] = {"K", "M", "G", NULL};
                int i = -1;

                do {
                        blocks /= 1024.0;
                        i++;
                } while (blocks >= 1024 && units[i + 1]);

                sprintf(string, "%.1f%s", blocks, units[i]);
        }

        return string;
}


void
print_flag(uint32_t deviceFlags, uint32_t testFlag, const char *yes,
        const char *no)
{
        printf("%s", (deviceFlags & testFlag) != 0 ? yes : no);
}


static void
list_entry(const char* file, const char* name = NULL)
{
        // construct path, if a leaf name is given
        std::string path;
        if (name) {
                path = file;
                path += '/';
                path += name;
                file = path.c_str();
        } else
                name = file;

        // stat the file
        struct fssh_stat st;
        fssh_status_t error = _kern_read_stat(-1, file, false, &st, sizeof(st));
        if (error != FSSH_B_OK) {
                fprintf(stderr, "Error: Failed to stat() \"%s\": %s\n", file,
                        fssh_strerror(error));
                return;
        }

        // get time
        struct tm time;
        time_t fileTime = st.fssh_st_mtime;
        localtime_r(&fileTime, &time);

        // get permissions
        std::string permissions;
        fssh_mode_t mode = st.fssh_st_mode;
        // user
        permissions += ((mode & FSSH_S_IRUSR) ? 'r' : '-');
        permissions += ((mode & FSSH_S_IWUSR) ? 'w' : '-');
        if (mode & FSSH_S_ISUID)
                permissions += 's';
        else
                permissions += ((mode & FSSH_S_IXUSR) ? 'x' : '-');
        // group
        permissions += ((mode & FSSH_S_IRGRP) ? 'r' : '-');
        permissions += ((mode & FSSH_S_IWGRP) ? 'w' : '-');
        if (mode & FSSH_S_ISGID)
                permissions += 's';
        else
                permissions += ((mode & FSSH_S_IXGRP) ? 'x' : '-');
        // others
        permissions += ((mode & FSSH_S_IROTH) ? 'r' : '-');
        permissions += ((mode & FSSH_S_IWOTH) ? 'w' : '-');
        permissions += ((mode & FSSH_S_IXOTH) ? 'x' : '-');

        // get file type
        char fileType = '?';
        if (FSSH_S_ISREG(mode)) {
                fileType = '-';
        } else if (FSSH_S_ISLNK(mode)) {
                fileType = 'l';
        } else if (FSSH_S_ISBLK(mode)) {
                fileType = 'b';
        } else if (FSSH_S_ISDIR(mode)) {
                fileType = 'd';
        } else if (FSSH_S_ISCHR(mode)) {
                fileType = 'c';
        } else if (FSSH_S_ISFIFO(mode)) {
                fileType = 'f';
        } else if (FSSH_S_ISINDEX(mode)) {
                fileType = 'i';
        }

        // get link target
        std::string nameSuffix;
        if (FSSH_S_ISLNK(mode)) {
                char buffer[FSSH_B_PATH_NAME_LENGTH];
                fssh_size_t size = sizeof(buffer) - 1;
                error = _kern_read_link(-1, file, buffer, &size);
                if (error != FSSH_B_OK)
                        snprintf(buffer, sizeof(buffer), "(%s)", fssh_strerror(error));

                buffer[size] = '\0';
                nameSuffix += " -> ";
                nameSuffix += buffer;
        }

        printf("%c%s %2d %2d %10" FSSH_B_PRIdOFF
                " %d-%02d-%02d %02d:%02d:%02d %s%s\n",
                fileType, permissions.c_str(), (int)st.fssh_st_uid, (int)st.fssh_st_gid,
                st.fssh_st_size,
                1900 + time.tm_year, 1 + time.tm_mon, time.tm_mday,
                time.tm_hour, time.tm_min, time.tm_sec,
                name, nameSuffix.c_str());
}


static fssh_status_t
create_file(const char* path)
{
        // stat the entry
        struct fssh_stat st;
        fssh_status_t error = _kern_read_stat(-1, path, false, &st, sizeof(st));

        if (error == FSSH_B_OK) {

                // TODO file/directory exists. Update access/modification time
                fprintf(stderr, "TODO file/directory exists. update time stamp: %s\n", path);
                return FSSH_B_FILE_EXISTS;
        }

        // create the file
        int fd_or_error = _kern_open(-1, path, FSSH_O_CREAT, 0);
        if (fd_or_error <= 0) {
                fprintf(stderr, "Error: Failed to make file \"%s\": %s\n", path,
                        fssh_strerror(fd_or_error));
                return fd_or_error;
        }

        // TODO update access/modification time?

        // close file
        _kern_close(fd_or_error);
        return FSSH_B_OK;
}


static fssh_status_t
create_dir(const char* path, bool createParents)
{
        // stat the entry
        struct fssh_stat st;
        fssh_status_t error = _kern_read_stat(-1, path, false, &st, sizeof(st));
        if (error == FSSH_B_OK) {
                if (createParents && FSSH_S_ISDIR(st.fssh_st_mode))
                        return FSSH_B_OK;

                fprintf(stderr, "Error: Cannot make dir, entry \"%s\" is in the way.\n",
                        path);
                return FSSH_B_FILE_EXISTS;
        }

        // the dir doesn't exist yet
        // if we shall create all parents, do that first
        if (createParents) {
                // create the parent dir path
                // eat the trailing '/'s
                int len = strlen(path);
                while (len > 0 && path[len - 1] == '/')
                        len--;

                // eat the last path component
                while (len > 0 && path[len - 1] != '/')
                        len--;

                // eat the trailing '/'s
                while (len > 0 && path[len - 1] == '/')
                        len--;

                // Now either nothing remains, which means we had a single component,
                // a root subdir -- in those cases we can just fall through (we should
                // actually never be here in case of the root dir, but anyway) -- or
                // there is something left, which we can call a parent directory and
                // try to create it.
                if (len > 0) {
                        char *parentPath = (char*)malloc(len + 1);
                        if (!parentPath) {
                                fprintf(stderr, "Error: Failed to allocate memory for parent "
                                        "path.\n");
                                return FSSH_B_NO_MEMORY;
                        }
                        memcpy(parentPath, path, len);
                        parentPath[len] = '\0';

                        error = create_dir(parentPath, createParents);

                        free(parentPath);

                        if (error != FSSH_B_OK)
                                return error;
                }
        }

        // make the directory
        error = _kern_create_dir(-1,
                path, (FSSH_S_IRWXU | FSSH_S_IRWXG | FSSH_S_IRWXO) & ~sUmask);
        if (error != FSSH_B_OK) {
                fprintf(stderr, "Error: Failed to make directory \"%s\": %s\n", path,
                        fssh_strerror(error));
                return error;
        }

        return FSSH_B_OK;
}


static fssh_status_t remove_entry(int dir, const char *entry, bool recursive,
        bool force);


static fssh_status_t
remove_dir_contents(int parentDir, const char *name, bool force)
{
        // open the dir
        int dir = _kern_open_dir(parentDir, name);
        if (dir < 0) {
                fprintf(stderr, "Error: Failed to open dir \"%s\": %s\n", name,
                        fssh_strerror(dir));
                return dir;
        }

        fssh_status_t error = FSSH_B_OK;

        // iterate through the entries
        fssh_ssize_t numRead;
        char buffer[sizeof(fssh_dirent) + FSSH_B_FILE_NAME_LENGTH];
        fssh_dirent *entry = (fssh_dirent*)buffer;
        while ((numRead = _kern_read_dir(dir, entry, sizeof(buffer), 1)) > 0) {
                // skip "." and ".."
                if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0)
                        continue;

                error = remove_entry(dir, entry->d_name, true, force);
                if (error != FSSH_B_OK)
                        break;
        }

        if (numRead < 0) {
                fprintf(stderr, "Error: Failed to read directory \"%s\": %s\n", name,
                        fssh_strerror(numRead));
                error = numRead;
        }

        // close
        _kern_close(dir);

        return error;
}


static fssh_status_t
remove_entry(int dir, const char *entry, bool recursive, bool force)
{
        // stat the file
        struct fssh_stat st;
        fssh_status_t error = _kern_read_stat(dir, entry, false, &st, sizeof(st));
        if (error != FSSH_B_OK) {
                if (force && error == FSSH_B_ENTRY_NOT_FOUND)
                        return FSSH_B_OK;

                fprintf(stderr, "Error: Failed to remove \"%s\": %s\n", entry,
                        fssh_strerror(error));
                return error;
        }

        if (FSSH_S_ISDIR(st.fssh_st_mode)) {
                if (!recursive) {
                        fprintf(stderr, "Error: \"%s\" is a directory.\n", entry);
                                // TODO: get the full path
                        return FSSH_EISDIR;
                }

                // remove the contents
                error = remove_dir_contents(dir, entry, force);
                if (error != FSSH_B_OK)
                        return error;

                // remove the directory
                error = _kern_remove_dir(dir, entry);
                if (error != FSSH_B_OK) {
                        fprintf(stderr, "Error: Failed to remove directory \"%s\": %s\n",
                                entry, fssh_strerror(error));
                        return error;
                }
        } else {
                // remove the entry
                error = _kern_unlink(dir, entry);
                if (error != FSSH_B_OK) {
                        fprintf(stderr, "Error: Failed to remove entry \"%s\": %s\n", entry,
                                fssh_strerror(error));
                        return error;
                }
        }

        return FSSH_B_OK;
}


static fssh_status_t
move_entry(int dir, const char *entry, int targetDir, const char* target,
        bool force)
{
        // stat the file
        struct fssh_stat st;
        fssh_status_t status = _kern_read_stat(dir, entry, false, &st, sizeof(st));
        if (status != FSSH_B_OK) {
                if (force && status == FSSH_B_ENTRY_NOT_FOUND)
                        return FSSH_B_OK;

                fprintf(stderr, "Error: Failed to move \"%s\": %s\n", entry,
                        fssh_strerror(status));
                return status;
        }

        return _kern_rename(dir, entry, targetDir, target);
}


// #pragma mark - Commands


static fssh_status_t
command_cd(int argc, const char* const* argv)
{
        if (argc != 2) {
                fprintf(stderr, "Usage: %s <directory>\n", argv[0]);
                return FSSH_B_BAD_VALUE;
        }
        const char* directory = argv[1];

        fssh_status_t error = FSSH_B_OK;
        if (directory[0] == ':') {
                if (chdir(directory + 1) < 0)
                        error = fssh_get_errno();
        } else
                error = _kern_setcwd(-1, directory);

        if (error != FSSH_B_OK) {
                fprintf(stderr, "Error: cd %s: %s\n", directory, fssh_strerror(error));
                return error;
        }

        return FSSH_B_OK;
}


static fssh_status_t
command_chmod(int argc, const char* const* argv)
{
        // parse parameters
        int argi = 1;
        for (argi = 1; argi < argc; argi++) {
                const char *arg = argv[argi];
                if (arg[0] != '-')
                        break;

                if (arg[1] == '\0') {
                        fprintf(stderr, "Error: Invalid option \"-\"\n");
                        return FSSH_B_BAD_VALUE;
                }

                for (int i = 1; arg[i]; i++) {
                        switch (arg[i]) {
                                case 'R':
                                        fprintf(stderr, "Sorry, recursive mode not supported "
                                                "yet.\n");
                                        return FSSH_B_BAD_VALUE;
                                default:
                                        fprintf(stderr, "Error: Unknown option \"-%c\"\n", arg[i]);
                                        return FSSH_B_BAD_VALUE;
                        }
                }
        }

        // get mode
        fssh_mode_t permissions;
        if (argi + 1 >= argc || !get_permissions(argv[argi++], permissions)) {
                printf("Usage: %s [ -R ] <octal mode> <file>...\n", argv[0]);
                return FSSH_B_BAD_VALUE;
        }

        fssh_struct_stat st;
        st.fssh_st_mode = permissions;

        // chmod loop
        for (; argi < argc; argi++) {
                const char *file = argv[argi];
                if (strlen(file) == 0) {
                        fprintf(stderr, "Error: An empty path is not a valid argument!\n");
                        return FSSH_B_BAD_VALUE;
                }

                fssh_status_t error = _kern_write_stat(-1, file, false, &st, sizeof(st),
                        FSSH_B_STAT_MODE);
                if (error != FSSH_B_OK) {
                        fprintf(stderr, "Error: Failed to change mode of \"%s\"!\n", file);
                        return error;
                }
        }

        return FSSH_B_OK;
}


static fssh_status_t
command_cat(int argc, const char* const* argv)
{
        size_t numBytes = 4096;
        int fileStart = 1;
        if (argc < 2 || strcmp(argv[1], "--help") == 0) {
                printf("Usage: %s [ -n ] [FILE]...\n"
                        "\t -n\tNumber of bytes to read\n",
                        argv[0]);
                return FSSH_B_OK;
        }

        bool isReadLengthGiven = false;
        if (argc > 3 && strcmp(argv[1], "-n") == 0) {
                fileStart += 2;
                numBytes = strtol(argv[2], NULL, 10);
                isReadLengthGiven = true;
        }

        const char* const* files = argv + fileStart;
        for (; *files; files++) {
                const char* file = *files;
                int fd = _kern_open(-1, file, FSSH_O_RDONLY, FSSH_O_RDONLY);
                if (fd < 0) {
                        fprintf(stderr, "error: %s\n", fssh_strerror(fd));
                        return FSSH_B_BAD_VALUE;
                }
                struct fssh_stat st;
                fssh_status_t error = _kern_read_stat(-1, file, false, &st, sizeof(st));
                if (error != FSSH_B_OK) {
                        fprintf(stderr, "Error: Failed to stat() \"%s\": %s\n", file,
                                fssh_strerror(error));
                        return error;
                }
                size_t fileLengthToRead;
                if (!isReadLengthGiven) {
                        fileLengthToRead = st.fssh_st_size;
                        numBytes = 4096;
                } else
                        fileLengthToRead = numBytes;
                size_t pos = 0;

                char buffer[numBytes + 1];
                while (fileLengthToRead > 0) {
                        if (fileLengthToRead < numBytes)
                                numBytes = fileLengthToRead;

                        ssize_t statusOrNumBytes = _kern_read(fd, pos, buffer, numBytes);
                        if (statusOrNumBytes != (ssize_t)numBytes) {
                                fprintf(stderr, "error: %s\n", fssh_strerror(statusOrNumBytes));
                                _kern_close(fd);
                                return FSSH_B_BAD_VALUE;
                        }
                        buffer[numBytes] = '\0';
                        printf("%s", buffer);
                        pos += numBytes;
                        fileLengthToRead -= numBytes;
                }
                printf("\n");
                _kern_close(fd);
        }

        return FSSH_B_OK;
}


static fssh_status_t
command_help(int argc, const char* const* argv)
{
        printf("supported commands:\n");
        CommandManager::Default()->ListCommands();
        return FSSH_B_OK;
}


static fssh_status_t
command_info(int argc, const char* const* argv)
{
        fssh_dev_t volumeID = get_volume_id();
        if (volumeID < 0)
                return volumeID;

        fssh_fs_info info;
        fssh_status_t status = _kern_read_fs_info(volumeID, &info);
        if (status != FSSH_B_OK)
                return status;

        printf("root inode:   %" FSSH_B_PRIdINO "\n", info.root);
        printf("flags:        ");
        print_flag(info.flags, FSSH_B_FS_HAS_QUERY, "Q", "-");
        print_flag(info.flags, FSSH_B_FS_HAS_ATTR, "A", "-");
        print_flag(info.flags, FSSH_B_FS_HAS_MIME, "M", "-");
        print_flag(info.flags, FSSH_B_FS_IS_SHARED, "S", "-");
        print_flag(info.flags, FSSH_B_FS_IS_PERSISTENT, "P", "-");
        print_flag(info.flags, FSSH_B_FS_IS_REMOVABLE, "R", "-");
        print_flag(info.flags, FSSH_B_FS_IS_READONLY, "-", "W");

        printf("\nblock size:   %" FSSH_B_PRIdOFF "\n", info.block_size);
        printf("I/O size:     %" FSSH_B_PRIdOFF "\n", info.io_size);
        printf("total size:   %s (%" FSSH_B_PRIdOFF " blocks)\n",
                byte_string(info.total_blocks, info.block_size), info.total_blocks);
        printf("free size:    %s (%" FSSH_B_PRIdOFF " blocks)\n",
                byte_string(info.free_blocks, info.block_size), info.free_blocks);
        printf("total nodes:  %" FSSH_B_PRIdOFF "\n", info.total_nodes);
        printf("free nodes:   %" FSSH_B_PRIdOFF "\n", info.free_nodes);
        printf("volume name:  %s\n", info.volume_name);
        printf("fs name:      %s\n", info.fsh_name);

        return FSSH_B_OK;
}


static fssh_status_t
command_ln(int argc, const char* const* argv)
{
        bool force = false;
        bool symbolic = false;
        bool dereference = true;

        // parse parameters
        int argi = 1;
        for (argi = 1; argi < argc; argi++) {
                const char *arg = argv[argi];
                if (arg[0] != '-')
                        break;

                if (arg[1] == '\0') {
                        fprintf(stderr, "Error: Invalid option \"-\"\n");
                        return FSSH_B_BAD_VALUE;
                }

                for (int i = 1; arg[i]; i++) {
                        switch (arg[i]) {
                                case 'f':
                                        force = true;
                                        break;
                                case 's':
                                        symbolic = true;
                                        break;
                                case 'n':
                                        dereference = false;
                                        break;
                                default:
                                        fprintf(stderr, "Error: Unknown option \"-%c\"\n", arg[i]);
                                        return FSSH_B_BAD_VALUE;
                        }
                }
        }

        if (argc - argi != 2) {
                fprintf(stderr, "Usage: %s [Options] <source> <target>\n", argv[0]);
                return FSSH_B_BAD_VALUE;
        }

        const char *source = argv[argi];
        const char *target = argv[argi + 1];

        // check, if the the target is an existing directory
        struct fssh_stat st;
        char targetBuffer[FSSH_B_PATH_NAME_LENGTH];
        fssh_status_t error = _kern_read_stat(-1, target, dereference, &st,
                sizeof(st));
        if (error == FSSH_B_OK) {
                if (FSSH_S_ISDIR(st.fssh_st_mode)) {
                        // get source leaf
                        char leaf[FSSH_B_FILE_NAME_LENGTH];
                        error = get_last_path_component(source, leaf, sizeof(leaf));
                        if (error != FSSH_B_OK) {
                                fprintf(stderr, "Error: Failed to get leaf name of source "
                                        "path: %s\n", fssh_strerror(error));
                                return error;
                        }

                        // compose a new path
                        int len = strlen(target) + 1 + strlen(leaf);
                        if (len > (int)sizeof(targetBuffer)) {
                                fprintf(stderr, "Error: Resulting target path is too long.\n");
                                return FSSH_B_BAD_VALUE;
                        }

                        strcpy(targetBuffer, target);
                        strcat(targetBuffer, "/");
                        strcat(targetBuffer, leaf);
                        target = targetBuffer;
                }
        }

        // check, if the target exists
        error = _kern_read_stat(-1, target, false, &st, sizeof(st));
        if (error == FSSH_B_OK) {
                if (!force) {
                        fprintf(stderr, "Error: Can't create link. \"%s\" is in the way.\n",
                                target);
                        return FSSH_B_FILE_EXISTS;
                }

                // unlink the entry
                error = _kern_unlink(-1, target);
                if (error != FSSH_B_OK) {
                        fprintf(stderr, "Error: Failed to remove \"%s\" to make way for "
                                "link: %s\n", target, fssh_strerror(error));
                        return error;
                }
        }

        // finally create the link
        if (symbolic) {
                error = _kern_create_symlink(-1, target, source,
                        FSSH_S_IRWXU | FSSH_S_IRWXG | FSSH_S_IRWXO);
        } else
                error = _kern_create_link(target, source);

        if (error != FSSH_B_OK) {
                fprintf(stderr, "Error: Failed to create link: %s\n",
                        fssh_strerror(error));
        }

        return error;
}


static fssh_status_t
command_ls(int argc, const char* const* argv)
{
        const char* const currentDirFiles[] = { ".", NULL };
        const char* const* files;
        if (argc >= 2)
                files = argv + 1;
        else
                files = currentDirFiles;

        for (; *files; files++) {
                const char* file = *files;
                // stat file
                struct fssh_stat st;
                fssh_status_t error = _kern_read_stat(-1, file, false, &st, sizeof(st));
                if (error != FSSH_B_OK) {
                        fprintf(stderr, "Error: Failed to stat() \"%s\": %s\n", file,
                                fssh_strerror(error));
                        continue;
                }

                // if it is a directory, print its entries
                if (FSSH_S_ISDIR(st.fssh_st_mode)) {
                        printf("%s:\n", file);

                        // open dir
                        int fd = _kern_open_dir(-1, file);
                        if (fd < 0) {
                                fprintf(stderr, "Error: Failed to open dir \"%s\": %s\n",
                                        file, fssh_strerror(fd));
                                continue;
                        }

                        // iterate through the entries
                        char buffer[sizeof(fssh_dirent) + FSSH_B_FILE_NAME_LENGTH];
                        fssh_dirent* entry = (fssh_dirent*)buffer;
                        fssh_ssize_t entriesRead = 0;
                        while ((entriesRead = _kern_read_dir(fd, entry, sizeof(buffer), 1))
                                        == 1) {
                                list_entry(file, entry->d_name);
                        }

                        if (entriesRead < 0) {
                                fprintf(stderr, "Error: reading dir \"%s\" failed: %s\n",
                                        file, fssh_strerror(entriesRead));
                        }

                        // close dir
                        error = _kern_close(fd);
                        if (error != FSSH_B_OK) {
                                fprintf(stderr, "Error: Closing dir \"%s\" (fd: %d) failed: "
                                        "%s\n", file, fd, fssh_strerror(error));
                                continue;
                        }
                } else
                        list_entry(file);
        }

        return FSSH_B_OK;
}


static fssh_status_t
command_touch(int argc, const char* const* argv)
{
        int argi = 1;
        if (argi >= argc) {
                printf("Usage: %s <file>...\n", argv[0]);
                return FSSH_B_BAD_VALUE;
        }

        for (; argi < argc; argi++) {
                const char* file = argv[argi];
                if (strlen(file) == 0) {
                        fprintf(stderr, "Error: An empty path is not a valid argument!\n");
                        return FSSH_B_BAD_VALUE;
                }

                fprintf(stderr, "creating file: %s\n", file);
                fssh_status_t error = create_file(file);
                if (error != FSSH_B_OK)
                        return error;
        }

        return FSSH_B_OK;
}


static fssh_status_t
command_mkdir(int argc, const char* const* argv)
{
        bool createParents = false;

        // parse parameters
        int argi = 1;
        for (argi = 1; argi < argc; argi++) {
                const char *arg = argv[argi];
                if (arg[0] != '-')
                        break;

                if (arg[1] == '\0') {
                        fprintf(stderr, "Error: Invalid option \"-\"\n");
                        return FSSH_B_BAD_VALUE;
                }

                for (int i = 1; arg[i]; i++) {
                        switch (arg[i]) {
                                case 'p':
                                        createParents = true;
                                        break;
                                default:
                                        fprintf(stderr, "Error: Unknown option \"-%c\"\n", arg[i]);
                                        return FSSH_B_BAD_VALUE;
                        }
                }
        }

        if (argi >= argc) {
                printf("Usage: %s [ -p ] <dir>...\n", argv[0]);
                return FSSH_B_BAD_VALUE;
        }

        // create loop
        for (; argi < argc; argi++) {
                const char *dir = argv[argi];
                if (strlen(dir) == 0) {
                        fprintf(stderr, "Error: An empty path is not a valid argument!\n");
                        return FSSH_B_BAD_VALUE;
                }

                fssh_status_t error = create_dir(dir, createParents);
                if (error != FSSH_B_OK)
                        return error;
        }

        return FSSH_B_OK;
}


static fssh_status_t
command_mkindex(int argc, const char* const* argv)
{
        if (argc < 2) {
                fprintf(stderr, "Usage: %s [-t <type>] <index name>\n", argv[0]);
                return FSSH_B_BAD_VALUE;
        }

        int fileArg = 1;
        int type = FSSH_B_STRING_TYPE;

        if (argc > 3 && strcmp(argv[1], "-t") == 0) {
                fileArg = 3;
                if (strcmp(argv[2], "string") == 0)
                        type = FSSH_B_STRING_TYPE;
                else if (strcmp(argv[2], "int32") == 0)
                        type = FSSH_B_INT32_TYPE;
                else {
                        fprintf(stderr, "Unhandled attribute type %s\n", argv[2]);
                        return FSSH_B_BAD_VALUE;
                }
        }

        const char* indexName = argv[fileArg];

        // get the volume ID
        fssh_dev_t volumeID = get_volume_id();
        if (volumeID < 0)
                return volumeID;

        // create the index
        fssh_status_t error =_kern_create_index(volumeID, indexName, type, 0);
        if (error != FSSH_B_OK) {
                fprintf(stderr, "Error: Failed to create index \"%s\": %s\n",
                        indexName, fssh_strerror(error));
                return error;
        }

        return FSSH_B_OK;
}


static fssh_status_t
command_mv(int argc, const char* const* argv)
{
        bool force = false;

        // parse parameters
        int argi = 1;
        for (argi = 1; argi < argc; argi++) {
                const char *arg = argv[argi];
                if (arg[0] != '-')
                        break;

                if (arg[1] == '\0') {
                        fprintf(stderr, "Error: Invalid option \"-\"\n");
                        return FSSH_B_BAD_VALUE;
                }

                for (int i = 1; arg[i]; i++) {
                        switch (arg[i]) {
                                case 'f':
                                        force = true;
                                        break;
                                default:
                                        fprintf(stderr, "Error: Unknown option \"-%c\"\n", arg[i]);
                                        return FSSH_B_BAD_VALUE;
                        }
                }
        }

        // check params
        int count = argc - 1 - argi;
        if (count <= 0) {
                fprintf(stderr, "Usage: %s [-f] <file>... <target>\n", argv[0]);
                return FSSH_B_BAD_VALUE;
        }

        const char* target = argv[argc - 1];

        // stat the target
        struct fssh_stat st;
        fssh_status_t status = _kern_read_stat(-1, target, true, &st, sizeof(st));
        if (status != FSSH_B_OK && count != 1) {
                fprintf(stderr, "Error: Failed to stat target \"%s\": %s\n", target,
                        fssh_strerror(status));
                return status;
        }

        if (status == FSSH_B_OK && FSSH_S_ISDIR(st.fssh_st_mode)) {
                // move several entries
                int targetDir = _kern_open_dir(-1, target);
                if (targetDir < 0) {
                        fprintf(stderr, "Error: Failed to open dir \"%s\": %s\n", target,
                                fssh_strerror(targetDir));
                        return targetDir;
                }

                // move loop
                for (; argi < argc - 1; argi++) {
                        status = move_entry(-1, argv[argi], targetDir, argv[argi], force);
                        if (status != FSSH_B_OK) {
                                _kern_close(targetDir);
                                return status;
                        }
                }

                _kern_close(targetDir);
                return FSSH_B_OK;
        }

        // rename single entry
        return move_entry(-1, argv[argi], -1, target, force);
}


static fssh_status_t
command_query(int argc, const char* const* argv)
{
        if (argc != 2) {
                fprintf(stderr, "Usage: %s <query string>\n", argv[0]);
                return FSSH_B_BAD_VALUE;
        }

        const char* query = argv[1];

        // get the volume ID
        fssh_dev_t volumeID = get_volume_id();
        if (volumeID < 0)
                return volumeID;

        // open query
        int fd = _kern_open_query(volumeID, query, strlen(query), 0, -1, -1);
        if (fd < 0) {
                fprintf(stderr, "Error: Failed to open query: %s\n", fssh_strerror(fd));
                return fd;
        }

        // iterate through the entries
        fssh_status_t error = FSSH_B_OK;
        char buffer[sizeof(fssh_dirent) + FSSH_B_FILE_NAME_LENGTH];
        fssh_dirent* entry = (fssh_dirent*)buffer;
        fssh_ssize_t entriesRead = 0;
        while ((entriesRead = _kern_read_dir(fd, entry, sizeof(buffer), 1)) == 1) {
                char path[FSSH_B_PATH_NAME_LENGTH];
                error = _kern_entry_ref_to_path(volumeID, entry->d_pino, entry->d_name,
                        path, sizeof(path));
                if (error == FSSH_B_OK) {
                        printf("  %s\n", path);
                } else {
                        fprintf(stderr, "  failed to resolve entry (%8" FSSH_B_PRIdINO
                                ", \"%s\")\n", entry->d_pino, entry->d_name);
                }
        }

        if (entriesRead < 0) {
                fprintf(stderr, "Error: reading query failed: %s\n",
                        fssh_strerror(entriesRead));
        }

        // close query
        error = _kern_close(fd);
        if (error != FSSH_B_OK) {
                fprintf(stderr, "Error: Closing query (fd: %d) failed: %s\n",
                        fd, fssh_strerror(error));
        }

        return error;
}


static fssh_status_t
command_quit(int argc, const char* const* argv)
{
        return COMMAND_RESULT_EXIT;
}


static fssh_status_t
command_rm(int argc, const char* const* argv)
{
        bool recursive = false;
        bool force = false;

        // parse parameters
        int argi = 1;
        for (argi = 1; argi < argc; argi++) {
                const char *arg = argv[argi];
                if (arg[0] != '-')
                        break;

                if (arg[1] == '\0') {
                        fprintf(stderr, "Error: Invalid option \"-\"\n");
                        return FSSH_B_BAD_VALUE;
                }

                for (int i = 1; arg[i]; i++) {
                        switch (arg[i]) {
                                case 'f':
                                        force = true;
                                        break;
                                case 'r':
                                        recursive = true;
                                        break;
                                default:
                                        fprintf(stderr, "Error: Unknown option \"-%c\"\n", arg[i]);
                                        return FSSH_B_BAD_VALUE;
                        }
                }
        }

        // check params
        if (argi >= argc) {
                fprintf(stderr, "Usage: %s [ -r ] <file>...\n", argv[0]);
                return FSSH_B_BAD_VALUE;
        }

        // remove loop
        for (; argi < argc; argi++) {
                fssh_status_t error = remove_entry(-1, argv[argi], recursive, force);
                if (error != FSSH_B_OK)
                        return error;
        }

        return FSSH_B_OK;
}


static fssh_status_t
command_sync(int argc, const char* const* argv)
{
        fssh_status_t error = _kern_sync();
        if (error != FSSH_B_OK) {
                fprintf(stderr, "Error: syncing: %s\n", fssh_strerror(error));
                return error;
        }

        return FSSH_B_OK;
}


static fssh_status_t
command_ioctl(int argc, const char* const* argv)
{
        if (argc != 2) {
                fprintf(stderr, "Usage: %s <opcode>\n", argv[0]);
                return FSSH_B_BAD_VALUE;
        }

        int rootDir = _kern_open_dir(-1, "/myfs");
        if (rootDir < 0)
                return rootDir;

        fssh_status_t status = _kern_ioctl(rootDir, atoi(argv[1]), NULL, 0);

        _kern_close(rootDir);

        if (status != FSSH_B_OK) {
                fprintf(stderr, "Error: ioctl failed: %s\n", fssh_strerror(status));
                return status;
        }

        return FSSH_B_OK;
}


static void
register_commands()
{
        CommandManager::Default()->AddCommands(
                command_cd,                     "cd",                   "change current directory",
                command_chmod,          "chmod",                "change file permissions",
                command_cp,                     "cp",                   "copy files and directories",
                command_cat,            "cat",  "concatenate file(s) to stdout",
                command_help,           "help",                 "list supported commands",
                command_info,           "info",                 "prints volume informations",
                command_ioctl,          "ioctl",                "ioctl() on root, for FS debugging only",
                command_ln,                     "ln",                   "create a hard or symbolic link",
                command_ls,                     "ls",                   "list files or directories",
                command_mkdir,          "mkdir",                "create directories",
                command_mkindex,        "mkindex",              "create an index",
                command_mv,                     "mv",                   "move/rename files and directories",
                command_query,          "query",                "query for files",
                command_quit,           "quit/exit",    "quit the shell",
                command_rm,                     "rm",                   "remove files and directories",
                command_sync,           "sync",                 "syncs the file system",
                command_touch,          "touch",                "create empty file",
                NULL
        );
}


// #pragma mark - ArgVector


class ArgVector {
public:
        ArgVector()
                : fArgc(0),
                  fArgv(NULL)
        {
        }

        ~ArgVector()
        {
                _Cleanup();
        }

        int Argc() const
        {
                return fArgc;
        }

        const char* const* Argv() const
        {
                return fArgv;
        }

        bool Parse(const char* commandLine)
        {
                _Cleanup();

                // init temporary arg/argv storage
                std::string currentArg;
                std::vector<std::string> argVector;

                fCurrentArg = &currentArg;
                fCurrentArgStarted = false;
                fArgVector = &argVector;

                for (; *commandLine; commandLine++) {
                        char c = *commandLine;

                        // whitespace delimits args and is otherwise ignored
                        if (isspace(c)) {
                                _PushCurrentArg();
                                continue;
                        }

                        switch (c) {
                                case '\'':
                                        // quoted string -- no quoting
                                        while (*++commandLine != '\'') {
                                                c = *commandLine;
                                                if (c == '\0') {
                                                        fprintf(stderr, "Error: Unterminated quoted "
                                                                "string.\n");
                                                        return false;
                                                }
                                                _PushCharacter(c);
                                        }
                                        break;

                                case '"':
                                        // quoted string -- some quoting
                                        while (*++commandLine != '"') {
                                                c = *commandLine;
                                                if (c == '\0') {
                                                        fprintf(stderr, "Error: Unterminated quoted "
                                                                "string.\n");
                                                        return false;
                                                }

                                                if (c == '\\') {
                                                        c = *++commandLine;
                                                        if (c == '\0') {
                                                                fprintf(stderr, "Error: Unterminated quoted "
                                                                        "string.\n");
                                                                return false;
                                                        }

                                                        // only '\' and '"' can be quoted, otherwise the
                                                        // the '\' is treated as a normal char
                                                        if (c != '\\' && c != '"')
                                                                _PushCharacter('\\');
                                                }

                                                _PushCharacter(c);
                                        }
                                        break;

                                case '\\':
                                        // quoted char
                                        c = *++commandLine;
                                        if (c == '\0') {
                                                fprintf(stderr, "Error: Command line ends with "
                                                        "'\\'.\n");
                                                return false;
                                        }
                                        _PushCharacter(c);
                                        break;

                                default:
                                        // normal char
                                        _PushCharacter(c);
                                        break;
                        }
                }

                // commit last arg
                _PushCurrentArg();

                // build arg vector
                fArgc = argVector.size();
                fArgv = new char*[fArgc + 1];
                for (int i = 0; i < fArgc; i++) {
                        int len = argVector[i].length();
                        fArgv[i] = new char[len + 1];
                        memcpy(fArgv[i], argVector[i].c_str(), len + 1);
                }
                fArgv[fArgc] = NULL;

                return true;
        }

private:
        void _Cleanup()
        {
                if (fArgv) {
                        for (int i = 0; i < fArgc; i++)
                                delete[] fArgv[i];
                        delete[] fArgv;
                }
        }

        void _PushCurrentArg()
        {
                if (fCurrentArgStarted) {
                        fArgVector->push_back(*fCurrentArg);
                        fCurrentArgStarted = false;
                }
        }

        void _PushCharacter(char c)
        {
                if (!fCurrentArgStarted) {
                        *fCurrentArg = "";
                        fCurrentArgStarted = true;
                }

                *fCurrentArg += c;
        }

private:
        // temporaries
        std::string*                            fCurrentArg;
        bool                                            fCurrentArgStarted;
        std::vector<std::string>*       fArgVector;

        int                                                     fArgc;
        char**                                          fArgv;
};


// #pragma mark - input loop


static char*
read_command_line(char* buffer, int bufferSize)
{
        // print prompt (including cwd, if available)
        char directory[FSSH_B_PATH_NAME_LENGTH];
        if (_kern_getcwd(directory, sizeof(directory)) == FSSH_B_OK)
                printf("fssh:%s> ", directory);
        else
                printf("fssh> ");
        fflush(stdout);

        // read input line
        return fgets(buffer, bufferSize, stdin);
}


static void
input_loop(bool interactive)
{
        static const int kInputBufferSize = 100 * 1024;
        char* inputBuffer = new char[kInputBufferSize];

        for (;;) {
                // read command line
                if (interactive) {
                        if (!read_command_line(inputBuffer, kInputBufferSize))
                                break;
                } else {
                        if (!get_external_command(inputBuffer, kInputBufferSize))
                                break;
                }

                // construct argv vector
                int result = FSSH_B_BAD_VALUE;
                ArgVector argVector;
                if (argVector.Parse(inputBuffer) && argVector.Argc() > 0) {
                        int argc = argVector.Argc();
                        const char* const* argv = argVector.Argv();

                        // find command
                        Command* command = CommandManager::Default()->FindCommand(argv[0]);
                        if (command) {
                                // execute it
                                result = command->Do(argc, argv);
                                if (result == COMMAND_RESULT_EXIT) {
                                        if (!interactive)
                                                reply_to_external_command(0);
                                        break;
                                }
                        } else {
                                fprintf(stderr, "Error: Invalid command \"%s\". Type \"help\" "
                                        "for a list of supported commands\n", argv[0]);
                        }
                }

                if (!interactive)
                        reply_to_external_command(fssh_to_host_error(result));
        }

        if (!interactive)
                external_command_cleanup();

        delete[] inputBuffer;
}


static int
standard_session(const char* device, const char* fsName, bool interactive)
{
        // mount FS
        fssh_dev_t fsDev = _kern_mount(kMountPoint, device, fsName, 0, NULL, 0);
        if (fsDev < 0) {
                fprintf(stderr, "Error: Mounting FS failed: %s\n",
                        fssh_strerror(fsDev));
                return 1;
        }

        // register commands
        register_commands();
        register_additional_commands();

        // process commands
        input_loop(interactive);

        // unmount FS
        _kern_setcwd(-1, "/");  // avoid a "busy" vnode
        fssh_status_t error = _kern_unmount(kMountPoint, 0);
        if (error != FSSH_B_OK) {
                fprintf(stderr, "Error: Unmounting FS failed: %s\n",
                        fssh_strerror(error));
                return 1;
        }

        return 0;
}


static int
initialization_session(const char* device, const char* fsName,
        const char* volumeName, const char* initParameters)
{
        fssh_status_t error = _kern_initialize_volume(fsName, device,
                volumeName, initParameters);
        if (error != FSSH_B_OK) {
                fprintf(stderr, "Error: Initializing volume failed: %s\n",
                        fssh_strerror(error));
                return 1;
        }

        return 0;
}


static void
print_usage(bool error)
{
        fprintf((error ? stderr : stdout),
                "Usage: %s [ --start-offset <startOffset>]\n"
                "          [ --end-offset <endOffset>] [-n] <device>\n"
                "       %s [ --start-offset <startOffset>]\n"
                "          [ --end-offset <endOffset>]\n"
                "          --initialize [-n] <device> <volume name> "
                        "[ <init parameters> ]\n",
                sArgv[0], sArgv[0]
        );
}


static void
print_usage_and_exit(bool error)
{
        print_usage(error);
        exit(error ? 1 : 0);
}


}       // namespace FSShell


using namespace FSShell;


int
main(int argc, const char* const* argv)
{
        sArgc = argc;
        sArgv = argv;

        // process arguments
        bool interactive = true;
        bool initialize = false;
        const char* device = NULL;
        const char* volumeName = NULL;
        const char* initParameters = NULL;
        fssh_off_t startOffset = 0;
        fssh_off_t endOffset = -1;

        // eat options
        int argi = 1;
        while (argi < argc && argv[argi][0] == '-') {
                const char* arg = argv[argi++];
                if (strcmp(arg, "--help") == 0) {
                        print_usage_and_exit(false);
                } else if (strcmp(arg, "--initialize") == 0) {
                        initialize = true;
                } else if (strcmp(arg, "-n") == 0) {
                        interactive = false;
                } else if (strcmp(arg, "--start-offset") == 0) {
                        if (argi >= argc)
                                print_usage_and_exit(true);
                        startOffset = atoll(argv[argi++]);
                } else if (strcmp(arg, "--end-offset") == 0) {
                        if (argi >= argc)
                                print_usage_and_exit(true);
                        endOffset = atoll(argv[argi++]);
                } else {
                        print_usage_and_exit(true);
                }
        }

        // get device
        if (argi >= argc)
                print_usage_and_exit(true);
        device = argv[argi++];

        // get volume name and init parameters
        if (initialize) {
                // volume name
                if (argi >= argc)
                        print_usage_and_exit(true);
                volumeName = argv[argi++];

                // (optional) init paramaters
                if (argi < argc)
                        initParameters = argv[argi++];
        }

        // more parameters are excess
        if (argi < argc)
                print_usage_and_exit(true);

        // get FS module
        if (!modules[0]) {
                fprintf(stderr, "Error: Couldn't find FS module!\n");
                return 1;
        }
        const char* fsName = modules[0]->name;

        fssh_status_t error;

        // init kernel
        error = init_kernel();
        if (error != FSSH_B_OK) {
                fprintf(stderr, "Error: Initializing kernel failed: %s\n",
                        fssh_strerror(error));
                return error;
        }

        // restrict access if requested
        if (startOffset != 0 || endOffset != -1)
                add_file_restriction(device, startOffset, endOffset);

        // start the action
        int result;
        if (initialize) {
                result = initialization_session(device, fsName, volumeName,
                        initParameters);
        } else
                result = standard_session(device, fsName, interactive);

        return result;
}