Browse Source

Merge pull request #4112 from Feoramund/fix-test-io-issues

Add `core:io` test suite
gingerBill 11 months ago
parent
commit
b020b91df2

+ 33 - 3
core/bytes/buffer.odin

@@ -144,6 +144,9 @@ buffer_grow :: proc(b: ^Buffer, n: int, loc := #caller_location) {
 }
 
 buffer_write_at :: proc(b: ^Buffer, p: []byte, offset: int, loc := #caller_location) -> (n: int, err: io.Error) {
+	if len(p) == 0 {
+		return 0, nil
+	}
 	b.last_read = .Invalid
 	if offset < 0 {
 		err = .Invalid_Offset
@@ -246,10 +249,13 @@ buffer_read_ptr :: proc(b: ^Buffer, ptr: rawptr, size: int) -> (n: int, err: io.
 }
 
 buffer_read_at :: proc(b: ^Buffer, p: []byte, offset: int) -> (n: int, err: io.Error) {
+	if len(p) == 0 {
+		return 0, nil
+	}
 	b.last_read = .Invalid
 
 	if uint(offset) >= len(b.buf) {
-		err = .Invalid_Offset
+		err = .EOF
 		return
 	}
 	n = copy(p, b.buf[offset:])
@@ -310,6 +316,27 @@ buffer_unread_rune :: proc(b: ^Buffer) -> io.Error {
 	return nil
 }
 
+buffer_seek :: proc(b: ^Buffer, offset: i64, whence: io.Seek_From) -> (i64, io.Error) {
+	abs: i64
+	switch whence {
+	case .Start:
+		abs = offset
+	case .Current:
+		abs = i64(b.off) + offset
+	case .End:
+		abs = i64(len(b.buf)) + offset
+	case:
+		return 0, .Invalid_Whence
+	}
+
+	abs_int := int(abs)
+	if abs_int < 0 {
+		return 0, .Invalid_Offset
+	}
+	b.last_read = .Invalid
+	b.off = abs_int
+	return abs, nil
+}
 
 buffer_read_bytes :: proc(b: ^Buffer, delim: byte) -> (line: []byte, err: io.Error) {
 	i := index_byte(b.buf[b.off:], delim)
@@ -395,14 +422,17 @@ _buffer_proc :: proc(stream_data: rawptr, mode: io.Stream_Mode, p: []byte, offse
 		return io._i64_err(buffer_write(b, p))
 	case .Write_At:
 		return io._i64_err(buffer_write_at(b, p, int(offset)))
+	case .Seek:
+		n, err = buffer_seek(b, offset, whence)
+		return
 	case .Size:
-		n = i64(buffer_capacity(b))
+		n = i64(buffer_length(b))
 		return
 	case .Destroy:
 		buffer_destroy(b)
 		return
 	case .Query:
-		return io.query_utility({.Read, .Read_At, .Write, .Write_At, .Size, .Destroy})
+		return io.query_utility({.Read, .Read_At, .Write, .Write_At, .Seek, .Size, .Destroy, .Query})
 	}
 	return 0, .Empty
 }

+ 9 - 2
core/bytes/reader.odin

@@ -9,10 +9,11 @@ Reader :: struct {
 	prev_rune: int,    // previous reading index of rune or < 0
 }
 
-reader_init :: proc(r: ^Reader, s: []byte) {
+reader_init :: proc(r: ^Reader, s: []byte) -> io.Stream {
 	r.s = s
 	r.i = 0
 	r.prev_rune = -1
+	return reader_to_stream(r)
 }
 
 reader_to_stream :: proc(r: ^Reader) -> (s: io.Stream) {
@@ -33,6 +34,9 @@ reader_size :: proc(r: ^Reader) -> i64 {
 }
 
 reader_read :: proc(r: ^Reader, p: []byte) -> (n: int, err: io.Error) {
+	if len(p) == 0 {
+		return 0, nil
+	}
 	if r.i >= i64(len(r.s)) {
 		return 0, .EOF
 	}
@@ -42,6 +46,9 @@ reader_read :: proc(r: ^Reader, p: []byte) -> (n: int, err: io.Error) {
 	return
 }
 reader_read_at :: proc(r: ^Reader, p: []byte, off: i64) -> (n: int, err: io.Error) {
+	if len(p) == 0 {
+		return 0, nil
+	}
 	if off < 0 {
 		return 0, .Invalid_Offset
 	}
@@ -97,7 +104,6 @@ reader_unread_rune :: proc(r: ^Reader) -> io.Error {
 	return nil
 }
 reader_seek :: proc(r: ^Reader, offset: i64, whence: io.Seek_From) -> (i64, io.Error) {
-	r.prev_rune = -1
 	abs: i64
 	switch whence {
 	case .Start:
@@ -114,6 +120,7 @@ reader_seek :: proc(r: ^Reader, offset: i64, whence: io.Seek_From) -> (i64, io.E
 		return 0, .Invalid_Offset
 	}
 	r.i = abs
+	r.prev_rune = -1
 	return abs, nil
 }
 reader_write_to :: proc(r: ^Reader, w: io.Writer) -> (n: i64, err: io.Error) {

+ 1 - 1
core/c/libc/stdio.odin

@@ -368,7 +368,7 @@ to_stream :: proc(file: ^FILE) -> io.Stream {
 			return 0, .Empty
 		
 		case .Query:
-			return io.query_utility({ .Close, .Flush, .Read, .Read_At, .Write, .Write_At, .Seek, .Size })
+			return io.query_utility({ .Close, .Flush, .Read, .Read_At, .Write, .Write_At, .Seek, .Size, .Query })
 		}
 		return
 	}

+ 12 - 2
core/io/util.odin

@@ -340,6 +340,9 @@ _limited_reader_proc :: proc(stream_data: rawptr, mode: Stream_Mode, p: []byte,
 	l := (^Limited_Reader)(stream_data)
 	#partial switch mode {
 	case .Read:
+		if len(p) == 0 {
+			return 0, nil
+		}
 		if l.n <= 0 {
 			return 0, .EOF
 		}
@@ -376,11 +379,12 @@ Section_Reader :: struct {
 	limit: i64,
 }
 
-section_reader_init :: proc(s: ^Section_Reader, r: Reader_At, off: i64, n: i64) {
+section_reader_init :: proc(s: ^Section_Reader, r: Reader_At, off: i64, n: i64) -> Reader {
 	s.r = r
+	s.base = off
 	s.off = off
 	s.limit = off + n
-	return
+	return section_reader_to_stream(s)
 }
 section_reader_to_stream :: proc(s: ^Section_Reader) -> (out: Stream) {
 	out.data = s
@@ -393,6 +397,9 @@ _section_reader_proc :: proc(stream_data: rawptr, mode: Stream_Mode, p: []byte,
 	s := (^Section_Reader)(stream_data)
 	#partial switch mode {
 	case .Read:
+		if len(p) == 0 {
+			return 0, nil
+		}
 		if s.off >= s.limit {
 			return 0, .EOF
 		}
@@ -404,6 +411,9 @@ _section_reader_proc :: proc(stream_data: rawptr, mode: Stream_Mode, p: []byte,
 		s.off += i64(n)
 		return
 	case .Read_At:
+		if len(p) == 0 {
+			return 0, nil
+		}
 		p, off := p, offset
 
 		if off < 0 || off >= s.limit - s.base {

+ 10 - 20
core/os/file_windows.odin

@@ -192,6 +192,8 @@ seek :: proc(fd: Handle, offset: i64, whence: int) -> (i64, Error) {
 	case 0: w = win32.FILE_BEGIN
 	case 1: w = win32.FILE_CURRENT
 	case 2: w = win32.FILE_END
+	case:
+		return 0, .Invalid_Whence
 	}
 	hi := i32(offset>>32)
 	lo := i32(offset)
@@ -223,11 +225,13 @@ file_size :: proc(fd: Handle) -> (i64, Error) {
 MAX_RW :: 1<<30
 
 @(private)
-pread :: proc(fd: Handle, data: []byte, offset: i64) -> (int, Error) {
+pread :: proc(fd: Handle, data: []byte, offset: i64) -> (n: int, err: Error) {
+	curr_off := seek(fd, 0, 1) or_return
+	defer seek(fd, curr_off, 0)
+
 	buf := data
 	if len(buf) > MAX_RW {
 		buf = buf[:MAX_RW]
-
 	}
 
 	o := win32.OVERLAPPED{
@@ -247,11 +251,13 @@ pread :: proc(fd: Handle, data: []byte, offset: i64) -> (int, Error) {
 	return int(done), e
 }
 @(private)
-pwrite :: proc(fd: Handle, data: []byte, offset: i64) -> (int, Error) {
+pwrite :: proc(fd: Handle, data: []byte, offset: i64) -> (n: int, err: Error) {
+	curr_off := seek(fd, 0, 1) or_return
+	defer seek(fd, curr_off, 0)
+
 	buf := data
 	if len(buf) > MAX_RW {
 		buf = buf[:MAX_RW]
-
 	}
 
 	o := win32.OVERLAPPED{
@@ -271,13 +277,6 @@ pwrite :: proc(fd: Handle, data: []byte, offset: i64) -> (int, Error) {
 
 /*
 read_at returns n: 0, err: 0 on EOF
-on Windows, read_at changes the position of the file cursor, on *nix, it does not.
-
-	bytes: [8]u8{}
-	read_at(fd, bytes, 0)
-	read(fd, bytes)
-
-will read from the location twice on *nix, and from two different locations on Windows
 */
 read_at :: proc(fd: Handle, data: []byte, offset: i64) -> (n: int, err: Error) {
 	if offset < 0 {
@@ -302,15 +301,6 @@ read_at :: proc(fd: Handle, data: []byte, offset: i64) -> (n: int, err: Error) {
 	return
 }
 
-/*
-on Windows, write_at changes the position of the file cursor, on *nix, it does not.
-
-	bytes: [8]u8{}
-	write_at(fd, bytes, 0)
-	write(fd, bytes)
-
-will write to the location twice on *nix, and to two different locations on Windows
-*/
 write_at :: proc(fd: Handle, data: []byte, offset: i64) -> (n: int, err: Error) {
 	if offset < 0 {
 		return 0, .Invalid_Offset

+ 8 - 0
core/os/os2/errors_linux.odin

@@ -154,6 +154,14 @@ _get_platform_error :: proc(errno: linux.Errno) -> Error {
 		return .Exist
 	case .ENOENT:
 		return .Not_Exist
+	case .ETIMEDOUT:
+		return .Timeout
+	case .EPIPE:
+		return .Broken_Pipe
+	case .EBADF:
+		return .Invalid_File
+	case .ENOMEM:
+		return .Out_Of_Memory
 	}
 
 	return Platform_Error(i32(errno))

+ 3 - 0
core/os/os2/errors_windows.odin

@@ -52,6 +52,9 @@ _get_platform_error :: proc() -> Error {
 	case win32.ERROR_INVALID_HANDLE:
 		return .Invalid_File
 
+	case win32.ERROR_NEGATIVE_SEEK:
+		return .Invalid_Offset
+
 	case
 		win32.ERROR_BAD_ARGUMENTS,
 		win32.ERROR_INVALID_PARAMETER,

+ 6 - 0
core/os/os2/file.odin

@@ -132,6 +132,12 @@ name :: proc(f: ^File) -> string {
 	return _name(f)
 }
 
+/*
+	Close a file and its stream.
+
+	Any further use of the file or its stream should be considered to be in the
+	same class of bugs as a use-after-free.
+*/
 close :: proc(f: ^File) -> Error {
 	if f != nil {
 		return io.close(f.stream)

+ 20 - 2
core/os/os2/file_linux.odin

@@ -170,11 +170,23 @@ _name :: proc(f: ^File) -> string {
 }
 
 _seek :: proc(f: ^File_Impl, offset: i64, whence: io.Seek_From) -> (ret: i64, err: Error) {
+	// We have to handle this here, because Linux returns EINVAL for both
+	// invalid offsets and invalid whences.
+	switch whence {
+	case .Start, .Current, .End:
+		break
+	case:
+		return 0, .Invalid_Whence
+	}
 	n, errno := linux.lseek(f.fd, offset, linux.Seek_Whence(whence))
-	if errno != .NONE {
+	#partial switch errno {
+	case .EINVAL:
+		return 0, .Invalid_Offset
+	case .NONE:
+		return n, nil
+	case:
 		return -1, _get_platform_error(errno)
 	}
-	return n, nil
 }
 
 _read :: proc(f: ^File_Impl, p: []byte) -> (i64, Error) {
@@ -189,6 +201,9 @@ _read :: proc(f: ^File_Impl, p: []byte) -> (i64, Error) {
 }
 
 _read_at :: proc(f: ^File_Impl, p: []byte, offset: i64) -> (i64, Error) {
+	if len(p) == 0 {
+		return 0, nil
+	}
 	if offset < 0 {
 		return 0, .Invalid_Offset
 	}
@@ -214,6 +229,9 @@ _write :: proc(f: ^File_Impl, p: []byte) -> (i64, Error) {
 }
 
 _write_at :: proc(f: ^File_Impl, p: []byte, offset: i64) -> (i64, Error) {
+	if len(p) == 0 {
+		return 0, nil
+	}
 	if offset < 0 {
 		return 0, .Invalid_Offset
 	}

+ 15 - 2
core/os/os2/file_posix.odin

@@ -419,9 +419,22 @@ _file_stream_proc :: proc(stream_data: rawptr, mode: io.Stream_Mode, p: []byte,
 		#assert(int(posix.Whence.CUR) == int(io.Seek_From.Current))
 		#assert(int(posix.Whence.END) == int(io.Seek_From.End))
 
+		switch whence {
+		case .Start, .Current, .End:
+			break
+		case:
+			err = .Invalid_Whence
+			return
+		}
+
 		n = i64(posix.lseek(fd, posix.off_t(offset), posix.Whence(whence)))
 		if n < 0 {
-			err = .Unknown
+			#partial switch posix.get_errno() {
+			case .EINVAL:
+				err = .Invalid_Offset
+			case:
+				err = .Unknown
+			}
 		}
 		return
 
@@ -446,7 +459,7 @@ _file_stream_proc :: proc(stream_data: rawptr, mode: io.Stream_Mode, p: []byte,
 		return
 
 	case .Query:
-		return io.query_utility({.Read, .Read_At, .Write, .Write_At, .Seek, .Size, .Flush, .Close, .Query})
+		return io.query_utility({.Read, .Read_At, .Write, .Write_At, .Seek, .Size, .Flush, .Close, .Destroy, .Query})
 
 	case:
 		return 0, .Empty

+ 16 - 6
core/os/os2/file_windows.odin

@@ -248,6 +248,8 @@ _seek :: proc(f: ^File_Impl, offset: i64, whence: io.Seek_From) -> (ret: i64, er
 	case .Start:   w = win32.FILE_BEGIN
 	case .Current: w = win32.FILE_CURRENT
 	case .End:     w = win32.FILE_END
+	case:
+		return 0, .Invalid_Whence
 	}
 	hi := i32(offset>>32)
 	lo := i32(offset)
@@ -264,6 +266,11 @@ _read :: proc(f: ^File_Impl, p: []byte) -> (n: i64, err: Error) {
 }
 
 _read_internal :: proc(f: ^File_Impl, p: []byte) -> (n: i64, err: Error) {
+	length := len(p)
+	if length == 0 {
+		return
+	}
+
 	read_console :: proc(handle: win32.HANDLE, b: []byte) -> (n: int, err: Error) {
 		if len(b) == 0 {
 			return 0, nil
@@ -318,7 +325,6 @@ _read_internal :: proc(f: ^File_Impl, p: []byte) -> (n: i64, err: Error) {
 
 	single_read_length: win32.DWORD
 	total_read: int
-	length := len(p)
 
 	sync.shared_guard(&f.rw_mutex) // multiple readers
 
@@ -337,6 +343,10 @@ _read_internal :: proc(f: ^File_Impl, p: []byte) -> (n: i64, err: Error) {
 
 		if single_read_length > 0 && ok {
 			total_read += int(single_read_length)
+		} else if single_read_length == 0 && ok {
+			// ok and 0 bytes means EOF:
+			// https://learn.microsoft.com/en-us/windows/win32/fileio/testing-for-the-end-of-a-file
+			err = .EOF
 		} else {
 			err = _get_platform_error()
 		}
@@ -352,7 +362,7 @@ _read_at :: proc(f: ^File_Impl, p: []byte, offset: i64) -> (n: i64, err: Error)
 			buf = buf[:MAX_RW]
 
 		}
-		curr_offset := _seek(f, offset, .Current) or_return
+		curr_offset := _seek(f, 0, .Current) or_return
 		defer _seek(f, curr_offset, .Start)
 
 		o := win32.OVERLAPPED{
@@ -421,7 +431,7 @@ _write_at :: proc(f: ^File_Impl, p: []byte, offset: i64) -> (n: i64, err: Error)
 			buf = buf[:MAX_RW]
 
 		}
-		curr_offset := _seek(f, offset, .Current) or_return
+		curr_offset := _seek(f, 0, .Current) or_return
 		defer _seek(f, curr_offset, .Start)
 
 		o := win32.OVERLAPPED{
@@ -466,13 +476,13 @@ _file_size :: proc(f: ^File_Impl) -> (n: i64, err: Error) {
 
 _sync :: proc(f: ^File) -> Error {
 	if f != nil && f.impl != nil {
-		return _flush((^File_Impl)(f.impl))
+		return _flush_internal((^File_Impl)(f.impl))
 	}
 	return nil
 }
 
 _flush :: proc(f: ^File_Impl) -> Error {
-	return _flush(f)
+	return _flush_internal(f)
 }
 _flush_internal :: proc(f: ^File_Impl) -> Error {
 	handle := _handle(&f.file)
@@ -813,7 +823,7 @@ _file_stream_proc :: proc(stream_data: rawptr, mode: io.Stream_Mode, p: []byte,
 		err = error_to_io_error(ferr)
 		return
 	case .Query:
-		return io.query_utility({.Read, .Read_At, .Write, .Write_At, .Seek, .Size, .Flush, .Close, .Query})
+		return io.query_utility({.Read, .Read_At, .Write, .Write_At, .Seek, .Size, .Flush, .Close, .Destroy, .Query})
 	}
 	return 0, .Empty
 }

+ 12 - 1
core/os/os_darwin.odin

@@ -777,10 +777,21 @@ write_at :: proc(fd: Handle, data: []byte, offset: i64) -> (int, Error) {
 
 seek :: proc(fd: Handle, offset: i64, whence: int) -> (i64, Error) {
 	assert(fd != -1)
+	switch whence {
+	case SEEK_SET, SEEK_CUR, SEEK_END:
+		break
+	case:
+		return 0, .Invalid_Whence
+	}
 
 	final_offset := i64(_unix_lseek(fd, int(offset), c.int(whence)))
 	if final_offset == -1 {
-		return 0, get_last_error()
+		errno := get_last_error()
+		switch errno {
+		case .EINVAL:
+			return 0, .Invalid_Offset
+		}
+		return 0, errno
 	}
 	return final_offset, nil
 }

+ 31 - 15
core/os/os_freebsd.odin

@@ -6,6 +6,7 @@ foreign import libc "system:c"
 import "base:runtime"
 import "core:strings"
 import "core:c"
+import "core:sys/freebsd"
 
 Handle :: distinct i32
 File_Time :: distinct u64
@@ -446,8 +447,7 @@ close :: proc(fd: Handle) -> Error {
 }
 
 flush :: proc(fd: Handle) -> Error {
-	// do nothing
-	return nil
+	return cast(_Platform_Error)freebsd.fsync(cast(freebsd.Fd)fd)
 }
 
 // If you read or write more than `INT_MAX` bytes, FreeBSD returns `EINVAL`.
@@ -481,29 +481,45 @@ write :: proc(fd: Handle, data: []byte) -> (int, Error) {
 }
 
 read_at :: proc(fd: Handle, data: []byte, offset: i64) -> (n: int, err: Error) {
-	curr := seek(fd, offset, SEEK_CUR) or_return
-	n, err = read(fd, data)
-	_, err1 := seek(fd, curr, SEEK_SET)
-	if err1 != nil && err == nil {
-		err = err1
+	if len(data) == 0 {
+		return 0, nil
 	}
-	return
+
+	to_read := min(uint(len(data)), MAX_RW)
+
+	bytes_read, errno := freebsd.pread(cast(freebsd.Fd)fd, data[:to_read], cast(freebsd.off_t)offset)
+
+	return bytes_read, cast(_Platform_Error)errno
 }
 
 write_at :: proc(fd: Handle, data: []byte, offset: i64) -> (n: int, err: Error) {
-	curr := seek(fd, offset, SEEK_CUR) or_return
-	n, err = write(fd, data)
-	_, err1 := seek(fd, curr, SEEK_SET)
-	if err1 != nil && err == nil {
-		err = err1
+	if len(data) == 0 {
+		return 0, nil
 	}
-	return
+
+	to_write := min(uint(len(data)), MAX_RW)
+
+	bytes_written, errno := freebsd.pwrite(cast(freebsd.Fd)fd, data[:to_write], cast(freebsd.off_t)offset)
+
+	return bytes_written, cast(_Platform_Error)errno
 }
 
 seek :: proc(fd: Handle, offset: i64, whence: int) -> (i64, Error) {
+	switch whence {
+	case SEEK_SET, SEEK_CUR, SEEK_END:
+		break
+	case:
+		return 0, .Invalid_Whence
+	}
 	res := _unix_seek(fd, offset, c.int(whence))
 	if res == -1 {
-		return -1, get_last_error()
+		errno := get_last_error()
+		switch errno {
+		case .EINVAL:
+			return 0, .Invalid_Offset
+		case:
+			return 0, errno
+		}
 	}
 	return res, nil
 }

+ 82 - 57
core/os/os_haiku.odin

@@ -119,49 +119,52 @@ S_ISSOCK :: #force_inline proc(m: u32) -> bool { return (m & S_IFMT) == S_IFSOCK
 
 
 foreign libc {
-	@(link_name="_errorp")	__error		:: proc() -> ^c.int ---
-
-	@(link_name="fork")	_unix_fork	:: proc() -> pid_t ---
-	@(link_name="getthrid")	_unix_getthrid	:: proc() -> int ---
-
-	@(link_name="open")	_unix_open	:: proc(path: cstring, flags: c.int, #c_vararg mode: ..u16) -> Handle ---
-	@(link_name="close")	_unix_close	:: proc(fd: Handle) -> c.int ---
-	@(link_name="read")	_unix_read	:: proc(fd: Handle, buf: rawptr, size: c.size_t) -> c.ssize_t ---
-	@(link_name="write")	_unix_write	:: proc(fd: Handle, buf: rawptr, size: c.size_t) -> c.ssize_t ---
-	@(link_name="lseek")	_unix_seek	:: proc(fd: Handle, offset: off_t, whence: c.int) -> off_t ---
-	@(link_name="stat")	_unix_stat	:: proc(path: cstring, sb: ^OS_Stat) -> c.int ---
-	@(link_name="fstat")	_unix_fstat	:: proc(fd: Handle, sb: ^OS_Stat) -> c.int ---
-	@(link_name="lstat")	_unix_lstat	:: proc(path: cstring, sb: ^OS_Stat) -> c.int ---
-	@(link_name="readlink")	_unix_readlink	:: proc(path: cstring, buf: ^byte, bufsiz: c.size_t) -> c.ssize_t ---
-	@(link_name="access")	_unix_access	:: proc(path: cstring, mask: c.int) -> c.int ---
-	@(link_name="getcwd")	_unix_getcwd	:: proc(buf: cstring, len: c.size_t) -> cstring ---
-	@(link_name="chdir")	_unix_chdir	:: proc(path: cstring) -> c.int ---
-	@(link_name="rename")	_unix_rename	:: proc(old, new: cstring) -> c.int ---
-	@(link_name="unlink")	_unix_unlink	:: proc(path: cstring) -> c.int ---
-	@(link_name="rmdir")	_unix_rmdir	:: proc(path: cstring) -> c.int ---
-	@(link_name="mkdir")	_unix_mkdir	:: proc(path: cstring, mode: mode_t) -> c.int ---
-
-	@(link_name="getpagesize") _unix_getpagesize :: proc() -> c.int ---
-	@(link_name="sysconf") _sysconf :: proc(name: c.int) -> c.long ---
-	@(link_name="fdopendir") _unix_fdopendir :: proc(fd: Handle) -> Dir ---
-	@(link_name="closedir")	_unix_closedir	:: proc(dirp: Dir) -> c.int ---
-	@(link_name="rewinddir") _unix_rewinddir :: proc(dirp: Dir) ---
-	@(link_name="readdir_r") _unix_readdir_r :: proc(dirp: Dir, entry: ^Dirent, result: ^^Dirent) -> c.int ---
-
-	@(link_name="malloc")	_unix_malloc	:: proc(size: c.size_t) -> rawptr ---
-	@(link_name="calloc")	_unix_calloc	:: proc(num, size: c.size_t) -> rawptr ---
-	@(link_name="free")	_unix_free	:: proc(ptr: rawptr) ---
-	@(link_name="realloc")	_unix_realloc	:: proc(ptr: rawptr, size: c.size_t) -> rawptr ---
-
-	@(link_name="getenv")	_unix_getenv	:: proc(cstring) -> cstring ---
-	@(link_name="realpath")	_unix_realpath	:: proc(path: cstring, resolved_path: rawptr) -> rawptr ---
-
-	@(link_name="exit")	_unix_exit	:: proc(status: c.int) -> ! ---
-
-	@(link_name="dlopen")	_unix_dlopen	:: proc(filename: cstring, flags: c.int) -> rawptr ---
-	@(link_name="dlsym")	_unix_dlsym	:: proc(handle: rawptr, symbol: cstring) -> rawptr ---
-	@(link_name="dlclose")	_unix_dlclose	:: proc(handle: rawptr) -> c.int ---
-	@(link_name="dlerror")	_unix_dlerror	:: proc() -> cstring ---
+	@(link_name="_errorp")        __error              :: proc() -> ^c.int ---
+
+	@(link_name="fork")           _unix_fork           :: proc() -> pid_t ---
+	@(link_name="getthrid")       _unix_getthrid       :: proc() -> int ---
+
+	@(link_name="open")           _unix_open           :: proc(path: cstring, flags: c.int, #c_vararg mode: ..u16) -> Handle ---
+	@(link_name="close")          _unix_close          :: proc(fd: Handle) -> c.int ---
+	@(link_name="read")           _unix_read           :: proc(fd: Handle, buf: rawptr, size: c.size_t) -> c.ssize_t ---
+	@(link_name="pread")          _unix_pread          :: proc(fd: Handle, buf: rawptr, size: c.size_t, offset: i64) -> c.ssize_t ---
+	@(link_name="write")          _unix_write          :: proc(fd: Handle, buf: rawptr, size: c.size_t) -> c.ssize_t ---
+	@(link_name="pwrite")         _unix_pwrite         :: proc(fd: Handle, buf: rawptr, size: c.size_t, offset: i64) -> c.ssize_t ---
+	@(link_name="lseek")          _unix_seek           :: proc(fd: Handle, offset: off_t, whence: c.int) -> off_t ---
+	@(link_name="stat")           _unix_stat           :: proc(path: cstring, sb: ^OS_Stat) -> c.int ---
+	@(link_name="fstat")          _unix_fstat          :: proc(fd: Handle, sb: ^OS_Stat) -> c.int ---
+	@(link_name="lstat")          _unix_lstat          :: proc(path: cstring, sb: ^OS_Stat) -> c.int ---
+	@(link_name="readlink")       _unix_readlink       :: proc(path: cstring, buf: ^byte, bufsiz: c.size_t) -> c.ssize_t ---
+	@(link_name="access")         _unix_access         :: proc(path: cstring, mask: c.int) -> c.int ---
+	@(link_name="getcwd")         _unix_getcwd         :: proc(buf: cstring, len: c.size_t) -> cstring ---
+	@(link_name="chdir")          _unix_chdir          :: proc(path: cstring) -> c.int ---
+	@(link_name="rename")         _unix_rename         :: proc(old, new: cstring) -> c.int ---
+	@(link_name="unlink")         _unix_unlink         :: proc(path: cstring) -> c.int ---
+	@(link_name="rmdir")          _unix_rmdir          :: proc(path: cstring) -> c.int ---
+	@(link_name="mkdir")          _unix_mkdir          :: proc(path: cstring, mode: mode_t) -> c.int ---
+	@(link_name="fsync")          _unix_fsync          :: proc(fd: Handle) -> c.int ---
+
+	@(link_name="getpagesize")    _unix_getpagesize    :: proc() -> c.int ---
+	@(link_name="sysconf")        _sysconf             :: proc(name: c.int) -> c.long ---
+	@(link_name="fdopendir")      _unix_fdopendir      :: proc(fd: Handle) -> Dir ---
+	@(link_name="closedir")       _unix_closedir       :: proc(dirp: Dir) -> c.int ---
+	@(link_name="rewinddir")      _unix_rewinddir      :: proc(dirp: Dir) ---
+	@(link_name="readdir_r")      _unix_readdir_r      :: proc(dirp: Dir, entry: ^Dirent, result: ^^Dirent) -> c.int ---
+
+	@(link_name="malloc")         _unix_malloc         :: proc(size: c.size_t) -> rawptr ---
+	@(link_name="calloc")         _unix_calloc         :: proc(num, size: c.size_t) -> rawptr ---
+	@(link_name="free")           _unix_free           :: proc(ptr: rawptr) ---
+	@(link_name="realloc")        _unix_realloc        :: proc(ptr: rawptr, size: c.size_t) -> rawptr ---
+
+	@(link_name="getenv")         _unix_getenv         :: proc(cstring) -> cstring ---
+	@(link_name="realpath")       _unix_realpath       :: proc(path: cstring, resolved_path: rawptr) -> rawptr ---
+
+	@(link_name="exit")           _unix_exit           :: proc(status: c.int) -> ! ---
+
+	@(link_name="dlopen")         _unix_dlopen         :: proc(filename: cstring, flags: c.int) -> rawptr ---
+	@(link_name="dlsym")          _unix_dlsym          :: proc(handle: rawptr, symbol: cstring) -> rawptr ---
+	@(link_name="dlclose")        _unix_dlclose        :: proc(handle: rawptr) -> c.int ---
+	@(link_name="dlerror")        _unix_dlerror        :: proc() -> cstring ---
 }
 
 MAXNAMLEN :: haiku.NAME_MAX
@@ -216,7 +219,10 @@ close :: proc(fd: Handle) -> Error {
 }
 
 flush :: proc(fd: Handle) -> Error {
-	// do nothing
+	result := _unix_fsync(fd)
+	if result == -1 {
+		return get_last_error()
+	}
 	return nil
 }
 
@@ -250,29 +256,48 @@ write :: proc(fd: Handle, data: []byte) -> (int, Error) {
 }
 
 read_at :: proc(fd: Handle, data: []byte, offset: i64) -> (n: int, err: Error) {
-	curr := seek(fd, offset, SEEK_CUR) or_return
-	n, err = read(fd, data)
-	_, err1 := seek(fd, curr, SEEK_SET)
-	if err1 != nil && err == nil {
-		err = err1
+	if len(data) == 0 {
+		return 0, nil
 	}
-	return
+
+	to_read := min(uint(len(data)), MAX_RW)
+
+	bytes_read := _unix_pread(fd, raw_data(data), to_read, offset)
+	if bytes_read < 0 {
+		return -1, get_last_error()
+	}
+	return bytes_read, nil
 }
 
 write_at :: proc(fd: Handle, data: []byte, offset: i64) -> (n: int, err: Error) {
-	curr := seek(fd, offset, SEEK_CUR) or_return
-	n, err = write(fd, data)
-	_, err1 := seek(fd, curr, SEEK_SET)
-	if err1 != nil && err == nil {
-		err = err1
+	if len(data) == 0 {
+		return 0, nil
 	}
-	return
+
+	to_write := min(uint(len(data)), MAX_RW)
+
+	bytes_written := _unix_pwrite(fd, raw_data(data), to_write, offset)
+	if bytes_written < 0 {
+		return -1, get_last_error()
+	}
+	return bytes_written, nil
 }
 
 seek :: proc(fd: Handle, offset: i64, whence: int) -> (i64, Error) {
+	switch whence {
+	case SEEK_SET, SEEK_CUR, SEEK_END:
+		break
+	case:
+		return 0, .Invalid_Whence
+	}
 	res := _unix_seek(fd, offset, c.int(whence))
 	if res == -1 {
-		return -1, get_last_error()
+		errno := get_last_error()
+		switch errno {
+		case .BAD_VALUE:
+			return 0, .Invalid_Offset
+		}
+		return 0, errno
 	}
 	return res, nil
 }

+ 13 - 3
core/os/os_linux.odin

@@ -584,8 +584,7 @@ close :: proc(fd: Handle) -> Error {
 }
 
 flush :: proc(fd: Handle) -> Error {
-	// do nothing
-	return nil
+	return _get_errno(unix.sys_fsync(int(fd)))
 }
 
 // If you read or write more than `SSIZE_MAX` bytes, result is implementation defined (probably an error).
@@ -654,9 +653,20 @@ write_at :: proc(fd: Handle, data: []byte, offset: i64) -> (int, Error) {
 }
 
 seek :: proc(fd: Handle, offset: i64, whence: int) -> (i64, Error) {
+	switch whence {
+	case SEEK_SET, SEEK_CUR, SEEK_END:
+		break
+	case:
+		return 0, .Invalid_Whence
+	}
 	res := unix.sys_lseek(int(fd), offset, whence)
 	if res < 0 {
-		return -1, _get_errno(int(res))
+		errno := _get_errno(int(res))
+		switch errno {
+		case .EINVAL:
+			return 0, .Invalid_Offset
+		}
+		return 0, errno
 	}
 	return i64(res), nil
 }

+ 39 - 14
core/os/os_netbsd.odin

@@ -426,7 +426,9 @@ foreign libc {
 	@(link_name="open")             _unix_open          :: proc(path: cstring, flags: c.int, #c_vararg mode: ..u32) -> Handle ---
 	@(link_name="close")            _unix_close         :: proc(fd: Handle) -> c.int ---
 	@(link_name="read")             _unix_read          :: proc(fd: Handle, buf: rawptr, size: c.size_t) -> c.ssize_t ---
+	@(link_name="pread")            _unix_pread         :: proc(fd: Handle, buf: rawptr, size: c.size_t, offset: i64) -> c.ssize_t ---
 	@(link_name="write")            _unix_write         :: proc(fd: Handle, buf: rawptr, size: c.size_t) -> c.ssize_t ---
+	@(link_name="pwrite")           _unix_pwrite        :: proc(fd: Handle, buf: rawptr, size: c.size_t, offset: i64) -> c.ssize_t ---
 	@(link_name="lseek")            _unix_seek          :: proc(fd: Handle, offset: i64, whence: c.int) -> i64 ---
 	@(link_name="getpagesize")      _unix_getpagesize   :: proc() -> c.int ---
 	@(link_name="stat")             _unix_stat          :: proc(path: cstring, stat: ^OS_Stat) -> c.int ---
@@ -441,6 +443,7 @@ foreign libc {
 	@(link_name="rmdir")            _unix_rmdir         :: proc(path: cstring) -> c.int ---
 	@(link_name="mkdir")            _unix_mkdir         :: proc(path: cstring, mode: mode_t) -> c.int ---
 	@(link_name="fcntl")            _unix_fcntl         :: proc(fd: Handle, cmd: c.int, #c_vararg args: ..any) -> c.int ---
+	@(link_name="fsync")            _unix_fsync         :: proc(fd: Handle) -> c.int ---
 	@(link_name="dup")              _unix_dup           :: proc(fd: Handle) -> Handle ---
 	
 	@(link_name="fdopendir")        _unix_fdopendir     :: proc(fd: Handle) -> Dir ---
@@ -504,7 +507,10 @@ close :: proc(fd: Handle) -> Error {
 }
 
 flush :: proc(fd: Handle) -> Error {
-	// do nothing
+	result := _unix_fsync(fd)
+	if result == -1 {
+		return get_last_error()
+	}
 	return nil
 }
 
@@ -535,29 +541,48 @@ write :: proc(fd: Handle, data: []byte) -> (int, Error) {
 }
 
 read_at :: proc(fd: Handle, data: []byte, offset: i64) -> (n: int, err: Error) {
-	curr := seek(fd, offset, SEEK_CUR) or_return
-	n, err = read(fd, data)
-	_, err1 := seek(fd, curr, SEEK_SET)
-	if err1 != nil && err == nil {
-		err = err1
+	if len(data) == 0 {
+		return 0, nil
 	}
-	return
+
+	to_read := min(uint(len(data)), MAX_RW)
+
+	bytes_read := _unix_pread(fd, raw_data(data), to_read, offset)
+	if bytes_read < 0 {
+		return -1, get_last_error()
+	}
+	return bytes_read, nil
 }
 
 write_at :: proc(fd: Handle, data: []byte, offset: i64) -> (n: int, err: Error) {
-	curr := seek(fd, offset, SEEK_CUR) or_return
-	n, err = write(fd, data)
-	_, err1 := seek(fd, curr, SEEK_SET)
-	if err1 != nil && err == nil {
-		err = err1
+	if len(data) == 0 {
+		return 0, nil
 	}
-	return
+
+	to_write := min(uint(len(data)), MAX_RW)
+
+	bytes_written := _unix_pwrite(fd, raw_data(data), to_write, offset)
+	if bytes_written < 0 {
+		return -1, get_last_error()
+	}
+	return bytes_written, nil
 }
 
 seek :: proc(fd: Handle, offset: i64, whence: int) -> (i64, Error) {
+	switch whence {
+	case SEEK_SET, SEEK_CUR, SEEK_END:
+		break
+	case:
+		return 0, .Invalid_Whence
+	}
 	res := _unix_seek(fd, offset, c.int(whence))
 	if res == -1 {
-		return -1, get_last_error()
+		errno := get_last_error()
+		switch errno {
+		case .EINVAL:
+			return 0, .Invalid_Offset
+		}
+		return 0, errno
 	}
 	return res, nil
 }

+ 83 - 58
core/os/os_openbsd.odin

@@ -343,50 +343,53 @@ AT_REMOVEDIR        :: 0x08
 
 @(default_calling_convention="c")
 foreign libc {
-	@(link_name="__error")	__error		:: proc() -> ^c.int ---
-
-	@(link_name="fork")	_unix_fork	:: proc() -> pid_t ---
-	@(link_name="getthrid")	_unix_getthrid	:: proc() -> int ---
-
-	@(link_name="open")	_unix_open	:: proc(path: cstring, flags: c.int, #c_vararg mode: ..u32) -> Handle ---
-	@(link_name="close")	_unix_close	:: proc(fd: Handle) -> c.int ---
-	@(link_name="read")	_unix_read	:: proc(fd: Handle, buf: rawptr, size: c.size_t) -> c.ssize_t ---
-	@(link_name="write")	_unix_write	:: proc(fd: Handle, buf: rawptr, size: c.size_t) -> c.ssize_t ---
-	@(link_name="lseek")	_unix_seek	:: proc(fd: Handle, offset: off_t, whence: c.int) -> off_t ---
-	@(link_name="stat")	_unix_stat	:: proc(path: cstring, sb: ^OS_Stat) -> c.int ---
-	@(link_name="fstat")	_unix_fstat	:: proc(fd: Handle, sb: ^OS_Stat) -> c.int ---
-	@(link_name="lstat")	_unix_lstat	:: proc(path: cstring, sb: ^OS_Stat) -> c.int ---
-	@(link_name="readlink")	_unix_readlink	:: proc(path: cstring, buf: ^byte, bufsiz: c.size_t) -> c.ssize_t ---
-	@(link_name="access")	_unix_access	:: proc(path: cstring, mask: c.int) -> c.int ---
-	@(link_name="getcwd")	_unix_getcwd	:: proc(buf: cstring, len: c.size_t) -> cstring ---
-	@(link_name="chdir")	_unix_chdir	:: proc(path: cstring) -> c.int ---
-	@(link_name="rename")	_unix_rename	:: proc(old, new: cstring) -> c.int ---
-	@(link_name="unlink")	_unix_unlink	:: proc(path: cstring) -> c.int ---
-	@(link_name="rmdir")	_unix_rmdir	:: proc(path: cstring) -> c.int ---
-	@(link_name="mkdir")	_unix_mkdir	:: proc(path: cstring, mode: mode_t) -> c.int ---
-	@(link_name="dup")      _unix_dup   :: proc(fd: Handle) -> Handle ---
-
-	@(link_name="getpagesize") _unix_getpagesize :: proc() -> c.int ---
-	@(link_name="sysconf") _sysconf :: proc(name: c.int) -> c.long ---
-	@(link_name="fdopendir") _unix_fdopendir :: proc(fd: Handle) -> Dir ---
-	@(link_name="closedir")	_unix_closedir	:: proc(dirp: Dir) -> c.int ---
-	@(link_name="rewinddir") _unix_rewinddir :: proc(dirp: Dir) ---
-	@(link_name="readdir_r") _unix_readdir_r :: proc(dirp: Dir, entry: ^Dirent, result: ^^Dirent) -> c.int ---
-
-	@(link_name="malloc")	_unix_malloc	:: proc(size: c.size_t) -> rawptr ---
-	@(link_name="calloc")	_unix_calloc	:: proc(num, size: c.size_t) -> rawptr ---
-	@(link_name="free")	_unix_free	:: proc(ptr: rawptr) ---
-	@(link_name="realloc")	_unix_realloc	:: proc(ptr: rawptr, size: c.size_t) -> rawptr ---
-
-	@(link_name="getenv")	_unix_getenv	:: proc(cstring) -> cstring ---
-	@(link_name="realpath")	_unix_realpath	:: proc(path: cstring, resolved_path: [^]byte = nil) -> cstring ---
-
-	@(link_name="exit")	_unix_exit	:: proc(status: c.int) -> ! ---
-
-	@(link_name="dlopen")	_unix_dlopen	:: proc(filename: cstring, flags: c.int) -> rawptr ---
-	@(link_name="dlsym")	_unix_dlsym	:: proc(handle: rawptr, symbol: cstring) -> rawptr ---
-	@(link_name="dlclose")	_unix_dlclose	:: proc(handle: rawptr) -> c.int ---
-	@(link_name="dlerror")	_unix_dlerror	:: proc() -> cstring ---
+	@(link_name="__error")        __error              :: proc() -> ^c.int ---
+
+	@(link_name="fork")           _unix_fork           :: proc() -> pid_t ---
+	@(link_name="getthrid")       _unix_getthrid       :: proc() -> int ---
+
+	@(link_name="open")           _unix_open           :: proc(path: cstring, flags: c.int, #c_vararg mode: ..u32) -> Handle ---
+	@(link_name="close")          _unix_close          :: proc(fd: Handle) -> c.int ---
+	@(link_name="read")           _unix_read           :: proc(fd: Handle, buf: rawptr, size: c.size_t) -> c.ssize_t ---
+	@(link_name="pread")          _unix_pread          :: proc(fd: Handle, buf: rawptr, size: c.size_t, offset: i64) -> c.ssize_t ---
+	@(link_name="write")          _unix_write          :: proc(fd: Handle, buf: rawptr, size: c.size_t) -> c.ssize_t ---
+	@(link_name="pwrite")         _unix_pwrite         :: proc(fd: Handle, buf: rawptr, size: c.size_t, offset: i64) -> c.ssize_t ---
+	@(link_name="lseek")          _unix_seek           :: proc(fd: Handle, offset: off_t, whence: c.int) -> off_t ---
+	@(link_name="stat")           _unix_stat           :: proc(path: cstring, sb: ^OS_Stat) -> c.int ---
+	@(link_name="fstat")          _unix_fstat          :: proc(fd: Handle, sb: ^OS_Stat) -> c.int ---
+	@(link_name="lstat")          _unix_lstat          :: proc(path: cstring, sb: ^OS_Stat) -> c.int ---
+	@(link_name="readlink")       _unix_readlink       :: proc(path: cstring, buf: ^byte, bufsiz: c.size_t) -> c.ssize_t ---
+	@(link_name="access")         _unix_access         :: proc(path: cstring, mask: c.int) -> c.int ---
+	@(link_name="getcwd")         _unix_getcwd         :: proc(buf: cstring, len: c.size_t) -> cstring ---
+	@(link_name="chdir")          _unix_chdir          :: proc(path: cstring) -> c.int ---
+	@(link_name="rename")         _unix_rename         :: proc(old, new: cstring) -> c.int ---
+	@(link_name="unlink")         _unix_unlink         :: proc(path: cstring) -> c.int ---
+	@(link_name="rmdir")          _unix_rmdir          :: proc(path: cstring) -> c.int ---
+	@(link_name="mkdir")          _unix_mkdir          :: proc(path: cstring, mode: mode_t) -> c.int ---
+	@(link_name="fsync")          _unix_fsync          :: proc(fd: Handle) -> c.int ---
+	@(link_name="dup")            _unix_dup            :: proc(fd: Handle) -> Handle ---
+
+	@(link_name="getpagesize")    _unix_getpagesize    :: proc() -> c.int ---
+	@(link_name="sysconf")        _sysconf             :: proc(name: c.int) -> c.long ---
+	@(link_name="fdopendir")      _unix_fdopendir      :: proc(fd: Handle) -> Dir ---
+	@(link_name="closedir")       _unix_closedir       :: proc(dirp: Dir) -> c.int ---
+	@(link_name="rewinddir")      _unix_rewinddir      :: proc(dirp: Dir) ---
+	@(link_name="readdir_r")      _unix_readdir_r      :: proc(dirp: Dir, entry: ^Dirent, result: ^^Dirent) -> c.int ---
+
+	@(link_name="malloc")         _unix_malloc         :: proc(size: c.size_t) -> rawptr ---
+	@(link_name="calloc")         _unix_calloc         :: proc(num, size: c.size_t) -> rawptr ---
+	@(link_name="free")           _unix_free           :: proc(ptr: rawptr) ---
+	@(link_name="realloc")        _unix_realloc        :: proc(ptr: rawptr, size: c.size_t) -> rawptr ---
+
+	@(link_name="getenv")         _unix_getenv         :: proc(cstring) -> cstring ---
+	@(link_name="realpath")       _unix_realpath       :: proc(path: cstring, resolved_path: [^]byte = nil) -> cstring ---
+
+	@(link_name="exit")           _unix_exit           :: proc(status: c.int) -> ! ---
+
+	@(link_name="dlopen")         _unix_dlopen         :: proc(filename: cstring, flags: c.int) -> rawptr ---
+	@(link_name="dlsym")          _unix_dlsym          :: proc(handle: rawptr, symbol: cstring) -> rawptr ---
+	@(link_name="dlclose")        _unix_dlclose        :: proc(handle: rawptr) -> c.int ---
+	@(link_name="dlerror")        _unix_dlerror        :: proc() -> cstring ---
 }
 
 @(require_results)
@@ -428,7 +431,10 @@ close :: proc(fd: Handle) -> Error {
 }
 
 flush :: proc(fd: Handle) -> Error {
-	// do nothing
+	result := _unix_fsync(fd)
+	if result == -1 {
+		return get_last_error()
+	}
 	return nil
 }
 
@@ -463,29 +469,48 @@ write :: proc(fd: Handle, data: []byte) -> (int, Error) {
 }
 
 read_at :: proc(fd: Handle, data: []byte, offset: i64) -> (n: int, err: Error) {
-	curr := seek(fd, offset, SEEK_CUR) or_return
-	n, err = read(fd, data)
-	_, err1 := seek(fd, curr, SEEK_SET)
-	if err1 != nil && err == nil {
-		err = err1
+	if len(data) == 0 {
+		return 0, nil
 	}
-	return
+
+	to_read := min(uint(len(data)), MAX_RW)
+
+	bytes_read := _unix_pread(fd, raw_data(data), to_read, offset)
+	if bytes_read < 0 {
+		return -1, get_last_error()
+	}
+	return bytes_read, nil
 }
 
 write_at :: proc(fd: Handle, data: []byte, offset: i64) -> (n: int, err: Error) {
-	curr := seek(fd, offset, SEEK_CUR) or_return
-	n, err = write(fd, data)
-	_, err1 := seek(fd, curr, SEEK_SET)
-	if err1 != nil && err == nil {
-		err = err1
+	if len(data) == 0 {
+		return 0, nil
 	}
-	return
+
+	to_write := min(uint(len(data)), MAX_RW)
+
+	bytes_written := _unix_pwrite(fd, raw_data(data), to_write, offset)
+	if bytes_written < 0 {
+		return -1, get_last_error()
+	}
+	return bytes_written, nil
 }
 
 seek :: proc(fd: Handle, offset: i64, whence: int) -> (i64, Error) {
+	switch whence {
+	case SEEK_SET, SEEK_CUR, SEEK_END:
+		break
+	case:
+		return 0, .Invalid_Whence
+	}
 	res := _unix_seek(fd, offset, c.int(whence))
 	if res == -1 {
-		return -1, get_last_error()
+		errno := get_last_error()
+		switch errno {
+		case .EINVAL:
+			return 0, .Invalid_Offset
+		}
+		return 0, errno
 	}
 	return res, nil
 }

+ 4 - 0
core/os/os_windows.odin

@@ -43,6 +43,7 @@ ERROR_BUFFER_OVERFLOW     :: _Platform_Error(111)
 ERROR_INSUFFICIENT_BUFFER :: _Platform_Error(122)
 ERROR_MOD_NOT_FOUND       :: _Platform_Error(126)
 ERROR_PROC_NOT_FOUND      :: _Platform_Error(127)
+ERROR_NEGATIVE_SEEK       :: _Platform_Error(131)
 ERROR_DIR_NOT_EMPTY       :: _Platform_Error(145)
 ERROR_ALREADY_EXISTS      :: _Platform_Error(183)
 ERROR_ENVVAR_NOT_FOUND    :: _Platform_Error(203)
@@ -91,6 +92,9 @@ get_last_error :: proc "contextless" () -> Error {
 	case win32.ERROR_INVALID_HANDLE:
 		return .Invalid_File
 
+	case win32.ERROR_NEGATIVE_SEEK:
+		return .Invalid_Offset
+
 	case
 		win32.ERROR_BAD_ARGUMENTS,
 		win32.ERROR_INVALID_PARAMETER,

+ 15 - 0
core/os/stream.odin

@@ -21,6 +21,9 @@ _file_stream_proc :: proc(stream_data: rawptr, mode: io.Stream_Mode, p: []byte,
 	case .Flush:
 		os_err = flush(fd)
 	case .Read:
+		if len(p) == 0 {
+			return 0, nil
+		}
 		n_int, os_err = read(fd, p)
 		n = i64(n_int)
 		if n == 0 && os_err == nil {
@@ -28,18 +31,27 @@ _file_stream_proc :: proc(stream_data: rawptr, mode: io.Stream_Mode, p: []byte,
 		}
 
 	case .Read_At:
+		if len(p) == 0 {
+			return 0, nil
+		}
 		n_int, os_err = read_at(fd, p, offset)
 		n = i64(n_int)
 		if n == 0 && os_err == nil {
 			err = .EOF
 		}
 	case .Write:
+		if len(p) == 0 {
+			return 0, nil
+		}
 		n_int, os_err = write(fd, p)
 		n = i64(n_int)
 		if n == 0 && os_err == nil {
 			err = .EOF
 		}
 	case .Write_At:
+		if len(p) == 0 {
+			return 0, nil
+		}
 		n_int, os_err = write_at(fd, p, offset)
 		n = i64(n_int)
 		if n == 0 && os_err == nil {
@@ -58,5 +70,8 @@ _file_stream_proc :: proc(stream_data: rawptr, mode: io.Stream_Mode, p: []byte,
 	if err == nil && os_err != nil {
 		err = error_to_io_error(os_err)
 	}
+	if err != nil {
+		n = 0
+	}
 	return
 }

+ 87 - 0
core/sys/freebsd/syscalls.odin

@@ -14,12 +14,15 @@ import "core:c"
 // FreeBSD 15 syscall numbers
 // See: https://alfonsosiciliano.gitlab.io/posts/2023-08-28-freebsd-15-system-calls.html
 
+SYS_read       : uintptr : 3
+SYS_write      : uintptr : 4
 SYS_open       : uintptr : 5
 SYS_close      : uintptr : 6
 SYS_getpid     : uintptr : 20
 SYS_recvfrom   : uintptr : 29
 SYS_accept     : uintptr : 30
 SYS_fcntl      : uintptr : 92
+SYS_fsync      : uintptr : 95
 SYS_socket     : uintptr : 97
 SYS_connect    : uintptr : 98
 SYS_bind       : uintptr : 104
@@ -29,12 +32,46 @@ SYS_shutdown   : uintptr : 134
 SYS_setsockopt : uintptr : 105
 SYS_sysctl     : uintptr : 202
 SYS__umtx_op   : uintptr : 454
+SYS_pread      : uintptr : 475
+SYS_pwrite     : uintptr : 476
 SYS_accept4    : uintptr : 541
 
 //
 // Odin syscall wrappers
 //
 
+// Read input.
+//
+// The read() function appeared in Version 1 AT&T UNIX.
+read :: proc "contextless" (fd: Fd, buf: []u8) -> (int, Errno) {
+	result, ok := intrinsics.syscall_bsd(SYS_read,
+		cast(uintptr)fd,
+		cast(uintptr)raw_data(buf),
+		cast(uintptr)len(buf))
+
+	if !ok {
+		return 0, cast(Errno)result
+	}
+
+	return cast(int)result, nil
+}
+
+// Write output.
+//
+// The write() function appeared in Version 1 AT&T UNIX.
+write :: proc "contextless" (fd: Fd, buf: []u8) -> (int, Errno) {
+	result, ok := intrinsics.syscall_bsd(SYS_pwrite,
+		cast(uintptr)fd,
+		cast(uintptr)raw_data(buf),
+		cast(uintptr)len(buf))
+
+	if !ok {
+		return 0, cast(Errno)result
+	}
+
+	return cast(int)result, nil
+}
+
 // Open or create a file for reading, writing or executing.
 //
 // The open() function appeared in Version 1 AT&T UNIX.
@@ -164,6 +201,16 @@ accept_nil :: proc "contextless" (s: Fd) -> (Fd, Errno) {
 
 accept :: proc { accept_T, accept_nil }
 
+// Synchronize changes to a file.
+//
+// The fsync() system call appeared in 4.2BSD.
+fsync :: proc "contextless" (fd: Fd) -> Errno {
+	result, _ := intrinsics.syscall_bsd(SYS_fsync,
+		cast(uintptr)fd)
+
+	return cast(Errno)result
+}
+
 // File control.
 //
 // The fcntl() system call appeared in 4.2BSD.
@@ -469,6 +516,46 @@ _umtx_op :: proc "contextless" (obj: rawptr, op: Userland_Mutex_Operation, val:
 	return cast(Errno)result
 }
 
+// Read input without modifying the file pointer.
+//
+// The pread() function appeared in AT&T System V Release 4 UNIX.
+pread :: proc "contextless" (fd: Fd, buf: []u8, offset: off_t) -> (int, Errno) {
+	result, ok := intrinsics.syscall_bsd(SYS_pread,
+		cast(uintptr)fd,
+		cast(uintptr)raw_data(buf),
+		cast(uintptr)len(buf),
+		cast(uintptr)offset)
+
+	if !ok {
+		return 0, cast(Errno)result
+	}
+
+	return cast(int)result, nil
+}
+
+// Write output without modifying the file pointer.
+//
+// The pwrite() function appeared in AT&T System V Release 4 UNIX.
+//
+// BUGS
+//
+// The pwrite() system call appends the file without changing the file
+// offset if O_APPEND is set, contrary to IEEE Std 1003.1-2008 (“POSIX.1”)
+// where pwrite() writes into offset regardless of whether O_APPEND is set.
+pwrite :: proc "contextless" (fd: Fd, buf: []u8, offset: off_t) -> (int, Errno) {
+	result, ok := intrinsics.syscall_bsd(SYS_pwrite,
+		cast(uintptr)fd,
+		cast(uintptr)raw_data(buf),
+		cast(uintptr)len(buf),
+		cast(uintptr)offset)
+
+	if !ok {
+		return 0, cast(Errno)result
+	}
+
+	return cast(int)result, nil
+}
+
 // Accept a connection on a socket.
 //
 // The accept4() system call appeared in FreeBSD 10.0.

+ 1 - 0
core/sys/windows/winerror.odin

@@ -213,6 +213,7 @@ ERROR_BROKEN_PIPE            : DWORD : 109
 ERROR_CALL_NOT_IMPLEMENTED   : DWORD : 120
 ERROR_INSUFFICIENT_BUFFER    : DWORD : 122
 ERROR_INVALID_NAME           : DWORD : 123
+ERROR_NEGATIVE_SEEK          : DWORD : 131
 ERROR_BAD_ARGUMENTS          : DWORD : 160
 ERROR_LOCK_FAILED            : DWORD : 167
 ERROR_ALREADY_EXISTS         : DWORD : 183

+ 735 - 0
tests/core/io/test_core_io.odin

@@ -0,0 +1,735 @@
+package test_core_io
+
+import "core:bufio"
+import "core:bytes"
+import "core:io"
+import "core:log"
+import "core:os"
+import "core:os/os2"
+import "core:strings"
+import "core:testing"
+
+Passed_Tests :: distinct io.Stream_Mode_Set
+
+_test_stream :: proc(
+	t: ^testing.T,
+	stream: io.Stream,
+	buffer: []u8,
+	
+	reading_consumes: bool = false,
+	resets_on_empty: bool = false,
+	do_destroy: bool = true,
+
+	loc := #caller_location
+) -> (passed: Passed_Tests, ok: bool) {
+	// We only test what the stream reports to support.
+
+	mode_set := io.query(stream)
+
+	// Can't feature-test anything if Query isn't supported.
+	testing.expectf(t, .Query in mode_set, "stream does not support .Query: %v", mode_set, loc = loc) or_return
+
+	passed += { .Query }
+
+	size := i64(len(buffer))
+
+	// Do some basic Seek sanity testing.
+	if .Seek in mode_set {
+		pos, err := io.seek(stream, 0, io.Seek_From(-1))
+		testing.expectf(t, err == .Invalid_Whence,
+			"Seek(-1) didn't fail with Invalid_Whence: %v, %v", pos, err, loc = loc) or_return
+
+		pos, err = io.seek(stream, 0, .Start)
+		testing.expectf(t, pos == 0 && err == nil,
+			"Seek Start isn't 0: %v, %v", pos, err, loc = loc) or_return
+
+		pos, err = io.seek(stream, 0, .Current)
+		testing.expectf(t, pos == 0 && err == nil,
+			"Seek Current isn't 0 at the start: %v, %v", pos, err, loc = loc) or_return
+
+		pos, err = io.seek(stream, -1, .Start)
+		testing.expectf(t, err == .Invalid_Offset,
+			"Seek Start-1 wasn't Invalid_Offset: %v, %v", pos, err, loc = loc) or_return
+
+		pos, err = io.seek(stream, -1, .Current)
+		testing.expectf(t, err == .Invalid_Offset,
+			"Seek Current-1 wasn't Invalid_Offset: %v, %v", pos, err, loc = loc) or_return
+
+		pos, err = io.seek(stream, 0, .End)
+		testing.expectf(t, pos == size && err == nil,
+			"Seek End+0 failed: %v != size<%i>, %v", pos, size, err, loc = loc) or_return
+
+		pos, err = io.seek(stream, 0, .Current)
+		testing.expectf(t, pos == size && err == nil,
+			"Seek Current isn't size<%v> at the End: %v, %v", size, pos, err, loc = loc) or_return
+
+		// Seeking past the End is accepted throughout the API.
+		//
+		// It's _reading_ past the End which is erroneous.
+		pos, err = io.seek(stream, 1, .End)
+		testing.expectf(t, pos == size+1 && err == nil,
+			"Seek End+1 failed: %v, %v", pos, err, loc = loc) or_return
+
+		// Reset our position for future tests.
+		pos, err = io.seek(stream, 0, .Start)
+		testing.expectf(t, pos == 0 && err == nil,
+			"Seek Start reset failed: %v, %v", pos, err, loc = loc) or_return
+
+		passed += { .Seek }
+	}
+
+	// Test Size.
+	if .Size in mode_set {
+		api_size, size_err := io.size(stream)
+		testing.expectf(t, api_size == size,
+			"Size reports %v for its size; expected %v", api_size, size, loc = loc) or_return
+		testing.expectf(t, size_err == nil,
+			"Size expected no error: %v", size_err, loc = loc) or_return
+
+		// Ensure Size does not move the underlying pointer from the start.
+		//
+		// Some implementations may use seeking to determine file sizes.
+		if .Seek in mode_set {
+			pos, seek_err := io.seek(stream, 0, .Current)
+			testing.expectf(t, pos == 0 && seek_err == nil,
+				"Size+Seek Current isn't 0 after getting size: %v, %v", pos, seek_err, loc = loc) or_return
+		}
+
+		passed += { .Size }
+	}
+
+	// Test Read_At.
+	if .Read_At in mode_set {
+		// Test reading into an empty buffer.
+		{
+			nil_slice: []u8
+			bytes_read, err := io.read_at(stream, nil_slice, 0)
+			testing.expectf(t, bytes_read == 0 && err == nil,
+				"Read_At into empty slice failed: bytes_read<%v>, %v", bytes_read, err, loc = loc) or_return
+		}
+
+		read_buf, alloc_err := make([]u8, size)
+		testing.expect_value(t, alloc_err, nil, loc = loc) or_return
+		defer delete(read_buf)
+
+		for start in 0..<size {
+			for end in 1+start..<size {
+				subsize := end - start
+				bytes_read, err := io.read_at(stream, read_buf[:subsize], start)
+				testing.expectf(t, i64(bytes_read) == subsize && err == nil,
+					"Read_At(%i) of %v bytes failed: %v, %v", start, subsize, bytes_read, err, loc = loc) or_return
+				testing.expectf(t, bytes.compare(read_buf[:subsize], buffer[start:end]) == 0,
+					"Read_At buffer compare failed: read_buf<%v> != buffer<%v>", read_buf, buffer, loc = loc) or_return
+			}
+		}
+
+		// Test empty streams and EOF.
+		one_buf: [1]u8
+		bytes_read, err := io.read_at(stream, one_buf[:], size)
+		testing.expectf(t, err == .EOF,
+			"Read_At at end of stream failed: %v, %v", bytes_read, err, loc = loc) or_return
+
+		// Make sure size is still sane.
+		if .Size in mode_set {
+			api_size, size_err := io.size(stream)
+			testing.expectf(t, api_size == size,
+				"Read_At+Size reports %v for its size after Read_At tests; expected %v", api_size, size, loc = loc) or_return
+			testing.expectf(t, size_err == nil,
+				"Read_At+Size expected no error: %v", size_err, loc = loc) or_return
+		}
+
+		// Ensure Read_At does not move the underlying pointer from the start.
+		if .Seek in mode_set {
+			pos, seek_err := io.seek(stream, 0, .Current)
+			testing.expectf(t, pos == 0 && seek_err == nil,
+				"Read_At+Seek Current isn't 0 after reading: %v, %v", pos, seek_err, loc = loc) or_return
+		}
+
+		passed += { .Read_At }
+	}
+
+	// Test Read.
+	if .Read in mode_set {
+		// Test reading into an empty buffer.
+		{
+			nil_slice: []u8
+			bytes_read, err := io.read(stream, nil_slice)
+			testing.expectf(t, bytes_read == 0 && err == nil,
+				"Read into empty slice failed: bytes_read<%v>, %v", bytes_read, err, loc = loc) or_return
+		}
+
+		if size > 0 {
+			read_buf, alloc_err := make([]u8, size)
+			testing.expectf(t, alloc_err == nil, "allocation failed", loc = loc) or_return
+			defer delete(read_buf)
+
+			bytes_read, err := io.read(stream, read_buf[:1])
+			testing.expectf(t, bytes_read == 1 && err == nil,
+				"Read 1 byte at start failed: %v, %v", bytes_read, err, loc = loc) or_return
+			testing.expectf(t, read_buf[0] == buffer[0],
+				"Read of first byte failed: read_buf[0]<%v> != buffer[0]<%v>", read_buf[0], buffer[0], loc = loc) or_return
+
+			// Test rolling back the stream one byte then reading it again.
+			if .Seek in mode_set {
+				pos, seek_err := io.seek(stream, -1, .Current)
+				testing.expectf(t, pos == 0 && err == nil,
+					"Read+Seek Current-1 reset to 0 failed: %v, %v", pos, seek_err, loc = loc) or_return
+
+				bytes_read, err = io.read(stream, read_buf[:1])
+				testing.expectf(t, bytes_read == 1 && err == nil,
+					"Read 1 byte at start after Seek reset failed: %v, %v", bytes_read, err, loc = loc) or_return
+				testing.expectf(t, read_buf[0] == buffer[0] ,
+					"re-Read of first byte failed: read_buf[0]<%v> != buffer[0]<%v>", read_buf[0], buffer[0], loc = loc) or_return
+			}
+
+			// Make sure size is still sane.
+			if .Size in mode_set {
+				api_size, size_err := io.size(stream)
+				expected_api_size := size - 1 if reading_consumes else size
+
+				testing.expectf(t, api_size == expected_api_size,
+					"Read+Size reports %v for its size after Read tests; expected %v", api_size, expected_api_size, loc = loc) or_return
+				testing.expectf(t, size_err == nil,
+					"Read+Size expected no error: %v", size_err, loc = loc) or_return
+			}
+
+			// Read the rest.
+			if size > 1 {
+				bytes_read, err = io.read(stream, read_buf[1:])
+				testing.expectf(t, i64(bytes_read) == size - 1 && err == nil,
+					"Read rest of stream failed: %v != %v, %v", bytes_read, size-1, err, loc = loc) or_return
+				testing.expectf(t, bytes.compare(read_buf, buffer) == 0,
+					"Read buffer compare failed: read_buf<%v> != buffer<%v>", read_buf, buffer, loc = loc) or_return
+			}
+		}
+
+		// Test empty streams and EOF.
+		one_buf: [1]u8
+		bytes_read, err := io.read(stream, one_buf[:])
+		testing.expectf(t, err == .EOF,
+			"Read at end of stream failed: %v, %v", bytes_read, err, loc = loc) or_return
+
+		if !resets_on_empty && .Size in mode_set {
+			// Make sure size is still sane.
+			api_size, size_err := io.size(stream)
+			testing.expectf(t, api_size == size,
+				"Read+Size reports %v for its size after Read tests; expected %v", api_size, size, loc = loc) or_return
+			testing.expectf(t, size_err == nil,
+				"Read+Size expected no error: %v", size_err, loc = loc) or_return
+		}
+
+		passed += { .Read }
+	}
+
+	// Test Write_At.
+	if .Write_At in mode_set {
+		// Test writing from an empty buffer.
+		{
+			nil_slice: []u8
+			bytes_written, err := io.write_at(stream, nil_slice, 0)
+			testing.expectf(t, bytes_written == 0 && err == nil,
+				"Write_At from empty slice failed: bytes_written<%v>, %v", bytes_written, err, loc = loc) or_return
+		}
+
+		// Ensure Write_At does not move the underlying pointer from the start.
+		starting_offset : i64 = -1
+		if .Seek in mode_set {
+			pos, seek_err := io.seek(stream, 0, .Current)
+			testing.expectf(t, pos >= 0 && seek_err == nil,
+				"Write_At+Seek Current failed: %v, %v", pos, seek_err, loc = loc) or_return
+			starting_offset = pos
+		}
+
+		if size > 0 {
+			write_buf, write_buf_alloc_err := make([]u8, size)
+			testing.expectf(t, write_buf_alloc_err == nil, "allocation failed", loc = loc) or_return
+			defer delete(write_buf)
+
+			for i in 0..<size {
+				write_buf[i] = buffer[i] ~ 0xAA
+			}
+
+			bytes_written, write_err := io.write_at(stream, write_buf[:], 0)
+			testing.expectf(t, i64(bytes_written) == size && write_err == nil,
+				"Write_At failed: bytes_written<%v> != size<%v>: %v", bytes_written, size, write_err, loc = loc) or_return
+
+			// Test reading what we've written.
+			if .Read_At in mode_set {
+				read_buf, read_buf_alloc_err := make([]u8, size)
+				testing.expectf(t, read_buf_alloc_err == nil, "allocation failed", loc = loc) or_return
+				defer delete(read_buf)
+				bytes_read, read_err := io.read_at(stream, read_buf[:], 0)
+				testing.expectf(t, i64(bytes_read) == size && read_err == nil,
+					"Write_At+Read_At failed: bytes_read<%i> != size<%i>, %v", bytes_read, size, read_err, loc = loc) or_return
+				testing.expectf(t, bytes.compare(read_buf, write_buf) == 0,
+					"Write_At+Read_At buffer compare failed: write_buf<%v> != read_buf<%v>", write_buf, read_buf, loc = loc) or_return
+			}
+		} else {
+			// Expect that it should be okay to write a single byte to an empty stream.
+			x_buf: [1]u8 = { 'Z' }
+
+			bytes_written, write_err := io.write_at(stream, x_buf[:], 0)
+			testing.expectf(t, i64(bytes_written) == 1 && write_err == nil,
+				"Write_At(0) with 'Z' on empty stream failed: bytes_written<%v>, %v", bytes_written, write_err, loc = loc) or_return
+
+			// Test reading what we've written.
+			if .Read_At in mode_set {
+				x_buf[0] = 0
+				bytes_read, read_err := io.read_at(stream, x_buf[:], 0)
+				testing.expectf(t, i64(bytes_read) == 1 && read_err == nil,
+					"Write_At(0)+Read_At(0) failed expectation: bytes_read<%v> != 1, %q != 'Z', %v", bytes_read, x_buf[0], read_err, loc = loc) or_return
+			}
+		}
+
+		// Ensure Write_At does not move the underlying pointer from the start.
+		if starting_offset != -1 && .Seek in mode_set {
+			pos, seek_err := io.seek(stream, 0, .Current)
+			testing.expectf(t, pos == starting_offset && seek_err == nil,
+				"Write_At+Seek Current isn't %v after writing: %v, %v", starting_offset, pos, seek_err, loc = loc) or_return
+		}
+
+		passed += { .Write_At }
+	}
+
+	// Test Write.
+	if .Write in mode_set {
+		// Test writing from an empty buffer.
+		{
+			nil_slice: []u8
+			bytes_written, err := io.write(stream, nil_slice)
+			testing.expectf(t, bytes_written == 0 && err == nil,
+				"Write from empty slice failed: bytes_written<%v>, %v", bytes_written, err, loc = loc) or_return
+		}
+
+		write_buf, write_buf_alloc_err := make([]u8, size)
+		testing.expectf(t, write_buf_alloc_err == nil, "allocation failed", loc = loc) or_return
+		defer delete(write_buf)
+
+		for i in 0..<size {
+			write_buf[i] = buffer[i] ~ 0xAA
+		}
+
+		pos: i64 = -1
+		before_write_size: i64 = -1
+
+		// Do a Seek sanity check after past tests.
+		if .Seek in mode_set {
+			seek_err: io.Error
+			pos, seek_err = io.seek(stream, 0, .Current)
+			testing.expectf(t, seek_err == nil,
+				"Write+Seek(Current) failed: pos<%i>, %v", pos, seek_err) or_return
+		}
+
+		// Get the Size before writing.
+		if .Size in mode_set {
+			size_err: io.Error
+			before_write_size, size_err = io.size(stream)
+			testing.expectf(t, size_err == nil,
+				"Write+Size failed: %v", size_err, loc = loc) or_return
+		}
+
+		bytes_written, write_err := io.write(stream, write_buf[:])
+		testing.expectf(t, i64(bytes_written) == size && write_err == nil,
+			"Write %i bytes failed: %i, %v", size, bytes_written, write_err, loc = loc) or_return
+
+		// Size sanity check, part 2.
+		if before_write_size >= 0 && .Size in mode_set {
+			after_write_size, size_err := io.size(stream)
+			testing.expectf(t, size_err == nil,
+				"Write+Size.part_2 failed: %v", size_err, loc = loc) or_return
+			testing.expectf(t, after_write_size == before_write_size + size,
+				"Write+Size.part_2 failed: %v != %v + %v", after_write_size, before_write_size, size, loc = loc) or_return
+		}
+
+		// Test reading what we've written directly with Read_At.
+		if pos >= 0 && .Read_At in mode_set {
+			read_buf, read_buf_alloc_err := make([]u8, size)
+			testing.expectf(t, read_buf_alloc_err == nil, "allocation failed", loc = loc) or_return
+			defer delete(read_buf)
+
+			bytes_read, read_err := io.read_at(stream, read_buf[:], pos)
+			testing.expectf(t, i64(bytes_read) == size && read_err == nil,
+				"Write+Read_At(%i) failed: bytes_read<%i> != size<%i>, %v", pos, bytes_read, size, read_err, loc = loc) or_return
+			testing.expectf(t, bytes.compare(read_buf, write_buf) == 0,
+				"Write+Read_At buffer compare failed: read_buf<%v> != write_buf<%v>", read_buf, write_buf, loc = loc) or_return
+		}
+
+		// Test resetting the pointer and reading what we've written with Read.
+		if .Read in mode_set && .Seek in mode_set {
+			seek_err: io.Error
+			pos, seek_err = io.seek(stream, 0, .Start)
+			testing.expectf(t, pos == 0 && seek_err == nil,
+				"Write+Read+Seek(Start) failed: pos<%i>, %v", pos, seek_err) or_return
+
+			read_buf, read_buf_alloc_err := make([]u8, size)
+			testing.expectf(t, read_buf_alloc_err == nil, "allocation failed", loc = loc) or_return
+			defer delete(read_buf)
+
+			bytes_read, read_err := io.read(stream, read_buf[:])
+			testing.expectf(t, i64(bytes_read) == size && read_err == nil,
+				"Write+Read failed: bytes_read<%i> != size<%i>, %v", bytes_read, size, read_err, loc = loc) or_return
+			testing.expectf(t, bytes.compare(read_buf, write_buf) == 0,
+				"Write+Readbuffer compare failed: read_buf<%v> != write_buf<%v>", read_buf, write_buf, loc = loc) or_return
+		}
+
+		passed += { .Write }
+	}
+
+	// Test the other modes.
+	if .Flush in mode_set {
+		err := io.flush(stream)
+		testing.expectf(t, err == nil, "stream failed to Flush: %v", err, loc = loc) or_return
+		passed += { .Flush }
+	}
+
+	if .Close in mode_set {
+		close_err := io.close(stream)
+		testing.expectf(t, close_err == nil, "stream failed to Close: %v", close_err, loc = loc) or_return
+		passed += { .Close }
+	}
+
+	if do_destroy && .Destroy in mode_set {
+		err := io.destroy(stream)
+		testing.expectf(t, err == nil, "stream failed to Destroy: %v", err, loc = loc) or_return
+		passed += { .Destroy }
+	}
+
+	ok = true
+	return
+}
+
+
+
+@test
+test_bytes_reader :: proc(t: ^testing.T) {
+	buf: [32]u8
+	for i in 0..<u8(len(buf)) {
+		buf[i] = 'A' + i
+	}
+
+	br: bytes.Reader
+
+	results: Passed_Tests
+	ok: bool
+
+	for end in 0..<i64(len(buf)) {
+		results, ok =_test_stream(t, bytes.reader_init(&br, buf[:end]), buf[:end])
+		if !ok {
+			log.debugf("buffer[:%i] := %v", end, buf[:end])
+			return
+		}
+	}
+
+	log.debugf("%#v", results)
+}
+
+@test
+test_bytes_buffer_stream :: proc(t: ^testing.T) {
+	buf: [32]u8
+	for i in 0..<u8(len(buf)) {
+		buf[i] = 'A' + i
+	}
+
+	results: Passed_Tests
+	ok: bool
+
+	for end in 0..<i64(len(buf)) {
+		bb: bytes.Buffer
+		// Mind that `bytes.buffer_init` copies the entire underlying slice.
+		bytes.buffer_init(&bb, buf[:end])
+
+		// `bytes.Buffer` has a behavior of decreasing its size with each read
+		// until it eventually clears the underlying buffer when it runs out of
+		// data to read.
+		results, ok = _test_stream(t, bytes.buffer_to_stream(&bb), buf[:end],
+			reading_consumes = true, resets_on_empty = true)
+		if !ok {
+			log.debugf("buffer[:%i] := %v", end, buf[:end])
+			return
+		}
+	}
+
+	log.debugf("%#v", results)
+}
+
+@test
+test_limited_reader :: proc(t: ^testing.T) {
+	buf: [32]u8
+	for i in 0..<u8(len(buf)) {
+		buf[i] = 'A' + i
+	}
+
+	br: bytes.Reader
+	bs := bytes.reader_init(&br, buf[:])
+
+	lr: io.Limited_Reader
+
+	results: Passed_Tests
+	ok: bool
+
+	for end in 0..<i64(len(buf)) {
+		pos, seek_err := io.seek(bs, 0, .Start)
+		if !testing.expectf(t, pos == 0 && seek_err == nil,
+			"Pre-test Seek reset failed: pos<%v>, %v", pos, seek_err) {
+			return
+		}
+
+		results, ok = _test_stream(t, io.limited_reader_init(&lr, bs, end), buf[:end])
+		if !ok {
+			log.debugf("buffer[:%i] := %v", end, buf[:end])
+			return
+		}
+	}
+
+	log.debugf("%#v", results)
+}
+
+@test
+test_section_reader :: proc(t: ^testing.T) {
+	buf: [32]u8
+	for i in 0..<u8(len(buf)) {
+		buf[i] = 'A' + i
+	}
+
+	br: bytes.Reader
+	bs := bytes.reader_init(&br, buf[:])
+
+	sr: io.Section_Reader
+
+	results: Passed_Tests
+	ok: bool
+
+	for start in 0..<i64(len(buf)) {
+		for end in start..<i64(len(buf)) {
+			results, ok = _test_stream(t, io.section_reader_init(&sr, bs, start, end-start), buf[start:end])
+			if !ok {
+				log.debugf("buffer[%i:%i] := %v", start, end, buf[start:end])
+				return
+			}
+		}
+	}
+
+	log.debugf("%#v", results)
+}
+
+@test
+test_string_builder_stream :: proc(t: ^testing.T) {
+	sb := strings.builder_make()
+	defer strings.builder_destroy(&sb)
+
+	// String builders do not support reading, so we'll have to set up a few
+	// things outside the main test.
+
+	buf: [32]u8
+	expected_buf: [64]u8
+	for i in 0..<u8(len(buf)) {
+		buf[i] = 'A' + i
+		expected_buf[i] = 'A' + i
+		strings.write_byte(&sb, 'A' + i)
+	}
+	for i in 32..<u8(len(expected_buf)) {
+		expected_buf[i] = ('A' + i-len(buf)) ~ 0xAA
+	}
+
+	results, _ := _test_stream(t, strings.to_stream(&sb), buf[:],
+		do_destroy = false)
+
+	testing.expectf(t, bytes.compare(sb.buf[:], expected_buf[:]) == 0, "string builder stream failed:\nbuilder<%q>\n!=\nbuffer <%q>", sb.buf[:], expected_buf[:])
+
+	log.debugf("%#v", results)
+}
+
+@test
+test_os_file_stream :: proc(t: ^testing.T) {
+	defer if !testing.failed(t) {
+		testing.expect_value(t, os.remove(TEMPORARY_FILENAME), nil)
+	}
+
+	buf: [32]u8
+	for i in 0..<u8(len(buf)) {
+		buf[i] = 'A' + i
+	}
+
+	TEMPORARY_FILENAME :: "test_core_io_os_file_stream"
+
+	fd, open_err := os.open(TEMPORARY_FILENAME, os.O_RDWR | os.O_CREATE | os.O_TRUNC, 0o644)
+	if !testing.expectf(t, open_err == nil, "error on opening %q: %v", TEMPORARY_FILENAME, open_err) {
+		return
+	}
+	
+	stream := os.stream_from_handle(fd)
+
+	bytes_written, write_err := io.write(stream, buf[:])
+	if !testing.expectf(t, bytes_written == len(buf) && write_err == nil,
+		"failed to Write initial buffer: bytes_written<%v> != len_buf<%v>, %v", bytes_written, len(buf), write_err) {
+		return
+	}
+
+	flush_err := io.flush(stream)
+	if !testing.expectf(t, flush_err == nil,
+		"failed to Flush initial buffer: %v", write_err) {
+		return
+	}
+
+	results, _ := _test_stream(t, stream, buf[:])
+
+	log.debugf("%#v", results)
+}
+
+@test
+test_os2_file_stream :: proc(t: ^testing.T) {
+	defer if !testing.failed(t) {
+		testing.expect_value(t, os2.remove(TEMPORARY_FILENAME), nil)
+	}
+
+	buf: [32]u8
+	for i in 0..<u8(len(buf)) {
+		buf[i] = 'A' + i
+	}
+
+	TEMPORARY_FILENAME :: "test_core_io_os2_file_stream"
+
+	fd, open_err := os2.open(TEMPORARY_FILENAME, {.Read, .Write, .Create, .Trunc})
+	if !testing.expectf(t, open_err == nil, "error on opening %q: %v", TEMPORARY_FILENAME, open_err) {
+		return
+	}
+
+	stream := os2.to_stream(fd)
+
+	bytes_written, write_err := io.write(stream, buf[:])
+	if !testing.expectf(t, bytes_written == len(buf) && write_err == nil,
+		"failed to Write initial buffer: bytes_written<%v> != len_buf<%v>, %v", bytes_written, len(buf), write_err) {
+		return
+	}
+
+	flush_err := io.flush(stream)
+	if !testing.expectf(t, flush_err == nil,
+		"failed to Flush initial buffer: %v", write_err) {
+		return
+	}
+
+	// os2 file stream proc close and destroy are the same.
+	results, _ := _test_stream(t, stream, buf[:], do_destroy = false)
+
+	log.debugf("%#v", results)
+}
+
+@test
+test_bufio_buffered_writer :: proc(t: ^testing.T) {
+	// Using a strings.Builder as the backing stream.
+
+	sb := strings.builder_make()
+	defer strings.builder_destroy(&sb)
+
+	buf: [32]u8
+	expected_buf: [64]u8
+	for i in 0..<u8(len(buf)) {
+		buf[i] = 'A' + i
+		expected_buf[i] = 'A' + i
+		strings.write_byte(&sb, 'A' + i)
+	}
+	for i in 32..<u8(len(expected_buf)) {
+		expected_buf[i] = ('A' + i-len(buf)) ~ 0xAA
+	}
+
+	writer: bufio.Writer
+	bufio.writer_init(&writer, strings.to_stream(&sb))
+	defer bufio.writer_destroy(&writer)
+
+	results, _ := _test_stream(t, bufio.writer_to_stream(&writer), buf[:],
+		do_destroy = false)
+
+	testing.expectf(t, bytes.compare(sb.buf[:], expected_buf[:]) == 0, "bufio buffered string builder stream failed:\nbuilder<%q>\n!=\nbuffer <%q>", sb.buf[:], expected_buf[:])
+
+	log.debugf("%#v", results)
+}
+
+@test
+test_bufio_buffered_reader :: proc(t: ^testing.T) {
+	// Using a bytes.Reader as the backing stream.
+
+	buf: [32]u8
+	for i in 0..<u8(len(buf)) {
+		buf[i] = 'A' + i
+	}
+
+	results: Passed_Tests
+	ok: bool
+
+	for end in 0..<i64(len(buf)) {
+		br: bytes.Reader
+		bs := bytes.reader_init(&br, buf[:end])
+
+		reader: bufio.Reader
+		bufio.reader_init(&reader, bs)
+		defer bufio.reader_destroy(&reader)
+
+		results, ok = _test_stream(t, bufio.reader_to_stream(&reader), buf[:end])
+		if !ok {
+			log.debugf("buffer[:%i] := %v", end, buf[:end])
+			return
+		}
+	}
+
+	log.debugf("%#v", results)
+}
+
+@test
+test_bufio_buffered_read_writer :: proc(t: ^testing.T) {
+	// Using an os2.File as the backing stream for both reader & writer.
+
+	defer if !testing.failed(t) {
+		testing.expect_value(t, os2.remove(TEMPORARY_FILENAME), nil)
+	}
+
+	buf: [32]u8
+	for i in 0..<u8(len(buf)) {
+		buf[i] = 'A' + i
+	}
+
+	TEMPORARY_FILENAME :: "test_core_io_bufio_read_writer_os2_file_stream"
+
+	fd, open_err := os2.open(TEMPORARY_FILENAME, {.Read, .Write, .Create, .Trunc})
+	if !testing.expectf(t, open_err == nil, "error on opening %q: %v", TEMPORARY_FILENAME, open_err) {
+		return
+	}
+	defer testing.expect_value(t, os2.close(fd), nil)
+
+	stream := os2.to_stream(fd)
+
+	bytes_written, write_err := io.write(stream, buf[:])
+	if !testing.expectf(t, bytes_written == len(buf) && write_err == nil,
+		"failed to Write initial buffer: bytes_written<%v> != len_buf<%v>, %v", bytes_written, len(buf), write_err) {
+		return
+	}
+
+	flush_err := io.flush(stream)
+	if !testing.expectf(t, flush_err == nil,
+		"failed to Flush initial buffer: %v", write_err) {
+		return
+	}
+
+	// bufio.Read_Writer isn't capable of seeking, so we have to reset the os2
+	// stream back to the start here.
+	pos, seek_err := io.seek(stream, 0, .Start)
+	if !testing.expectf(t, pos == 0 && seek_err == nil,
+		"Pre-test Seek reset failed: pos<%v>, %v", pos, seek_err) {
+		return
+	}
+
+	reader: bufio.Reader
+	writer: bufio.Writer
+	read_writer: bufio.Read_Writer
+
+	bufio.reader_init(&reader, stream)
+	defer bufio.reader_destroy(&reader)
+	bufio.writer_init(&writer, stream)
+	defer bufio.writer_destroy(&writer)
+
+	bufio.read_writer_init(&read_writer, &reader, &writer)
+
+	// os2 file stream proc close and destroy are the same.
+	results, _ := _test_stream(t, bufio.read_writer_to_stream(&read_writer), buf[:], do_destroy = false)
+
+	log.debugf("%#v", results)
+}

+ 1 - 0
tests/core/normal.odin

@@ -23,6 +23,7 @@ download_assets :: proc() {
 @(require) import "encoding/xml"
 @(require) import "flags"
 @(require) import "fmt"
+@(require) import "io"
 @(require) import "math"
 @(require) import "math/big"
 @(require) import "math/linalg/glsl"