#include "moar.h" #include "platform/io.h" #ifndef _WIN32 #include #include #define DEFAULT_MODE 0x01B6 typedef struct stat STAT_t; #else #include #include #define O_CREAT _O_CREAT #define O_RDONLY _O_RDONLY #define O_WRONLY _O_WRONLY #define O_TRUNC _O_TRUNC #define O_EXCL _O_EXCL #define O_RDWR _O_RDWR #define DEFAULT_MODE _S_IWRITE #define open _open #define close _close #define read _read #define write _write #define isatty _isatty #define ftruncate _chsize #define fstat _fstat typedef struct _stat STAT_t; #endif /* Data that we keep for a file-based handle. */ typedef struct { /* File descriptor. */ int fd; /* Is it seekable? */ short seekable; /* Is it known to be writable? */ short known_writable; /* How many bytes have we read/written? Used to fake tell on handles that * are not seekable. */ MVMint64 byte_position; /* Did read already report EOF? */ int eof_reported; /* Output buffer, for buffered output. */ char *output_buffer; /* Size of the output buffer, for buffered output; 0 if not buffering. */ size_t output_buffer_size; /* How much of the output buffer has been used so far. */ size_t output_buffer_used; } MVMIOFileData; /* Checks if the file is a TTY. */ static MVMint64 is_tty(MVMThreadContext *tc, MVMOSHandle *h) { MVMIOFileData *data = (MVMIOFileData *)h->body.data; return isatty(data->fd); } /* Gets the file descriptor. */ static MVMint64 mvm_fileno(MVMThreadContext *tc, MVMOSHandle *h) { MVMIOFileData *data = (MVMIOFileData *)h->body.data; return (MVMint64)data->fd; } /* Performs a write, either because a buffer filled or because we are not * buffering output. */ static void perform_write(MVMThreadContext *tc, MVMIOFileData *data, char *buf, MVMint64 bytes) { MVMint64 bytes_written = 0; MVM_gc_mark_thread_blocked(tc); while (bytes > 0) { int r; do { r = write(data->fd, buf, bytes > 0x40000000 ? 0x40000000 : bytes); } while (r == -1 && errno == EINTR); if (r == -1) { int save_errno = errno; MVM_gc_mark_thread_unblocked(tc); MVM_exception_throw_adhoc(tc, "Failed to write %"PRId64" bytes to filehandle: %s", bytes, strerror(save_errno)); } bytes_written += r; buf += r; bytes -= r; } MVM_gc_mark_thread_unblocked(tc); data->byte_position += bytes_written; data->known_writable = 1; } /* Flushes any existing output buffer and clears use back to 0. */ static void flush_output_buffer(MVMThreadContext *tc, MVMIOFileData *data) { if (data->output_buffer_used) { perform_write(tc, data, data->output_buffer, data->output_buffer_used); data->output_buffer_used = 0; } } /* Seek to the specified position in the file. */ static void seek(MVMThreadContext *tc, MVMOSHandle *h, MVMint64 offset, MVMint64 whence) { MVMIOFileData *data = (MVMIOFileData *)h->body.data; if (!data->seekable) MVM_exception_throw_adhoc(tc, "It is not possible to seek this kind of handle"); flush_output_buffer(tc, data); if (MVM_platform_lseek(data->fd, offset, whence) == -1) MVM_exception_throw_adhoc(tc, "Failed to seek in filehandle: %d", errno); } /* Get current position in the file. */ static MVMint64 mvm_tell(MVMThreadContext *tc, MVMOSHandle *h) { MVMIOFileData *data = (MVMIOFileData *)h->body.data; flush_output_buffer(tc, data); if (data->seekable) { MVMint64 r; if ((r = MVM_platform_lseek(data->fd, 0, SEEK_CUR)) == -1) MVM_exception_throw_adhoc(tc, "Failed to tell in filehandle: %d", errno); return r; } else { return data->byte_position; } } /* Reads the specified number of bytes into the supplied buffer, returning * the number actually read. */ static MVMint64 read_bytes(MVMThreadContext *tc, MVMOSHandle *h, char **buf_out, MVMuint64 bytes) { MVMIOFileData *data = (MVMIOFileData *)h->body.data; char *buf = MVM_malloc(bytes); unsigned int interval_id = MVM_telemetry_interval_start(tc, "syncfile.read_to_buffer"); MVMint32 bytes_read; #ifdef _WIN32 /* Can only perform relatively small reads from a Windows console; * trying to do larger ones gives back ENOMEM, most likely due to * limitations of the Windows console subsystem. */ if (bytes > 16387 && _isatty(data->fd)) bytes = 16387; #endif flush_output_buffer(tc, data); do { MVM_gc_mark_thread_blocked(tc); bytes_read = read(data->fd, buf, bytes); MVM_gc_mark_thread_unblocked(tc); } while(bytes_read == -1 && errno == EINTR); if (bytes_read == -1) { int save_errno = errno; MVM_free(buf); MVM_exception_throw_adhoc(tc, "Reading from filehandle failed: %s", strerror(save_errno)); } *buf_out = buf; MVM_telemetry_interval_annotate(bytes_read, interval_id, "read this many bytes"); MVM_telemetry_interval_stop(tc, interval_id, "syncfile.read_to_buffer"); data->byte_position += bytes_read; if (bytes_read == 0 && bytes != 0) data->eof_reported = 1; return bytes_read; } /* Checks if the end of file has been reached. */ static MVMint64 mvm_eof(MVMThreadContext *tc, MVMOSHandle *h) { MVMIOFileData *data = (MVMIOFileData *)h->body.data; if (data->seekable) { MVMint64 seek_pos; STAT_t statbuf; if (fstat(data->fd, &statbuf) == -1) MVM_exception_throw_adhoc(tc, "Failed to stat file descriptor: %s", strerror(errno)); if ((seek_pos = MVM_platform_lseek(data->fd, 0, SEEK_CUR)) == -1) MVM_exception_throw_adhoc(tc, "Failed to seek in filehandle: %d", errno); /* For some special files, like those in /proc, the file size is 0, * so in those cases, fall back to eof_reported flag to detect EOF. */ return statbuf.st_size ? statbuf.st_size <= seek_pos : data->eof_reported; } else { return data->eof_reported; } } /* Sets the output buffer size; if <= 0, means no buffering. Flushes any * existing buffer before changing. */ static void set_buffer_size(MVMThreadContext *tc, MVMOSHandle *h, MVMint64 size) { MVMIOFileData *data = (MVMIOFileData *)h->body.data; /* Flush and clear up any existing output buffer. */ flush_output_buffer(tc, data); MVM_free(data->output_buffer); /* Set up new buffer if needed. */ if (size > 0) { data->output_buffer_size = size; data->output_buffer = MVM_malloc(size); } else { data->output_buffer_size = 0; data->output_buffer = NULL; } } /* Writes the specified bytes to the file handle. */ static MVMint64 write_bytes(MVMThreadContext *tc, MVMOSHandle *h, char *buf, MVMuint64 bytes) { MVMIOFileData *data = (MVMIOFileData *)h->body.data; if (data->output_buffer_size && data->known_writable) { /* If we can't fit it on the end of the buffer, flush the buffer. */ if (data->output_buffer_used + bytes > data->output_buffer_size) flush_output_buffer(tc, data); /* If we can fit it in the buffer now, memcpy it there, and we're * done. */ if (bytes < data->output_buffer_size) { memcpy(data->output_buffer + data->output_buffer_used, buf, bytes); data->output_buffer_used += bytes; return bytes; } } perform_write(tc, data, buf, bytes); return bytes; } /* Flushes the file handle. */ static void flush(MVMThreadContext *tc, MVMOSHandle *h, MVMint32 sync){ MVMIOFileData *data = (MVMIOFileData *)h->body.data; flush_output_buffer(tc, data); if (sync) { if (MVM_platform_fsync(data->fd) == -1) { /* If this is something that can't be synched, we let that pass. */ if (errno != EROFS && errno != EINVAL && errno != ENOTSUP #ifdef WSL_BASH_ON_WIN && ! (errno == EIO && ! data->seekable) /* Bash on Win10 doesn't seem to handle TTYs right when flushing: * https://github.com/borgbackup/borg/issues/1961#issuecomment-335653560 */ #endif ) MVM_exception_throw_adhoc(tc, "Failed to flush filehandle: %s", strerror(errno)); } } } /* Truncates the file handle. */ static void truncatefh(MVMThreadContext *tc, MVMOSHandle *h, MVMint64 bytes) { MVMIOFileData *data = (MVMIOFileData *)h->body.data; if (ftruncate(data->fd, bytes) == -1) MVM_exception_throw_adhoc(tc, "Failed to truncate filehandle: %s", strerror(errno)); } /* Closes the file. */ static MVMint64 closefh(MVMThreadContext *tc, MVMOSHandle *h) { MVMIOFileData *data = (MVMIOFileData *)h->body.data; if (data->fd != -1) { int r; flush_output_buffer(tc, data); MVM_free_null(data->output_buffer); r = close(data->fd); data->fd = -1; if (r == -1) MVM_exception_throw_adhoc(tc, "Failed to close filehandle: %s", strerror(errno)); } return 0; } /* Locks a file. */ static MVMint64 lock(MVMThreadContext *tc, MVMOSHandle *h, MVMint64 flag) { MVMIOFileData *data = (MVMIOFileData *)h->body.data; #ifdef _WIN32 const DWORD len = 0xffffffff; const HANDLE hf = (HANDLE)_get_osfhandle(data->fd); OVERLAPPED offset; if (hf == INVALID_HANDLE_VALUE) { MVM_exception_throw_adhoc(tc, "Failed to lock filehandle: bad file descriptor"); } flag = ((flag & MVM_FILE_FLOCK_NONBLOCK) ? LOCKFILE_FAIL_IMMEDIATELY : 0) + ((flag & MVM_FILE_FLOCK_TYPEMASK) == MVM_FILE_FLOCK_SHARED ? 0 : LOCKFILE_EXCLUSIVE_LOCK); memset (&offset, 0, sizeof(offset)); MVM_gc_mark_thread_blocked(tc); if (LockFileEx(hf, flag, 0, len, len, &offset)) { MVM_gc_mark_thread_unblocked(tc); return 1; } MVM_gc_mark_thread_unblocked(tc); MVM_exception_throw_adhoc(tc, "Failed to lock filehandle: %lu", GetLastError()); return 0; #else struct flock l; ssize_t r; int fc; const int fd = data->fd; l.l_whence = SEEK_SET; l.l_start = 0; l.l_len = 0; if ((flag & MVM_FILE_FLOCK_TYPEMASK) == MVM_FILE_FLOCK_SHARED) l.l_type = F_RDLCK; else l.l_type = F_WRLCK; fc = (flag & MVM_FILE_FLOCK_NONBLOCK) ? F_SETLK : F_SETLKW; do { MVM_gc_mark_thread_blocked(tc); r = fcntl(fd, fc, &l); MVM_gc_mark_thread_unblocked(tc); } while (r == -1 && errno == EINTR); if (r == -1) { MVM_exception_throw_adhoc(tc, "Failed to lock filehandle: %d", errno); } return 1; #endif } /* Unlocks a file. */ static void unlock(MVMThreadContext *tc, MVMOSHandle *h) { MVMIOFileData *data = (MVMIOFileData *)h->body.data; #ifdef _WIN32 const DWORD len = 0xffffffff; const HANDLE hf = (HANDLE)_get_osfhandle(data->fd); OVERLAPPED offset; if (hf == INVALID_HANDLE_VALUE) { MVM_exception_throw_adhoc(tc, "Failed to seek in filehandle: bad file descriptor"); } memset (&offset, 0, sizeof(offset)); MVM_gc_mark_thread_blocked(tc); if (UnlockFileEx(hf, 0, len, len, &offset)) { MVM_gc_mark_thread_unblocked(tc); return; } MVM_gc_mark_thread_unblocked(tc); MVM_exception_throw_adhoc(tc, "Failed to unlock filehandle: %lu", GetLastError()); #else struct flock l; ssize_t r; const int fd = data->fd; l.l_whence = SEEK_SET; l.l_start = 0; l.l_len = 0; l.l_type = F_UNLCK; do { MVM_gc_mark_thread_blocked(tc); r = fcntl(fd, F_SETLKW, &l); MVM_gc_mark_thread_unblocked(tc); } while (r == -1 && errno == EINTR); if (r == -1) { MVM_exception_throw_adhoc(tc, "Failed to unlock filehandle: %d", errno); } #endif } /* Frees data associated with the handle. */ static void gc_free(MVMThreadContext *tc, MVMObject *h, void *d) { MVMIOFileData *data = (MVMIOFileData *)d; if (data) { MVM_free(data->output_buffer); MVM_free(data); } } /* IO ops table, populated with functions. */ static const MVMIOClosable closable = { closefh }; static const MVMIOSyncReadable sync_readable = { read_bytes, mvm_eof }; static const MVMIOSyncWritable sync_writable = { write_bytes, flush, truncatefh }; static const MVMIOSeekable seekable = { seek, mvm_tell }; static const MVMIOLockable lockable = { lock, unlock }; static const MVMIOIntrospection introspection = { is_tty, mvm_fileno }; static const MVMIOOps op_table = { &closable, &sync_readable, &sync_writable, NULL, NULL, NULL, &seekable, NULL, NULL, &lockable, &introspection, &set_buffer_size, NULL, gc_free }; /* Builds POSIX flag from mode string. */ static int resolve_open_mode(int *flag, const char *cp) { switch (*cp++) { case 'r': *flag = O_RDONLY; break; case '-': *flag = O_WRONLY; break; case '+': *flag = O_RDWR; break; /* alias for "-c" or "-ct" if by itself */ case 'w': *flag = *cp ? O_WRONLY | O_CREAT : O_WRONLY | O_CREAT | O_TRUNC; break; default: return 0; } for (;;) switch (*cp++) { case 0: return 1; case 'a': *flag |= O_APPEND; break; case 'c': *flag |= O_CREAT; break; case 't': *flag |= O_TRUNC; break; case 'x': *flag |= O_EXCL; break; default: return 0; } } /* Opens a file, returning a synchronous file handle. */ MVMObject * MVM_file_open_fh(MVMThreadContext *tc, MVMString *filename, MVMString *mode) { char * const fname = MVM_string_utf8_c8_encode_C_string(tc, filename); int fd; int flag; STAT_t statbuf; /* Resolve mode description to flags. */ char * const fmode = MVM_string_utf8_encode_C_string(tc, mode); if (!resolve_open_mode(&flag, fmode)) { char *waste[] = { fname, fmode, NULL }; MVM_exception_throw_adhoc_free(tc, waste, "Invalid open mode for file %s: %s", fname, fmode); } MVM_free(fmode); /* Try to open the file. */ #ifdef _WIN32 flag |= _O_BINARY; #endif if ((fd = MVM_platform_open((const char *)fname, flag, DEFAULT_MODE)) == -1) { char *waste[] = { fname, NULL }; const char *err = strerror(errno); MVM_exception_throw_adhoc_free(tc, waste, "Failed to open file %s: %s", fname, err); } /* Check that we didn't open a directory by accident. If fstat fails, just move on: Most of the documented error cases should already have triggered when opening the file, and we can't do anything about the others; a failure also does not necessarily imply that the file descriptor cannot be used for reading/writing. */ if (fstat(fd, &statbuf) == 0 && (statbuf.st_mode & S_IFMT) == S_IFDIR) { char *waste[] = { fname, NULL }; if (close(fd) == -1) { const char *err = strerror(errno); MVM_exception_throw_adhoc_free(tc, waste, "Tried to open directory %s, which we failed to close: %s", fname, err); } MVM_exception_throw_adhoc_free(tc, waste, "Tried to open directory %s", fname); } /* Set up handle. */ MVM_free(fname); { MVMIOFileData * const data = MVM_calloc(1, sizeof(MVMIOFileData)); MVMOSHandle * const result = (MVMOSHandle *)MVM_repr_alloc_init(tc, tc->instance->boot_types.BOOTIO); data->fd = fd; data->seekable = MVM_platform_is_fd_seekable(fd); result->body.ops = &op_table; result->body.data = data; return (MVMObject *)result; } } /* Opens a file, returning a synchronous file handle. */ MVMObject * MVM_file_handle_from_fd(MVMThreadContext *tc, int fd) { MVMOSHandle * const result = (MVMOSHandle *)MVM_repr_alloc_init(tc, tc->instance->boot_types.BOOTIO); MVMIOFileData * const data = MVM_calloc(1, sizeof(MVMIOFileData)); data->fd = fd; data->seekable = MVM_platform_is_fd_seekable(fd); result->body.ops = &op_table; result->body.data = data; #ifdef _WIN32 _setmode(fd, _O_BINARY); #endif return (MVMObject *)result; }