/* file.cpp --

   This file is part of the UPX executable compressor.

   Copyright (C) Markus Franz Xaver Johannes Oberhumer
   Copyright (C) Laszlo Molnar
   All Rights Reserved.

   UPX and the UCL library are free software; you can redistribute them
   and/or modify them 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 program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program; see the file COPYING.
   If not, write to the Free Software Foundation, Inc.,
   59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.

   Markus F.X.J. Oberhumer              Laszlo Molnar
   <markus@oberhumer.com>               <ezerotven+github@gmail.com>
 */

#include "conf.h"
#include "file.h"

/*************************************************************************
// static file-related util functions; will throw on error
**************************************************************************/

/*static*/ void FileBase::chmod(const char *name, int mode) {
    assert(name != nullptr && name[0] != 0);
#if HAVE_CHMOD
    if (::chmod(name, mode) != 0)
        throwIOException(name, errno);
#else
    UNUSED(name);
    UNUSED(mode);
    // no error
#endif
}

/*static*/ void FileBase::rename(const char *old_, const char *new_) {
#if (ACC_OS_DOS32) && defined(__DJGPP__)
    if (::_rename(old_, new_) != 0)
#else
    if (::rename(old_, new_) != 0)
#endif
        throwIOException("rename error", errno);
}

/*static*/ bool FileBase::unlink_noexcept(const char *name) noexcept {
    assert_noexcept(name != nullptr && name[0] != 0);
    bool success = ::unlink(name) == 0;
#if HAVE_CHMOD
    if (!success)
        success = (::chmod(name, 0666) == 0 && ::unlink(name) == 0);
#endif
    return success;
}

/*static*/ void FileBase::unlink(const char *name) {
    if (!unlink_noexcept(name))
        throwIOException(name, errno);
}

/*************************************************************************
// FileBase
**************************************************************************/

FileBase::~FileBase() may_throw {
#if 0 && defined(__GNUC__) // debug
    if (isOpen())
        fprintf(stderr, "%s: %s\n", _name, __PRETTY_FUNCTION__);
#endif
    if (std::uncaught_exceptions() == 0)
        closex(); // may_throw
    else
        close_noexcept(); // currently in exception unwinding, use noexcept variant
}

bool FileBase::do_sopen() {
    if (_shflags < 0)
        _fd = ::open(_name, _flags, _mode);
    else {
#if (ACC_OS_DOS32) && defined(__DJGPP__)
        _fd = ::open(_name, _flags | _shflags, _mode);
#elif (ACC_ARCH_M68K && ACC_OS_TOS && ACC_CC_GNUC) && defined(__MINT__)
        _fd = ::open(_name, _flags | (_shflags & O_SHMODE), _mode);
#elif defined(SH_DENYRW)
        _fd = ::sopen(_name, _flags, _shflags, _mode);
#else
        throwInternalError("bad usage of do_sopen()");
#endif
    }
    if (_fd < 0)
        return false;
    st.st_size = 0;
    if (::fstat(_fd, &st) != 0)
        throwIOException(_name, errno);
    _length = st.st_size;
    return true;
}

bool FileBase::close_noexcept() noexcept {
    bool ok = true;
    if (isOpen() && _fd != STDIN_FILENO && _fd != STDOUT_FILENO && _fd != STDERR_FILENO)
        if (::close(_fd) == -1)
            ok = false;
    _fd = -1;
    _flags = 0;
    _mode = 0;
    _name = nullptr;
    _offset = 0;
    _length = 0;
    return ok;
}

void FileBase::closex() may_throw {
    if (!close_noexcept())
        throwIOException("close failed", errno);
}

