Skip to content

Commit

Permalink
os: add File.set_buffer and related helpers + tests (#20661)
Browse files Browse the repository at this point in the history
  • Loading branch information
spytheman authored Jan 26, 2024
1 parent 2a68e2b commit 7f5f92d
Show file tree
Hide file tree
Showing 2 changed files with 129 additions and 0 deletions.
42 changes: 42 additions & 0 deletions vlib/os/file_buffering.c.v
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
module os

fn C.setvbuf(stream &C.FILE, buffer &char, mode int, size usize) int

// FileBufferMode describes the available buffering modes for an os.File: unbuffered, line buffered and block/fully buffered.
// Normally all files are block buffered. If a stream refers to a terminal (as stdout normally does), it is line buffered.
// The standard error stream stderr is always unbuffered by default.
pub enum FileBufferMode {
fully_buffered = C._IOFBF // many characters are saved up and written as a block
line_buffered = C._IOLBF // characters are saved up until a newline is output or input is read from any stream attached to a terminal device (typically stdin)
not_buffered = C._IONBF // information appears on the destination file or terminal as soon as it is written
}

// setvbuf sets the buffer and buffering mode for the given file `f`. See also os.FileBufferMode.
// It returns 0 on success. It returns nonzero on failure (the mode is invalid, or the request cannot be honored).
// Except for unbuffered files, the buffer argument should point to a buffer at least size bytes long; this buffer will be used instead of the current buffer.
// If the argument buffer is nil, only the mode is affected; a new buffer will be allocated on the next read or write operation.
// Note: make sure, that the space, that buffer points to (when != 0), still exists by the time the file stream is closed, which also happens at program termination.
// Note: f.setvbuf() may be used only after opening a file stream, and before any other operations have been performed on it.
@[unsafe]
pub fn (mut f File) setvbuf(buffer &char, mode FileBufferMode, size usize) int {
return C.setvbuf(f.cfile, buffer, int(mode), size)
}

// set_buffer sets the buffer for the file, and the file buffering mode (see also os.FileBufferMode).
// Unlike File.setvbuf, it allows you to pass an existing V []u8 array directly.
// Note: f.set_buffer() may be used only after opening a file stream, and before any other operations have been performed on it.
pub fn (mut f File) set_buffer(mut buffer []u8, mode FileBufferMode) int {
return unsafe { f.setvbuf(&char(buffer.data), mode, usize(buffer.len)) }
}

// set_line_buffered sets the file buffering mode to FileBufferMode.line_buffered.
// Note: f.set_line_buffered() may be used only after opening a file stream, and before any other operations have been performed on it.
pub fn (mut f File) set_line_buffered() {
unsafe { f.setvbuf(&char(nil), .line_buffered, usize(0)) }
}

// set_unbuffered sets the file buffering mode to FileBufferMode.not_buffered.
// Note: f.set_unbuffered() may be used only after opening a file stream, and before any other operations have been performed on it.
pub fn (mut f File) set_unbuffered() {
unsafe { f.setvbuf(&char(nil), .not_buffered, usize(0)) }
}
87 changes: 87 additions & 0 deletions vlib/os/file_buffering_test.v
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import os

const tfolder = os.join_path(os.vtmp_dir(), 'tests', 'os_file_buffering_test')

fn testsuite_begin() {
os.rmdir_all(tfolder) or {}
assert !os.is_dir(tfolder)
os.mkdir_all(tfolder)!
os.chdir(tfolder)!
assert os.is_dir(tfolder)
}

fn testsuite_end() {
os.rmdir_all(tfolder) or {}
}

fn test_set_buffer_line_buffered() {
dump(@LOCATION)
mut buf := []u8{len: 25}
dump(buf)
mut wfile := os.open_file('text.txt', 'wb', 0o666)!
wfile.set_buffer(mut buf, .line_buffered)
wfile.write_string('----------------------------------\n')!
for line in ['hello\n', 'world\n', 'hi\n'] {
wfile.write_string(line)!
wfile.flush()
dump(buf)
print(buf.bytestr())
// assert buf.bytestr().contains(line) // this works on GLIBC, but fails on MUSL.
unsafe { buf.reset() }
}
wfile.close()
//
content := os.read_lines('text.txt')!
dump(content)
assert content == ['----------------------------------', 'hello', 'world', 'hi']
}

fn test_set_buffer_fully_buffered() {
dump(@LOCATION)
mut buf := []u8{len: 30}
dump(buf)
mut wfile := os.open_file('text.txt', 'wb', 0o666)!
wfile.set_buffer(mut buf, .fully_buffered)
// Ubuntu GLIBC 2.31 seems to not use the buffer for the first write call, but it does write to the buffer first for the subsequent ones.
// MUSL (detecting the MUSL version is deliberately made hard by its authors, because of course it is :-( ...), will skip the first 8 bytes
// of the buffer, and write everything after those.
wfile.write_string('S')!
wfile.write_string('---\n')!
dump(buf)
for line in ['hello\n', 'world\n', 'hi\n'] {
wfile.write_string(line)!
dump(buf)
// print(buf.bytestr())
}
wfile.close()
dump(buf)
// assert buf.bytestr().starts_with('---\nhello\nworld\nhi\n') // works on GLIBC, fails on MUSL
assert buf.bytestr().contains('---\nhello\nworld\n')
//
content := os.read_lines('text.txt')!
dump(content)
assert content == ['S---', 'hello', 'world', 'hi']
}

fn test_set_unbuffered() {
dump(@LOCATION)
mut buf := []u8{len: 30}
dump(buf)
mut wfile := os.open_file('text.txt', 'wb', 0o666)!
wfile.set_buffer(mut buf, .not_buffered)
wfile.write_string('S')!
wfile.write_string('---\n')!
dump(buf)
for line in ['hello\n', 'world\n', 'hi\n'] {
wfile.write_string(line)!
dump(buf)
// print(buf.bytestr())
}
wfile.close()
// dump(buf.bytestr())
assert buf.all(it == 0)
//
content := os.read_lines('text.txt')!
dump(content)
assert content == ['S---', 'hello', 'world', 'hi']
}

0 comments on commit 7f5f92d

Please sign in to comment.