diff --git a/vlib/io/os_file_reader_test.v b/vlib/io/os_file_reader_test.v index 39606c8a03..409845f9f4 100644 --- a/vlib/io/os_file_reader_test.v +++ b/vlib/io/os_file_reader_test.v @@ -20,7 +20,7 @@ fn read_file(file string, cap int) []string { } fn test_file_reader() { - for cap := 64; cap <= 10000; cap += 256 { + for cap := 1; cap <= 10000; cap += 256 { lines := read_file(@FILE, cap) assert lines.last() == '// my last line' } diff --git a/vlib/os/file.c.v b/vlib/os/file.c.v index 5e008a3a24..2f88ca0c22 100644 --- a/vlib/os/file.c.v +++ b/vlib/os/file.c.v @@ -13,8 +13,132 @@ struct FileInfo { size int } +// open_file can be used to open or create a file with custom flags and permissions and returns a `File` object. +pub fn open_file(path string, mode string, options ...int) ?File { + mut flags := 0 + for m in mode { + match m { + `w` { flags |= o_create | o_trunc } + `a` { flags |= o_create | o_append } + `r` { flags |= o_rdonly } + `b` { flags |= o_binary } + `s` { flags |= o_sync } + `n` { flags |= o_nonblock } + `c` { flags |= o_noctty } + `+` { flags |= o_rdwr } + else {} + } + } + if mode == 'r+' { + flags = o_rdwr + } + if mode == 'w' { + flags = o_wronly | o_create | o_trunc + } + if mode == 'a' { + flags = o_wronly | o_create | o_append + } + mut permission := 0o666 + if options.len > 0 { + permission = options[0] + } + $if windows { + if permission < 0o600 { + permission = 0x0100 + } else { + permission = 0x0100 | 0x0080 + } + } + mut p := path + $if windows { + p = path.replace('/', '\\') + } + fd := C.open(charptr(p.str), flags, permission) + if fd == -1 { + return error(posix_get_error_msg(C.errno)) + } + cfile := C.fdopen(fd, charptr(mode.str)) + if isnil(cfile) { + return error('Failed to open or create file "$path"') + } + return File{ + cfile: cfile + fd: fd + is_opened: true + } +} + +// open tries to open a file for reading and returns back a read-only `File` object. +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 { + /* + // NB: 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 + } +} + +// open_stdin - return an os.File for stdin, so that you can use .get_line on it too. +pub fn open_stdin() File { + return File{ + fd: 0 + cfile: C.stdin + is_opened: true + } +} + // **************************** Write ops *************************** -// write implements the Writer interface +// write implements the Writer interface. +// It returns how many bytes were actually written. pub fn (mut f File) write(buf []byte) ?int { if !f.is_opened { return error('file is not opened') @@ -22,8 +146,8 @@ pub fn (mut f File) write(buf []byte) ?int { /* $if linux { $if !android { - C.syscall(sys_write, f.fd, s.str, s.len) - return + res := C.syscall(sys_write, f.fd, s.str, s.len) + return res } } */ @@ -34,6 +158,8 @@ pub fn (mut f File) write(buf []byte) ?int { 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 is not opened') @@ -59,6 +185,8 @@ pub fn (mut f File) writeln(s string) ?int { 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 { if !f.is_opened { return error('file is not opened') @@ -71,19 +199,34 @@ pub fn (mut f File) write_string(s string) ?int { return written } -// write_to implements the RandomWriter interface +// 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 int, buf []byte) ?int { + if !f.is_opened { + return error('file is not opened') + } C.fseek(f.cfile, pos, C.SEEK_SET) res := int(C.fwrite(buf.data, 1, buf.len, f.cfile)) + if res == 0 && buf.len != 0 { + return error('0 bytes written') + } C.fseek(f.cfile, 0, C.SEEK_END) return res } +// write_bytes writes `size` bytes to the file, starting from the address in `data`. +// NB: write_bytes 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_bytes(data voidptr, size int) int { return int(C.fwrite(data, 1, size, f.cfile)) } +// write_bytes_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). +// NB: write_bytes_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_bytes_at(data voidptr, size int, pos int) int { C.fseek(f.cfile, pos, C.SEEK_SET) @@ -93,12 +236,13 @@ pub fn (mut f File) write_bytes_at(data voidptr, size int, pos int) int { } // **************************** Read ops *************************** -// read_bytes reads bytes from the beginning of the file +// read_bytes reads bytes from the beginning of the file. +// Utility method, same as .read_bytes_at(size, 0). pub fn (f &File) read_bytes(size int) []byte { return f.read_bytes_at(size, 0) } -// read_bytes_at reads bytes at the given position in the file +// read_bytes_at reads `size` bytes at the given position in the file. pub fn (f &File) read_bytes_at(size int, pos int) []byte { mut arr := []byte{len: size} nreadbytes := f.read_bytes_into(pos, mut arr) or { @@ -109,8 +253,8 @@ pub fn (f &File) read_bytes_at(size int, pos int) []byte { } // read_bytes_into fills `buf` with bytes at the given position in the file. -// `buf` must have length greater than zero. -// Returns number of bytes read or an error. +// `buf` *must* have length greater than zero. +// Returns the number of read bytes, or an error. pub fn (f &File) read_bytes_into(pos int, mut buf []byte) ?int { if buf.len == 0 { panic(@FN + ': `buf.len` == 0') @@ -129,7 +273,7 @@ pub fn (f &File) read_bytes_into(pos int, mut buf []byte) ?int { return nbytes } -// read implements the Reader interface +// read implements the Reader interface. pub fn (f &File) read(mut buf []byte) ?int { if buf.len == 0 { return 0 @@ -142,7 +286,7 @@ pub fn (f &File) read(mut buf []byte) ?int { return nbytes } -// read_at reads buf.len bytes from pos in the file +// read_at reads `buf.len` bytes starting at file byte offset `pos`, in `buf`. pub fn (f &File) read_at(pos int, mut buf []byte) ?int { if buf.len == 0 { return 0 @@ -157,7 +301,7 @@ pub fn (f &File) read_at(pos int, mut buf []byte) ?int { } // **************************** Utility ops *********************** -// flush writes any unwritten data in stream's buffer +// flush writes any buffered unwritten data left in the file stream. pub fn (mut f File) flush() { if !f.is_opened { return @@ -165,18 +309,49 @@ pub fn (mut f File) flush() { C.fflush(f.cfile) } -// open_stdin - return an os.File for stdin, so that you can use .get_line on it too. -pub fn open_stdin() File { - return File{ - fd: 0 - cfile: C.stdin - is_opened: true - } -} - +// write_str writes the bytes of a string into a file, +// *including* the terminating 0 byte. pub fn (mut f File) write_str(s string) ? { if !f.is_opened { return error('file is closed') } f.write(s.bytes()) ? } + +// read_struct reads a single struct of type `T` +pub fn (mut f File) read_struct(mut t T) ? { + if !f.is_opened { + return none + } + tsize := int(sizeof(*t)) + if tsize == 0 { + return none + } + C.errno = 0 + nbytes := int(C.fread(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 read', nbytes) + } +} + +// write_struct writes a single struct of type `T` +pub fn (mut f File) write_struct(t &T) ? { + if !f.is_opened { + return error('file is not opened') + } + tsize := int(sizeof(*t)) + if tsize == 0 { + return error('struct size is 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) + } +} diff --git a/vlib/os/file_test.v b/vlib/os/file_test.v new file mode 100644 index 0000000000..47ec3b79c7 --- /dev/null +++ b/vlib/os/file_test.v @@ -0,0 +1,56 @@ +import os + +struct Point { + x f64 + y f64 + z f64 +} + +const unit_point = Point{1.0, 1.0, 1.0} + +const tfolder = os.join_path(os.temp_dir(), 'os_file_test') + +const tfile = os.join_path(tfolder, 'test_file') + +const another_point = Point{0.25, 2.25, 6.25} + +fn testsuite_begin() { + os.rmdir_all(tfolder) or { } + assert !os.is_dir(tfolder) + os.mkdir_all(tfolder) or { panic(err) } + os.chdir(tfolder) + assert os.is_dir(tfolder) +} + +fn testsuite_end() { + os.chdir(os.wd_at_startup) + os.rmdir_all(tfolder) or { panic(err) } + assert !os.is_dir(tfolder) +} + +fn test_write_struct() { + size_of_point := int(sizeof(Point)) + mut f := os.open_file(tfile, 'w') or { panic(err) } + f.write_struct(another_point) or { panic(err) } + f.close() + x := os.read_file(tfile) or { panic(err) } + y := unsafe { byteptr(memdup(&another_point, size_of_point)).vstring_with_len(size_of_point) } + assert x == y + $if debug { + eprintln(x.bytes()) + eprintln(y.bytes()) + } +} + +fn test_read_struct() { + mut f := os.open_file(tfile, 'w') or { panic(err) } + f.write_struct(another_point) or { panic(err) } + f.close() + + f = os.open_file(tfile, 'r') or { panic(err) } + mut p := Point{} + f.read_struct(mut p) or { panic(err) } + f.close() + + assert p == another_point +} diff --git a/vlib/os/os_c.v b/vlib/os/os_c.v index e48721c41d..8ed3e98dac 100644 --- a/vlib/os/os_c.v +++ b/vlib/os/os_c.v @@ -214,74 +214,6 @@ pub fn fileno(cfile voidptr) int { } } -// open_append opens `path` file for appending. -pub fn open_append(path string) ?File { - mut file := File{} - $if windows { - wpath := path.replace('/', '\\').to_wide() - mode := 'ab' - file = File{ - cfile: C._wfopen(wpath, mode.to_wide()) - } - } $else { - cpath := path.str - file = File{ - cfile: C.fopen(charptr(cpath), 'ab') - } - } - if isnil(file.cfile) { - return error('failed to create(append) file "$path"') - } - file.is_opened = true - return file -} - -// open_file can be used to open or create a file with custom flags and permissions and returns a `File` object. -pub fn open_file(path string, mode string, options ...int) ?File { - mut flags := 0 - for m in mode { - match m { - `r` { flags |= o_rdonly } - `w` { flags |= o_create | o_trunc } - `b` { flags |= o_binary } - `a` { flags |= o_create | o_append } - `s` { flags |= o_sync } - `n` { flags |= o_nonblock } - `c` { flags |= o_noctty } - `+` { flags |= o_rdwr } - else {} - } - } - mut permission := 0o666 - if options.len > 0 { - permission = options[0] - } - $if windows { - if permission < 0o600 { - permission = 0x0100 - } else { - permission = 0x0100 | 0x0080 - } - } - mut p := path - $if windows { - p = path.replace('/', '\\') - } - fd := C.open(charptr(p.str), flags, permission) - if fd == -1 { - return error(posix_get_error_msg(C.errno)) - } - cfile := C.fdopen(fd, charptr(mode.str)) - if isnil(cfile) { - return error('Failed to open or create file "$path"') - } - return File{ - cfile: cfile - fd: fd - is_opened: true - } -} - // vpopen system starts the specified command, waits for it to complete, and returns its code. fn vpopen(path string) voidptr { // *C.FILE { @@ -850,63 +782,26 @@ pub fn chmod(path string, mode int) { C.chmod(charptr(path.str), mode) } -// open tries to open a file for reading and returns back a read-only `File` object. -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 - } +// open_append opens `path` file for appending. +pub fn open_append(path string) ?File { + mut file := File{} + $if windows { + wpath := path.replace('/', '\\').to_wide() + mode := 'ab' + file = File{ + cfile: C._wfopen(wpath, mode.to_wide()) + } + } $else { + cpath := path.str + file = File{ + cfile: C.fopen(charptr(cpath), 'ab') } } - */ - 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 { - /* - // NB: 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 + if isnil(file.cfile) { + return error('failed to create(append) file "$path"') } + file.is_opened = true + return file } // execvp - loads and executes a new child process, *in place* of the current process.