// Return value of ::seek is the resulting file offset (same as ::tell())
upx_off_t FileBase::seek(upx_off_t off, int whence) {
    if (!isOpen())
        throwIOException("bad seek 1");
    if (!mem_size_valid_bytes(off >= 0 ? off : -off)) // sanity check
        throwIOException("bad seek");
    if (whence == SEEK_SET) {
        if (off < 0)
            throwIOException("bad seek 2");
        off += _offset;
    } else if (whence == SEEK_END) {
        if (off > 0)
            throwIOException("bad seek 3");
        off += _offset + _length;
        whence = SEEK_SET;
    } else if (whence == SEEK_CUR) {
    } else
        throwInternalError("bad seek: whence");
    upx_off_t l = ::lseek(_fd, off, whence);
    if (l < 0)
        throwIOException("seek error", errno);
    return l - _offset;
}

upx_off_t FileBase::tell() const {
    if (!isOpen())
        throwIOException("bad tell");
    upx_off_t l = ::lseek(_fd, 0, SEEK_CUR);
    if (l < 0)
        throwIOException("tell error", errno);
    return l - _offset;
}

void FileBase::set_extent(upx_off_t offset, upx_off_t length) {
    _offset = offset;
    _length = length;
}

upx_off_t FileBase::st_size() const { return _length; }

/*************************************************************************
// InputFile
**************************************************************************/

void InputFile::sopen(const char *name, int flags, int shflags) {
    closex();
    _name = name;
    _flags = flags;
    _shflags = shflags;
    _mode = 0;
    _offset = 0;
    _length = 0;
    if (!super::do_sopen()) {
        if (errno == ENOENT)
            throw FileNotFoundException(_name, errno);
        else if (errno == EEXIST)
            throw FileAlreadyExistsException(_name, errno);
        else
            throwIOException(_name, errno);
    }
    _length_orig = _length;
}

int InputFile::read(SPAN_P(void) buf, upx_int64_t blen) {
    if (!isOpen() || blen < 0)
        throwIOException("bad read");
    int len = (int) mem_size(1, blen); // sanity check
    errno = 0;
    long l = acc_safe_hread(_fd, raw_bytes(buf, len), len);
    if (errno)
        throwIOException("read error", errno);
    return (int) l;
}

int InputFile::readx(SPAN_P(void) buf, upx_int64_t blen) {
    int l = this->read(buf, blen);
    if (l != blen)
        throwEOFException();
    return l;
}

upx_off_t InputFile::seek(upx_off_t off, int whence) {
    upx_off_t pos = super::seek(off, whence);
    if (_length < pos)
        throwIOException("bad seek 4");
    return pos;
}

upx_off_t InputFile::st_size_orig() const { return _length_orig; }

int InputFile::dupFd() may_throw {
    if (!isOpen())
        throwIOException("bad dup");
#if defined(HAVE_DUP) && (HAVE_DUP + 0 == 0)
    errno = ENOSYS;
    int r = -1;
#else
    int r = ::dup(getFd());
#endif
    if (r < 0)
        throwIOException("dup", errno);
    return r;
}

/*************************************************************************
// OutputFile
**************************************************************************/

void OutputFile::sopen(const char *name, int flags, int shflags, int mode) {
    closex();
    _name = name;
    _flags = flags;
    _shflags = shflags;
    _mode = mode;
    _offset = 0;
    _length = 0;
    if (!super::do_sopen()) {
#if 0
        // don't throw FileNotFound here -- this is confusing
        if (errno == ENOENT)
            throw FileNotFoundException(_name,errno);
        else
#endif
        if (errno == EEXIST)
            throw FileAlreadyExistsException(_name, errno);
        else
            throwIOException(_name, errno);
    }
}

bool OutputFile::openStdout(int flags, bool force) {
    closex();
    int fd = STDOUT_FILENO;
    if (!force && acc_isatty(fd))
        return false;
    _name = "<stdout>";
    _flags = flags;
    _shflags = -1;
    _mode = 0;
    _offset = 0;
    _length = 0;
    if (flags && acc_set_binmode(fd, 1) == -1)
        throwIOException(_name, errno);
    _fd = fd;
    return true;
}

