/***************************************************************************
 *             __________               __   ___.
 *   Open      \______   \ ____   ____ |  | _\_ |__   _______  ___
 *   Source     |       _//  _ \_/ ___\|  |/ /| __ \ /  _ \  \/  /
 *   Jukebox    |    |   (  <_> )  \___|    < | \_\ (  <_> > <  <
 *   Firmware   |____|_  /\____/ \___  >__|_ \|___  /\____/__/\_ \
 *                     \/            \/     \/    \/            \/
 *
 * Copyright (C) 2002 by Björn Stenberg
 * Copyright (C) 2014 by Michael Sevakis
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
 * KIND, either express or implied.
 *
 ****************************************************************************/
#define RB_FILESYSTEM_OS
#include "config.h"
#include "system.h"
#include <string.h>
#include <errno.h>
#include "debug.h"
#include "file.h"
#include "fileobj_mgr.h"
#include "disk_cache.h"
#include "rb_namespace.h"
#include "string-extra.h"

/* Define LOGF_ENABLE to enable logf output in this file */
//#define LOGF_ENABLE
#ifdef LOGF_ENABLE
#include "logf.h"
#undef DEBUGF
#define DEBUGF logf
#endif


/**
 * These functions provide a roughly POSIX-compatible file I/O API.
 */

/* structure used for open file descriptors */
static struct filestr_desc
{
    struct filestr_base stream; /* basic stream info (first!) */
    file_size_t         offset; /* current offset for stream */
    file_size_t         *sizep; /* shortcut to file size in fileobj */
} open_streams[MAX_OPEN_FILES];

/* check and return a struct filestr_desc* from a file descriptor number */
static struct filestr_desc * get_filestr(int fildes)
{
    struct filestr_desc *file = &open_streams[fildes];

    if ((unsigned int)fildes >= MAX_OPEN_FILES)
        file = NULL;
    else if (file->stream.flags & FDO_BUSY)
        return file;

    DEBUGF("fildes %d: bad file number\n", fildes);
    errno = (file && (file->stream.flags & FD_NONEXIST)) ? ENXIO : EBADF;
    return NULL;
}

#define GET_FILESTR(type, fildes) \
    ({                                                     \
        file_internal_lock_##type();                       \
        struct filestr_desc * _file = get_filestr(fildes); \
        if (_file)                                         \
            FILESTR_LOCK(type, &_file->stream);            \
        else                                               \
            file_internal_unlock_##type();                 \
        _file;                                             \
    })

/* release the lock on the filestr_desc* */
#define RELEASE_FILESTR(type, file) \
    ({                                         \
        FILESTR_UNLOCK(type, &(file)->stream); \
        file_internal_unlock_##type();         \
    })

/* find a free file descriptor */
static int alloc_filestr(struct filestr_desc **filep)
{
    for (int fildes = 0; fildes < MAX_OPEN_FILES; fildes++)
    {
        struct filestr_desc *file = &open_streams[fildes];
        if (!file->stream.flags)
        {
            *filep = file;
            return fildes;
        }
    }

    DEBUGF("Too many files open\n");
    return -1;
}

/* return the file size in sectors */
static inline unsigned long filesize_sectors(uint16_t sector_size, file_size_t size)
{
    /* overflow proof whereas "(x + y - 1) / y" is not */
    unsigned long numsectors = size / sector_size;

    if (size % sector_size)
        numsectors++;

    return numsectors;
}

/* flush a dirty cache buffer */
static int flush_cache(struct filestr_desc *file)
{
    int rc;
    struct filestr_cache *cachep = file->stream.cachep;

    DEBUGF("Flushing dirty sector cache (%llu)\n", (uint64_t)cachep->sector);

    if (fat_query_sectornum(&file->stream.fatstr) != cachep->sector)
    {
        /* get on the correct sector */
        rc = fat_seek(&file->stream.fatstr, cachep->sector);
        if (rc < 0)
            FILE_ERROR(EIO, rc * 10 - 1);
    }

    rc = fat_readwrite(&file->stream.fatstr, 1, cachep->buffer, true);
    if (rc < 0)
    {
        if (rc == FAT_RC_ENOSPC)
            FILE_ERROR(ENOSPC, RC);
        else
            FILE_ERROR(EIO, rc * 10 - 2);
    }

    cachep->flags = 0;
    return 1;
file_error:
    DEBUGF("Failed flushing cache: %d\n", rc);
    return rc;
}

static void discard_cache(struct filestr_desc *file)
{
    struct filestr_cache *const cachep = file->stream.cachep;
    cachep->flags = 0;
}

