// license:BSD-3-Clause
// copyright-holders:Aaron Giles, Vas Crabb
/***************************************************************************

	corefile.c

	File access functions.

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

#include "corefile.h"

#include "coretmpl.h"
#include "osdcore.h"
#include "path.h"
#include "unicode.h"
#include "vecstream.h"

#include <algorithm>
#include <cassert>
#include <cctype>
#include <cstring>
#include <iterator>
#include <limits>


namespace util {

	namespace {

		/***************************************************************************
			VALIDATION
		***************************************************************************/

#if !defined(CRLF) || (CRLF < 1) || (CRLF > 3)
//#error CRLF undefined: must be 1 (CR), 2 (LF) or 3 (CR/LF)
#define CRLF 2
#endif



		/***************************************************************************
			TYPE DEFINITIONS
		***************************************************************************/

		class core_proxy_file : public core_file
		{
		public:
			core_proxy_file(core_file& file) noexcept : m_file(file) { }

			virtual std::error_condition seek(std::int64_t offset, int whence) noexcept override { return m_file.seek(offset, whence); }
			virtual std::error_condition tell(std::uint64_t& result) noexcept override { return m_file.tell(result); }
			virtual std::error_condition length(std::uint64_t& result) noexcept override { return m_file.length(result); }

			virtual std::error_condition read(void* buffer, std::size_t length, std::size_t& actual) noexcept override { return m_file.read(buffer, length, actual); }
			virtual std::error_condition read_at(std::uint64_t offset, void* buffer, std::size_t length, std::size_t& actual) noexcept override { return m_file.read_at(offset, buffer, length, actual); }

			virtual std::error_condition finalize() noexcept override { return m_file.finalize(); }
			virtual std::error_condition flush() noexcept override { return m_file.flush(); }
			virtual std::error_condition write(void const* buffer, std::size_t length, std::size_t& actual) noexcept override { return m_file.write(buffer, length, actual); }
			virtual std::error_condition write_at(std::uint64_t offset, void const* buffer, std::size_t length, std::size_t& actual) noexcept override { return m_file.write_at(offset, buffer, length, actual); }

			virtual bool eof() const override { return m_file.eof(); }

			virtual int getc() override { return m_file.getc(); }
			virtual int ungetc(int c) override { return m_file.ungetc(c); }
			virtual char* gets(char* s, int n) override { return m_file.gets(s, n); }
			virtual void const* buffer() override { return m_file.buffer(); }

			virtual int puts(std::string_view s) override { return m_file.puts(s); }
			virtual int vprintf(util::format_argument_pack<std::ostream> const& args) override { return m_file.vprintf(args); }
			virtual std::error_condition truncate(std::uint64_t offset) override { return m_file.truncate(offset); }

		private:
			core_file& m_file;
		};


		class core_text_file : public core_file
		{
		public:
			enum class text_file_type
			{
				OSD,        // OSD dependent encoding format used when BOMs missing
				UTF8,       // UTF-8
				UTF16BE,    // UTF-16 (big endian)
				UTF16LE,    // UTF-16 (little endian)
				UTF32BE,    // UTF-32 (UCS-4) (big endian)
				UTF32LE     // UTF-32 (UCS-4) (little endian)
			};

			virtual int getc() override;
			virtual int ungetc(int c) override;
			virtual char* gets(char* s, int n) override;
			virtual int puts(std::string_view s) override;
			virtual int vprintf(util::format_argument_pack<std::ostream> const& args) override;

		protected:
			core_text_file(std::uint32_t openflags)
				: m_openflags(openflags)
				, m_text_type(text_file_type::OSD)
				, m_back_char_head(0)
				, m_back_char_tail(0)
				, m_printf_buffer()
			{
			}

			bool read_access() const noexcept { return 0U != (m_openflags & OPEN_FLAG_READ); }
			bool write_access() const noexcept { return 0U != (m_openflags & OPEN_FLAG_WRITE); }
			bool no_bom() const noexcept { return 0U != (m_openflags & OPEN_FLAG_NO_BOM); }

			bool has_putback() const noexcept { return m_back_char_head != m_back_char_tail; }
			void clear_putback() noexcept { m_back_char_head = m_back_char_tail = 0; }

		private:
			std::uint32_t const m_openflags;                    // flags we were opened with
			text_file_type      m_text_type;                    // text output format
			char                m_back_chars[UTF8_CHAR_MAX];    // buffer to hold characters for ungetc
			int                 m_back_char_head;               // head of ungetc buffer
			int                 m_back_char_tail;               // tail of ungetc buffer
			ovectorstream       m_printf_buffer;                // persistent buffer for formatted output
		};


		class core_in_memory_file : public core_text_file
		{
		public:
			core_in_memory_file(std::uint32_t openflags, void const* data, std::size_t length, bool copy) noexcept
				: core_text_file(openflags)
				, m_data_allocated(false)
				, m_data(copy ? nullptr : data)
				, m_offset(0)
				, m_length(length)
			{
				if (copy)
				{
					void* const buf = allocate();
					if (buf)
						std::memcpy(buf, data, length);
				}
			}

			~core_in_memory_file() override { purge(); }

			virtual std::error_condition seek(std::int64_t offset, int whence) noexcept override;
			virtual std::error_condition tell(std::uint64_t& result) noexcept override { result = m_offset; return std::error_condition(); }
			virtual std::error_condition length(std::uint64_t& result) noexcept override { result = m_length; return std::error_condition(); }

			virtual std::error_condition read(void* buffer, std::size_t length, std::size_t& actual) noexcept override;
			virtual std::error_condition read_at(std::uint64_t offset, void* buffer, std::size_t length, std::size_t& actual) noexcept override;

			virtual std::error_condition finalize() noexcept override { return std::error_condition(); }
			virtual std::error_condition flush() noexcept override { clear_putback(); return std::error_condition(); }
			virtual std::error_condition write(void const* buffer, std::size_t length, std::size_t& actual) noexcept override { actual = 0; return std::errc::bad_file_descriptor; }
			virtual std::error_condition write_at(std::uint64_t offset, void const* buffer, std::size_t length, std::size_t& actual) noexcept override { actual = 0; return std::errc::bad_file_descriptor; }

			virtual bool eof() const override;

			virtual void const* buffer() override { return m_data; }

			virtual std::error_condition truncate(std::uint64_t offset) override;

		protected:
			core_in_memory_file(std::uint32_t openflags, std::uint64_t length) noexcept
				: core_text_file(openflags)
				, m_data_allocated(false)
				, m_data(nullptr)
				, m_offset(0)
				, m_length(length)
			{
			}

			bool is_loaded() const noexcept { return nullptr != m_data; }

			void* allocate() noexcept
			{
				if (m_data || (std::numeric_limits<std::size_t>::max() < m_length))
					return nullptr;
				void* data = malloc(m_length);
				if (data)
				{
					m_data_allocated = true;
					m_data = data;
				}
				return data;
			}

			void purge() noexcept
			{
				if (m_data && m_data_allocated)
					free(const_cast<void*>(m_data));
				m_data_allocated = false;
				m_data = nullptr;
			}

			std::uint64_t offset() const noexcept { return m_offset; }
			void add_offset(std::size_t increment) noexcept { m_offset += increment; m_length = (std::max)(m_length, m_offset); }
			std::uint64_t length() const noexcept { return m_length; }
			void set_length(std::uint64_t value) noexcept { m_length = value; }

			static std::size_t safe_buffer_copy(
				void const* source, std::size_t sourceoffs, std::size_t sourcelen,
				void* dest, std::size_t destoffs, std::size_t destlen) noexcept;

		private:
			bool            m_data_allocated;   // was the data allocated by us?
			void const* m_data;             // file data, if RAM-based
			std::uint64_t   m_offset;           // current file offset
			std::uint64_t   m_length;           // total file length
		};


		class core_osd_file final : public core_in_memory_file
		{
		public:
			core_osd_file(std::uint32_t openmode, osd_file::ptr&& file, std::uint64_t length)
				: core_in_memory_file(openmode, length)
				, m_file(std::move(file))
			{
			}
			~core_osd_file() override;

			virtual std::error_condition read(void* buffer, std::size_t length, std::size_t& actual) noexcept override;
			virtual std::error_condition read_at(std::uint64_t offset, void* buffer, std::size_t length, std::size_t& actual) noexcept override;

			virtual std::error_condition finalize() noexcept override;
			virtual std::error_condition flush() noexcept override;
			virtual std::error_condition write(void const* buffer, std::size_t length, std::size_t& actual) noexcept override;
			virtual std::error_condition write_at(std::uint64_t offset, void const* buffer, std::size_t length, std::size_t& actual) noexcept override;

			virtual void const* buffer() override;

			virtual std::error_condition truncate(std::uint64_t offset) override;

		protected:

			bool is_buffered(std::uint64_t offset) const noexcept { return (offset >= m_bufferbase) && (offset < (m_bufferbase + m_bufferbytes)); }

		private:
			static constexpr std::size_t FILE_BUFFER_SIZE = 512;

			osd_file::ptr   m_file;                     // OSD file handle
			std::uint64_t   m_bufferbase = 0U;          // base offset of internal buffer
			std::uint32_t   m_bufferbytes = 0U;         // bytes currently loaded into buffer
			std::uint8_t    m_buffer[FILE_BUFFER_SIZE]; // buffer data
		};



		/***************************************************************************
			core_text_file
		***************************************************************************/

		/*-------------------------------------------------
			getc - read a character from a file
		-------------------------------------------------*/

		int core_text_file::getc()
		{
			// refresh buffer, if necessary
			if (m_back_char_head == m_back_char_tail)
			{
				// do we need to check the byte order marks?
				std::uint64_t pos;
				if (!tell(pos))
				{
					if (!pos)
					{
						std::size_t readlen;
						std::uint8_t bom[4];
						read(bom, 4, readlen);
						if (readlen == 4)
						{
							if (bom[0] == 0xef && bom[1] == 0xbb && bom[2] == 0xbf)
							{
								m_text_type = text_file_type::UTF8;
								pos = 3;
							}
							else if (bom[0] == 0x00 && bom[1] == 0x00 && bom[2] == 0xfe && bom[3] == 0xff)
							{
								m_text_type = text_file_type::UTF32BE;
								pos = 4;
							}
							else if (bom[0] == 0xff && bom[1] == 0xfe && bom[2] == 0x00 && bom[3] == 0x00)
							{
								m_text_type = text_file_type::UTF32LE;
								pos = 4;
							}
							else if (bom[0] == 0xfe && bom[1] == 0xff)
							{
								m_text_type = text_file_type::UTF16BE;
								pos = 2;
							}
							else if (bom[0] == 0xff && bom[1] == 0xfe)
							{
								m_text_type = text_file_type::UTF16LE;
								pos = 2;
							}
							else
							{
								m_text_type = text_file_type::OSD;
								pos = 0;
							}
						}
						seek(pos, SEEK_SET); // FIXME: don't assume seeking is possible, check for errors
					}
				}

				// fetch the next character
				// FIXME: all of this plays fast and loose with error checking and seeks backwards far too frequently
				char16_t utf16_buffer[UTF16_CHAR_MAX];
				auto uchar = char32_t(~0);
				switch (m_text_type)
				{
				default:
				case text_file_type::OSD:
				{
					char default_buffer[16];
					std::size_t readlen;
					read(default_buffer, sizeof(default_buffer), readlen);
					if (readlen > 0)
					{
						auto const charlen = osd_uchar_from_osdchar(&uchar, default_buffer, readlen / sizeof(default_buffer[0]));
						seek(std::int64_t(charlen * sizeof(default_buffer[0])) - readlen, SEEK_CUR);
					}
				}
				break;

				case text_file_type::UTF8:
				{
					char utf8_buffer[UTF8_CHAR_MAX];
					std::size_t readlen;
					read(utf8_buffer, sizeof(utf8_buffer), readlen);
					if (readlen > 0)
					{
						auto const charlen = uchar_from_utf8(&uchar, utf8_buffer, readlen / sizeof(utf8_buffer[0]));
						seek(std::int64_t(charlen * sizeof(utf8_buffer[0])) - readlen, SEEK_CUR);
					}
				}
				break;

				case text_file_type::UTF16BE:
				{
					std::size_t readlen;
					read(utf16_buffer, sizeof(utf16_buffer), readlen);
					if (readlen > 0)
					{
						auto const charlen = uchar_from_utf16be(&uchar, utf16_buffer, readlen / sizeof(utf16_buffer[0]));
						seek(std::int64_t(charlen * sizeof(utf16_buffer[0])) - readlen, SEEK_CUR);
					}
				}
				break;

				case text_file_type::UTF16LE:
				{
					std::size_t readlen;
					read(utf16_buffer, sizeof(utf16_buffer), readlen);
					if (readlen > 0)
					{
						auto const charlen = uchar_from_utf16le(&uchar, utf16_buffer, readlen / sizeof(utf16_buffer[0]));
						seek(std::int64_t(charlen * sizeof(utf16_buffer[0])) - readlen, SEEK_CUR);
					}
				}
				break;

				case text_file_type::UTF32BE:
				{
					// FIXME: deal with read returning short
					std::size_t readlen;
					read(&uchar, sizeof(uchar), readlen);
					if (sizeof(uchar) == readlen)
						uchar = big_endianize_int32(uchar);
				}
				break;

				case text_file_type::UTF32LE:
				{
					// FIXME: deal with read returning short
					std::size_t readlen;
					read(&uchar, sizeof(uchar), readlen);
					if (sizeof(uchar) == readlen)
						uchar = little_endianize_int32(uchar);
				}
				break;
				}

				if (uchar != ~0)
				{
					// place the new character in the ring buffer
					m_back_char_head = 0;
					m_back_char_tail = utf8_from_uchar(m_back_chars, std::size(m_back_chars), uchar);
					//assert(file->back_char_tail != -1);
				}
			}

			// now read from the ring buffer
			int result;
			if (m_back_char_head == m_back_char_tail)
				result = EOF;
			else
			{
				result = m_back_chars[m_back_char_head++];
				m_back_char_head %= std::size(m_back_chars);
			}

			return result;
		}


		/*-------------------------------------------------
			ungetc - put back a character read from a
			file
		-------------------------------------------------*/

		int core_text_file::ungetc(int c)
		{
			m_back_chars[m_back_char_tail++] = char(c);
			m_back_char_tail %= std::size(m_back_chars);
			return c;
		}


		/*-------------------------------------------------
			gets - read a line from a text file
		-------------------------------------------------*/

		char* core_text_file::gets(char* s, int n)
		{
			char* cur = s;

			// loop while we have characters
			while (n > 0)
			{
				int const c = getc();
				if (c == EOF)
					break;
				else if (c == 0x0d) // if there's a CR, look for an LF afterwards
				{
					int const c2 = getc();
					if (c2 != 0x0a)
						ungetc(c2);
					*cur++ = 0x0d;
					n--;
					break;
				}
				else if (c == 0x0a) // if there's an LF, reinterp as a CR for consistency
				{
					*cur++ = 0x0d;
					n--;
					break;
				}
				else // otherwise, pop the character in and continue
				{
					*cur++ = c;
					n--;
				}
			}

			// if we put nothing in, return nullptr
			if (cur == s)
				return nullptr;

			/* otherwise, terminate */
			if (n > 0)
				*cur++ = 0;
			return s;
		}


		/*-------------------------------------------------
			puts - write a string to a text file
		-------------------------------------------------*/

		int core_text_file::puts(std::string_view s)
		{
			// TODO: what to do about write errors or short writes (interrupted)?
			// The API doesn't lend itself to reporting the error as the return
			// value includes extra bytes inserted like the UTF-8 marker and
			// carriage returns.
			char convbuf[1024];
			char* pconvbuf = convbuf;
			int count = 0;

			// is this the beginning of the file?  if so, write a byte order mark
			if (!no_bom())
			{
				std::uint64_t offset;
				if (!tell(offset))
				{
					if (!offset)
					{
						*pconvbuf++ = char(0xef);
						*pconvbuf++ = char(0xbb);
						*pconvbuf++ = char(0xbf);
					}
				}
			}

			// convert '\n' to platform dependant line endings
			for (char ch : s)
			{
				if (ch == '\n')
				{
					if (CRLF == 1)      // CR only
						*pconvbuf++ = 13;
					else if (CRLF == 2) // LF only
						*pconvbuf++ = 10;
					else if (CRLF == 3) // CR+LF
					{
						*pconvbuf++ = 13;
						*pconvbuf++ = 10;
					}
				}
				else
					*pconvbuf++ = ch;

				// if we overflow, break into chunks
				if (pconvbuf >= convbuf + std::size(convbuf) - 10)
				{
					std::size_t written;
					write(convbuf, pconvbuf - convbuf, written); // FIXME: error ignored here
					count += written;
					pconvbuf = convbuf;
				}
			}

			// final flush
			if (pconvbuf != convbuf)
			{
				std::size_t written;
				write(convbuf, pconvbuf - convbuf, written); // FIXME: error ignored here
				count += written;
			}

			return count;
		}


		/*-------------------------------------------------
			vprintf - vfprintf to a text file
		-------------------------------------------------*/

		int core_text_file::vprintf(util::format_argument_pack<std::ostream> const& args)
		{
			m_printf_buffer.clear();
			m_printf_buffer.reserve(1024);
			m_printf_buffer.seekp(0, ovectorstream::beg);
			util::stream_format<std::ostream, std::ostream>(m_printf_buffer, args);
			return puts(buf_to_string_view(m_printf_buffer));
		}



		/***************************************************************************
			core_in_memory_file
		***************************************************************************/

		/*-------------------------------------------------
			seek - seek within a file
		-------------------------------------------------*/

		std::error_condition core_in_memory_file::seek(std::int64_t offset, int whence) noexcept
		{
			// flush any buffered char
			clear_putback(); // TODO: report errors; also, should the argument check happen before this?

			// switch off the relative location
			switch (whence)
			{
			case SEEK_SET:
				if (0 > offset)
					return std::errc::invalid_argument;
				m_offset = offset;
				return std::error_condition();

			case SEEK_CUR:
				if (0 > offset)
				{
					if (std::uint64_t(-offset) > m_offset)
						return std::errc::invalid_argument;
				}
				else if ((std::numeric_limits<std::uint64_t>::max() - offset) < m_offset)
				{
					return std::errc::invalid_argument;
				}
				m_offset += offset;
				return std::error_condition();

			case SEEK_END:
				if (0 > offset)
				{
					if (std::uint64_t(-offset) > m_length)
						return std::errc::invalid_argument;
				}
				else if ((std::numeric_limits<std::uint64_t>::max() - offset) < m_length)
				{
					return std::errc::invalid_argument;
				}
				m_offset = m_length + offset;
				return std::error_condition();

			default:
				return std::errc::invalid_argument;
			}
		}


		/*-------------------------------------------------
			eof - return 1 if we're at the end of file
		-------------------------------------------------*/

		bool core_in_memory_file::eof() const
		{
			// check for buffered chars
			if (has_putback())
				return false;

			// if the offset == length, we're at EOF
			return (m_offset >= m_length);
		}


		/*-------------------------------------------------
			read - read from a file
		-------------------------------------------------*/

		std::error_condition core_in_memory_file::read(void* buffer, std::size_t length, std::size_t& actual) noexcept
		{
			clear_putback();

			// handle RAM-based files
			if (m_offset < m_length)
				actual = safe_buffer_copy(m_data, std::size_t(m_offset), std::size_t(m_length), buffer, 0, length);
			else
				actual = 0U;
			m_offset += actual;
			return std::error_condition();
		}

		std::error_condition core_in_memory_file::read_at(std::uint64_t offset, void* buffer, std::size_t length, std::size_t& actual) noexcept
		{
			clear_putback();

			// handle RAM-based files
			if (offset < m_length)
				actual = safe_buffer_copy(m_data, std::size_t(offset), std::size_t(m_length), buffer, 0, length);
			else
				actual = 0U;
			return std::error_condition();
		}


		/*-------------------------------------------------
			truncate - truncate a file
		-------------------------------------------------*/

		std::error_condition core_in_memory_file::truncate(std::uint64_t offset)
		{
			if (m_length < offset)
				return std::errc::io_error; // TODO: revisit this error code

			// adjust to new length and offset
			set_length(offset);
			return std::error_condition();
		}


		/*-------------------------------------------------
			safe_buffer_copy - copy safely from one
			bounded buffer to another
		-------------------------------------------------*/

		std::size_t core_in_memory_file::safe_buffer_copy(
			void const* source, std::size_t sourceoffs, std::size_t sourcelen,
			void* dest, std::size_t destoffs, std::size_t destlen) noexcept
		{
			auto const sourceavail = sourcelen - sourceoffs;
			auto const destavail = destlen - destoffs;
			auto const bytes_to_copy = (std::min)(sourceavail, destavail);
			if (bytes_to_copy > 0)
			{
				std::memcpy(
					reinterpret_cast<std::uint8_t*>(dest) + destoffs,
					reinterpret_cast<std::uint8_t const*>(source) + sourceoffs,
					bytes_to_copy);
			}
			return bytes_to_copy;
		}



		/***************************************************************************
			core_osd_file
		***************************************************************************/

		/*-------------------------------------------------
			closes a file
		-------------------------------------------------*/

		core_osd_file::~core_osd_file()
		{
			// close files and free memory
		}


		/*-------------------------------------------------
			read - read from a file
		-------------------------------------------------*/

		std::error_condition core_osd_file::read(void* buffer, std::size_t length, std::size_t& actual) noexcept
		{
			// since osd_file works like pread/pwrite, implement in terms of read_at
			// core_osd_file is delcared final, so a derived class can't interfere
			std::error_condition err = read_at(offset(), buffer, length, actual);
			add_offset(actual);
			return err;
		}


		std::error_condition core_osd_file::read_at(std::uint64_t offset, void* buffer, std::size_t length, std::size_t& actual) noexcept
		{
			if (!m_file || is_loaded())
				return core_in_memory_file::read_at(offset, buffer, length, actual);

			// flush any buffered char
			clear_putback();

			actual = 0U;
			std::error_condition err;

			// if we're within the buffer, consume that first
			if (is_buffered(offset))
				actual += safe_buffer_copy(m_buffer, offset - m_bufferbase, m_bufferbytes, buffer, actual, length);

			// if we've got a small amount left, read it into the buffer first
			if (actual < length)
			{
				if ((length - actual) < (sizeof(m_buffer) / 2))
				{
					// read as much as makes sense into the buffer
					m_bufferbase = offset + actual;
					err = m_file->read(m_buffer, m_bufferbase, sizeof(m_buffer), m_bufferbytes);

					// do a bounded copy from the buffer to the destination if it succeeded
					if (!err)
						actual += safe_buffer_copy(m_buffer, 0, m_bufferbytes, buffer, actual, length);
					else
						m_bufferbytes = 0U;
				}
				else
				{
					// read the remainder directly from the file
					do
					{
						// may need to split into chunks if size_t is larger than 32 bits
						std::uint32_t const chunk = std::min<std::common_type_t<std::uint32_t, std::size_t> >(std::numeric_limits<std::uint32_t>::max(), length - actual);
						std::uint32_t bytes_read;
						err = m_file->read(reinterpret_cast<std::uint8_t*>(buffer) + actual, offset + actual, chunk, bytes_read);
						if (err || !bytes_read)
							break;
						actual += bytes_read;
					} while (actual < length);
				}
			}

			// return any errors
			return err;
		}


		/*-------------------------------------------------
			buffer - return a pointer to the file buffer;
			if it doesn't yet exist, load the file into
			RAM first
		-------------------------------------------------*/

		void const* core_osd_file::buffer()
		{
			// if we already have data, just return it
			if (!is_loaded() && length())
			{
				// allocate some memory
				void* const buf = allocate();
				if (!buf)
					return nullptr;

				// read the file
				std::uint64_t bytes_read = 0;
				std::uint64_t remaining = length();
				std::uint8_t* ptr = reinterpret_cast<std::uint8_t*>(buf);
				while (remaining)
				{
					std::uint32_t const chunk = std::min<std::common_type_t<std::uint32_t, std::size_t> >(std::numeric_limits<std::uint32_t>::max(), remaining);
					std::uint32_t read_length;
					std::error_condition const filerr = m_file->read(ptr, bytes_read, chunk, read_length);
					if (filerr || !read_length)
					{
						purge();
						return core_in_memory_file::buffer();
					}
					bytes_read += read_length;
					remaining -= read_length;
					ptr += read_length;
				}
				m_file.reset(); // close the file because we don't need it anymore
			}
			return core_in_memory_file::buffer();
		}


		/*-------------------------------------------------
			write - write to a file
		-------------------------------------------------*/

		std::error_condition core_osd_file::write(void const* buffer, std::size_t length, std::size_t& actual) noexcept
		{
			// since osd_file works like pread/pwrite, implement in terms of write_at
			// core_osd_file is delcared final, so a derived class can't interfere
			std::error_condition err = write_at(offset(), buffer, length, actual);
			add_offset(actual);
			return err;
		}


		std::error_condition core_osd_file::write_at(std::uint64_t offset, void const* buffer, std::size_t length, std::size_t& actual) noexcept
		{
			// can't write to RAM-based stuff
			if (is_loaded())
				return core_in_memory_file::write_at(offset, buffer, length, actual);

			// flush any buffered char
			clear_putback();

			// invalidate any buffered data
			m_bufferbytes = 0U;

			// do the write - may need to split into chunks if size_t is larger than 32 bits
			actual = 0U;
			while (length)
			{
				// bytes written not valid on error
				std::uint32_t const chunk = std::min<std::common_type_t<std::uint32_t, std::size_t> >(std::numeric_limits<std::uint32_t>::max(), length);
				std::uint32_t bytes_written;
				std::error_condition err = m_file->write(buffer, offset, chunk, bytes_written);
				if (err)
					return err;
				assert(chunk >= bytes_written);
				offset += bytes_written;
				buffer = reinterpret_cast<std::uint8_t const*>(buffer) + bytes_written;
				length -= bytes_written;
				actual += bytes_written;
				set_length((std::max)(this->length(), offset));
			}
			return std::error_condition();
		}


		/*-------------------------------------------------
			truncate - truncate a file
		-------------------------------------------------*/

		std::error_condition core_osd_file::truncate(std::uint64_t offset)
		{
			if (is_loaded())
				return core_in_memory_file::truncate(offset);

			// truncate file
			std::error_condition err = m_file->truncate(offset);
			if (err)
				return err;

			// and adjust to new length and offset
			set_length(offset);
			return std::error_condition();
		}


		/*-------------------------------------------------
			flush - flush file buffers
		-------------------------------------------------*/

		std::error_condition core_osd_file::finalize() noexcept
		{
			if (is_loaded())
				return core_in_memory_file::finalize();

			return std::error_condition();
		}


		std::error_condition core_osd_file::flush() noexcept
		{
			if (is_loaded())
				return core_in_memory_file::flush();

			// flush any buffered char
			clear_putback();

			// invalidate any buffered data
			m_bufferbytes = 0U;

			return m_file->flush();
		}

	} // anonymous namespace



	/***************************************************************************
		core_file
	***************************************************************************/

	/*-------------------------------------------------
		open - open a file for access and
		return an error code
	-------------------------------------------------*/

	std::error_condition core_file::open(std::string_view filename, std::uint32_t openflags, ptr& file) noexcept
	{
		// attempt to open the file
		osd_file::ptr f;
		std::uint64_t length = 0;
		auto const filerr = osd_file::open(std::string(filename), openflags, f, length); // FIXME: allow osd_file to accept std::string_view
		if (filerr)
			return filerr;

		try { file = std::make_unique<core_osd_file>(openflags, std::move(f), length); }
		catch (...) { return std::errc::not_enough_memory; }

		return std::error_condition();
	}


	/*-------------------------------------------------
		open_ram - open a RAM-based buffer for file-
		like access and return an error code
	-------------------------------------------------*/

	std::error_condition core_file::open_ram(void const* data, std::size_t length, std::uint32_t openflags, ptr& file) noexcept
	{
		// can only do this for read access
		if ((openflags & OPEN_FLAG_WRITE) || (openflags & OPEN_FLAG_CREATE))
			return std::errc::invalid_argument;

		// if length is non-zero, data must be non-null
		if (length && !data)
			return std::errc::invalid_argument;

		// platforms where size_t is larger than 64 bits are theoretically possible
		if (std::uint64_t(length) != length)
			return std::errc::file_too_large;

		ptr result(new (std::nothrow) core_in_memory_file(openflags, data, length, false));
		if (!result)
			return std::errc::not_enough_memory;

		file = std::move(result);
		return std::error_condition();
	}


	/*-------------------------------------------------
		open_ram_copy - open a copy of a RAM-based
		buffer for file-like access and return an
		error code
	-------------------------------------------------*/

	std::error_condition core_file::open_ram_copy(void const* data, std::size_t length, std::uint32_t openflags, ptr& file) noexcept
	{
		// can only do this for read access
		if ((openflags & OPEN_FLAG_WRITE) || (openflags & OPEN_FLAG_CREATE))
			return std::errc::invalid_argument;

		// if length is non-zero, data must be non-null
		if (length && !data)
			return std::errc::invalid_argument;

		// platforms where size_t is larger than 64 bits are theoretically possible
		if (std::uint64_t(length) != length)
			return std::errc::file_too_large;

		ptr result(new (std::nothrow) core_in_memory_file(openflags, data, length, true));
		if (!result || !result->buffer())
			return std::errc::not_enough_memory;

		file = std::move(result);
		return std::error_condition();
	}


	/*-------------------------------------------------
		open_proxy - open a proxy to an existing file
		object and return an error code
	-------------------------------------------------*/

	std::error_condition core_file::open_proxy(core_file& file, ptr& proxy) noexcept
	{
		ptr result(new (std::nothrow) core_proxy_file(file));
		if (!result)
			return std::errc::not_enough_memory;

		proxy = std::move(result);
		return std::error_condition();
	}


	/*-------------------------------------------------
		closes a file
	-------------------------------------------------*/

	core_file::~core_file()
	{
	}


	/*-------------------------------------------------
		load - open a file with the specified
		filename, read it into memory, and return a
		pointer
	-------------------------------------------------*/

	std::error_condition core_file::load(std::string_view filename, void** data, std::uint32_t& length) noexcept
	{
		std::error_condition err;

		// attempt to open the file
		ptr file;
		err = open(filename, OPEN_FLAG_READ, file);
		if (err)
			return err;

		// get the size
		std::uint64_t size;
		err = file->length(size);
		if (err)
			return err;
		else if (std::uint32_t(size) != size) // TODO: change interface to use size_t rather than uint32_t for output size
			return std::errc::file_too_large;

		// allocate memory
		*data = std::malloc(std::size_t(size));
		if (!*data)
			return std::errc::not_enough_memory;
		length = std::uint32_t(size);

		// read the data
		if (size)
		{
			std::size_t actual;
			err = file->read(*data, std::size_t(size), actual);
			if (err || (size != actual))
			{
				std::free(*data);
				*data = nullptr;
				if (err)
					return err;
				else
					return std::errc::io_error; // TODO: revisit this error code - either interrupted by an async signal or file truncated out from under us
			}
		}

		// close the file and return data
		return std::error_condition();
	}

	std::error_condition core_file::load(std::string_view filename, std::vector<uint8_t>& data) noexcept
	{
		std::error_condition err;

		// attempt to open the file
		ptr file;
		err = open(filename, OPEN_FLAG_READ, file);
		if (err)
			return err;

		// get the size
		std::uint64_t size;
		err = file->length(size);
		if (err)
			return err;
		else if (std::size_t(size) != size)
			return std::errc::file_too_large;

		// allocate memory
		try { data.resize(std::size_t(size)); }
		catch (...) { return std::errc::not_enough_memory; }

		// read the data
		if (size)
		{
			std::size_t actual;
			err = file->read(&data[0], std::size_t(size), actual);
			if (err || (size != actual))
			{
				data.clear();
				if (err)
					return err;
				else
					return std::errc::io_error; // TODO: revisit this error code - either interrupted by an async signal or file truncated out from under us
			}
		}

		// close the file and return data
		return std::error_condition();
	}

} // namespace util



