/* * libfud * Copyright 2024 Dominick Allen * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include "fud_file.hpp" #include "fud_algorithm.hpp" #include #include #include #include #include #include namespace fud { FileResult RegularFile::open(StringView filename, FileAccessMode mode, OpenFlags flags, Option dirFdOption) { if (!filename.nullTerminated()) { return FileResult::error(FudStatus::ArgumentInvalid); } int dirFd = dirFdOption.valueOr(AT_FDCWD); RegularFile file{}; uint32_t openFlags = 0; switch (mode) { case FileAccessMode::Read: openFlags = O_RDONLY; break; case FileAccessMode::Write: openFlags = O_WRONLY; break; case FileAccessMode::ReadWrite: openFlags = O_RDWR; break; default: return FileResult::error(FudStatus::ArgumentInvalid); } if (flags.hasFlag(OpenFlagEnum::Append) && flags.hasFlag(OpenFlagEnum::Truncate)) { return FileResult::error(FudStatus::OperationInvalid); } openFlags |= flags.flags(); open_how openHow{}; zeroObject(openHow); openHow.flags = openFlags; openHow.resolve = RESOLVE_NO_SYMLINKS; auto status = syscall(SYS_openat2, dirFd, filename.data(), &openHow, sizeof(openHow)); if (status == -1) { if constexpr (EAGAIN != EWOULDBLOCK && status == EWOULDBLOCK) { return FileResult::error(FudStatus::Partial); } switch (errno) { case ETXTBSY: case EAGAIN: return FileResult::error(FudStatus::Partial); case ENOENT: return FileResult::error(FudStatus::NotFound); case EBADF: case EFBIG: case EOVERFLOW: case EINVAL: case EISDIR: case ENAMETOOLONG: return FileResult::error(FudStatus::ArgumentInvalid); case EROFS: case EACCES: case EPERM: return FileResult::error(FudStatus::PermissionDenied); case ELOOP: case EXDEV: case ENFILE: case E2BIG: default: return FileResult::error(FudStatus::Failure); } } fudAssert(status <= std::numeric_limits::max()); file.m_fd = static_cast(status); using Stat = struct stat; Stat sBuffer{}; auto fStatus = fstat(file.m_fd, &sBuffer); if (fStatus == -1) { return FileResult::error(FudStatus::Failure); } if ((sBuffer.st_mode & S_IFMT) != S_IFREG) { return FileResult::error(FudStatus::ObjectInvalid); } file.m_openFlags = flags; return FileResult::okay(std::move(file)); } FileResult RegularFile::create( StringView filename, FileAccessMode mode, OpenFlags flags, Permissions permissions, bool createOnly, Option dirFdOption) { if (!filename.nullTerminated()) { return FileResult::error(FudStatus::ArgumentInvalid); } int dirFd = dirFdOption.valueOr(AT_FDCWD); RegularFile file{}; uint32_t openFlags = 0; switch (mode) { case FileAccessMode::Read: openFlags = O_RDONLY; break; case FileAccessMode::Write: openFlags = O_WRONLY; break; case FileAccessMode::ReadWrite: openFlags = O_RDWR; break; default: return FileResult::error(FudStatus::ArgumentInvalid); } if (flags.hasFlag(OpenFlagEnum::Append) && flags.hasFlag(OpenFlagEnum::Truncate)) { return FileResult::error(FudStatus::OperationInvalid); } openFlags |= flags.flags() | O_CREAT | (O_EXCL * static_cast(createOnly)); open_how openHow{}; zeroObject(openHow); openHow.flags = openFlags; openHow.resolve = RESOLVE_NO_SYMLINKS; openHow.mode = permissions.mode(); auto status = syscall(SYS_openat2, dirFd, filename.data(), &openHow, sizeof(openHow)); if (status == -1) { if constexpr (EAGAIN != EWOULDBLOCK && status == EWOULDBLOCK) { return FileResult::error(FudStatus::Partial); } switch (errno) { case ETXTBSY: case EAGAIN: return FileResult::error(FudStatus::Partial); case EBADF: case EFBIG: case EOVERFLOW: case EINVAL: case EISDIR: case ENAMETOOLONG: return FileResult::error(FudStatus::ArgumentInvalid); case EROFS: case EACCES: case EPERM: return FileResult::error(FudStatus::PermissionDenied); case EDQUOT: case ENOENT: case ELOOP: case EXDEV: case ENFILE: case E2BIG: default: return FileResult::error(FudStatus::Failure); } } fudAssert(status <= std::numeric_limits::max()); file.m_fd = static_cast(status); using Stat = struct stat; Stat sBuffer{}; auto fStatus = fstat(file.m_fd, &sBuffer); if (fStatus == -1) { return FileResult::error(FudStatus::Failure); } if ((sBuffer.st_mode & S_IFMT) != S_IFREG) { return FileResult::error(FudStatus::ObjectInvalid); } return FileResult::okay(std::move(file)); } RegularFile::~RegularFile() { static_cast(this->close()); } RegularFile& RegularFile::operator=(RegularFile&& rhs) noexcept { if (&rhs == this) { return *this; } static_cast(this->close()); m_position = rhs.m_position; m_fd = rhs.m_fd; m_openFlags = rhs.m_openFlags; m_modeFlags = rhs.m_modeFlags; rhs.m_fd = -1; return *this; } FudStatus RegularFile::take(RegularFile& rhs) { if (&rhs == this) { return FudStatus::Success; } auto status = this->close(); if (status != FudStatus::Success) { return status; } m_fd = rhs.m_fd; m_openFlags = rhs.m_openFlags; m_modeFlags = rhs.m_modeFlags; rhs.m_fd = -1; return status; } FudStatus RegularFile::close() { FudStatus status = FudStatus::Success; if (m_fd != -1) { auto closeStatus = ::close(m_fd); if (closeStatus == -1) { switch (errno) { case EBADF: status = FudStatus::HandleInvalid; break; case EINTR: case EIO: case ENOSPC: case EDQUOT: status = FudStatus::Partial; break; default: status = FudStatus::Failure; break; } } m_fd = -1; } return status; } Result RegularFile::size() const { auto fileSize = lseek(m_fd, 0, SEEK_END); if (fileSize == -1) { switch (errno) { case EBADF: return FudError{FudStatus::HandleInvalid}; case ESPIPE: return FudError{FudStatus::ObjectInvalid}; default: return FudError{FudStatus::Failure}; } } auto seekBegin = lseek(m_fd, 0, SEEK_SET); if (seekBegin == -1) { return FudError{FudStatus::Failure}; } return Okay{static_cast(fileSize)}; } FudStatus RegularFile::seekStart() { return seek(0); } FudStatus RegularFile::seekEnd() { auto fileSize = lseek(m_fd, 0, SEEK_END); if (fileSize == -1) { switch (errno) { case EBADF: return FudStatus::HandleInvalid; case ESPIPE: return FudStatus::ObjectInvalid; case EOVERFLOW: return FudStatus::RangeError; case EINVAL: default: return FudStatus::Failure; } } fudAssert(fileSize >= 0); m_position = static_cast(fileSize); return FudStatus::Success; } FudStatus RegularFile::seek(size_t position) { if (position > std::numeric_limits::max()) { return FudStatus::RangeError; } auto seekStatus = lseek(m_fd, static_cast(position), SEEK_SET); if (seekStatus == -1) { switch (errno) { case EBADF: return FudStatus::HandleInvalid; case ESPIPE: return FudStatus::ObjectInvalid; case EOVERFLOW: return FudStatus::RangeError; case EINVAL: default: return FudStatus::Failure; } } fudAssert(seekStatus >= 0); fudAssert(static_cast(seekStatus) == position); m_position = position; return FudStatus::Success; } FudStatus RegularFile::validateIOParameters(const std::byte* source) const { if (source == nullptr) { return FudStatus::NullPointer; } if (not isOpen()) { return FudStatus::HandleInvalid; } return FudStatus::Success; } DrainResult RegularFile::write(const std::byte* source, size_t length, size_t maxExtraAttempts) { DrainResult result{0, FudStatus::Success}; result.status = validateIOParameters(source); if (result.status != FudStatus::Success || length == 0) { return result; } size_t bytesRemaining = length; size_t numAttempts = 0; while (bytesRemaining > 0 && numAttempts <= maxExtraAttempts) { auto writeStatus = ::write(m_fd, source + result.bytesDrained, bytesRemaining); bool okay = writeStatus != -1; size_t bytesWritten = 0; if (writeStatus > 0) { bytesWritten = static_cast(writeStatus); } fudAssert(bytesWritten <= bytesRemaining); if constexpr (EAGAIN != EWOULDBLOCK) { if (writeStatus == EWOULDBLOCK) { okay = true; numAttempts++; } } if (not okay) { switch (errno) { case EAGAIN: case EINTR: okay = true; numAttempts++; break; case EINVAL: result.status = FudStatus::ObjectInvalid; break; case EBADF: result.status = FudStatus::HandleInvalid; break; case EPERM: result.status = FudStatus::PermissionDenied; break; default: result.status = FudStatus::Failure; break; } } if (not okay) { break; } if (writeStatus > 0) { numAttempts = 0; } else if (writeStatus == 0) { numAttempts++; } m_position += bytesWritten; bytesRemaining -= bytesWritten; result.bytesDrained += bytesWritten; } if (bytesRemaining > 0 && result.status == FudStatus::Success) { result.status = FudStatus::Partial; } return result; } DrainResult RegularFile::read(std::byte* sink, size_t length, size_t maxExtraAttempts) { DrainResult result{0, FudStatus::Success}; result.status = validateIOParameters(sink); if (result.status != FudStatus::Success || length == 0) { return result; } size_t bytesRemaining = length; size_t numAttempts = 0; while (bytesRemaining > 0 && numAttempts <= maxExtraAttempts) { auto readStatus = ::read(m_fd, sink + result.bytesDrained, bytesRemaining); size_t bytesRead = 0; if (readStatus > 0) { bytesRead = static_cast(readStatus); } bool okay = readStatus != -1; if constexpr (EAGAIN != EWOULDBLOCK) { if (readStatus == EWOULDBLOCK) { okay = true; numAttempts++; } } if (not okay) { switch (errno) { case EAGAIN: case EINTR: okay = true; numAttempts++; break; case EINVAL: case EISDIR: result.status = FudStatus::ObjectInvalid; break; case EBADF: result.status = FudStatus::HandleInvalid; break; case EPERM: result.status = FudStatus::PermissionDenied; break; default: result.status = FudStatus::Failure; break; } } if (not okay) { break; } if (readStatus > 0) { numAttempts = 0; } else if (readStatus == 0) { numAttempts++; } m_position += bytesRead; bytesRemaining -= bytesRead; result.bytesDrained += bytesRead; } if (bytesRemaining > 0 && result.status == FudStatus::Success) { result.status = FudStatus::Partial; } return result; } BufferedRegularFile BufferedRegularFile::make(RegularFile&& file, Vector&& buffer) { return BufferedRegularFile{std::move(file), std::move(buffer)}; } FudStatus BufferedRegularFile::close(bool discardBuffer) { if (not discardBuffer) { static_cast(flush()); } else { discard(); } return m_file.close(); } void BufferedRegularFile::discard() { m_bufferLength = 0; } FudStatus BufferedRegularFile::seekStart() { if (not bufferEmpty()) { return FudStatus::OperationInvalid; } return m_file.seekStart(); } FudStatus BufferedRegularFile::seekEnd() { if (not bufferEmpty()) { return FudStatus::OperationInvalid; } return m_file.seekEnd(); } FudStatus BufferedRegularFile::seek(size_t position) { if (not bufferEmpty()) { return FudStatus::OperationInvalid; } return m_file.seek(position); } FudStatus BufferedRegularFile::resizeBuffer(size_t size) { if (m_bufferLength > 0) { return FudStatus::OperationInvalid; } return m_buffer.resize(size); } DrainResult BufferedRegularFile::validateBufferedIO(const std::byte* pointer, Operation requestedOperation) { DrainResult result{0, FudStatus::Success}; if (pointer == nullptr) { result.status = FudStatus::NullPointer; return result; } if (not m_file.isOpen()) { result.status = FudStatus::HandleInvalid; return result; } if (requestedOperation == Operation::Read && m_lastOperation == Operation::Write && m_bufferLength > 0) { result.status = FudStatus::OperationInvalid; return result; } if (m_lastOperation != requestedOperation) { m_bufferLength = 0; m_bufferPosition = 0; m_lastOperation = requestedOperation; } return result; } DrainResult BufferedRegularFile::write(const std::byte* source, size_t length, Option maxExtraAttempts) { DrainResult result{validateBufferedIO(source, Operation::Write)}; if (result.status != FudStatus::Success) { return result; } if (length == 0) { return result; } if (m_bufferLength < m_buffer.size()) { auto cap = m_buffer.size() - m_bufferLength; size_t count = length; if (count > cap) { count = cap; } auto copyStatus = copyMem(m_buffer.data() + m_bufferLength, cap, source, count); fudAssert(copyStatus == FudStatus::Success); source += count; length -= count; m_bufferLength += count; result.bytesDrained += count; } fudAssert(m_bufferLength <= m_buffer.size()); if (m_bufferLength == m_buffer.size()) { auto writeBufferResult = flush(maxExtraAttempts.valueOr(0)); if (writeBufferResult.status != FudStatus::Success) { result.status = writeBufferResult.status; return result; } } if (length > m_buffer.size()) { auto drainResult = m_file.write(source, length, maxExtraAttempts.valueOr(0)); fudAssert(drainResult.bytesDrained <= length); source += drainResult.bytesDrained; length -= drainResult.bytesDrained; result.bytesDrained += drainResult.bytesDrained; result.status = drainResult.status; } if (result.status != FudStatus::Success) { return result; } if (length > 0) { fudAssert(m_bufferLength == 0); fudAssert(length < m_buffer.size()); auto copyStatus = copyMem(m_buffer.data(), m_buffer.size(), source, length); fudAssert(copyStatus == FudStatus::Success); } return result; } DrainResult BufferedRegularFile::read(std::byte* sink, size_t length, Option maxExtraAttempts) { auto extraAttempts = maxExtraAttempts.valueOr(0); DrainResult result{validateBufferedIO(sink, Operation::Read)}; if (result.status != FudStatus::Success) { return result; } if (length == 0) { return result; } drainReadBuffer(sink, length, result); fudAssert(length == 0 || m_bufferPosition == m_bufferLength); if (m_bufferPosition == m_bufferLength) { m_bufferPosition = 0; m_bufferLength = 0; } if (length == 0) { return result; } fudAssert(m_bufferLength == 0); fudAssert(m_bufferPosition == 0); if (length > m_buffer.size()) { auto drainResult = m_file.read(sink, length, extraAttempts); sink += drainResult.bytesDrained; length -= drainResult.bytesDrained; result.status = drainResult.status; result.bytesDrained += drainResult.bytesDrained; return result; } fudAssert(length > 0); if (result.status != FudStatus::Success) { return result; } fudAssert(m_bufferLength == 0); fudAssert(m_bufferPosition == 0); auto drainResult = m_file.read(m_buffer.data(), m_buffer.size(), extraAttempts); result.status = drainResult.status; if (drainResult.status == FudStatus::Success || drainResult.status == FudStatus::Partial) { m_bufferLength = drainResult.bytesDrained; auto count = min(length, m_bufferLength); auto copyStatus = copyMem(sink, count, m_buffer.data(), count); fudAssert(copyStatus == FudStatus::Success); sink += count; length -= count; if (drainResult.status == FudStatus::Partial && length == 0) { drainResult.status = FudStatus::Success; } m_bufferPosition = count; result.bytesDrained += count; } return result; } /** \brief Attempt to read one UTF8 sequence. */ DrainResult BufferedRegularFile::readUtf8(Utf8& sink, Option maxExtraAttempts) { size_t extraAttempts{maxExtraAttempts.valueOr(0)}; Array utf8Data{}; auto drainResult = read(reinterpret_cast(utf8Data.data()), 1, maxExtraAttempts); if (drainResult.status != FudStatus::Success) { return drainResult; } auto utf8Type = utf8TypeFromByte(utf8Data[0]); uint8_t bytesToRead{0}; switch (utf8Type) { case Utf8Type::Ascii: break; case Utf8Type::Utf82Byte: bytesToRead = 1; break; case Utf8Type::Utf83Byte: bytesToRead = 2; break; case Utf8Type::Utf84Byte: bytesToRead = 3; break; case Utf8Type::Invalid: default: sink = Utf8{Ascii{utf8Data[0]}}; drainResult.status = FudStatus::Utf8Invalid; return drainResult; } if (bytesToRead > 0) { auto utf8ReadResult = read(reinterpret_cast(utf8Data.data() + 1), bytesToRead, extraAttempts); drainResult.status = utf8ReadResult.status; drainResult.bytesDrained += utf8ReadResult.bytesDrained; } sink = Utf8::make(utf8Data); return drainResult; } void BufferedRegularFile::drainReadBuffer(std::byte*& sink, size_t& length, DrainResult& result) { if (m_bufferLength > 0 && m_bufferPosition < m_bufferLength) { auto remainingLength = m_bufferLength - m_bufferPosition; auto count = min(length, remainingLength); auto copyStatus = copyMem(sink, length, m_buffer.data() + m_bufferPosition, count); fudAssert(copyStatus == FudStatus::Success); sink += count; length -= count; m_bufferPosition += count; result.bytesDrained += count; } } FudStatus BufferedRegularFile::setBuffer(Vector&& buffer, bool discardOldBuffer) { static_cast(buffer); static_cast(discardOldBuffer); return FudStatus::NotImplemented; } DrainResult BufferedRegularFile::flush(size_t maxExtraAttempts) { fudAssert(m_bufferLength <= m_buffer.size()); if (m_bufferLength == 0) { return {0, FudStatus::Success}; } if (m_lastOperation != Operation::Write) { m_bufferLength = 0; m_bufferPosition = 0; return {0, FudStatus::Success}; } auto drainResult = m_file.write(m_buffer.data(), m_bufferLength, maxExtraAttempts); if (drainResult.status == FudStatus::Success) { m_bufferLength = 0; } else if (drainResult.status == FudStatus::Partial) { fudAssert(drainResult.bytesDrained <= m_bufferLength); if (drainResult.bytesDrained < m_bufferLength) { auto diff = m_bufferLength - drainResult.bytesDrained; const auto* src = m_buffer.data() + drainResult.bytesDrained; auto moveStatus = copyMem(m_buffer.data(), m_buffer.size(), src, diff); fudAssert(moveStatus == FudStatus::Success); m_bufferLength = diff; } } return drainResult; } } // namespace fud