/* set the file pointer */
static off_t lseek_internal(struct filestr_desc *file, off_t offset,
                            int whence)
{
    off_t rc;
    file_size_t pos;

    file_size_t size = MIN(*file->sizep, FILE_SIZE_MAX);

    switch (whence)
    {
    case SEEK_SET:
        if (offset < 0 || (file_size_t)offset > size)
            FILE_ERROR(EINVAL, -1);

        pos = offset;
        break;

    case SEEK_CUR:
        if ((offset < 0 && (file_size_t)-offset > file->offset) ||
            (offset > 0 && (file_size_t)offset > size - file->offset))
            FILE_ERROR(EINVAL, -1);

        pos = file->offset + offset;
        break;

    case SEEK_END:
        if (offset > 0 || (file_size_t)-offset > size)
            FILE_ERROR(EINVAL, -1);

        pos = size + offset;
        break;

    default:
        FILE_ERROR(EINVAL, -1);
    }

    file->offset = pos;

    return pos;
file_error:
    return rc;
}

/* Handle syncing all file's streams to the truncation */
static void handle_truncate(struct filestr_desc * const file, file_size_t size)
{
    uint16_t sector_size = fat_file_sector_size(IF_MV(file->stream.fatstr.fatfilep));
    unsigned long filesectors = filesize_sectors(sector_size, size);

    struct filestr_base *s = NULL;
    while ((s = fileobj_get_next_stream(&file->stream, s)))
    {
        /* caches with data beyond new extents are invalid */
        sector_t sector = s->cachep->sector;
        if (sector != INVALID_SECNUM && sector >= filesectors)
            filestr_discard_cache(s);

        /* files outside bounds must be rewound */
        if (fat_query_sectornum(&s->fatstr) > filesectors)
            fat_seek_to_stream(&s->fatstr, &file->stream.fatstr);

        /* clip file offset too if needed */
        struct filestr_desc *f = (struct filestr_desc *)s;
        if (f->offset > size)
            f->offset = size;
    }
}

/* truncate the file to the specified length */
static int ftruncate_internal(struct filestr_desc *file, file_size_t size,
                              bool write_now)
{
    int rc = 0, rc2 = 1;

    file_size_t cursize = *file->sizep;
    file_size_t truncsize = MIN(size, cursize);

    uint16_t sector_size = fat_file_sector_size(IF_MV(file->stream.fatstr.fatfilep));

    if (write_now)
    {
        unsigned long sector = filesize_sectors(sector_size, truncsize);
        struct filestr_cache *const cachep = file->stream.cachep;

        if (cachep->flags == (FSC_NEW|FSC_DIRTY) &&
            cachep->sector + 1 == sector)
        {
            /* sector created but may have never been added to the cluster
               chain; flush it now or the subsequent may fail */
            rc2 = flush_cache(file);
            if (rc2 == FAT_RC_ENOSPC)
            {
                /* no space left on device; further truncation needed */
                discard_cache(file);
                truncsize = ALIGN_DOWN(truncsize - 1, sector_size);
                sector--;
                rc = rc2;
            }
            else if (rc2 < 0)
                FILE_ERROR(ERRNO, rc2 * 10 - 1);
        }

        rc2 = fat_seek(&file->stream.fatstr, sector);
        if (rc2 < 0)
            FILE_ERROR(EIO, rc2 * 10 - 2);

        rc2 = fat_truncate(&file->stream.fatstr);
        if (rc2 < 0)
            FILE_ERROR(EIO, rc2 * 10 - 3);

        /* never needs to be done this way again since any data beyond the
           cached size is now gone */
        fileobj_change_flags(&file->stream, 0, FO_TRUNC);
    }
    /* else just change the cached file size */

    if (truncsize < cursize)
    {
        *file->sizep = truncsize;
        handle_truncate(file, truncsize);
    }

    /* if truncation was partially successful, it effectively destroyed
       everything after the truncation point; still, indicate failure
       after adjusting size */
    if (rc2 == 0)
        FILE_ERROR(EIO, -4);
    else if (rc2 < 0)
        FILE_ERROR(ERRNO, rc2);

file_error:
    return rc;
}

/* flush back all outstanding writes to the file */
static int fsync_internal(struct filestr_desc *file)
{
    /* call only when holding WRITER lock (updates directory entries) */
    int rc = 0;

    file_size_t size = *file->sizep;
    unsigned int foflags = fileobj_get_flags(&file->stream);
    uint16_t sector_size = fat_file_sector_size(IF_MV(file->stream.fatstr.fatfilep));

    /* flush sector cache? */
    struct filestr_cache *const cachep = file->stream.cachep;
    if (cachep->flags & FSC_DIRTY)
    {
        int rc2 = flush_cache(file);
        if (rc2 == FAT_RC_ENOSPC && (cachep->flags & FSC_NEW))
        {
            /* no space left on device so this must be dropped */
            discard_cache(file);
            size = ALIGN_DOWN(size - 1, sector_size);
            foflags |= FO_TRUNC;
            rc = rc2;
        }
        else if (rc2 < 0)
            FILE_ERROR(ERRNO, rc2 * 10 - 1);
    }

    /* truncate? */
    if (foflags & FO_TRUNC)
    {
        int rc2 = ftruncate_internal(file, size, rc == 0);
        if (rc2 < 0)
            FILE_ERROR(ERRNO, rc2 * 10 - 2);
    }

file_error:;
    /* tie up all loose ends (try to close the file even if failing) */
    int rc2 = fat_closewrite(&file->stream.fatstr, size,
                             get_dir_fatent_dircache());
    if (rc2 >= 0)
        fileop_onsync_internal(&file->stream); /* dir_fatent is implicit arg */

    if (rc2 < 0 && rc >= 0)
    {
        errno = EIO;
        rc = rc2 * 10 - 3;
    }

    return rc;
}

