module os // Eof error means that we reach the end of the file. pub struct Eof { Error } // NotExpected is a generic error that means that we receave a not expected error. pub struct NotExpected { cause string code int } fn (err NotExpected) msg() string { return err.cause } fn (err NotExpected) code() int { return err.code } pub struct File { mut: cfile voidptr // Using void* instead of FILE* pub: fd int pub mut: is_opened bool } fn C.fseeko(&C.FILE, u64, int) int fn C._fseeki64(&C.FILE, u64, int) int fn C.getc(&C.FILE) int fn C.freopen(&char, &char, &C.FILE) &C.FILE fn C._wfreopen(&u16, &u16, &C.FILE) &C.FILE fn fix_windows_path(path string) string { mut p := path $if windows { p = path.replace('/', '\\') } return p } // open_file tries to open or create a file with custom flags and permissions. @[noinline] pub fn open_file(path string, mode string, options ...int) !File { mut flags := 0 mut seek_to_end := false for m in mode { match m { `w` { flags |= o_create | o_trunc | o_wronly } `a` { flags |= o_create | o_append | o_wronly seek_to_end = true } `r` { flags |= o_rdonly } `b` { flags |= o_binary } `s` { flags |= o_sync } `n` { flags |= o_nonblock } `c` { flags |= o_noctty } `+` { flags &= ~o_wronly flags |= o_rdwr } else {} } } if mode == 'r+' { flags = o_rdwr } mut permission := 0o666 if options.len > 0 { permission = options[0] } $if windows { if permission < 0o600 { permission = 0x0100 } else { permission = 0x0100 | 0x0080 } } p := fix_windows_path(path) fd := $if windows { C._wopen(p.to_wide(), flags, permission) } $else { C.open(&char(p.str), flags, permission) } if fd == -1 { return error(posix_get_error_msg(C.errno)) } mut cfile := C.fdopen(fd, &char(mode.str)) if isnil(cfile) { return error('Failed to open or create file "${path}"') } mut res := File{ cfile: cfile fd: fd is_opened: true } if seek_to_end { // ensure appending will work, even on bsd/macos systems: res.seek(0, .end) or {} } return res } // open tries to open a file from a given path for reading. pub fn open(path string) !File { /* $if linux { $if !android { fd := C.syscall(sys_open, path.str, 511) if fd == -1 { return error('failed to open file "$path"') } return File{ fd: fd is_opened: true } } } */ cfile := vfopen(path, 'rb')! fd := fileno(cfile) return File{ cfile: cfile fd: fd is_opened: true } } // create creates or opens a file at a specified location and returns a write-only `File` object. pub fn create(path string) !File { /* // Note: android/termux/bionic is also a kind of linux, // but linux syscalls there sometimes fail, // while the libc version should work. $if linux { $if !android { //$if macos { // fd = C.syscall(398, path.str, 0x601, 0x1b6) //} //$if linux { fd = C.syscall(sys_creat, path.str, 511) //} if fd == -1 { return error('failed to create file "$path"') } file = File{ fd: fd is_opened: true } return file } } */ cfile := vfopen(path, 'wb')! fd := fileno(cfile) return File{ cfile: cfile fd: fd is_opened: true } } // stdin returns an os.File for stdin. pub fn stdin() File { return File{ fd: 0 cfile: voidptr(C.stdin) is_opened: true } } // stdout returns an os.File for stdout. pub fn stdout() File { return File{ fd: 1 cfile: voidptr(C.stdout) is_opened: true } } // stderr returns an os.File for stderr. pub fn stderr() File { return File{ fd: 2 cfile: voidptr(C.stderr) is_opened: true } } // eof returns true, when the end of file has been reached. pub fn (f &File) eof() bool { cfile := unsafe { &C.FILE(f.cfile) } return C.feof(cfile) != 0 } // reopen allows a `File` to be reused. It is mostly useful for reopening standard input and output. pub fn (mut f File) reopen(path string, mode string) ! { p := fix_windows_path(path) mut cfile := &C.FILE(unsafe { nil }) $if windows { cfile = C._wfreopen(p.to_wide(), mode.to_wide(), f.cfile) } $else { cfile = C.freopen(&char(p.str), &char(mode.str), f.cfile) } if isnil(cfile) { return error('Failed to reopen file "${path}"') } f.cfile = cfile } // read implements the Reader interface. pub fn (f &File) read(mut buf []u8) !int { if buf.len == 0 { return Eof{} } // the following is needed, because on FreeBSD, C.feof is a macro: nbytes := int(C.fread(buf.data, 1, buf.len, unsafe { &C.FILE(f.cfile) })) // if no bytes were read, check for errors and end-of-file. if nbytes <= 0 { if C.feof(unsafe { &C.FILE(f.cfile) }) != 0 { return Eof{} } if C.ferror(unsafe { &C.FILE(f.cfile) }) != 0 { return NotExpected{ cause: 'unexpected error from fread' code: -1 } } } return nbytes } // **************************** Write ops *************************** // write implements the Writer interface. // It returns how many bytes were actually written. pub fn (mut f File) write(buf []u8) !int { if !f.is_opened { return error_file_not_opened() } /* $if linux { $if !android { res := C.syscall(sys_write, f.fd, s.str, s.len) return res } } */ written := int(C.fwrite(buf.data, 1, buf.len, f.cfile)) if written == 0 && buf.len != 0 { return error('0 bytes written') } return written } // writeln writes the string `s` into the file, and appends a \n character. // It returns how many bytes were written, including the \n character. pub fn (mut f File) writeln(s string) !int { if !f.is_opened { return error_file_not_opened() } written := f.write_string(s)! x := C.fputs(c'\n', f.cfile) if x < 0 { return error('could not add newline') } return written + 1 } // write_string writes the string `s` into the file. // It returns how many bytes were actually written. pub fn (mut f File) write_string(s string) !int { unsafe { f.write_full_buffer(s.str, usize(s.len))! } return s.len } // write_to implements the RandomWriter interface. // It returns how many bytes were actually written. // It resets the seek position to the end of the file. pub fn (mut f File) write_to(pos u64, buf []u8) !int { if !f.is_opened { return error_file_not_opened() } f.seek(pos, .start) or {} res := int(C.fwrite(buf.data, 1, buf.len, f.cfile)) if res == 0 && buf.len != 0 { return error('0 bytes written') } f.seek(0, .end) or {} return res } // write_ptr writes `size` bytes to the file, starting from the address in `data`. // Note: write_ptr is unsafe and should be used carefully, since if you pass invalid // pointers to it, it will cause your programs to segfault. @[unsafe] pub fn (mut f File) write_ptr(data voidptr, size int) int { return int(C.fwrite(data, 1, size, f.cfile)) } // write_full_buffer writes a whole buffer of data to the file, starting from the // address in `buffer`, no matter how many tries/partial writes it would take. // The size in bytes, of the `buffer`, should be passed in `buffer_len`. @[unsafe] pub fn (mut f File) write_full_buffer(buffer voidptr, buffer_len usize) ! { if buffer_len <= usize(0) { return } if !f.is_opened { return error_file_not_opened() } mut ptr := &u8(buffer) mut remaining_bytes := i64(buffer_len) for remaining_bytes > 0 { unsafe { C.errno = 0 x := i64(C.fwrite(ptr, 1, remaining_bytes, f.cfile)) cerror := int(C.errno) ptr += x remaining_bytes -= x if cerror != 0 { if cerror == C.EINTR { continue } if i64(x) != i64(buffer_len) { return error(posix_get_error_msg(cerror)) } } if x <= 0 { return error('C.fwrite returned 0') } } } } // write_ptr_at writes `size` bytes to the file, starting from the address in `data`, // at byte offset `pos`, counting from the start of the file (pos 0). // Note: write_ptr_at is unsafe and should be used carefully, since if you pass invalid // pointers to it, it will cause your programs to segfault. @[unsafe] pub fn (mut f File) write_ptr_at(data voidptr, size int, pos u64) int { f.seek(pos, .start) or {} res := int(C.fwrite(data, 1, size, f.cfile)) f.seek(0, .end) or {} return res } // **************************** Read ops *************************** // fread wraps C.fread and handles error and end-of-file detection. fn fread(ptr voidptr, item_size int, items int, stream &C.FILE) !int { nbytes := int(C.fread(ptr, item_size, items, stream)) // If no bytes were read, check for errors and end-of-file. if nbytes <= 0 { // If fread encountered end-of-file return the none error. Note that fread // may read data and encounter the end-of-file, but we shouldn't return none // in that case which is why we only check for end-of-file if no data was // read. The caller will get none on their next call because there will be // no data available and the end-of-file will be encountered again. if C.feof(stream) != 0 { return Eof{} } // If fread encountered an error, return it. Note that fread and ferror do // not tell us what the error was, so we can't return anything more specific // than there was an error. This is because fread and ferror do not set // errno. if C.ferror(stream) != 0 { return error('file read error') } } return nbytes } // read_bytes reads `size` bytes from the beginning of the file. // Utility method, same as .read_bytes_at(size, 0). pub fn (f &File) read_bytes(size int) []u8 { return f.read_bytes_at(size, 0) } // read_bytes_at reads `size` bytes at the given position in the file. pub fn (f &File) read_bytes_at(size int, pos u64) []u8 { mut arr := []u8{len: size} nreadbytes := f.read_bytes_into(pos, mut arr) or { // return err return [] } return arr[0..nreadbytes] } // read_bytes_with_newline reads from the current position of the file into the provided buffer. // Each consecutive call on the same file, continues reading, from where it previously ended. // A read call is either stopped, if the buffer is full, a newline was read or EOF. // On EOF, the method returns 0. The methods will also return any IO error encountered. pub fn (f &File) read_bytes_with_newline(mut buf []u8) !int { if buf.len == 0 { return error(@FN + ': `buf.len` == 0') } newline := 10 mut c := 0 mut buf_ptr := 0 mut nbytes := 0 stream := unsafe { &C.FILE(f.cfile) } for (buf_ptr < buf.len) { c = C.getc(stream) match c { C.EOF { if C.feof(stream) != 0 { return nbytes } if C.ferror(stream) != 0 { return error('file read error') } } newline { buf[buf_ptr] = u8(c) nbytes++ return nbytes } else { buf[buf_ptr] = u8(c) buf_ptr++ nbytes++ } } } return nbytes } // read_bytes_into fills `buf` with bytes at the given position in the file. // `buf` *must* have length greater than zero. // Returns the number of read bytes, or an error. pub fn (f &File) read_bytes_into(pos u64, mut buf []u8) !int { if buf.len == 0 { return error(@FN + ': `buf.len` == 0') } // Note: fseek errors if pos == os.file_size, which we accept unsafe { f.seek(pos, .start) or {} } nbytes := fread(buf.data, 1, buf.len, f.cfile)! return nbytes } // read_from implements the RandomReader interface. pub fn (f &File) read_from(pos u64, mut buf []u8) !int { if buf.len == 0 { return 0 } unsafe { f.seek(pos, .start) or {} } nbytes := fread(buf.data, 1, buf.len, f.cfile)! return nbytes } // read_into_ptr reads at most `max_size` bytes from the file and writes it into ptr. // Returns the amount of bytes read or an error. pub fn (f &File) read_into_ptr(ptr &u8, max_size int) !int { return fread(ptr, 1, max_size, f.cfile) } // **************************** Utility ops *********************** // flush writes any buffered unwritten data left in the file stream. pub fn (mut f File) flush() { if !f.is_opened { return } C.fflush(f.cfile) } pub struct FileNotOpenedError { Error } pub fn (err FileNotOpenedError) msg() string { return 'os: file not opened' } pub struct SizeOfTypeIs0Error { Error } pub fn (err SizeOfTypeIs0Error) msg() string { return 'os: size of type is 0' } fn error_file_not_opened() IError { return &FileNotOpenedError{} } fn error_size_of_type_0() IError { return &SizeOfTypeIs0Error{} } // read_struct reads a single struct of type `T`. pub fn (mut f File) read_struct[T](mut t T) ! { if !f.is_opened { return error_file_not_opened() } tsize := int(sizeof(*t)) if tsize == 0 { return error_size_of_type_0() } nbytes := fread(t, 1, tsize, f.cfile)! if nbytes != tsize { return error_with_code('incomplete struct read', nbytes) } } // read_struct_at reads a single struct of type `T` at position specified in file. pub fn (mut f File) read_struct_at[T](mut t T, pos u64) ! { if !f.is_opened { return error_file_not_opened() } tsize := int(sizeof(*t)) if tsize == 0 { return error_size_of_type_0() } f.seek(pos, .start) or {} nbytes := fread(t, 1, tsize, f.cfile)! f.seek(0, .end) or {} if nbytes != tsize { return error_with_code('incomplete struct read', nbytes) } } // read_raw reads and returns a single instance of type `T`. pub fn (mut f File) read_raw[T]() !T { if !f.is_opened { return error_file_not_opened() } tsize := int(sizeof(T)) if tsize == 0 { return error_size_of_type_0() } mut t := T{} nbytes := fread(&t, 1, tsize, f.cfile)! if nbytes != tsize { return error_with_code('incomplete struct read', nbytes) } return t } // read_raw_at reads and returns a single instance of type `T` starting at file byte offset `pos`. pub fn (mut f File) read_raw_at[T](pos u64) !T { if !f.is_opened { return error_file_not_opened() } tsize := int(sizeof(T)) if tsize == 0 { return error_size_of_type_0() } mut t := T{} f.seek(pos, .start)! nbytes := fread(&t, 1, tsize, f.cfile)! f.seek(0, .end)! if nbytes != tsize { return error_with_code('incomplete struct read', nbytes) } return t } // write_struct writes a single struct of type `T`. pub fn (mut f File) write_struct[T](t &T) ! { if !f.is_opened { return error_file_not_opened() } tsize := int(sizeof(T)) if tsize == 0 { return error_size_of_type_0() } C.errno = 0 nbytes := int(C.fwrite(t, 1, tsize, f.cfile)) if C.errno != 0 { return error(posix_get_error_msg(C.errno)) } if nbytes != tsize { return error_with_code('incomplete struct write', nbytes) } } // write_struct_at writes a single struct of type `T` at file byte offset `pos`. pub fn (mut f File) write_struct_at[T](t &T, pos u64) ! { if !f.is_opened { return error_file_not_opened() } tsize := usize(sizeof(T)) if tsize == 0 { return error_size_of_type_0() } f.seek(pos, .start) or {} unsafe { f.write_full_buffer(t, tsize)! } f.seek(0, .end) or {} } // TODO: `write_raw[_at]` implementations are copy-pasted from `write_struct[_at]` // write_raw writes a single instance of type `T`. pub fn (mut f File) write_raw[T](t &T) ! { if !f.is_opened { return error_file_not_opened() } tsize := usize(sizeof(T)) if tsize == 0 { return error_size_of_type_0() } unsafe { f.write_full_buffer(t, tsize)! } } // write_raw_at writes a single instance of type `T` starting at file byte offset `pos`. pub fn (mut f File) write_raw_at[T](t &T, pos u64) ! { if !f.is_opened { return error_file_not_opened() } tsize := usize(sizeof(T)) if tsize == 0 { return error_size_of_type_0() } f.seek(pos, .start)! unsafe { f.write_full_buffer(t, tsize)! } f.seek(0, .end)! } pub enum SeekMode { start current end } // seek moves the file cursor (if any) associated with a file to a new location, offset `pos` bytes from the origin. // The origin is dependent on the `mode` and can be: // .start -> the origin is the start of the file // .current -> the current position/cursor in the file // .end -> the end of the file // If the file is not seek-able, or an error occurs, the error will // be returned to the caller. // A successful call to the fseek() function clears the end-of-file // indicator for the file. pub fn (mut f File) seek(pos i64, mode SeekMode) ! { if !f.is_opened { return error_file_not_opened() } whence := int(mode) mut res := 0 $if x64 { $if windows { res = C._fseeki64(f.cfile, pos, whence) } $else { res = C.fseeko(f.cfile, pos, whence) } } $if x32 { res = C.fseek(f.cfile, pos, whence) } if res == -1 { return error(posix_get_error_msg(C.errno)) } } // tell will return the current offset of the file cursor measured from the start of the file, in bytes. // It is complementary to seek, i.e. you can use the return value as the `pos` parameter to .seek( pos, .start ), // so that your next read will happen from the same place. pub fn (f &File) tell() !i64 { if !f.is_opened { return error_file_not_opened() } mut pos := i64(0) mut ret := 0 $if windows { ret = C.fgetpos(f.cfile, &pos) } $else { pos = i64(C.ftell(f.cfile)) } if ret == -1 || pos == -1 { return error(posix_get_error_msg(C.errno)) } return pos }