OverviewFilesCommitsIssuesDocsReleasesPeers

hyperswarm-peer / liboverlay / include / overlay / fileio.h

200 lines · c ref:
#ifndef HYPEROVERLAY_FILEIO_H
#define HYPEROVERLAY_FILEIO_H

// Positioned file I/O — the only place in liboverlay that touches file
// descriptors, so a platform port is a contained change rather than a hunt.
//
// WHY POSITIONED, AND WHY ONE HANDLE. A transfer writes chunks at their offsets
// in whatever order they arrive, while the whole-file hash reads earlier chunks
// back to advance its frontier — two access patterns over one file at once. With
// a shared cursor those interleave into corruption; pread/pwrite carry their own
// offset and do not move one, so a single handle serves both. The reference makes
// the same call for the same reason (transfer.js:319-323), and it replaced an
// open+close per chunk.
//
// SHORT COUNTS ARE NOT SUCCESS. pwrite may write fewer bytes than asked without
// setting an error — the reference has a hard-won note about exactly this
// (transfer.js:630-640): libuv swallows an error after partial progress and
// returns short, leaving a hole that the digest, fed from memory, never sees. So
// every read and write loops to completion and a stalled count is an error, not a
// silent partial.
//
// PLATFORM: POSIX (Linux/macOS) and Windows. The Windows half is exactly what
// this comment used to predict -- ReadFile/WriteFile + OVERLAPPED, below, and
// nothing else in the library changed. Offsets are cast to int64_t rather than
// off_t because mingw's off_t is 32 bits, which would silently truncate any
// file past 2GB.

#include <cerrno>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <string>

#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>

#ifdef _WIN32
#include <io.h>
#include <windows.h>

// POSIX positional I/O, for Windows. pread/pwrite/fsync/ftruncate do not exist in
// the MSVC CRT; these are the Win32 equivalents, kept as free functions so the
// class below reads identically on both platforms rather than growing an #ifdef
// through every method.
//
// OVERLAPPED carries the offset. It is legal on a handle opened WITHOUT
// FILE_FLAG_OVERLAPPED: the call completes synchronously and the offset in the
// structure is used instead of the file pointer, which is exactly pread's
// contract and the reason this does not need a seek+read pair (that pair is not
// atomic, and this class is read concurrently from the chunk scheduler).
static inline ssize_t pread(int fd, void *buf, size_t n, int64_t off) {
	const HANDLE h = (HANDLE)_get_osfhandle(fd);
	if (h == INVALID_HANDLE_VALUE) { errno = EBADF; return -1; }
	OVERLAPPED ov = {};
	ov.Offset = (DWORD)(off & 0xFFFFFFFFu);
	ov.OffsetHigh = (DWORD)((off >> 32) & 0xFFFFFFFFu);
	DWORD got = 0;
	if (!ReadFile(h, buf, (DWORD)n, &got, &ov)) {
		// Reading AT or past the end reports an error rather than 0 bytes; the
		// caller's short-file branch wants the 0.
		if (GetLastError() == ERROR_HANDLE_EOF) return 0;
		errno = EIO;
		return -1;
	}
	return (ssize_t)got;
}

static inline ssize_t pwrite(int fd, const void *buf, size_t n, int64_t off) {
	const HANDLE h = (HANDLE)_get_osfhandle(fd);
	if (h == INVALID_HANDLE_VALUE) { errno = EBADF; return -1; }
	OVERLAPPED ov = {};
	ov.Offset = (DWORD)(off & 0xFFFFFFFFu);
	ov.OffsetHigh = (DWORD)((off >> 32) & 0xFFFFFFFFu);
	DWORD put = 0;
	if (!WriteFile(h, buf, (DWORD)n, &put, &ov)) { errno = EIO; return -1; }
	return (ssize_t)put;
}

static inline int fsync(int fd) {
	const HANDLE h = (HANDLE)_get_osfhandle(fd);
	if (h == INVALID_HANDLE_VALUE) { errno = EBADF; return -1; }
	return FlushFileBuffers(h) ? 0 : -1;
}