/* finish with the file and free resources */
static int close_internal(struct filestr_desc *file)
{
    /* call only when holding WRITER lock (updates directory entries) */
    int rc;

    if ((file->stream.flags & (FD_WRITE|FD_NONEXIST)) == FD_WRITE)
    {
        rc = fsync_internal(file);
        if (rc < 0)
            FILE_ERROR(ERRNO, rc * 10 - 1);
    }

    rc = 0;
file_error:;
    int rc2 = close_stream_internal(&file->stream);
    if (rc2 < 0 && rc >= 0)
        rc = rc2 * 10 - 2;
    return rc;
}

/* actually do the open gruntwork */
static int open_internal_inner2(const char *path,
                                struct filestr_desc *file,
                                unsigned int callflags,
                                int oflag)
{
    int rc;

    struct path_component_info compinfo;

    if (oflag & O_CREAT)
        callflags |= FF_PARENTINFO;

    rc = open_stream_internal(path, callflags, &file->stream, &compinfo);
    if (rc < 0)
    {
        DEBUGF("Open failed: %d\n", rc);
        FILE_ERROR_RETURN(ERRNO, rc * 10 - 1);
    }

    bool created = false;

    if (rc > 0)
    {
        if (oflag & O_EXCL)
        {
            DEBUGF("File exists\n");
            FILE_ERROR(EEXIST, -2);
        }

        if (compinfo.attr & ATTR_DIRECTORY)
        {
            if ((callflags & FD_WRITE) || !(callflags & FF_ANYTYPE))
            {
                DEBUGF("File is a directory\n");
                FILE_ERROR(EISDIR, -3);
            }

            compinfo.filesize = MAX_DIRECTORY_SIZE; /* allow file ops */
        }
    }
    else if (oflag & O_CREAT)
    {
        if (compinfo.attr & ATTR_DIRECTORY)
        {
            DEBUGF("File is a directory\n");
            FILE_ERROR(EISDIR, -5);
        }

        /* not found; try to create it */

        callflags &= ~FO_TRUNC;
        rc = create_stream_internal(&compinfo.parentinfo, compinfo.name,
                                    compinfo.length, ATTR_NEW_FILE, callflags,
                                    &file->stream);
        if (rc < 0)
            FILE_ERROR(ERRNO, rc * 10 - 6);

        created = true;
    }
    else
    {
        DEBUGF("File not found\n");
        FILE_ERROR(ENOENT, -7);
    }

    fat_rewind(&file->stream.fatstr);
    file->sizep = fileobj_get_sizep(&file->stream);
    file->offset = 0;

    if (!created)
    {
        /* size from storage applies to first stream only otherwise it's
           already up to date */
        const bool first = fileobj_get_flags(&file->stream) & FO_SINGLE;
        if (first)
            *file->sizep = compinfo.filesize;

        if (callflags & FO_TRUNC)
        {
            /* if the file is kind of "big" then free some space now */
            rc = ftruncate_internal(file, 0, *file->sizep >= O_TRUNC_THRESH);
            if (rc < 0)
            {
                DEBUGF("O_TRUNC failed: %d\n", rc);
                FILE_ERROR(ERRNO, rc * 10 - 4);
            }
        }
    }

    rc = 0;
file_error:
    if (rc < 0)
        close_stream_internal(&file->stream);

    return rc;
}

/* allocate a file descriptor, if needed, assemble stream flags and open
   a new stream */
static int open_internal_inner1(const char *path, int oflag,
                                unsigned int callflags)
{
    DEBUGF("%s(path=\"%s\",oflag=%X,callflags=%X)\n", __func__,
           path, oflag, callflags);

    int rc;

    struct filestr_desc *file;
    int fildes = alloc_filestr(&file);
    if (fildes < 0)
        FILE_ERROR(EMFILE, -1);

    callflags &= ~FDO_MASK;

    if (oflag & O_ACCMODE)
    {
        callflags |= FD_WRITE;

        if ((oflag & O_ACCMODE) == O_WRONLY)
            callflags |= FD_WRONLY;

        if (oflag & O_APPEND)
            callflags |= FD_APPEND;

        if (oflag & O_TRUNC)
            callflags |= FO_TRUNC;
    }
    else if (oflag & O_TRUNC)
    {
        /* O_TRUNC requires write mode */
        DEBUGF("No write mode but have O_TRUNC\n");
        FILE_ERROR(EINVAL, -2);
    }

    /* O_CREAT and O_APPEND are fine without write mode
     * for the former, an empty file is created but no data may be written
     * for the latter, no append will be allowed anyway */
    if (!(oflag & O_CREAT))
        oflag &= ~O_EXCL; /* result is undefined: we choose "ignore" */

    rc = open_internal_inner2(path, file, callflags, oflag);
    if (rc < 0)
        FILE_ERROR(ERRNO, rc * 10 - 3);

    return fildes;

file_error:
    if (fildes >= 0)
        close(fildes);
    return rc;
}