/***************************************************************************
	FILENAME UTILITIES
***************************************************************************/

// -------------------------------------------------
// core_filename_extract_base - extract the base
// name from a filename; note that this makes
// assumptions about path separators
// -------------------------------------------------

std::string_view core_filename_extract_base(std::string_view name, bool strip_extension) noexcept
{
	// find the start of the basename
	auto const start = std::find_if(name.rbegin(), name.rend(), &util::is_directory_separator);
	if (start == name.rbegin())
		return std::string_view();

	// find the end of the basename
	auto const chop_position = strip_extension
		? std::find(name.rbegin(), start, '.')
		: start;
	auto const end = ((chop_position != start) && (std::next(chop_position) != start))
		? std::next(chop_position)
		: name.rbegin();

	return std::string_view(&*start.base(), end.base() - start.base());
}


// -------------------------------------------------
// core_filename_extract_extension
// -------------------------------------------------

std::string_view core_filename_extract_extension(std::string_view filename, bool strip_period) noexcept
{
	auto loc = filename.find_last_of('.');
	if (loc != std::string_view::npos)
		return filename.substr(loc + (strip_period ? 1 : 0));
	else
		return std::string_view();
}


// -------------------------------------------------
// core_filename_ends_with - does the given
// filename end with the specified extension?
// -------------------------------------------------

bool core_filename_ends_with(std::string_view filename, std::string_view extension) noexcept
{
	auto namelen = filename.length();
	auto extlen = extension.length();

	// first if the extension is bigger than the name, we definitely don't match
	bool matches = namelen >= extlen;

	// work backwards checking for a match
	while (matches && extlen > 0 && namelen > 0)
	{
		if (tolower((uint8_t)filename[--namelen]) != tolower((uint8_t)extension[--extlen]))
			matches = false;
	}

	return matches;
}