static inline int ftruncate(int fd, int64_t size) {
	return _chsize_s(fd, size);
}

// AND THE FILES MUST BE OPENED BINARY. Windows defaults to text mode, which
// translates \n to \r\n on write and back on read — silent, and catastrophic for
// content-addressed chunks: every byte pair would survive a round trip on the
// same host and hash differently from the Linux peer that sent it.
#ifndef O_BINARY
#define O_BINARY 0
#endif
#define OVERLAY_O_BINARY O_BINARY
#else
#define OVERLAY_O_BINARY 0
#endif

namespace hyperoverlay {
namespace io {

class File {
public:
	File() = default;
	~File() { close(); }
	File(const File &) = delete;
	File &operator=(const File &) = delete;

	// Fresh file: create, truncating anything already there.
	bool create_rw(const std::string &path) {
		close();
		fd_ = ::open(path.c_str(), O_RDWR | O_CREAT | O_TRUNC | OVERLAY_O_BINARY, 0644);
		return fd_ >= 0;
	}
	// Existing file, kept as-is — the resume case.
	bool open_rw(const std::string &path) {
		close();
		fd_ = ::open(path.c_str(), O_RDWR | OVERLAY_O_BINARY);
		return fd_ >= 0;
	}
	bool open_read(const std::string &path) {
		close();
		fd_ = ::open(path.c_str(), O_RDONLY | OVERLAY_O_BINARY);
		return fd_ >= 0;
	}

	// Preallocate so chunks can be written at their offsets from the first one,
	// rather than the file growing in whatever order bytes happen to arrive.
	bool truncate_to(uint64_t size) { return fd_ >= 0 && ::ftruncate(fd_, (int64_t)size) == 0; }

	// Reads `n` bytes at `off`, looping over short reads. False on error or EOF
	// before n bytes — a caller asking for a chunk wants all of it or nothing.
	bool read_at(uint64_t off, uint8_t *buf, size_t n, int *err = nullptr) const {
		size_t done = 0;
		while (done < n) {
			const ssize_t r = ::pread(fd_, buf + done, n - done, (int64_t)(off + done));
			if (r < 0) {
				if (errno == EINTR) continue;
				if (err) *err = errno;
				return false;
			}
			if (r == 0) { if (err) *err = 0; return false; }   // short file
			done += (size_t)r;
		}
		return true;
	}

	bool write_at(uint64_t off, const uint8_t *buf, size_t n, int *err = nullptr) {
		size_t done = 0;
		while (done < n) {
			const ssize_t w = ::pwrite(fd_, buf + done, n - done, (int64_t)(off + done));
			if (w < 0) {
				if (errno == EINTR) continue;
				if (err) *err = errno;
				return false;
			}
			if (w == 0) { if (err) *err = EIO; return false; }   // stalled, not partial success
			done += (size_t)w;
		}
		return true;
	}

	// Best-effort: a read-only or unusual handle can refuse fsync harmlessly, and
	// the reference swallows it for that reason. The durability this backs up is
	// re-established by the whole-file hash at finalize either way.
	void sync() { if (fd_ >= 0) ::fsync(fd_); }

	void close() { if (fd_ >= 0) { ::close(fd_); fd_ = -1; } }
	bool is_open() const { return fd_ >= 0; }

private:
	int fd_ = -1;
};

inline bool file_size(const std::string &path, uint64_t &out) {
	struct stat st;
	if (::stat(path.c_str(), &st) != 0) return false;
	if (!S_ISREG(st.st_mode)) return false;
	out = (uint64_t)st.st_size;
	return true;
}
inline bool file_exists(const std::string &path) {
	uint64_t n = 0;
	return file_size(path, n);
}
// Atomic within a filesystem — the last step of a transfer, so a reader never
// sees a partially written file under the real name.
inline bool rename_file(const std::string &from, const std::string &to) {
	return ::rename(from.c_str(), to.c_str()) == 0;
}
inline bool unlink_file(const std::string &path) { return ::unlink(path.c_str()) == 0; }

} // namespace io
} // namespace hyperoverlay

#endif // HYPEROVERLAY_FILEIO_H