static int open_internal_locked(const char *path, int oflag,
                                unsigned int callflags)
{
    file_internal_lock_WRITER();
    int rc = open_internal_inner1(path, oflag, callflags);
    file_internal_unlock_WRITER();
    return rc;
}

/* fill a cache buffer with a new sector */
static int readwrite_fill_cache(struct filestr_desc *file, unsigned long sector,
                                unsigned long filesectors, bool write)
{
    /* sector != cachep->sector should have been checked by now */

    int rc;
    struct filestr_cache *cachep = filestr_get_cache(&file->stream);

    if (cachep->flags & FSC_DIRTY)
    {
        rc = flush_cache(file);
        if (rc < 0)
            FILE_ERROR(ERRNO, rc * 10 - 1);
    }

    if (fat_query_sectornum(&file->stream.fatstr) != sector)
    {
        /* get on the correct sector */
        rc = fat_seek(&file->stream.fatstr, sector);
        if (rc < 0)
            FILE_ERROR(EIO, rc * 10 - 2);
    }

    if (!write || sector < filesectors)
    {
        /* only reading or this sector would have been flushed if the cache
           was previously needed for a different sector */
        rc = fat_readwrite(&file->stream.fatstr, 1, cachep->buffer, false);
        if (rc < 0)
            FILE_ERROR(rc == FAT_RC_ENOSPC ? ENOSPC : EIO, rc * 10 - 3);
    }
    else
    {
        /* create a fresh, shiny, new sector with that new sector smell */
        cachep->flags = FSC_NEW;
    }

    cachep->sector = sector;
    return 1;
file_error:
    DEBUGF("Failed caching sector: %d\n", rc);
    return rc;
}

/* read or write to part or all of the cache buffer */
static inline void readwrite_cache(struct filestr_cache *cachep, void *buf,
                                   unsigned long secoffset, size_t nbyte,
                                   bool write)
{
    void *dst, *cbufp = cachep->buffer + secoffset;

    if (write)
    {
        dst = cbufp;
        cachep->flags |= FSC_DIRTY;
    }
    else
    {
        dst = buf;
        buf = cbufp;
    }

    memcpy(dst, buf, nbyte);
}

/* read or write a partial sector using the file's cache */
static inline ssize_t readwrite_partial(struct filestr_desc *file,
                                        struct filestr_cache *cachep,
                                        unsigned long sector,
                                        unsigned long secoffset,
                                        void *buf,
                                        size_t nbyte,
                                        unsigned long filesectors,
                                        unsigned int flags)
{
    if (sector != cachep->sector)
    {
        /* wrong sector in buffer */
        int rc = readwrite_fill_cache(file, sector, filesectors, flags);
        if (rc <= 0)
            return rc;
    }

    readwrite_cache(cachep, buf, secoffset, nbyte, flags);
    return nbyte;
}

