feat: add bounded read-to-end and compatible file paths
This commit is contained in:
@@ -29,6 +29,8 @@ main proc() int {
|
|||||||
|
|
||||||
this prints `42`. `main` must be a top-level function with no parameters; nonvoid results are printed, not used as process exit statuses. the current macOS bootstrap obtains arguments from the native startup environment.
|
this prints `42`. `main` must be a top-level function with no parameters; nonvoid results are printed, not used as process exit statuses. the current macOS bootstrap obtains arguments from the native startup environment.
|
||||||
|
|
||||||
|
source loading uses the supplied `io.Io` for opening, reading, and closing files. `io.open` accepts a borrowed zero-terminated pointer, including paths returned by `process.argument`. `io.read_all(allocator, reader, maximum)` reads to EOF into a caller-owned `ArrayList(u8)`; release it with `arraylist.deinit`. it frees partial storage on error and consumes one extra byte to distinguish an exact-size input from a limit violation. the driver maps `limit_exceeded` to `source_too_large` at its `u32` source-size limit.
|
||||||
|
|
||||||
the parser supports arithmetic expressions with nested parenthesized groups, such as `a + (b - c) * 2 / 3`. groups control precedence without adding AST nodes; inner expression spans exclude the grouping parentheses. parsing accepts integer division syntax, but type checking requires float operands for `/`.
|
the parser supports arithmetic expressions with nested parenthesized groups, such as `a + (b - c) * 2 / 3`. groups control precedence without adding AST nodes; inner expression spans exclude the grouping parentheses. parsing accepts integer division syntax, but type checking requires float operands for `/`.
|
||||||
|
|
||||||
function calls are postfix expressions: `f()`, `f(a, b)`, and `factory()(x)`. calls bind tighter than unary and binary arithmetic and can appear in initializers or as expression statements inside function bodies. argument lists allow a trailing comma; newlines remain statement boundaries. type checking validates callable values, argument counts and argument types.
|
function calls are postfix expressions: `f()`, `f(a, b)`, and `factory()(x)`. calls bind tighter than unary and binary arithmetic and can appear in initializers or as expression statements inside function bodies. argument lists allow a trailing comma; newlines remain statement boundaries. type checking validates callable values, argument counts and argument types.
|
||||||
|
|||||||
+8
-31
@@ -1,4 +1,3 @@
|
|||||||
import "@ffi/c"
|
|
||||||
import "@std/mem"
|
import "@std/mem"
|
||||||
import "@std/arraylist"
|
import "@std/arraylist"
|
||||||
import "@std/io"
|
import "@std/io"
|
||||||
@@ -21,6 +20,7 @@ test import "@std/arraylist"
|
|||||||
test import "@std/hashmap"
|
test import "@std/hashmap"
|
||||||
test import "@std/strmap"
|
test import "@std/strmap"
|
||||||
test import "@std/meta"
|
test import "@std/meta"
|
||||||
|
test import "@std/io"
|
||||||
test import "@source/diagnostics"
|
test import "@source/diagnostics"
|
||||||
test import "@source/strpool"
|
test import "@source/strpool"
|
||||||
test import "@source/lexer"
|
test import "@source/lexer"
|
||||||
@@ -102,41 +102,18 @@ argument_text proc(pointer [*;0]u8) []u8 {
|
|||||||
|
|
||||||
@hide:file
|
@hide:file
|
||||||
read_source proc(allocator mem.Allocator, runtime io.Io, path [*;0]u8) arraylist.ArrayList(u8) ! DriverError {
|
read_source proc(allocator mem.Allocator, runtime io.Io, path [*;0]u8) arraylist.ArrayList(u8) ! DriverError {
|
||||||
# bootstrap cannot construct a dynamic sentinel slice from argv's sentinel pointer.
|
file :: try io.open(runtime, path, .read_only)
|
||||||
descriptor := c_int(-1)
|
|
||||||
while true {
|
|
||||||
descriptor = c.open(ptrcast!(path), c.O_RDONLY)
|
|
||||||
if (descriptor >= 0) break
|
|
||||||
if (c.__error()?^ != c.EINTR) return .open_failed
|
|
||||||
}
|
|
||||||
file :: io.File{ io = runtime, handle = io.Handle{ file_desc = descriptor } }
|
|
||||||
closing := false
|
closing := false
|
||||||
defer {
|
defer {
|
||||||
if (!closing) io.close(file) catch |_| {}
|
if (!closing) io.close(file) catch |_| {}
|
||||||
}
|
}
|
||||||
bytes := arraylist.init(u8, allocator)
|
bytes := io.read_all(allocator, io.reader(file), usize(maxval!(u32))) catch |err| {
|
||||||
|
match err {
|
||||||
|
.limit_exceeded: return .source_too_large
|
||||||
|
else: return err
|
||||||
|
}
|
||||||
|
}
|
||||||
errdefer arraylist.deinit(&bytes)
|
errdefer arraylist.deinit(&bytes)
|
||||||
input :: io.reader(file)
|
|
||||||
maximum :: usize(maxval!(u32))
|
|
||||||
while true {
|
|
||||||
length :: bytes.items.len
|
|
||||||
if (length == maximum) {
|
|
||||||
probe [1]mut u8 := undefined
|
|
||||||
if (try io.read(input, (&probe)[..]) != 0) return .source_too_large
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if (length == bytes.capacity) {
|
|
||||||
growth := usize(4096)
|
|
||||||
if (growth > maximum - length) growth = maximum - length
|
|
||||||
try arraylist.reserve(&bytes, length + growth)
|
|
||||||
}
|
|
||||||
storage :: bytes.items.ptr[..bytes.capacity]
|
|
||||||
end := bytes.capacity
|
|
||||||
if (end > maximum) end = maximum
|
|
||||||
count :: try io.read(input, storage[length..end])
|
|
||||||
if (count == 0) break
|
|
||||||
bytes.items = storage[..length + count]
|
|
||||||
}
|
|
||||||
closing = true
|
closing = true
|
||||||
try io.close(file)
|
try io.close(file)
|
||||||
return bytes
|
return bytes
|
||||||
|
|||||||
+4
-4
@@ -19,8 +19,8 @@ CloseError :: enum {
|
|||||||
close_failed
|
close_failed
|
||||||
}
|
}
|
||||||
|
|
||||||
#! open a caller-owned handle; release it with close.
|
#! open a caller-owned handle from a borrowed zero-terminated path; release it with close.
|
||||||
open proc(io Io, path [;0]u8, mode FileMode) File ! OpenError {
|
open proc(io Io, path [*;0]u8, mode FileMode) File ! OpenError {
|
||||||
handle Handle :: try io.vtable.open(io.context, path, mode)
|
handle Handle :: try io.vtable.open(io.context, path, mode)
|
||||||
return File{ io = io, handle = handle }
|
return File{ io = io, handle = handle }
|
||||||
}
|
}
|
||||||
@@ -83,7 +83,7 @@ system_write proc(_ ?@mut anyopaque, handle Handle, bytes []u8) usize ! WriteErr
|
|||||||
}
|
}
|
||||||
|
|
||||||
@hide:file
|
@hide:file
|
||||||
system_open proc(_ ?@mut anyopaque, path [;0]u8, mode FileMode) Handle ! OpenError {
|
system_open proc(_ ?@mut anyopaque, path [*;0]u8, mode FileMode) Handle ! OpenError {
|
||||||
flags := c.O_RDONLY
|
flags := c.O_RDONLY
|
||||||
match mode {
|
match mode {
|
||||||
.read_only: flags = c.O_RDONLY
|
.read_only: flags = c.O_RDONLY
|
||||||
@@ -91,7 +91,7 @@ system_open proc(_ ?@mut anyopaque, path [;0]u8, mode FileMode) Handle ! OpenErr
|
|||||||
.read_write: flags = c.O_RDWR
|
.read_write: flags = c.O_RDWR
|
||||||
}
|
}
|
||||||
while true {
|
while true {
|
||||||
fd c_int :: c.open(ptrcast!(path.ptr), flags)
|
fd c_int :: c.open(ptrcast!(path), flags)
|
||||||
if (fd >= 0) return Handle{ file_desc = fd }
|
if (fd >= 0) return Handle{ file_desc = fd }
|
||||||
if (c.__error()?^ != c.EINTR) return .open_failed
|
if (c.__error()?^ != c.EINTR) return .open_failed
|
||||||
}
|
}
|
||||||
|
|||||||
+36
-1
@@ -1,5 +1,6 @@
|
|||||||
import "@ffi/c"
|
import "@ffi/c"
|
||||||
import "@std/mem"
|
import "@std/mem"
|
||||||
|
import "@std/arraylist"
|
||||||
# typeinfo! requires this import even without a qualified meta reference.
|
# typeinfo! requires this import even without a qualified meta reference.
|
||||||
import "@std/meta"
|
import "@std/meta"
|
||||||
|
|
||||||
@@ -8,6 +9,12 @@ ReadError :: enum {
|
|||||||
read_failed
|
read_failed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ReadLimitError :: enum {
|
||||||
|
limit_exceeded
|
||||||
|
}
|
||||||
|
|
||||||
|
ReadAllError :: alias (mem.AllocError | ReadError | ReadLimitError)
|
||||||
|
|
||||||
WriteError :: enum {
|
WriteError :: enum {
|
||||||
not_open_for_writing
|
not_open_for_writing
|
||||||
write_failed
|
write_failed
|
||||||
@@ -34,7 +41,7 @@ Io :: struct {
|
|||||||
IoVTable :: struct {
|
IoVTable :: struct {
|
||||||
read @proc(context ?@mut anyopaque, handle Handle, buffer []mut u8) usize ! ReadError
|
read @proc(context ?@mut anyopaque, handle Handle, buffer []mut u8) usize ! ReadError
|
||||||
write @proc(context ?@mut anyopaque, handle Handle, bytes []u8) usize ! WriteError
|
write @proc(context ?@mut anyopaque, handle Handle, bytes []u8) usize ! WriteError
|
||||||
open @proc(context ?@mut anyopaque, path [;0]u8, mode FileMode) Handle ! OpenError
|
open @proc(context ?@mut anyopaque, path [*;0]u8, mode FileMode) Handle ! OpenError
|
||||||
close @proc(context ?@mut anyopaque, handle Handle) void ! CloseError
|
close @proc(context ?@mut anyopaque, handle Handle) void ! CloseError
|
||||||
stdin @proc(context ?@mut anyopaque) Handle
|
stdin @proc(context ?@mut anyopaque) Handle
|
||||||
stdout @proc(context ?@mut anyopaque) Handle
|
stdout @proc(context ?@mut anyopaque) Handle
|
||||||
@@ -64,6 +71,34 @@ read proc(input Reader, buffer []mut u8) usize ! ReadError {
|
|||||||
return count
|
return count
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#! read to EOF, accepting at most `maximum` bytes; the reader remains caller-owned.
|
||||||
|
#! exceeding the limit consumes one extra byte and returns limit_exceeded.
|
||||||
|
#! returned storage retains `allocator`; release it with arraylist.deinit.
|
||||||
|
#! all allocated storage is released on error.
|
||||||
|
read_all proc(allocator mem.Allocator, input Reader, maximum usize) arraylist.ArrayList(u8) ! ReadAllError {
|
||||||
|
bytes := arraylist.init(u8, allocator)
|
||||||
|
errdefer arraylist.deinit(&bytes)
|
||||||
|
while true {
|
||||||
|
length :: bytes.items.len
|
||||||
|
if (length == maximum) {
|
||||||
|
probe [1]mut u8 := undefined
|
||||||
|
if (try read(input, (&probe)[..]) != 0) return .limit_exceeded
|
||||||
|
return bytes
|
||||||
|
}
|
||||||
|
if (length == bytes.capacity) {
|
||||||
|
growth := usize(4096)
|
||||||
|
if (growth > maximum - length) growth = maximum - length
|
||||||
|
try arraylist.reserve(&bytes, length + growth)
|
||||||
|
}
|
||||||
|
storage :: bytes.items.ptr[..bytes.capacity]
|
||||||
|
end := bytes.capacity
|
||||||
|
if (end > maximum) end = maximum
|
||||||
|
count :: try read(input, storage[length..end])
|
||||||
|
if (count == 0) return bytes
|
||||||
|
bytes.items = storage[..length + count]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
write proc(output Writer, bytes []u8) usize ! WriteError {
|
write proc(output Writer, bytes []u8) usize ! WriteError {
|
||||||
if (bytes.len == 0) return 0
|
if (bytes.len == 0) return 0
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,127 @@
|
|||||||
|
import "@std/mem"
|
||||||
|
import "@std/arraylist"
|
||||||
|
import "@std/testing"
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
Input :: struct {
|
||||||
|
bytes []u8
|
||||||
|
offset usize = 0
|
||||||
|
chunk usize = 137
|
||||||
|
fail_at usize = maxval!(usize)
|
||||||
|
remaining usize = maxval!(usize)
|
||||||
|
live usize = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
input_state proc(context ?@mut anyopaque) @mut Input {
|
||||||
|
if (context) |pointer| return ptrcast!(pointer)
|
||||||
|
unreachable
|
||||||
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
input_read proc(context ?@mut anyopaque, _ Handle, buffer []mut u8) usize ! ReadError {
|
||||||
|
state :: input_state(context)
|
||||||
|
if (state.offset >= state.fail_at) return .read_failed
|
||||||
|
count := state.bytes.len - state.offset
|
||||||
|
if (count > buffer.len) count = buffer.len
|
||||||
|
if (count > state.chunk) count = state.chunk
|
||||||
|
memcopy!(buffer[..count], state.bytes[state.offset..state.offset + count])
|
||||||
|
state.offset += count
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
input_reader proc(state @mut Input) Reader {
|
||||||
|
return Reader{ context = state, handle = Handle{ file_desc = -1 }, read = input_read }
|
||||||
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
tracked_alloc proc(context ?@mut anyopaque, size usize, alignment usize) ?*mut u8 {
|
||||||
|
state :: input_state(context)
|
||||||
|
if (state.remaining == 0) return null
|
||||||
|
state.remaining -= 1
|
||||||
|
memory :: mem.raw_alloc(mem.c_allocator, size, alignment)
|
||||||
|
if (memory) |_| state.live += 1
|
||||||
|
return memory
|
||||||
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
tracked_realloc proc(context ?@mut anyopaque, memory ?*mut u8, old_size usize, new_size usize, alignment usize) ?*mut u8 {
|
||||||
|
state :: input_state(context)
|
||||||
|
if (state.remaining == 0) return null
|
||||||
|
state.remaining -= 1
|
||||||
|
resized :: mem.raw_realloc(mem.c_allocator, memory, old_size, new_size, alignment)
|
||||||
|
if (resized) |_| {
|
||||||
|
if (memory) |_| {} else state.live += 1
|
||||||
|
}
|
||||||
|
return resized
|
||||||
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
tracked_free proc(context ?@mut anyopaque, memory ?*mut u8, size usize, alignment usize) void {
|
||||||
|
state :: input_state(context)
|
||||||
|
if (memory) |_| state.live -= 1
|
||||||
|
mem.raw_free(mem.c_allocator, memory, size, alignment)
|
||||||
|
}
|
||||||
|
|
||||||
|
@hide:file
|
||||||
|
tracked_vtable :: mem.AllocatorVTable{ alloc = tracked_alloc, realloc = tracked_realloc, free = tracked_free }
|
||||||
|
|
||||||
|
read_all_preserves_short_reads_across_buffer_growth test {
|
||||||
|
source [9000]mut u8 := undefined
|
||||||
|
for (0..source.len) |i| source[i] = u8(rem!(i, 251))
|
||||||
|
state := Input{ bytes = source[..] }
|
||||||
|
bytes := try read_all(mem.c_allocator, input_reader(&state), maxval!(usize))
|
||||||
|
defer arraylist.deinit(&bytes)
|
||||||
|
try testing.expect_equal(source[..], bytes.items)
|
||||||
|
}
|
||||||
|
|
||||||
|
read_all_accepts_exact_limits_and_probes_for_excess test {
|
||||||
|
for (usize(0)..4) |maximum| {
|
||||||
|
state := Input{ bytes = "abc"[..maximum], chunk = 1 }
|
||||||
|
bytes := try read_all(mem.c_allocator, input_reader(&state), maximum)
|
||||||
|
defer arraylist.deinit(&bytes)
|
||||||
|
try testing.expect_equal(state.bytes, bytes.items)
|
||||||
|
|
||||||
|
excess := Input{ bytes = "abcd", chunk = 1 }
|
||||||
|
unexpected := read_all(mem.c_allocator, input_reader(&excess), maximum) catch |err| {
|
||||||
|
try testing.expect(err == .limit_exceeded)
|
||||||
|
try testing.expect_equal(maximum + 1, excess.offset)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
arraylist.deinit(&unexpected)
|
||||||
|
try testing.expect(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
read_all_releases_partial_storage_on_read_failure_and_limit_excess test {
|
||||||
|
for (0..2) |failure| {
|
||||||
|
state := Input{ bytes = "abcdef", chunk = 2 }
|
||||||
|
if (failure == 0) state.fail_at = 2
|
||||||
|
allocator :: mem.Allocator{ context = &state, vtable = &tracked_vtable }
|
||||||
|
unexpected := read_all(allocator, input_reader(&state), 4) catch |err| {
|
||||||
|
expected ReadAllError :: if (failure == 0) .read_failed else .limit_exceeded
|
||||||
|
try testing.expect_equal(expected, err)
|
||||||
|
try testing.expect_equal(0, state.live)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
arraylist.deinit(&unexpected)
|
||||||
|
try testing.expect(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
read_all_releases_storage_at_each_failed_growth test {
|
||||||
|
source [9000]mut u8 := undefined
|
||||||
|
mem.set(source[..], u8('x'))
|
||||||
|
for (usize(0)..3) |remaining| {
|
||||||
|
state := Input{ bytes = source[..], remaining = remaining }
|
||||||
|
allocator :: mem.Allocator{ context = &state, vtable = &tracked_vtable }
|
||||||
|
unexpected := read_all(allocator, input_reader(&state), source.len) catch |err| {
|
||||||
|
try testing.expect(err == .out_of_memory)
|
||||||
|
try testing.expect_equal(0, state.live)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
arraylist.deinit(&unexpected)
|
||||||
|
try testing.expect(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user