Initial project import

This commit is contained in:
Agila Forge Importer committed 2026-07-13 15:20:32 +00:00
commit 23f2d29e32
11957 files changed
+1754577

No files matched your search

+3
View File
@@ -0,0 +1,3 @@
## Description
`io` provides common interfaces for buffered reading/writing of data.
+168
View File
@@ -0,0 +1,168 @@
module io
// BufferedReader provides a buffered interface for a reader.
pub struct BufferedReader {
mut:
reader Reader
buf []u8
offset int // current offset in the buffer
len int
fails int // how many times fill_buffer has read 0 bytes in a row
mfails int // maximum fails, after which we can assume that the stream has ended
pub mut:
end_of_stream bool // whether we reached the end of the upstream reader
total_read int // total number of bytes read
}
// BufferedReaderConfig are options that can be given to a buffered reader.
pub struct BufferedReaderConfig {
pub:
reader Reader
cap int = 128 * 1024 // large for fast reading of big(ish) files
retries int = 2 // how many times to retry before assuming the stream ended
}
// BufferedReadLineConfig are options that can be given to the read_line() function.
@[params]
pub struct BufferedReadLineConfig {
pub:
delim u8 = `\n` // line delimiter; default `\n` mode strips both `\n` and `\r\n`
}
// new_buffered_reader creates a new BufferedReader.
pub fn new_buffered_reader(o BufferedReaderConfig) &BufferedReader {
if o.cap <= 0 {
panic('new_buffered_reader should be called with a positive `cap`')
}
// create
r := &BufferedReader{
reader: o.reader
buf: []u8{len: o.cap, cap: o.cap}
offset: 0
mfails: o.retries
}
return r
}
// read fufills the Reader interface.
pub fn (mut r BufferedReader) read(mut buf []u8) !int {
if buf.len == 0 {
return 0
}
if r.end_of_stream {
return Eof{}
}
// read data out of the buffer if we dont have any
if r.needs_fill() {
if !r.fill_buffer() {
// end of stream
return Eof{}
}
}
read := copy(mut buf, r.buf[r.offset..r.len])
if read == 0 {
return NotExpected{
cause: 'invalid copy of buffer'
code: -1
}
}
r.offset += read
r.total_read += read
return read
}
// free deallocates the memory for a buffered reader's internal buffer.
pub fn (mut r BufferedReader) free() {
unsafe {
r.buf.free()
}
}
// fill_buffer attempts to refill the internal buffer
// and returns whether it got any data.
fn (mut r BufferedReader) fill_buffer() bool {
if r.end_of_stream {
// we know we have already reached the end of stream
// so return early
return false
}
for {
r.offset = 0
r.len = 0
r.len = r.reader.read(mut r.buf) or {
// end of stream was reached
r.end_of_stream = true
return false
}
if r.len > 0 {
r.fails = 0
return true
}
r.fails++
if r.fails >= r.mfails {
// When reading 0 bytes several times in a row, assume the stream has ended.
// This prevents infinite loops ¯\_(ツ)_/¯ ...
r.end_of_stream = true
return false
}
}
return false
}
// needs_fill returns whether the buffer needs refilling.
fn (r BufferedReader) needs_fill() bool {
return r.offset >= r.len
}
// end_of_stream returns whether the end of the stream was reached.
pub fn (r BufferedReader) end_of_stream() bool {
return r.end_of_stream
}
// read_line attempts to read a line from the buffered reader.
// It reads until it finds the specified delimiter or the end of stream.
// The returned string does not include the delimiter.
// With the default delimiter `\n`, both `\n` and `\r\n` line endings are
// accepted, and neither terminator byte is included in the returned string.
pub fn (mut r BufferedReader) read_line(config BufferedReadLineConfig) !string {
if r.end_of_stream {
return Eof{}
}
mut line := []u8{}
for {
if r.needs_fill() {
// go fetch some new data
if !r.fill_buffer() {
// We are at the end of the stream
if line.len == 0 {
// we had nothing so return nothing
return Eof{}
}
return line.bytestr()
}
}
// try and find a newline character
mut i := r.offset
for ; i < r.len; i++ {
r.total_read++
c := r.buf[i]
if c == config.delim {
// great, we hit something
mut end := i
if config.delim == `\n` {
if i > r.offset && r.buf[i - 1] == `\r` {
end--
} else if i == r.offset && line.len > 0 && line[line.len - 1] == `\r` {
line.delete_last()
}
}
line << r.buf[r.offset..end]
r.offset = i + 1
return line.bytestr()
}
}
line << r.buf[r.offset..i]
r.offset = i
}
return Eof{}
}
+105
View File
@@ -0,0 +1,105 @@
module io
pub struct BufferedWriter {
mut:
n int
wr Writer
pub mut:
buf []u8
}
@[params]
pub struct BufferedWriterConfig {
pub:
writer Writer
cap int = 128 * 1024
}
// new_buffered_writer creates a new BufferedWriter with the specified BufferedWriterConfig.
// Returns an error when cap is 0 or negative.
pub fn new_buffered_writer(o BufferedWriterConfig) !&BufferedWriter {
if o.cap < 1 {
return error('`o.cap` must be a positive integer')
}
return &BufferedWriter{
buf: []u8{len: o.cap}
wr: o.writer
}
}
// reset resets the buffer to its initial state.
pub fn (mut b BufferedWriter) reset() {
cap := b.buf.len
b.buf = []u8{len: cap}
b.n = 0
}
// buffered returns the number of bytes currently stored in the buffer.
pub fn (b BufferedWriter) buffered() int {
return b.n
}
// flush writes the buffered data to the underlying writer and clears the buffer, ensures all data is
// written.
// Returns an error if the writer fails to write all buffered data.
pub fn (mut b BufferedWriter) flush() ! {
if b.buffered() == 0 {
return
}
mut written := 0
for written < b.n {
n := b.wr.write(b.buf[written..b.n]) or {
b.shift_unwritten_to_front(written)
return err
}
if n <= 0 || n > b.n - written {
b.shift_unwritten_to_front(written)
return error('writer returned an invalid number of bytes while flushing')
}
written += n
}
b.n = 0
}
// available returns the amount of available space left in the buffer.
pub fn (b BufferedWriter) available() int {
return b.buf.len - b.n
}
fn (mut b BufferedWriter) shift_unwritten_to_front(written int) {
if written <= 0 {
return
}
remaining := b.n - written
if remaining <= 0 {
b.n = 0
return
}
copy(mut b.buf[..remaining], b.buf[written..b.n])
b.n = remaining
}
// write writes `src` in the buffer, flushing it to the underlying writer as needed, and returns the
// number of bytes written.
pub fn (mut b BufferedWriter) write(src []u8) !int {
mut written := 0
for written < src.len {
remaining := src.len - written
if b.buffered() == 0 && remaining > b.available() {
n := b.wr.write(src[written..])!
if n <= 0 || n > remaining {
return error('writer returned an invalid number of bytes while writing')
}
written += n
continue
}
n := copy(mut b.buf[b.n..], src[written..])
b.n += n
written += n
if b.available() == 0 {
b.flush()!
}
}
return written
}
+232
View File
@@ -0,0 +1,232 @@
import io
import rand
fn small_amount() int {
return int(rand.u8()) + 1
}
struct ArrayWriter {
pub mut:
result []u8
}
// implement io.Writer
fn (mut aw ArrayWriter) write(buf []u8) !int {
len := buf.len
mut res := 0
for i := 0; i < len; i++ {
aw.result << buf[i]
res++
}
return res
}
struct ChunkedArrayWriter {
chunk int
pub mut:
result []u8
}
fn (mut cw ChunkedArrayWriter) write(buf []u8) !int {
if buf.len == 0 {
return 0
}
n := if buf.len < cw.chunk { buf.len } else { cw.chunk }
for i := 0; i < n; i++ {
cw.result << buf[i]
}
return n
}
struct FailOnceChunkedWriter {
chunk int
pub mut:
result []u8
mut:
writes int
}
fn (mut fw FailOnceChunkedWriter) write(buf []u8) !int {
if fw.writes == 1 {
fw.writes++
return error('forced write failure')
}
fw.writes++
n := if buf.len < fw.chunk { buf.len } else { fw.chunk }
for i := 0; i < n; i++ {
fw.result << buf[i]
}
return n
}
struct ZeroProgressWriter {}
fn (mut w ZeroProgressWriter) write(buf []u8) !int {
return 0
}
// write less than max bytes, returns number of bytes written
// data is written in chunks.
fn write_random_data(mut aw ArrayWriter, mut bw io.BufferedWriter, max int) !int {
less_than_max := max - 255 // guarantee 1 full u8 less than max
mut total := 0
for total < less_than_max {
r := rand.u8()
d := rand.bytes(r)!
w := bw.write(d)!
total += w
}
return total
}
fn test_flush() {
mut aw := ArrayWriter{}
max := 65536
mut bw := io.new_buffered_writer(writer: &aw, cap: max)!
// write less data than buffer capacity, the underlying writer should receive no data.
written := write_random_data(mut aw, mut bw, 65536)!
assert written == bw.buffered()
assert aw.result.len == 0
bw.flush()!
assert aw.result.len == written
assert bw.buffered() == 0
assert bw.available() == max
}
fn test_write() {
mut aw := ArrayWriter{}
max := 65536
mut bw := io.new_buffered_writer(writer: &aw, cap: max)!
// write less data than buffer capacity, the underlying writer should receive no data.
written := write_random_data(mut aw, mut bw, 65536)!
// now exceed buffer capacity by a little
little := small_amount()
excess := bw.available() + little
excess_data := rand.bytes(excess)!
w := bw.write(excess_data)!
assert bw.buffered() == little
assert bw.available() == max - little
assert aw.result.len == max
}
fn test_write_big() {
mut aw := ArrayWriter{}
max := 65536
mut bw := io.new_buffered_writer(writer: &aw, cap: max)!
more_than_max := max + small_amount()
big_source := rand.bytes(more_than_max)!
w := bw.write(big_source)!
assert w == more_than_max
assert bw.buffered() == 0
assert bw.available() == max
assert aw.result.len == more_than_max
}
// create_data returns an array with `n` elements plus `\n` as last character.
fn create_data(n int) []u8 {
mut res := []u8{}
for i := 0; i < n; i++ {
res << `X`
}
res << `\n`
return res
}
fn test_simple_write() {
mut aw := ArrayWriter{}
mut bw := io.new_buffered_writer(writer: &aw, cap: 10)!
mut data := create_data(6)
w1 := bw.write(data)!
// add data to buffer, less than cap
// data is written to buffer, not to underlying writer.
assert w1 == 7 // 6*x + \n
assert bw.buffered() == 7
assert aw.result.len == 0
// add more data, exceed cap
// data is flushed to underlying writer when buffer is full, then more data is written to buffer.
w2 := bw.write(data)!
assert w2 == 7
assert bw.buffered() == 4
assert aw.result.len == 10
// exceed cap immediately
// all data is written without buffering
aw.result = []u8{}
data = create_data(33)
bw.reset()
w3 := bw.write(data)!
assert bw.buffered() == 0
assert aw.result.len == 34 // 33*x + \n
}
fn test_simple_flush() {
mut aw := ArrayWriter{}
mut bw := io.new_buffered_writer(writer: &aw, cap: 10)!
data := create_data(6)
w := bw.write(data)!
assert w == 7 // 6*x + \n
assert bw.buffered() == 7
bw.flush()!
assert bw.buffered() == 0
assert aw.result.len == 7
}
fn test_flush_handles_partial_writes() {
mut cw := ChunkedArrayWriter{
chunk: 2
}
mut bw := io.new_buffered_writer(writer: &cw, cap: 8)!
data := 'abcdefg'.bytes()
written := bw.write(data)!
assert written == data.len
assert bw.buffered() == data.len
assert cw.result.len == 0
bw.flush()!
assert bw.buffered() == 0
assert cw.result == data
}
fn test_flush_preserves_unwritten_data_on_error() {
mut fw := FailOnceChunkedWriter{
chunk: 3
}
mut bw := io.new_buffered_writer(writer: &fw, cap: 8)!
data := 'abcdefg'.bytes()
_ = bw.write(data)!
if _ := bw.flush() {
assert false
} else {
assert err.msg() == 'forced write failure'
}
assert fw.result == 'abc'.bytes()
assert bw.buffered() == 4
assert bw.buf[..bw.buffered()] == 'defg'.bytes()
bw.flush()!
assert bw.buffered() == 0
assert fw.result == data
}
fn test_write_returns_error_for_zero_progress_writes() {
mut zw := ZeroProgressWriter{}
mut bw := io.new_buffered_writer(writer: &zw, cap: 4)!
if _ := bw.write('12345'.bytes()) {
assert false
} else {
assert err.msg() == 'writer returned an invalid number of bytes while writing'
}
assert bw.buffered() == 0
}
@@ -0,0 +1,77 @@
import io
struct StringReader {
text string
mut:
place int
}
fn imin(a int, b int) int {
return if a < b { a } else { b }
}
fn (mut s StringReader) read(mut buf []u8) !int {
$if debug {
eprintln('>>>> StringReader.read output buf.len: ${buf.len}')
}
if s.place > s.text.len + 1 {
return io.Eof{}
}
mut howmany := imin(buf.len, s.text.len - s.place)
xxx := s.text[s.place..s.place + howmany].bytes()
read := copy(mut buf, xxx)
s.place += read
return read
}
fn read_from_string(text string, capacity int) []u8 {
mut str := StringReader{
text: text
}
mut stream := io.new_buffered_reader(reader: str, cap: capacity)
mut buf := []u8{len: 1}
mut res := []u8{}
mut i := 0
for {
z := stream.read(mut buf) or { break }
res << buf
$if debug {
println('capacity: ${capacity}, i: ${i}, buf: ${buf} | z: ${z}')
}
i++
}
return res
}
pub fn test_reading_from_a_string() {
for capacity in 1 .. 1000 {
assert read_from_string('a', capacity) == [u8(`a`)]
assert read_from_string('ab', capacity) == [u8(`a`), `b`]
assert read_from_string('abc', capacity) == [u8(`a`), `b`, `c`]
assert read_from_string('abcde', capacity) == [u8(`a`), `b`, `c`, `d`, `e`]
large_string_bytes := []u8{len: 1000, init: `x`}
large_string := large_string_bytes.bytestr()
assert read_from_string(large_string, capacity) == large_string_bytes
}
}
pub fn test_buffered_reader_readline() {
text := 'hello\nworld'
want := text.split_into_lines()
mut str := StringReader{
text: text
}
mut stream := io.new_buffered_reader(reader: str)
mut buf := []u8{len: 1}
mut i := 0
for {
line := stream.read_line() or {
assert err is io.Eof
break
}
assert i < want.len
assert want[i] == line
i++
}
}
+15
View File
@@ -0,0 +1,15 @@
# io.fs
`io.fs` defines small filesystem interfaces that can be shared by different
backends and virtual filesystems.
The module is modeled after Go's `io/fs` package, but follows V naming and type
conventions.
Current helpers:
- `valid_path(name string) bool`
- `read_file(filesystem FS, name string) ![]u8`
- `read_dir(filesystem FS, name string) ![]DirEntry`
- `stat(filesystem FS, name string) !FileInfo`
- `file_info_to_dir_entry(info FileInfo) DirEntry`
+163
View File
@@ -0,0 +1,163 @@
module fs
import io
import os
// FileInfo describes a file or directory entry.
pub interface FileInfo {
name() string
size() u64
mode() os.FileMode
mod_time() i64
is_dir() bool
}
// DirEntry describes a single directory entry.
pub interface DirEntry {
name() string
is_dir() bool
typ() os.FileType
info() !FileInfo
}
// File is the minimum interface required to read from a filesystem entry.
pub interface File {
io.Reader
stat() !FileInfo
mut:
close()
}
// FS opens slash-separated paths that pass `valid_path`.
pub interface FS {
open(name string) !File
}
// ReadDirFile can read directory entries directly from an opened file.
pub interface ReadDirFile {
File
mut:
read_dir(n int) ![]DirEntry
}
// ReadDirFS can read a directory without first opening it as a `File`.
pub interface ReadDirFS {
read_dir(name string) ![]DirEntry
}
// ReadFileFS can read a whole file without first opening it as a `File`.
pub interface ReadFileFS {
read_file(name string) ![]u8
}
// StatFS can stat a path without first opening it as a `File`.
pub interface StatFS {
stat(name string) !FileInfo
}
// GlobFS can expand glob patterns within a filesystem.
pub interface GlobFS {
glob(pattern string) ![]string
}
// SubFS can return a filesystem rooted at a subdirectory.
pub interface SubFS {
sub(dir string) !FS
}
struct FileInfoDirEntry {
source FileInfo
}
fn (entry FileInfoDirEntry) name() string {
return entry.source.name()
}
fn (entry FileInfoDirEntry) is_dir() bool {
return entry.source.is_dir()
}
fn (entry FileInfoDirEntry) typ() os.FileType {
return entry.source.mode().typ
}
fn (entry FileInfoDirEntry) info() !FileInfo {
return entry.source
}
fn sort_dir_entries(mut entries []DirEntry) {
entries.sort(a.name() < b.name())
}
// valid_path reports whether `name` is a valid slash-separated path for `FS.open`.
pub fn valid_path(name string) bool {
if name == '.' {
return true
}
if name.len == 0 {
return false
}
for elem in name.split('/') {
if elem.len == 0 || elem == '.' || elem == '..' {
return false
}
}
return true
}
// file_info_to_dir_entry wraps a `FileInfo` in a `DirEntry`.
pub fn file_info_to_dir_entry(info FileInfo) DirEntry {
return FileInfoDirEntry{
source: info
}
}
// read_file reads the named file from `filesystem`.
pub fn read_file(filesystem FS, name string) ![]u8 {
if filesystem is ReadFileFS {
reader := filesystem as ReadFileFS
return reader.read_file(name)
}
mut file := filesystem.open(name)!
defer {
file.close()
}
reader := file as io.Reader
return io.read_all(
reader: reader
read_to_end_of_stream: true
)
}
// read_dir reads and sorts the named directory from `filesystem`.
pub fn read_dir(filesystem FS, name string) ![]DirEntry {
if filesystem is ReadDirFS {
reader := filesystem as ReadDirFS
mut entries := reader.read_dir(name)!
sort_dir_entries(mut entries)
return entries
}
mut file := filesystem.open(name)!
defer {
file.close()
}
if mut file is ReadDirFile {
mut entries := file.read_dir(-1)!
sort_dir_entries(mut entries)
return entries
}
return error('fs: `${name}` does not support read_dir')
}
// stat returns file information for the named path from `filesystem`.
pub fn stat(filesystem FS, name string) !FileInfo {
if filesystem is StatFS {
stats := filesystem as StatFS
return stats.stat(name)
}
mut file := filesystem.open(name)!
defer {
file.close()
}
return file.stat()
}
+216
View File
@@ -0,0 +1,216 @@
module fs
import io
import os
struct MockInfo {
name_ string
size_ u64
mode_ os.FileMode
mod_time_ i64
}
fn (info MockInfo) name() string {
return info.name_
}
fn (info MockInfo) size() u64 {
return info.size_
}
fn (info MockInfo) mode() os.FileMode {
return info.mode_
}
fn (info MockInfo) mod_time() i64 {
return info.mod_time_
}
fn (info MockInfo) is_dir() bool {
return info.mode_.typ == .directory
}
struct MockFile {
data []u8
info_ MockInfo
mut:
offset int
entries []DirEntry
}
fn (mut file MockFile) read(mut buf []u8) !int {
if file.offset >= file.data.len {
return io.Eof{}
}
read := copy(mut buf, file.data[file.offset..])
file.offset += read
return read
}
fn (file MockFile) stat() !FileInfo {
return file.info_
}
fn (mut file MockFile) close() {}
fn (mut file MockFile) read_dir(n int) ![]DirEntry {
if n <= 0 || n >= file.entries.len {
return file.entries.clone()
}
return file.entries[..n].clone()
}
struct FallbackFS {}
fn (filesystem FallbackFS) open(name string) !File {
match name {
'notes.txt' {
return File(MockFile{
data: 'fallback'.bytes()
info_: MockInfo{
name_: 'notes.txt'
size_: u64('fallback'.len)
mode_: os.FileMode{
typ: .regular
}
mod_time_: 101
}
})
}
'dir' {
return File(MockFile{
info_: MockInfo{
name_: 'dir'
mode_: os.FileMode{
typ: .directory
}
}
entries: [
file_info_to_dir_entry(MockInfo{
name_: 'b.txt'
mode_: os.FileMode{
typ: .regular
}
}),
file_info_to_dir_entry(MockInfo{
name_: 'a.txt'
mode_: os.FileMode{
typ: .regular
}
}),
]
})
}
else {
return error('missing ${name}')
}
}
}
struct OptimizedFS {}
fn (filesystem OptimizedFS) open(name string) !File {
return error('unexpected open for ${name}')
}
fn (filesystem OptimizedFS) read_file(name string) ![]u8 {
return 'optimized'.bytes()
}
fn (filesystem OptimizedFS) read_dir(name string) ![]DirEntry {
return [
file_info_to_dir_entry(MockInfo{
name_: 'z.txt'
mode_: os.FileMode{
typ: .regular
}
}),
file_info_to_dir_entry(MockInfo{
name_: 'm.txt'
mode_: os.FileMode{
typ: .regular
}
}),
]
}
fn (filesystem OptimizedFS) stat(name string) !FileInfo {
return MockInfo{
name_: name
size_: 42
mode_: os.FileMode{
typ: .regular
}
mod_time_: 202
}
}
fn entry_names(entries []DirEntry) []string {
mut names := []string{cap: entries.len}
for entry in entries {
names << entry.name()
}
return names
}
fn test_valid_path() {
assert valid_path('.')
assert valid_path('alpha')
assert valid_path('alpha/beta')
assert valid_path(r'alpha\beta')
assert !valid_path('')
assert !valid_path('/alpha')
assert !valid_path('alpha/')
assert !valid_path('alpha//beta')
assert !valid_path('alpha/./beta')
assert !valid_path('alpha/../beta')
}
fn test_file_info_to_dir_entry() {
info := MockInfo{
name_: 'subdir'
size_: 12
mode_: os.FileMode{
typ: .directory
}
mod_time_: 303
}
entry := file_info_to_dir_entry(info)
assert entry.name() == 'subdir'
assert entry.is_dir()
assert entry.typ() == .directory
assert entry.info()!.mod_time() == 303
}
fn test_read_file_uses_open_fallback() {
data := read_file(FallbackFS{}, 'notes.txt')!
assert data == 'fallback'.bytes()
}
fn test_read_file_uses_read_file_fs_when_available() {
data := read_file(OptimizedFS{}, 'notes.txt')!
assert data == 'optimized'.bytes()
}
fn test_read_dir_uses_open_fallback_and_sorts() {
entries := read_dir(FallbackFS{}, 'dir')!
assert entry_names(entries) == ['a.txt', 'b.txt']
}
fn test_read_dir_uses_read_dir_fs_when_available() {
entries := read_dir(OptimizedFS{}, 'dir')!
assert entry_names(entries) == ['m.txt', 'z.txt']
}
fn test_stat_uses_open_fallback() {
info := stat(FallbackFS{}, 'notes.txt')!
assert info.name() == 'notes.txt'
assert info.size() == u64('fallback'.len)
}
fn test_stat_uses_stat_fs_when_available() {
info := stat(OptimizedFS{}, 'notes.txt')!
assert info.name() == 'notes.txt'
assert info.size() == 42
assert info.mod_time() == 202
}
+26
View File
@@ -0,0 +1,26 @@
module io
// CopySettings provides additional options to io.cp
@[params]
pub struct CopySettings {
pub mut:
buffer_size int = 64 * 1024 // The buffer size used during the copying. A larger buffer is more performant, but uses more RAM.
}
// cp copies from `src` to `dst` by allocating
// a maximum of 1024 bytes buffer for reading
// until either EOF is reached on `src` or an error occurs.
// An error is returned if an error is encountered during write.
@[manualfree]
pub fn cp(mut src Reader, mut dst Writer, params CopySettings) ! {
mut buf := []u8{len: params.buffer_size}
defer {
unsafe {
buf.free()
}
}
for {
bytes := src.read(mut buf) or { break }
dst.write(buf[..bytes]) or { return err }
}
}
+13
View File
@@ -0,0 +1,13 @@
import io
import os
fn test_cp() {
mut f := os.open(@FILE) or { panic(err) }
defer {
f.close()
}
mut r := io.new_buffered_reader(reader: f)
mut stdout := os.stdout()
io.cp(mut r, mut stdout)!
assert true
}
+41
View File
@@ -0,0 +1,41 @@
import io
struct Buf {
pub:
bytes []u8
mut:
i int
}
struct Writ {
pub mut:
bytes []u8
}
fn (mut b Buf) read(mut buf []u8) !int {
if !(b.i < b.bytes.len) {
return io.Eof{}
}
n := copy(mut buf, b.bytes[b.i..])
b.i += n
return n
}
fn (mut w Writ) write(buf []u8) !int {
if buf.len <= 0 {
return error('none')
}
w.bytes << buf
return buf.len
}
fn test_copy() {
mut src := Buf{
bytes: 'abcdefghij'.repeat(10).bytes()
}
mut dst := Writ{
bytes: []u8{}
}
io.cp(mut src, mut dst) or { assert false }
assert dst.bytes == src.bytes
}
+33
View File
@@ -0,0 +1,33 @@
module io
// new_multi_writer returns a Writer that writes to all writers. The write
// function of the returned Writer writes to all writers of the MultiWriter,
// returns the length of bytes written, and if any writer fails to write the
// full length an error is returned and writing to other writers stops, and if
// any writer returns an error the error is returned immediately and writing to
// other writers stops.
pub fn new_multi_writer(writers ...Writer) Writer {
return &MultiWriter{
writers: writers
}
}
// MultiWriter writes to all its writers.
pub struct MultiWriter {
pub mut:
writers []Writer
}
// write writes to all writers of the MultiWriter. Returns the length of bytes
// written. If any writer fails to write the full length an error is returned
// and writing to other writers stops. If any writer returns an error the error
// is returned immediately and writing to other writers stops.
pub fn (mut m MultiWriter) write(buf []u8) !int {
for mut w in m.writers {
n := w.write(buf)!
if n != buf.len {
return error('io: incomplete write to writer of MultiWriter')
}
}
return buf.len
}
+49
View File
@@ -0,0 +1,49 @@
module io
fn test_multi_writer_write_successful() {
mut mw := new_multi_writer(&TestWriter{}, &TestWriter{})
n := mw.write('0123456789'.bytes()) or {
assert false
return
}
assert n == 10
}
fn test_multi_writer_write_incomplete() {
mut mw := new_multi_writer(&TestWriter{}, &TestIncompleteWriter{})
_ := mw.write('0123456789'.bytes()) or { return }
assert false
}
fn test_multi_writer_write_error() {
mut mw := new_multi_writer(&TestWriter{}, &TestErrorWriter{}, &TestWriter{})
_ := mw.write('0123456789'.bytes()) or { return }
assert false
}
struct TestWriter {
pub mut:
bytes []u8
}
fn (mut w TestWriter) write(buf []u8) !int {
w.bytes << buf
return buf.len
}
struct TestIncompleteWriter {
pub mut:
bytes []u8
}
fn (mut w TestIncompleteWriter) write(buf []u8) !int {
b := buf[..buf.len - 1]
w.bytes << b
return b.len
}
struct TestErrorWriter {}
fn (mut w TestErrorWriter) write(buf []u8) !int {
return error('error writer errored')
}
+29
View File
@@ -0,0 +1,29 @@
import os
import io
fn read_file(file string, cap int) []string {
mut lines := []string{}
mut f := os.open(file) or { panic(err) }
defer {
f.close()
}
mut r := io.new_buffered_reader(reader: f, cap: cap)
for {
l := r.read_line() or { break }
lines << l
// println('Line: ${l}')
}
assert lines.len > 0
assert r.end_of_stream == true
println('------------------------------------------------ cap: ${cap:6}; read: ${lines.len:3} lines')
return lines
}
fn test_file_reader() {
for cap := 1; cap <= 10000; cap += 256 {
lines := read_file(@FILE, cap)
assert lines.last() == '// my last line'
}
}
// my last line
+91
View File
@@ -0,0 +1,91 @@
module io
// Eof error means that we reach the end of the stream.
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
}
// Reader represents a stream of data that can be read.
pub interface Reader {
// read reads up to buf.len bytes and places
// them into buf.
// A type that implements this should return
// `io.Eof` on end of stream (EOF) instead of just returning 0
mut:
read(mut buf []u8) !int
}
pub const read_all_len = 10 * 1024
pub const read_all_grow_len = 1024
// ReadAllConfig allows options to be passed for the behaviour
// of read_all.
pub struct ReadAllConfig {
pub:
read_to_end_of_stream bool
reader Reader
}
// read_all reads all bytes from a reader until either a 0 length read
// or if read_to_end_of_stream is true then the end of the stream (`none`).
pub fn read_all(config ReadAllConfig) ![]u8 {
mut r := config.reader
read_till_eof := config.read_to_end_of_stream
mut b := []u8{len: read_all_len}
mut read := 0
for {
new_read := r.read(mut b[read..]) or { break }
if new_read < 0 {
return error('io.read_all: reader returned a negative read count (${new_read})')
}
read += new_read
if !read_till_eof && new_read == 0 {
break
}
if b.len == read {
unsafe { b.grow_len(read_all_grow_len) }
}
}
return b[..read]
}
// read_any reads any available bytes from a reader
// (until the reader returns a read of 0 length).
pub fn read_any(mut r Reader) ![]u8 {
mut b := []u8{len: read_all_len}
mut read := 0
for {
new_read := r.read(mut b[read..]) or { return error('none') }
if new_read < 0 {
return error('io.read_any: reader returned a negative read count (${new_read})')
}
read += new_read
if new_read == 0 {
break
}
if b.len == read {
unsafe { b.grow_len(read_all_grow_len) }
}
}
return b[..read]
}
// RandomReader represents a stream of readable data from at a random location.
pub interface RandomReader {
read_from(pos u64, mut buf []u8) !int
}
+302
View File
@@ -0,0 +1,302 @@
module io
struct Buf {
pub:
bytes []u8
mut:
i int
}
fn (mut b Buf) read(mut buf []u8) !int {
if !(b.i < b.bytes.len) {
return Eof{}
}
n := copy(mut buf, b.bytes[b.i..])
b.i += n
return n
}
fn test_read_all() {
buf := Buf{
bytes: '123'.repeat(10).bytes()
}
res := read_all(reader: buf) or {
assert false
''.bytes()
}
assert res == '123'.repeat(10).bytes()
}
fn test_read_all_huge() {
buf := Buf{
bytes: '123'.repeat(100000).bytes()
}
res := read_all(reader: buf) or {
assert false
''.bytes()
}
assert res == '123'.repeat(100000).bytes()
}
struct ZeroReadAfterDataReader {
mut:
call_count int
}
fn (mut r ZeroReadAfterDataReader) read(mut buf []u8) !int {
match r.call_count {
0 {
r.call_count++
return copy(mut buf, 'hello'.bytes())
}
1 {
r.call_count++
return 0
}
else {
return error('unexpected extra read after zero-length read')
}
}
}
fn test_read_all_stops_on_zero_length_read_after_partial_data() {
mut reader := &ZeroReadAfterDataReader{}
res := read_all(reader: reader) or {
assert false
''.bytes()
}
assert res == 'hello'.bytes()
assert reader.call_count == 2
}
struct StringReaderTest {
text string
mut:
place int
}
fn (mut s StringReaderTest) read(mut buf []u8) !int {
if s.place >= s.text.len {
return Eof{}
}
read := copy(mut buf, s.text[s.place..].bytes())
s.place += read
return read
}
struct ZeroThenDataReader {
pub:
text string
mut:
place int
empty_reads int
}
fn (mut s ZeroThenDataReader) read(mut buf []u8) !int {
if buf.len == 0 {
return 0
}
if s.empty_reads == 0 {
s.empty_reads++
return 0
}
if s.place >= s.text.len {
return Eof{}
}
read := copy(mut buf, s.text[s.place..].bytes())
s.place += read
return read
}
const newline_count = 100000
fn test_stringreadertest() {
text := '12345\n'.repeat(newline_count)
mut s := StringReaderTest{
text: text
}
mut r := new_buffered_reader(reader: s)
for i := 0; true; i++ {
if _ := r.read_line() {
} else {
assert i == newline_count
break
}
}
if _ := r.read_line() {
assert false
}
leftover := read_all(reader: r) or {
assert false
panic('bad')
}
if leftover.len > 0 {
assert false
}
}
fn test_stringreadertest2() {
text := '12345\r\n'.repeat(newline_count)
mut s := StringReaderTest{
text: text
}
mut r := new_buffered_reader(reader: s)
for i := 0; true; i++ {
if _ := r.read_line() {
} else {
assert i == newline_count
break
}
}
if _ := r.read_line() {
assert false
}
leftover := read_all(reader: r) or {
assert false
panic('bad')
}
if leftover.len > 0 {
assert false
}
}
fn test_leftover() {
text := 'This is a test\r\nNice try!'
mut s := StringReaderTest{
text: text
}
mut r := new_buffered_reader(reader: s)
_ := r.read_line() or {
assert false
panic('bad')
}
line2 := r.read_line() or {
assert false
panic('bad')
}
assert line2 == 'Nice try!'
if _ := r.read_line() {
assert false
panic('bad')
}
assert r.end_of_stream()
}
fn test_read_line_strips_crlf_across_buffer_fills() {
text := '12345\r\n67890\r\n'
mut s := StringReaderTest{
text: text
}
mut r := new_buffered_reader(reader: s, cap: 2)
assert r.read_line()! == '12345'
assert r.read_line()! == '67890'
if _ := r.read_line() {
assert false
}
assert r.end_of_stream()
}
fn test_totalread_read() {
text := 'Some testing text'
mut s := StringReaderTest{
text: text
}
mut r := new_buffered_reader(reader: s)
mut buf := []u8{len: text.len}
total := r.read(mut buf) or {
assert false
panic('bad')
}
assert r.total_read == total
}
fn test_buffered_reader_retries_zero_length_reads() {
text := 'Some testing text'
mut s := ZeroThenDataReader{
text: text
}
mut r := new_buffered_reader(reader: s, retries: 2)
mut buf := []u8{len: text.len}
total := r.read(mut buf) or {
assert false
panic('bad')
}
assert total == text.len
assert buf[..total] == text.bytes()
assert r.total_read == total
}
struct NegativeReader {
mut:
read_count int
}
fn (mut r NegativeReader) read(mut _ []u8) !int {
r.read_count++
return -1
}
fn test_read_all_errors_on_negative_read_count() {
mut reader := &NegativeReader{}
if _ := read_all(reader: reader) {
assert false
} else {
assert err.msg() == 'io.read_all: reader returned a negative read count (-1)'
}
assert reader.read_count == 1
}
fn test_read_any_errors_on_negative_read_count() {
mut reader := &NegativeReader{}
if _ := read_any(mut reader) {
assert false
} else {
assert err.msg() == 'io.read_any: reader returned a negative read count (-1)'
}
assert reader.read_count == 1
}
fn test_totalread_readline() {
text := 'Some testing text\nmore_enters'
mut s := StringReaderTest{
text: text
}
mut r := new_buffered_reader(reader: s)
_ := r.read_line() or {
assert false
panic('bad')
}
_ := r.read_line() or {
assert false
panic('bad')
}
assert r.total_read == text.len
}
fn test_read_line_until_zero_terminated() {
text := 'This is a test\0Nice try!\0'
mut s := StringReaderTest{
text: text
}
mut r := new_buffered_reader(reader: s)
line1 := r.read_line(delim: `\0`) or {
assert false
panic('bad')
}
assert line1 == 'This is a test'
line2 := r.read_line(delim: `\0`) or {
assert false
panic('bad')
}
assert line2 == 'Nice try!'
if _ := r.read_line(delim: `\0`) {
assert false
panic('bad')
}
assert r.end_of_stream()
}
+37
View File
@@ -0,0 +1,37 @@
module io
// ReaderWriter represents a stream that can be read and written.
pub interface ReaderWriter {
Reader
Writer
}
// ReaderWriterImpl is a ReaderWriter that can be made from
// a separate reader and writer (see fn make_readerwriter).
struct ReaderWriterImpl {
mut:
r Reader
w Writer
}
// read reads up to `buf.len` bytes into `buf`. It returns
// the number of bytes read or any error encountered.
pub fn (mut r ReaderWriterImpl) read(mut buf []u8) !int {
return r.r.read(mut buf)
}
// write writes `buf.len` bytes from `buf` to the underlying
// data stream. It returns the number of bytes written or any error
// encountered.
pub fn (mut r ReaderWriterImpl) write(buf []u8) !int {
return r.w.write(buf)
}
// make_readerwriter takes a rstream and a wstream and makes
// an rwstream with them.
pub fn make_readerwriter(r Reader, w Writer) ReaderWriterImpl {
return ReaderWriterImpl{
r: r
w: w
}
}
@@ -0,0 +1,329 @@
@[deprecated: 'If you use it, create a local module named `string_reader`, and copy https://github.com/vlang/v/blob/f24d49259db5d2d7211e79bcf6b6f507d22847f3/vlib/io/string_reader/string_reader.v there.']
@[deprecated_after: '2026-01-06']
module string_reader
import io
import strings
@[params]
pub struct StringReaderParams {
pub:
// the reader interface
reader ?io.Reader
// initialize the builder with this source string
source ?string
// if no source is given the string builder is initialized with this size
initial_size int
}
// StringReader is able to read data from a Reader interface and/or source string to a dynamically
// growing buffer using a string builder. Unlike the BufferedReader, StringReader will
// keep the entire contents of the buffer in memory, allowing the incoming data to be reused
// and read in an efficient matter. The StringReader will not set a maximum capacity to the string
// builders buffer and could grow very large.
pub struct StringReader {
mut:
reader ?io.Reader
offset int // current offset in the buffer
pub mut:
end_of_stream bool // whether we reached the end of the upstream reader
builder strings.Builder
}
// new creates a new StringReader and sets the string builder size to `initial_size`.
// If a source
pub fn StringReader.new(params StringReaderParams) StringReader {
mut r := StringReader{
reader: params.reader
}
if source := params.source {
r.builder = strings.new_builder(source.len)
r.builder.write_string(source)
if params.reader == none {
// There is no upstream reader; only the preloaded buffer can be consumed.
r.end_of_stream = true
}
} else {
r.builder = strings.new_builder(params.initial_size)
}
return r
}
// needs_fill returns whether the buffer needs refilling
pub fn (r StringReader) needs_fill() bool {
return r.offset >= r.builder.len
}
// needs_fill_until returns whether the buffer needs refilling in order to read
// `n` bytes
pub fn (r StringReader) needs_fill_until(n int) bool {
return r.offset + n > r.builder.len
}
// fill_bufer tries to read data into the buffer until either a 0 length read or if read_to_end_of_stream
// is true then the end of the stream. It returns the number of bytes read
pub fn (mut r StringReader) fill_buffer(read_till_end_of_stream bool) !int {
if r.end_of_stream {
return io.Eof{}
}
mut reader := r.reader or { return error('reader is not set') }
start := r.builder.len
mut end := start
// make sure there is enough room in the string builder
unsafe { r.builder.grow_len(io.read_all_len) }
defer {
// shrink the length of the buffer to the total of bytes read
r.builder.go_back(r.builder.len - end)
}
for {
read := reader.read(mut r.builder[end..]) or {
r.end_of_stream = true
break
}
end += read
if !read_till_end_of_stream || read == 0 {
break
} else if r.builder.len == end {
unsafe { r.builder.grow_len(io.read_all_grow_len) }
}
}
if end == start {
return io.Eof{}
}
return end - start
}
// fill_buffer_until tries read `n` amount of bytes from the reader into the buffer
// and returns the actual number of bytes read
pub fn (mut r StringReader) fill_buffer_until(n int) !int {
if r.end_of_stream {
return io.Eof{}
}
mut reader := r.reader or { return error('reader is not set') }
start := r.builder.len
// make sure there is enough room in the string builder
if n > io.read_all_len {
unsafe { r.builder.grow_len(io.read_all_len) }
} else {
unsafe { r.builder.grow_len(n) }
}
mut end := start
defer {
// shrink the length of the buffer to the total of bytes read
r.builder.go_back(r.builder.len - end)
}
for {
read := reader.read(mut r.builder[end..]) or {
r.end_of_stream = true
break
}
end += read
if read == 0 || end - start == n {
break
} else if r.builder.len == end {
remaining := n - (end - start)
if remaining > io.read_all_grow_len {
unsafe { r.builder.grow_len(io.read_all_grow_len) }
} else {
unsafe { r.builder.grow_len(remaining) }
}
}
}
if end == start {
return io.Eof{}
}
return end - start
}
// read_all_bytes reads all bytes from a reader until either a 0 length read or if read_to_end_of_stream
// is true then the end of the stream. It returns a copy of the read data
pub fn (mut r StringReader) read_all_bytes(read_till_end_of_stream bool) ![]u8 {
start := r.offset
// ignore Eof error from fill buffer
r.fill_buffer(read_till_end_of_stream) or {}
r.offset = r.builder.len
// check if there was still data in the buffer, but the reader has reached its end of stream
if start == r.offset {
return io.Eof{}
}
return r.get_part(start, r.offset - start)!
}
// read_all reads all bytes from a reader until either a 0 length read or if read_to_end_of_stream
// is true then the end of the stream. It produces a string from the read data
pub fn (mut r StringReader) read_all(read_till_end_of_stream bool) !string {
buf := r.read_all_bytes(read_till_end_of_stream)!
return unsafe { tos(buf.data, buf.len) }
}
// read_bytes tries to read n amount of bytes from the reader
pub fn (mut r StringReader) read_bytes(n int) ![]u8 {
start := r.offset
if r.needs_fill_until(n) {
actual_read := r.fill_buffer_until(n - (r.builder.len - r.offset))!
r.offset += actual_read
} else {
r.offset += n
}
return r.get_part(start, r.offset - start)!
}
// read_bytes tries to read `n` amount of bytes from the reader and produces a string
// from the read data
pub fn (mut r StringReader) read_string(n int) !string {
buf := r.read_bytes(n)!
return unsafe { tos(buf.data, buf.len) }
}
// read implements the Reader interface
pub fn (mut r StringReader) read(mut buf []u8) !int {
if buf.len == 0 {
return 0
}
available := r.builder.len - r.offset
if available < buf.len && !r.end_of_stream {
if r.reader != none {
missing := buf.len - available
r.fill_buffer_until(missing) or {
if err !is io.Eof {
return err
}
}
}
}
available_after_fill := r.builder.len - r.offset
if available_after_fill == 0 {
if r.reader == none && !r.end_of_stream {
return error('reader is not set')
}
return io.Eof{}
}
read := if available_after_fill < buf.len { available_after_fill } else { buf.len }
start := r.offset
r.offset += read
copy(mut buf, r.builder[start..start + read])
return read
}
// read_line attempts to read a line from the reader.
// It reads until it finds the specified delimiter or the end of stream.
// The returned string does not include the delimiter.
// When the delimiter is `\n`, a preceding `\r` is treated as part of CRLF
// and is also omitted from the returned string.
@[direct_array_access]
pub fn (mut r StringReader) read_line(config io.BufferedReadLineConfig) !string {
if r.end_of_stream && r.needs_fill() {
return io.Eof{}
}
start := r.offset
for {
if r.needs_fill() {
r.fill_buffer(false) or {
// we are at the end of the stream
if r.offset == start {
return io.Eof{}
}
return r.get_string_part(start, r.offset - start)!
}
}
// try to find a newline character
mut i := r.offset
for ; i < r.builder.len; i++ {
c := r.builder[i]
if c == config.delim {
// great, we hit something
// do some checking for whether we hit \r\n or just \n
mut x := i
if i > start && config.delim == `\n` && r.builder[i - 1] == `\r` {
x--
}
r.offset = i + 1
return r.get_string_part(start, x - start)!
}
}
r.offset = i
}
return io.Eof{}
}
// write implements the Writer interface
pub fn (mut r StringReader) write(buf []u8) !int {
return r.builder.write(buf)!
}
// get_data returns a copy of the buffer
@[inline]
pub fn (r StringReader) get_data() []u8 {
unsafe {
mut x := malloc_noscan(r.builder.len)
vmemcpy(x, &u8(r.builder.data), r.builder.len)
return x.vbytes(r.builder.len)
}
}
// get get_part returns a copy of a part of the buffer from `start` till `start` + `n`
pub fn (r StringReader) get_part(start int, n int) ![]u8 {
if start + n > r.builder.len {
return io.Eof{}
}
unsafe {
mut x := malloc_noscan(n)
vmemcpy(x, &u8(r.builder.data) + start, n)
return x.vbytes(n)
}
}
// get_string produces a string from all the bytes in the buffer
@[inline]
pub fn (r StringReader) get_string() string {
return r.builder.spart(0, r.builder.len)
}
// get_string_part produces a string from `start` till `start` + `n` of the buffer
pub fn (r StringReader) get_string_part(start int, n int) !string {
if start + n > r.builder.len {
return io.Eof{}
}
return r.builder.spart(start, n)
}
// flush clears the stringbuilder and returns the resulting string and the stringreaders
// offset is reset to 0
pub fn (mut r StringReader) flush() string {
r.offset = 0
return r.builder.str()
}
// free frees the memory block used for the string builders buffer,
// a new string builder with size 0 is initialized and the stringreaders offset is reset to 0
@[unsafe]
pub fn (mut r StringReader) free() {
unsafe { r.builder.free() }
r.builder = strings.new_builder(0)
r.offset = 0
}
@@ -0,0 +1,409 @@
module string_reader
import io
struct Buf {
pub:
bytes []u8
mut:
i int
}
struct TwoByteReader {
mut:
data string
pos int
}
fn (mut b Buf) read(mut buf []u8) !int {
if !(b.i < b.bytes.len) {
return io.Eof{}
}
n := copy(mut buf, b.bytes[b.i..])
b.i += n
return n
}
fn (mut r TwoByteReader) read(mut buf []u8) !int {
if r.pos >= r.data.len {
return io.Eof{}
}
min := int_min(int_min(r.data.len - r.pos, 2), buf.len)
for i in 0 .. min {
buf[i] = r.data[r.pos]
r.pos++
}
return min
}
fn read_from_reader_interface(mut r io.Reader, mut buf []u8) !int {
return r.read(mut buf)
}
fn test_read_all() {
mut reader := StringReader.new()
if _ := reader.read_all(false) {
assert false, 'should return io.Eof'
} else {
assert err is io.Eof
}
contents := 'test string'
buf := Buf{
bytes: contents.bytes()
}
reader = StringReader.new(reader: buf)
data := reader.read_all(false)!
assert data == contents
}
fn test_read_bytes() {
mut reader := StringReader.new()
if _ := reader.read_bytes(4) {
assert false, 'should return io.Eof'
} else {
assert err.msg() == 'reader is not set'
}
buf := Buf{
bytes: '12345678'.bytes()
}
reader = StringReader.new(reader: buf)
mut data := reader.read_bytes(4)!
assert data == buf.bytes[..4]
data = reader.read_bytes(4)!
assert data == buf.bytes[4..8]
}
fn test_read_line() {
mut reader := StringReader.new()
if _ := reader.read_line() {
assert false, 'should return io.Eof'
} else {
assert err is io.Eof
}
buf := Buf{
bytes: 'first line\r\nsecond line\n'.bytes()
}
reader = StringReader.new(reader: buf)
assert reader.read_line()! == 'first line'
assert reader.read_line()! == 'second line'
}
fn test_from_string() {
mut reader := StringReader.new(source: 'test')
assert reader.read_all(false)! == 'test'
if _ := reader.read_all(false) {
assert false, 'should return Eof'
} else {
assert err is io.Eof
}
}
fn test_read_from_source_read() {
source := 'line 1\nline 2\n'
mut reader := StringReader.new(source: source)
mut buf := []u8{len: 100}
read := reader.read(mut buf)!
assert read == source.len
assert buf[..read].bytestr() == source
if _ := reader.read(mut buf) {
assert false, 'should return io.Eof'
} else {
assert err is io.Eof
}
}
fn test_read_from_source_read_via_reader_interface() {
source := 'line 1\nline 2\n'
mut reader := StringReader.new(source: source)
mut buf := []u8{len: 100}
read := read_from_reader_interface(mut reader, mut buf)!
assert read == source.len
assert buf[..read].bytestr() == source
if _ := read_from_reader_interface(mut reader, mut buf) {
assert false, 'should return io.Eof'
} else {
assert err is io.Eof
}
}
fn test_from_string_read_byte_one_by_one() {
mut reader := StringReader.new(source: 'test')
assert reader.read_bytes(1)![0].ascii_str() == 't'
assert reader.read_bytes(1)![0].ascii_str() == 'e'
assert reader.read_bytes(1)![0].ascii_str() == 's'
assert reader.read_bytes(1)![0].ascii_str() == 't'
if _ := reader.read_all(false) {
assert false, 'should return Eof'
} else {
assert err is io.Eof
}
}
fn test_from_string_and_reader() {
buf := Buf{
bytes: 'buffer'.bytes()
}
mut reader := StringReader.new(reader: buf, source: 'string')
assert reader.read_all(false)! == 'stringbuffer'
}
fn test_flush() {
mut reader := StringReader.new(source: 'flushed data')
str := reader.flush()
assert str == 'flushed data'
assert reader.offset == 0
assert reader.builder.len == 0
}
fn test_fill_buffer_true() {
mut two := TwoByteReader{
data: '12345'
}
mut reader := StringReader.new(reader: two)
assert reader.fill_buffer(true)! == 5
assert reader.get_string() == '12345'
}
fn test_fill_buffer_false() {
mut two := TwoByteReader{
data: '12345'
}
mut reader := StringReader.new(reader: two)
assert reader.fill_buffer(false)! == 2
assert reader.get_string() == '12'
}
fn test_fill_buffer_until() {
mut two := TwoByteReader{
data: '12345'
}
mut reader := StringReader.new(reader: two)
assert reader.fill_buffer_until(1)! == 1
assert reader.builder.len == 1
assert reader.fill_buffer_until(2)! == 2
assert reader.builder.len == 3
assert reader.fill_buffer_until(2)! == 2
assert reader.builder.len == 5
assert reader.get_string() == '12345'
}
fn test_fill_buffer_until_one() {
mut two := TwoByteReader{
data: '12345'
}
mut reader := StringReader.new(reader: two)
assert reader.fill_buffer_until(1)! == 1
assert reader.builder.len == 1
assert reader.fill_buffer_until(1)! == 1
assert reader.builder.len == 2
assert reader.fill_buffer_until(1)! == 1
assert reader.builder.len == 3
assert reader.fill_buffer_until(1)! == 1
assert reader.builder.len == 4
assert reader.fill_buffer_until(1)! == 1
assert reader.builder.len == 5
assert reader.get_string() == '12345'
}
fn test_fill_buffer_until_many() {
mut two := TwoByteReader{
data: '12345'
}
mut reader := StringReader.new(reader: two)
assert reader.fill_buffer_until(1)! == 1
assert reader.builder.len == 1
assert reader.fill_buffer_until(12)! == 4
assert reader.builder.len == 5
assert reader.fill_buffer_until(123) or { -1 } == -1
assert reader.builder.len == 5
assert reader.get_string() == '12345'
}
fn test_read_all_bytes_false() {
mut two := TwoByteReader{
data: '12345'
}
mut reader := StringReader.new(reader: two)
assert reader.read_all_bytes(false)! == [u8(49), 50]
assert reader.read_all_bytes(false)! == [u8(51), 52]
assert reader.read_all_bytes(false)! == [u8(53)]
}
fn test_read_all_bytes_true() {
mut two := TwoByteReader{
data: '12345'
}
mut reader := StringReader.new(reader: two)
assert reader.read_all_bytes(false)! == [u8(49), 50]
assert reader.read_all_bytes(true)! == [u8(51), 52, 53]
}
fn test_read_all_false() {
mut two := TwoByteReader{
data: '12345'
}
mut reader := StringReader.new(reader: two)
assert reader.read_all(false)! == '12'
assert reader.read_all(false)! == '34'
assert reader.read_all(false)! == '5'
}
fn test_read_all_true() {
mut two := TwoByteReader{
data: '12345'
}
mut reader := StringReader.new(reader: two)
assert reader.read_all(false)! == '12'
assert reader.read_all(true)! == '345'
}
fn test_read_bytes_1() {
mut two := TwoByteReader{
data: '12345'
}
mut reader := StringReader.new(reader: two)
assert reader.read_bytes(1)! == [u8(49)]
assert reader.read_bytes(2)! == [u8(50), 51]
assert reader.read_bytes(2)! == [u8(52), 53]
assert reader.read_bytes(2) or { [u8(255)] } == [u8(255)]
}
fn test_read_bytes_many() {
mut two := TwoByteReader{
data: '12345'
}
mut reader := StringReader.new(reader: two)
assert reader.read_bytes(1)! == [u8(49)]
assert reader.builder.len == 1
assert reader.offset == 1
assert reader.read_bytes(123)! == [u8(50), 51, 52, 53]
assert reader.builder.len == 5
assert reader.offset == 5
}
fn test_read_string() {
mut two := TwoByteReader{
data: '12345'
}
mut reader := StringReader.new(reader: two)
assert reader.read_string(1)! == '1'
assert reader.read_string(2)! == '23'
assert reader.read_string(3)! == '45'
}
fn test_read() {
mut two := TwoByteReader{
data: '12345'
}
mut reader := StringReader.new(reader: two)
mut o1 := []u8{len: 1}
mut o5 := []u8{len: 5}
mut res := reader.read(mut o1)!
assert res == 1
assert o1[..res] == [u8(49)]
res = reader.read(mut o5)!
assert res == 4
assert o5[..res] == [u8(50), 51, 52, 53]
}
fn test_read_many() {
mut two := TwoByteReader{
data: '12345'
}
mut reader := StringReader.new(reader: two)
mut o4 := []u8{len: 4}
mut res := reader.read(mut o4)!
assert res == 4
assert o4[..res] == [u8(49), 50, 51, 52]
res = reader.read(mut o4)!
assert res == 1
assert o4[..res] == [u8(53)]
res = reader.read(mut o4) or { -1 }
assert res == -1
assert o4 == [u8(53), 50, 51, 52]
}
fn test_read_line_2() {
mut two := TwoByteReader{
data: '12345\n67890'
}
mut reader := StringReader.new(reader: two)
assert reader.read_line()! == '12345'
assert reader.read_line()! == '67890'
}
fn test_read_line_3() {
mut two := TwoByteReader{
data: '12345\t67890'
}
mut reader := StringReader.new(reader: two)
assert reader.read_line(delim: `\t`)! == '12345'
assert reader.read_line()! == '67890'
}
fn test_read_line_4() {
mut two := TwoByteReader{
data: '12345\t67890'
}
mut reader := StringReader.new(reader: two)
assert reader.read_line(delim: `\t`)! == '12345'
assert reader.read_line(delim: `\t`)! == '67890'
}
fn test_read_line_5() {
mut two := TwoByteReader{
data: '\n12345\n67890'
}
mut reader := StringReader.new(reader: two)
assert reader.read_line()! == ''
assert reader.read_line()! == '12345'
assert reader.read_line()! == '67890'
}
fn test_read_line_6() {
mut two := TwoByteReader{
data: '\r\n12345\r\n67890'
}
mut reader := StringReader.new(reader: two)
assert reader.read_line()! == ''
assert reader.read_line()! == '12345'
assert reader.read_line()! == '67890'
}
fn test_read_line_7() {
mut two := TwoByteReader{
data: '1234567890'
}
mut reader := StringReader.new(reader: two)
assert reader.read_line(delim: `6`)! == '12345'
assert reader.read_line(delim: `6`)! == '7890'
assert reader.read_line() or { '777' } == '777'
}
fn test_read_line_8() {
mut two := TwoByteReader{
data: '12345\r\n67890'
}
mut reader := StringReader.new(reader: two)
assert reader.read_bytes(6)! == [u8(49), 50, 51, 52, 53, 13]
reader.read_line()! // \n
assert reader.read_line()! == '67890'
}
+85
View File
@@ -0,0 +1,85 @@
module util
import os
import rand
const retries = 10000
@[params]
pub struct TempFileOptions {
pub:
path string = os.temp_dir()
pattern string
}
// temp_file returns a uniquely named, open, writable, `os.File` and it's path.
pub fn temp_file(tfo TempFileOptions) !(os.File, string) {
mut d := tfo.path
if d == '' {
d = os.temp_dir()
}
os.ensure_folder_is_writable(d) or {
return error(@FN +
' could not create temporary file in "${d}". Please ensure write permissions.')
}
d = d.trim_right(os.path_separator)
prefix, suffix := prefix_and_suffix(tfo.pattern) or { return error(@FN + ' ${err.msg()}') }
for retry := 0; retry < retries; retry++ {
path := os.join_path(d, prefix + random_number() + suffix)
mut mode := 'rw+'
$if windows {
mode = 'w+'
}
mut file := os.open_file(path, mode, 0o600) or { continue }
if os.exists(path) && os.is_file(path) {
return file, path
}
}
return error(@FN +
' could not create temporary file in "${d}". Retry limit (${retries}) exhausted. Please ensure write permissions.')
}
@[params]
pub struct TempDirOptions {
pub:
path string = os.temp_dir()
pattern string
}
fn error_for_temporary_folder(fn_name string, d string) !string {
return error('${fn_name} could not create temporary directory "${d}". Please ensure you have write permissions for it.')
}
// temp_dir returns a uniquely named, writable, directory path.
pub fn temp_dir(tdo TempDirOptions) !string {
mut d := tdo.path
if d == '' {
d = os.temp_dir()
}
os.ensure_folder_is_writable(d) or { return error_for_temporary_folder(@FN, d) }
d = d.trim_right(os.path_separator)
prefix, suffix := prefix_and_suffix(tdo.pattern) or { return error(@FN + ' ${err.msg()}') }
for retry := 0; retry < retries; retry++ {
path := os.join_path(d, prefix + random_number() + suffix)
os.mkdir_all(path) or { continue }
if os.is_dir(path) && os.exists(path) {
os.ensure_folder_is_writable(path) or { return error_for_temporary_folder(@FN, d) }
return path
}
}
return error('${@FN} could not create temporary directory "${d}". Retry limit (${retries}) exhausted.')
}
// * Utility functions
fn random_number() string {
s := (1_000_000_000 + (u32(os.getpid()) + rand.u32n(1_000_000_000) or { 0 })).str()
return s.substr(1, s.len)
}
fn prefix_and_suffix(pattern string) !(string, string) {
if pattern.contains(os.path_separator) {
return error('pattern cannot contain path separators (${os.path_separator}).')
}
prefix, suffix := pattern.rsplit_once('*') or { pattern, '' }
return prefix, suffix
}
+123
View File
@@ -0,0 +1,123 @@
import os
import io.util
// tfolder will contain all the temporary files/subfolders made by
// the different tests. It would be removed in testsuite_end(), so
// individual os tests do not need to clean up after themselves.
const tfolder = os.join_path(os.vtmp_dir(), 'io_util_tests')
fn testsuite_begin() {
eprintln('testsuite_begin, tfolder = ${tfolder}')
os.rmdir_all(tfolder) or {}
assert !os.is_dir(tfolder)
os.mkdir_all(tfolder) or { panic(err) }
os.chdir(tfolder) or {}
assert os.is_dir(tfolder)
}
fn testsuite_end() {
os.chdir(os.wd_at_startup) or {}
os.rmdir_all(tfolder) or {}
assert !os.is_dir(tfolder)
// eprintln('testsuite_end , tfolder = ${tfolder} removed.')
}
fn test_temp_file() {
// Test defaults
mut f, mut path := util.temp_file() or {
assert false
return
}
mut prev_path := path
defer {
f.close()
}
assert os.is_file(path)
assert f.is_opened
// Test pattern
f.close()
f, path = util.temp_file(
pattern: 'some_*_test.file'
) or {
assert false
return
}
assert path != prev_path
assert os.is_file(path)
assert f.is_opened
mut filename := os.file_name(path)
assert filename.contains('_test.file')
// Check for 9 digits where the wildcard is placed in the pattern
for i, c in filename {
if i > 4 && i <= 4 + 9 {
assert c.is_digit()
}
}
// Test custom path
prev_path = path
f.close()
f, path = util.temp_file(
path: tfolder
) or {
assert false
return
}
assert path != prev_path
assert os.is_file(path)
assert path.contains(tfolder)
assert f.is_opened
filename = os.file_name(path)
for c in filename {
assert c.is_digit()
}
}
fn test_temp_dir() {
// Test defaults
mut path := util.temp_dir() or {
assert false
return
}
assert os.is_dir(path)
os.ensure_folder_is_writable(path) or {
assert false
return
}
mut prev_path := path
// Test pattern
path = util.temp_dir(
pattern: 'some_*_test_dir'
) or {
assert false
return
}
assert path != prev_path
assert os.is_dir(path)
mut filename := os.file_name(path)
assert filename.contains('_test_dir')
// Check for 9 digits where the wildcard is placed in the pattern
for i, c in filename {
if i > 4 && i <= 4 + 9 {
assert c.is_digit()
}
}
// Test custom path
prev_path = path
path = util.temp_dir(
path: tfolder
) or {
assert false
return
}
assert path != prev_path
assert os.is_dir(path)
os.ensure_folder_is_writable(path) or {
assert false
return
}
assert path.contains(tfolder)
filename = os.file_name(path)
for c in filename {
assert c.is_digit()
}
}
+14
View File
@@ -0,0 +1,14 @@
module io
// Writer is the interface that wraps the `write` method, which
// writes `buf.len` bytes to the underlying data stream.
pub interface Writer {
mut:
write(buf []u8) !int
}
// RandomWriter is the interface that wraps the `write_to` method,
// which writes `buf.len` bytes to the underlying data stream at a random `pos`.
pub interface RandomWriter {
write_to(pos u64, buf []u8) !int
}