/* read from or write to the file; back end to read() and write() */
static ssize_t readwrite(struct filestr_desc *file, void *buf, size_t nbyte,
                         bool write)
{
#ifndef LOGF_ENABLE /* wipes out log before you can save it */
    DEBUGF("readwrite(%p,%lx,%lu,%s)\n",
           file, (long)buf, (unsigned long)nbyte, write ? "write" : "read");
#endif

    const file_size_t size = *file->sizep;
    file_size_t filerem;

    if (write)
    {
        /* if opened in append mode, move pointer to end */
        if (file->stream.flags & FD_APPEND)
            file->offset = MIN(size, FILE_SIZE_MAX);

        filerem = FILE_SIZE_MAX - file->offset;
    }
    else
    {
        /* limit to maximum possible offset (EOF or FILE_SIZE_MAX) */
        filerem = MIN(size, FILE_SIZE_MAX) - file->offset;
    }

    if (nbyte > filerem)
    {
        nbyte = filerem;
        if (nbyte > 0)
            {}
        else if (write)
            FILE_ERROR_RETURN(EFBIG, -1);     /* would get too large */
        else if (file->offset >= FILE_SIZE_MAX)
            FILE_ERROR_RETURN(EOVERFLOW, -2); /* can't read here */
    }

    if (nbyte == 0)
        return 0;

    int rc = 0;

    struct filestr_cache * const cachep = file->stream.cachep;
    void * const bufstart = buf;
    uint16_t sector_size = fat_file_sector_size(IF_MV(file->stream.fatstr.fatfilep));

    const unsigned long filesectors = filesize_sectors(sector_size, size);
    unsigned long sector = file->offset / sector_size;
    unsigned long sectoroffs = file->offset % sector_size;

    /* any head bytes? */
    if (sectoroffs)
    {
        size_t headbytes = MIN(nbyte, sector_size - sectoroffs);
        rc = readwrite_partial(file, cachep, sector, sectoroffs, buf, headbytes,
                               filesectors, write);
        if (rc <= 0)
        {
            if (rc < 0)
                FILE_ERROR(ERRNO, rc * 10 - 3);

            nbyte = 0; /* eof, skip the rest */
        }
        else
        {
            buf += rc;
            nbyte -= rc;
            sector++;  /* if nbyte goes to 0, the rest is skipped anyway */
        }
    }

    /* read/write whole sectors right into/from the supplied buffer */
    unsigned long sectorcount = nbyte / sector_size;

    while (sectorcount)
    {
        unsigned long runlen = sectorcount;

        /* if a cached sector is inside the transfer range, split the transfer
           into two parts and use the cache for that sector to keep it coherent
           without writeback */
        if (UNLIKELY(cachep->sector >= sector &&
                     cachep->sector < sector + sectorcount))
        {
            runlen = cachep->sector - sector;
        }

        if (runlen)
        {
            if (fat_query_sectornum(&file->stream.fatstr) != sector)
            {
                /* get on the correct sector */
                rc = 0;

                /* If the dirty bit isn't set, we're somehow beyond the file
                   size and you can't explain _that_ */
                if (sector >= filesectors && cachep->flags == (FSC_NEW|FSC_DIRTY))
                {
                    rc = flush_cache(file);
                    if (rc < 0)
                        FILE_ERROR(ERRNO, rc * 10 - 4);

                    if (cachep->sector + 1 == sector)
                        rc = 1; /* if now ok, don't seek */
                }

                if (rc == 0)
                {
                    rc = fat_seek(&file->stream.fatstr, sector);
                    if (rc < 0)
                        FILE_ERROR(EIO, rc * 10 - 5);
                }
            }

            rc = fat_readwrite(&file->stream.fatstr, runlen, buf, write);
            if (rc < 0)
            {
                DEBUGF("I/O error %sing %ld sectors\n",
                       write ? "writ" : "read", runlen);
                FILE_ERROR(rc == FAT_RC_ENOSPC ? ENOSPC : EIO,
                           rc * 10 - 6);
            }
            else
            {
                buf += rc * sector_size;
                nbyte -= rc * sector_size;
                sector += rc;
                sectorcount -= rc;

                /* if eof, skip tail bytes */
                if ((unsigned long)rc < runlen)
                    nbyte = 0;

                if (!nbyte)
                    break;
            }
        }

        if (UNLIKELY(sectorcount && sector == cachep->sector))
        {
            /* do this one sector with the cache */
            readwrite_cache(cachep, buf, 0, sector_size, write);
            buf += sector_size;
            nbyte -= sector_size;
            sector++;
            sectorcount--;
        }
    }

    /* any tail bytes? */
    if (nbyte)
    {
        /* tail bytes always start at sector offset 0 */
        rc = readwrite_partial(file, cachep, sector, 0, buf, nbyte,
                               filesectors, write);
        if (rc < 0)
            FILE_ERROR(ERRNO, rc * 10 - 7);

        buf += rc;
    }

file_error:;
#ifdef DEBUG
    if (errno == ENOSPC)
        DEBUGF("No space left on device\n");
#endif

    size_t done = buf - bufstart;
    if (done)
    {
        /* error or not, update the file offset and size if anything was
           transferred */
        file->offset += done;
#ifndef LOGF_ENABLE /* wipes out log before you can save it */
        DEBUGF("file offset: %ld\n", file->offset);
#endif
        /* adjust file size to length written */
        if (write && file->offset > size)
            *file->sizep = file->offset;

        if (rc > 0)
            return done;
    }

    return rc;
}


/** Internal interface **/

/* open a file without codepage conversion during the directory search;
   required to avoid any reentrancy when opening codepages and when scanning
   directories internally, which could infinitely recurse and would corrupt
   the static data */
int open_noiso_internal(const char *path, int oflag)
{
    return open_internal_locked(path, oflag, FF_ANYTYPE | FF_NOISO);
}

void force_close_writer_internal(struct filestr_base *stream)
{
    /* only we do writers so we know this is our guy */
    close_internal((struct filestr_desc *)stream);
}


/** POSIX **/

/* open a file */
int open(const char *path, int oflag)
{
    DEBUGF("open(path=\"%s\",oflag=%X)\n", path, (unsigned)oflag);
    return open_internal_locked(path, oflag, FF_ANYTYPE);
}

