240 lines
7.4 KiB
Plaintext
240 lines
7.4 KiB
Plaintext
// Expect retcode = 0
|
|
|
|
// sys/file 之上的 I/O 便利层:C 字符串路径 open、精确读取/完整写入、整读、stdout/stderr 打印。
|
|
// 短读/短写零进展与关闭失败返回 Err(.Io);写入失败不承诺回滚。
|
|
|
|
type .{ .Error, .Status, .make } = @import("error.spl");
|
|
type .{ .ByteView } = @import("byte_view.spl");
|
|
type .{ .ByteBuffer } = @import("byte_buffer.spl");
|
|
type File = @import("../sys/file.spl");
|
|
type Mem = @import("../sys/mem.spl");
|
|
|
|
// 把 view 复制到以 NUL 结尾的临时缓冲,返回其 data(归属 tmp,调用后勿再用)。
|
|
fn cstr(tmp: *ByteBuffer, view: ByteView): *u8 {
|
|
ByteBuffer.clear(tmp);
|
|
if view.len != 0 {
|
|
match ByteBuffer.append(tmp, ByteView.as_slice(view)) {
|
|
.Ok => {},
|
|
.Err(error) => { ret null; },
|
|
}
|
|
}
|
|
match ByteBuffer.append_byte(tmp, '\0') {
|
|
.Ok => {},
|
|
.Err(error) => { ret null; },
|
|
}
|
|
ret tmp.storage.data;
|
|
}
|
|
|
|
fn open_cstr(path: ByteView, mode: ByteView): File {
|
|
var pbuf = ByteBuffer.new();
|
|
var mbuf = ByteBuffer.new();
|
|
var p = cstr(&pbuf, path);
|
|
var m = cstr(&mbuf, mode);
|
|
var f = File { .handle = null };
|
|
if p != null && m != null { f = File.open(p, m); }
|
|
ByteBuffer.drop(&pbuf);
|
|
ByteBuffer.drop(&mbuf);
|
|
ret f;
|
|
}
|
|
|
|
// 反复写入直到完成。发生错误时文件可能已经包含前缀,不承诺回滚。
|
|
fn write_all(f: *File, bytes: []u8): Status {
|
|
if f == null || !File.valid(f) { ret .Err(make(.Io)); }
|
|
if bytes.len != 0 && bytes.ptr == null { ret .Err(make(.InvalidArgument)); }
|
|
var total: usize = 0;
|
|
while total < bytes.len {
|
|
var written = File.write(f, bytes.ptr + total, (bytes.len - total) as u64);
|
|
if written == 0 || written > (bytes.len - total) as u64 {
|
|
ret .Err(make(.Io));
|
|
}
|
|
total = total + written as usize;
|
|
}
|
|
ret .Ok;
|
|
}
|
|
|
|
fn print(bytes: []u8): Status {
|
|
var out = File.stdout();
|
|
ret write_all(&out, bytes);
|
|
}
|
|
|
|
fn println(bytes: []u8): Status {
|
|
match print(bytes) {
|
|
.Ok => {},
|
|
.Err(error) => { ret .Err(error); },
|
|
}
|
|
ret print("\n");
|
|
}
|
|
|
|
fn eprint(bytes: []u8): Status {
|
|
var err = File.stderr();
|
|
ret write_all(&err, bytes);
|
|
}
|
|
|
|
// 仅供已经隔离的临时存储使用;失败时可能写入目标前缀。
|
|
fn read_fully(f: *File, data: *u8, len: usize): Status {
|
|
var total: usize = 0;
|
|
while total < len {
|
|
var got = File.read(f, data + total, len - total);
|
|
if got == 0 || got > len - total { ret .Err(make(.Io)); }
|
|
total = total + got;
|
|
}
|
|
ret .Ok;
|
|
}
|
|
|
|
// 精确读取 bytes.len 字节。短读返回 Err(.Io),失败保持 bytes 原内容不变。
|
|
fn read_exact(f: *File, bytes: []u8): Status {
|
|
if f == null || !File.valid(f) { ret .Err(make(.Io)); }
|
|
if bytes.len == 0 { ret .Ok; }
|
|
if bytes.ptr == null { ret .Err(make(.InvalidArgument)); }
|
|
|
|
var temporary = ByteBuffer.new();
|
|
defer { ByteBuffer.drop(&temporary); }
|
|
match ByteBuffer.reserve(&temporary, bytes.len) {
|
|
.Ok => {},
|
|
.Err(error) => { ret .Err(error); },
|
|
}
|
|
match read_fully(f, temporary.storage.data, bytes.len) {
|
|
.Ok => {},
|
|
.Err(error) => { ret .Err(error); },
|
|
}
|
|
Mem.copy(bytes.ptr as *any, temporary.storage.data as *any, bytes.len);
|
|
ret .Ok;
|
|
}
|
|
|
|
// 读取整个普通文件到 out。打开、短读或关闭失败时保持 out 不变。
|
|
fn read_all(path: ByteView, out: *ByteBuffer): Status {
|
|
if out == null { ret .Err(make(.InvalidArgument)); }
|
|
var f = open_cstr(path, ByteView.from_slice("rb"));
|
|
if !File.valid(&f) { ret .Err(make(.Io)); }
|
|
|
|
var sz = File.size(&f);
|
|
var fresh = ByteBuffer.new();
|
|
if sz != 0 {
|
|
match ByteBuffer.reserve(&fresh, sz) {
|
|
.Ok => {},
|
|
.Err(error) => { File.close(&f); ByteBuffer.drop(&fresh); ret .Err(error); },
|
|
}
|
|
match read_fully(&f, fresh.storage.data, sz) {
|
|
.Ok => {},
|
|
.Err(error) => { File.close(&f); ByteBuffer.drop(&fresh); ret .Err(error); },
|
|
}
|
|
fresh.storage.len = sz;
|
|
}
|
|
if File.close(&f) != 0 { ByteBuffer.drop(&fresh); ret .Err(make(.Io)); }
|
|
ByteBuffer.drop(out);
|
|
out.* = ByteBuffer.take(&fresh);
|
|
ret .Ok;
|
|
}
|
|
|
|
|
|
fn view(text: []u8): ByteView {
|
|
ret ByteView.from_slice(text);
|
|
}
|
|
|
|
fn expect_ok(result: Status) {
|
|
match result {
|
|
.Ok => {},
|
|
.Err(error) => { @assert(false, "unexpected io error"); },
|
|
}
|
|
}
|
|
|
|
fn expect_io(result: Status) {
|
|
match result {
|
|
.Ok => { @assert(false, "expected io error"); },
|
|
.Err(error) => {
|
|
match error.kind {
|
|
.Io => {},
|
|
_ => { @assert(false, "expected Io kind"); },
|
|
}
|
|
},
|
|
}
|
|
}
|
|
|
|
fn test_roundtrip() {
|
|
var path = view("build/_io_roundtrip.tmp");
|
|
|
|
var f = open_cstr(path, view("wb"));
|
|
@assert(File.valid(&f), "open wb");
|
|
expect_ok(write_all(&f, "hello\nworld"));
|
|
@assert(File.close(&f) == 0, "close writer");
|
|
|
|
var out = ByteBuffer.new();
|
|
match read_all(path, &out) {
|
|
.Ok => {},
|
|
.Err(error) => { @assert(false, "read_all"); },
|
|
}
|
|
var got = ByteBuffer.as_slice(out);
|
|
@assert(got.len == 11, "roundtrip length");
|
|
@assert(got.ptr[0] == 'h' && got.ptr[5] == '\n' && got.ptr[10] == 'd', "roundtrip bytes");
|
|
ByteBuffer.drop(&out);
|
|
}
|
|
|
|
fn test_append_and_missing() {
|
|
var path = view("build/_io_append.tmp");
|
|
|
|
var f = open_cstr(path, view("wb"));
|
|
@assert(File.valid(&f), "open wb");
|
|
expect_ok(write_all(&f, "ab"));
|
|
@assert(File.close(&f) == 0, "close initial writer");
|
|
|
|
f = open_cstr(path, view("ab"));
|
|
@assert(File.valid(&f), "open ab");
|
|
expect_ok(write_all(&f, "cd"));
|
|
@assert(File.close(&f) == 0, "close append writer");
|
|
|
|
var out = ByteBuffer.new();
|
|
match read_all(path, &out) {
|
|
.Ok => {},
|
|
.Err(error) => { @assert(false, "read append"); },
|
|
}
|
|
var got = ByteBuffer.as_slice(out);
|
|
@assert(got.len == 4 && got.ptr[2] == 'c' && got.ptr[3] == 'd', "append content");
|
|
|
|
// 不存在的文件
|
|
match read_all(view("build/_io_no_such_file.tmp"), &out) {
|
|
.Ok => { @assert(false, "missing should fail"); },
|
|
.Err(error) => {
|
|
match error.kind { .Io => {}, _ => { @assert(false, "error kind"); } }
|
|
},
|
|
}
|
|
got = ByteBuffer.as_slice(out);
|
|
@assert(got.len == 4 && got.ptr[0] == 'a' && got.ptr[3] == 'd',
|
|
"failed read_all preserves output");
|
|
ByteBuffer.drop(&out);
|
|
}
|
|
|
|
fn test_read_exact_and_errors() {
|
|
var path = view("build/_io_exact.tmp");
|
|
var f = open_cstr(path, view("wb"));
|
|
@assert(File.valid(&f));
|
|
expect_ok(write_all(&f, "xyz"));
|
|
@assert(File.close(&f) == 0);
|
|
|
|
f = open_cstr(path, view("rb"));
|
|
@assert(File.valid(&f));
|
|
var too_long: [4]u8 = .[9, 8, 7, 6];
|
|
expect_io(read_exact(&f, too_long[0..4]));
|
|
@assert(too_long[0] == 9 && too_long[1] == 8 &&
|
|
too_long[2] == 7 && too_long[3] == 6,
|
|
"short read preserves destination");
|
|
@assert(File.close(&f) == 0);
|
|
|
|
f = open_cstr(path, view("rb"));
|
|
@assert(File.valid(&f));
|
|
var exact: [3]u8 = .[0, 0, 0];
|
|
expect_ok(read_exact(&f, exact[0..3]));
|
|
@assert(exact[0] == 'x' && exact[1] == 'y' && exact[2] == 'z');
|
|
@assert(File.close(&f) == 0);
|
|
|
|
var invalid = File { .handle = null };
|
|
expect_io(write_all(&invalid, "x"));
|
|
expect_io(read_exact(&invalid, exact[0..3]));
|
|
}
|
|
|
|
fn main() {
|
|
test_roundtrip();
|
|
test_append_and_missing();
|
|
test_read_exact_and_errors();
|
|
expect_ok(println("io: ok"));
|
|
}
|