void OutputFile::write(SPAN_0(const void) buf, upx_int64_t blen) {
    if (!isOpen() || blen < 0)
        throwIOException("bad write");
    // allow nullptr if blen == 0
    if (blen == 0)
        return;
    int len = (int) mem_size(1, blen); // sanity check
    errno = 0;
#if WITH_XSPAN >= 2
    NO_fprintf(stderr, "write %p %zd (%p) %d\n", buf.raw_ptr(), buf.raw_size_in_bytes(),
               buf.raw_base(), len);
#endif
    long l = acc_safe_hwrite(_fd, raw_bytes(buf, len), len);
    if (l != len)
        throwIOException("write error", errno);
    bytes_written += len;
#if TESTING && 0
    static upx_std_atomic(bool) dumping;
    if (!dumping) {
        dumping = true;
        char fn[64];
        static int part = 0;
        snprintf(fn, sizeof(fn), "upx-dump-%04d.data", part++);
        OutputFile::dump(fn, buf, len);
        dumping = false;
    }
#endif
}

upx_off_t OutputFile::st_size() const {
    if (opt->to_stdout) {     // might be a pipe ==> .st_size is invalid
        return bytes_written; // too big if seek()+write() instead of rewrite()
    }
    struct stat my_st;
    my_st.st_size = 0;
    if (::fstat(_fd, &my_st) != 0)
        throwIOException(_name, errno);
    return my_st.st_size;
}

void OutputFile::rewrite(SPAN_P(const void) buf, int len) {
    assert(!opt->to_stdout);
    write(buf, len);
    bytes_written -= len; // restore
}

upx_off_t OutputFile::seek(upx_off_t off, int whence) {
    if (!mem_size_valid_bytes(off >= 0 ? off : -off)) // sanity check
        throwIOException("bad seek");
    assert(!opt->to_stdout);
    switch (whence) {
    case SEEK_SET:
        if (bytes_written < off)
            bytes_written = off;
        _length = bytes_written; // cheap, lazy update; needed?
        break;
    case SEEK_END:
        _length = bytes_written; // necessary
        break;
    }
    return super::seek(off, whence);
}

// WARNING: fsync() does not exist in some Windows environments.
// This trick works only on UNIX-like systems.
// int OutputFile::read(void *buf, int len) {
//    fsync(_fd);
//    InputFile infile;
//    infile.open(this->getName(), O_RDONLY | O_BINARY);
//    infile.seek(this->tell(), SEEK_SET);
//    return infile.read(buf, len);
//}

void OutputFile::set_extent(upx_off_t offset, upx_off_t length) {
    super::set_extent(offset, length);
    bytes_written = 0;
    if (0 == offset && 0xffffffffLL == length) { // TODO: check all callers of this method
        st.st_size = 0;
        if (::fstat(_fd, &st) != 0)
            throwIOException(_name, errno);
        _length = st.st_size - offset;
    }
}

upx_off_t OutputFile::unset_extent() {
    upx_off_t l = ::lseek(_fd, 0, SEEK_END);
    if (l < 0)
        throwIOException("lseek error", errno);
    _offset = 0;
    _length = l;
    bytes_written = _length;
    return _length;
}

/*static*/ void OutputFile::dump(const char *name, SPAN_P(const void) buf, int len, int flags) {
    if (flags < 0)
        flags = O_CREAT | O_TRUNC;
    flags |= O_WRONLY | O_BINARY;
    OutputFile f;
    f.open(name, flags, 0600);
    f.write(raw_bytes(buf, len), len);
    f.closex();
}

/*************************************************************************
//
**************************************************************************/

TEST_CASE("file") {
    InputFile fi;
    CHECK(!fi.isOpen());
    CHECK(fi.getFd() == -1);
    CHECK(fi.st_size() == 0);
    OutputFile fo;
    CHECK(!fo.isOpen());
    CHECK(fo.getFd() == -1);
    CHECK(fo.getBytesWritten() == 0);
}

/* vim:set ts=4 sw=4 et: */