/* create a new file or rewrite an existing one */
int creat(const char *path)
{
    DEBUGF("creat(path=\"%s\")\n", path);
    return open_internal_locked(path, O_WRONLY|O_CREAT|O_TRUNC, FF_ANYTYPE);
}

/* close a file descriptor */
int close(int fildes)
{
    DEBUGF("close(fd=%d)\n", fildes);

    int rc;

    file_internal_lock_WRITER();

    /* needs to work even if marked "nonexistant" */
    struct filestr_desc *file = &open_streams[fildes];
    if ((unsigned int)fildes >= MAX_OPEN_FILES || !file->stream.flags)
    {
        DEBUGF("filedes %d not open\n", fildes);
        FILE_ERROR(EBADF, -2);
    }

    rc = close_internal(file);
    if (rc < 0)
        FILE_ERROR(ERRNO, rc * 10 - 3);

file_error:
    file_internal_unlock_WRITER();
    return rc;
}

/* truncate a file to a specified length */
int ftruncate(int fildes, off_t length)
{
    DEBUGF("ftruncate(fd=%d,len=%ld)\n", fildes, (long)length);

    struct filestr_desc * const file = GET_FILESTR(READER, fildes);
    if (!file)
        FILE_ERROR_RETURN(ERRNO, -1);

    int rc;

    if (!(file->stream.flags & FD_WRITE))
    {
        DEBUGF("Descriptor is read-only mode\n");
        FILE_ERROR(EBADF, -2);
    }

    if (length < 0)
    {
        DEBUGF("Length %ld is invalid\n", (long)length);
        FILE_ERROR(EINVAL, -3);
    }

    rc = ftruncate_internal(file, length, true);
    if (rc < 0)
        FILE_ERROR(ERRNO, rc * 10 - 4);

file_error:
    RELEASE_FILESTR(READER, file);
    return rc;
}

/* synchronize changes to a file */
int fsync(int fildes)
{
    DEBUGF("fsync(fd=%d)\n", fildes);

    struct filestr_desc * const file = GET_FILESTR(WRITER, fildes);
    if (!file)
        FILE_ERROR_RETURN(ERRNO, -1);

    int rc;

    if (!(file->stream.flags & FD_WRITE))
    {
        DEBUGF("Descriptor is read-only mode\n");
        FILE_ERROR(EINVAL, -2);
    }

    rc = fsync_internal(file);
    if (rc < 0)
        FILE_ERROR(ERRNO, rc * 10 - 3);

file_error:
    RELEASE_FILESTR(WRITER, file);
    return rc;
}

/* move the read/write file offset */
off_t lseek(int fildes, off_t offset, int whence)
{
#ifndef LOGF_ENABLE /* wipes out log before you can save it */
    DEBUGF("lseek(fd=%d,ofs=%ld,wh=%d)\n", fildes, (long)offset, whence);
#endif
    struct filestr_desc * const file = GET_FILESTR(READER, fildes);
    if (!file)
        FILE_ERROR_RETURN(ERRNO, -1);

    off_t rc = lseek_internal(file, offset, whence);
    if (rc < 0)
        FILE_ERROR(ERRNO, rc * 10 - 2);

file_error:
    RELEASE_FILESTR(READER, file);
    return rc;
}

/* read from a file */
ssize_t read(int fildes, void *buf, size_t nbyte)
{
    struct filestr_desc * const file = GET_FILESTR(READER, fildes);
    if (!file)
        FILE_ERROR_RETURN(ERRNO, -1);

    ssize_t rc;

    if (file->stream.flags & FD_WRONLY)
    {
        DEBUGF("read(fd=%d,buf=%p,nb=%lu) - "
               "descriptor is write-only mode\n",
               fildes, buf, (unsigned long)nbyte);
        FILE_ERROR(EBADF, -2);
    }

    rc = readwrite(file, buf, nbyte, false);
    if (rc < 0)
        FILE_ERROR(ERRNO, rc * 10 - 3);

file_error:
    RELEASE_FILESTR(READER, file);
    return rc;
}

/* write on a file */
ssize_t write(int fildes, const void *buf, size_t nbyte)
{
    struct filestr_desc * const file = GET_FILESTR(READER, fildes);
    if (!file)
        FILE_ERROR_RETURN(ERRNO, -1);

    ssize_t rc;

    if (!(file->stream.flags & FD_WRITE))
    {
        DEBUGF("write(fd=%d,buf=%p,nb=%lu) - "
               "descriptor is read-only mode\n",
               fildes, buf, (unsigned long)nbyte);
        FILE_ERROR(EBADF, -2);
    }

    rc = readwrite(file, (void *)buf, nbyte, true);
    if (rc < 0)
        FILE_ERROR(ERRNO, rc * 10 - 3);

file_error:
    RELEASE_FILESTR(READER, file);
    return rc;
}

/* remove a file */
int remove(const char *path)
{
    DEBUGF("remove(path=\"%s\")\n", path);

    file_internal_lock_WRITER();
    int rc = remove_stream_internal(path, NULL, FF_FILE);
    file_internal_unlock_WRITER();
    return rc;
}

/* rename a file */
int rename(const char *old, const char *new)
{
    DEBUGF("rename(old=\"%s\",new=\"%s\")\n", old, new);

    int rc, open1rc = -1, open2rc = -1;
    struct filestr_base oldstr, newstr;
    struct path_component_info oldinfo, newinfo;

    file_internal_lock_WRITER();

    /* open 'old'; it must exist */
    open1rc = open_stream_internal(old, FF_ANYTYPE | FF_PARENTINFO, &oldstr,
                                   &oldinfo);
    if (open1rc <= 0)
    {
        DEBUGF("Failed opening old: %d\n", open1rc);
        if (open1rc == 0)
            FILE_ERROR(ENOENT, -1);
        else
            FILE_ERROR(ERRNO, open1rc * 10 - 1);
    }

    /* if 'old' is a directory then 'new' is also required to be one if 'new'
       is to be overwritten */
    const bool are_dirs = oldinfo.attr & ATTR_DIRECTORY;

    /* open new (may or may not exist) */
    unsigned int callflags = FF_FILE;
    if (are_dirs)
    {
        /* if 'old' is found while parsing the new directory components then
           'new' contains path prefix that names 'old'; if new and old are in
           the same directory, this tests positive but that is checked later */
        callflags = FF_DIR | FF_CHECKPREFIX;
        newinfo.prefixp = oldstr.infop;
    }

    open2rc = open_stream_internal(new, callflags | FF_PARENTINFO, &newstr,
                                   &newinfo);
    if (open2rc < 0)
    {
        DEBUGF("Failed opening new file: %d\n", open2rc);
        FILE_ERROR(ERRNO, open2rc * 10 - 2);
    }

#ifdef HAVE_MULTIVOLUME
    if (oldinfo.parentinfo.volume != newinfo.parentinfo.volume)
    {
        DEBUGF("Cross-device link\n");
        FILE_ERROR(EXDEV, -3);
    }
#endif /* HAVE_MULTIVOLUME */

    /* if the parent is changing then this is a move, not a simple rename */
    const bool is_move = !fat_file_is_same(&oldinfo.parentinfo.fatfile,
                                           &newinfo.parentinfo.fatfile);
    /* prefix found and moving? */
    if (is_move && (newinfo.attr & ATTR_PREFIX))
    {
        DEBUGF("New contains prefix that names old\n");
        FILE_ERROR(EINVAL, -4);
    }

    const char * const oldname = strmemdupa(oldinfo.name, oldinfo.length);
    const char * const newname = strmemdupa(newinfo.name, newinfo.length);
    bool is_overwrite = false;

    if (open2rc > 0)
    {
        /* new name exists in parent; check if 'old' is overwriting 'new';
           if it's the very same file, then it's just a rename */
        is_overwrite = oldstr.bindp != newstr.bindp;

        if (is_overwrite)
        {
            if (are_dirs)
            {
                /* the directory to be overwritten must be empty */
                rc = test_dir_empty_internal(&newstr);
                if (rc < 0)
                    FILE_ERROR(ERRNO, rc * 10 - 5);
            }
        }
        else if (!strcmp(newname, oldname)) /* case-only is ok */
        {
            DEBUGF("No name change (success)\n");
            rc = 0;
            FILE_ERROR(ERRNO, RC);
        }
    }
    else if (!are_dirs && (newinfo.attr & ATTR_DIRECTORY))
    {
        /* even if new doesn't exist, canonical path type must match
           (ie. a directory path such as "/foo/bar/" when old names a file) */
        DEBUGF("New path is a directory\n");
        FILE_ERROR(EISDIR, -6);
    }

    /* first, create the new entry so that there's never a time that the
       victim's data has no reference in the directory tree, that is, until
       everything else first succeeds */
    struct file_base_info old_fileinfo = *oldstr.infop;
    rc = fat_rename(&newinfo.parentinfo.fatfile, &oldstr.infop->fatfile,
                    newname);
    if (rc < 0)
    {
        DEBUGF("I/O error renaming file: %d\n", rc);
        FILE_ERROR(rc == FAT_RC_ENOSPC ? ENOSPC : EIO, rc * 10 - 7);
    }

    if (is_overwrite)
    {
        /* 'new' would have been assigned its own directory entry and
           succeeded so at this point it is treated like a remove() call
           on the victim which preserves data until the last reference is
           closed */
        rc = remove_stream_internal(NULL, &newstr, callflags);
        if (rc < 0)
            FILE_ERROR(ERRNO, rc * 10 - 8);
    }

    fileop_onrename_internal(&oldstr, is_move ? &old_fileinfo : NULL,
                             &newinfo.parentinfo, newname);

file_error:
    /* for now, there is nothing to fail upon closing the old stream */
    if (open1rc >= 0)
        close_stream_internal(&oldstr);

    /* the 'new' stream could fail to close cleanly because it became
       impossible to remove its data if this was an overwrite operation */
    if (open2rc >= 0)
    {
        int rc2 = close_stream_internal(&newstr);
        if (rc2 < 0 && rc >= 0)
        {
            DEBUGF("Success but failed closing new: %d\n", rc2);
            rc = rc2 * 10 - 9;
        }
    }

    file_internal_unlock_WRITER();
    return rc;
}

/** Extensions **/

int modtime(const char *path, time_t modtime)
{
    DEBUGF("modtime(path=\"%s\",modtime=%d)\n", path, (int) modtime);

    int rc, open1rc = -1;
    struct filestr_base pathstr;
    struct path_component_info pathinfo;

    file_internal_lock_WRITER();

    open1rc = open_stream_internal(path, FF_ANYTYPE | FF_PARENTINFO,
                                   &pathstr, &pathinfo);
    if (open1rc <= 0)
    {
        DEBUGF("Failed opening path: %d\n", open1rc);
        if (open1rc == 0)
            FILE_ERROR(ENOENT, -1);
        else
            FILE_ERROR(ERRNO, open1rc * 10 - 1);
    }

    rc = fat_modtime(&pathinfo.parentinfo.fatfile, pathstr.fatstr.fatfilep,
                     modtime);
    if (rc < 0)
    {
        DEBUGF("I/O error during modtime: %d\n", rc);
        FILE_ERROR(ERRNO, rc * 10 - 2);
    }

file_error:
    if (open1rc >= 0)
        close_stream_internal(&pathstr);
    file_internal_unlock_WRITER();
    return rc;
}

/* get the binary size of a file (in bytes) */
off_t filesize(int fildes)
{
    struct filestr_desc * const file = GET_FILESTR(READER, fildes);
    if (!file)
        FILE_ERROR_RETURN(ERRNO, -1);

    off_t rc;
    file_size_t size = *file->sizep;

    if (size > FILE_SIZE_MAX)
        FILE_ERROR(EOVERFLOW, -2);

    rc = (off_t)size;
file_error:
    RELEASE_FILESTR(READER, file);
    return rc;
}

/* test if two file descriptors refer to the same file */
int fsamefile(int fildes1, int fildes2)
{
    struct filestr_desc * const file1 = GET_FILESTR(WRITER, fildes1);
    if (!file1)
        FILE_ERROR_RETURN(ERRNO, -1);

    int rc = -2;

    struct filestr_desc * const file2 = get_filestr(fildes2);
    if (file2)
        rc = file1->stream.bindp == file2->stream.bindp ? 1 : 0;

    RELEASE_FILESTR(WRITER, file1);
    return rc;
}

/* tell the relationship of path1 to path2 */
int relate(const char *path1, const char *path2)
{
    /* this is basically what rename() does but reduced to the relationship
       determination */
    DEBUGF("relate(path1=\"%s\",path2=\"%s\")\n", path1, path2);

    int rc, open1rc = -1, open2rc = -1;
    struct filestr_base str1, str2;
    struct path_component_info info1, info2;

    file_internal_lock_WRITER();

    open1rc = open_stream_internal(path1, FF_ANYTYPE, &str1, &info1);
    if (open1rc <= 0)
    {
        DEBUGF("Failed opening path1: %d\n", open1rc);
        if (open1rc < 0)
            FILE_ERROR(ERRNO, open1rc * 10 - 1);
        else
            FILE_ERROR(ENOENT, -1);
    }

    info2.prefixp = str1.infop;
    open2rc = open_stream_internal(path2, FF_ANYTYPE | FF_CHECKPREFIX,
                                   &str2, &info2);
    if (open2rc < 0)
    {
        DEBUGF("Failed opening path2: %d\n", open2rc);
        FILE_ERROR(ERRNO, open2rc * 10 - 2);
    }

    rc = RELATE_DIFFERENT;

    if (open2rc > 0)
    {
        if (str1.bindp == str2.bindp)
            rc = RELATE_SAME;
        else if (info2.attr & ATTR_PREFIX)
            rc = RELATE_PREFIX;
    }
    else /* open2rc == 0 */
    {
        /* path1 existing and path2's final part not can only be a prefix or
           different */
        if (info2.attr & ATTR_PREFIX)
            rc = RELATE_PREFIX;
    }

file_error:
    if (open1rc >= 0)
        close_stream_internal(&str1);

    if (open2rc >= 0)
        close_stream_internal(&str2);

    file_internal_unlock_WRITER();
    return rc;
}

/* test file or directory existence */
bool file_exists(const char *path)
{
    file_internal_lock_WRITER();
    bool rc = test_stream_exists_internal(path, FF_ANYTYPE) > 0;
    file_internal_unlock_WRITER();
    return rc;
}
