diff --git a/lib/memio/memio.ww b/lib/memio/memio.ww index 919037d1..7fa5028f 100644 --- a/lib/memio/memio.ww +++ b/lib/memio/memio.ww @@ -54,6 +54,7 @@ import errors; import io; import os; import rt; +import types; // stream — Hare's memio::stream (ref/hare/memio/stream.ha:18). `vt` at // offset 0 for the intrusive stream→io.stream cast (`&s.vt`) and the @@ -122,6 +123,26 @@ export fn dynamicfrom(buf: []u8) stream = { return r; }; +// memiomove copies n bytes correctly for disjoint or overlapping regions. +// Copy backwards whenever the destination starts above the source; that is +// the only overlap direction a forward loop can corrupt. +fn memiomove(dst: *u8, src: *u8, n: i32) void = { + if (n <= 0 || dst == src) { return; }; + let i: i32 = 0; + if ((dst: uintptr) > (src: uintptr)) { + i = n; + for (i > 0) { + i -= 1; + dst[i] = src[i]; + }; + } else { + for (i < n) { + dst[i] = src[i]; + i += 1; + }; + }; +}; + // readfn — recover the stream from the io.stream's `*vtable` via the // intrusive offset-0 cast. Single fn over the common header (Hare's // single `read` at ref/hare/memio/stream.ha:103); fixed and dynamic @@ -135,11 +156,7 @@ fn readfn(s: io.stream, buf: []u8) (size | io.eof | io.error) = { let avail: i32 = m.len - m.pos; let n: i32 = buf.len; if (avail < n) { n = avail; }; - let i: i32 = 0; - for (i < n) { - buf[i] = m.ptr[m.pos + i]; - i += 1; - }; + memiomove(buf.ptr, m.ptr + (m.pos: u64), n); m.pos += n; return n: size; }; @@ -166,7 +183,7 @@ fn seekfn(s: io.stream, off: io.off, w: io.whence) (io.off | io.error) = { case io.whence.END: start = m.len: i64; }; if (n < 0) { - if (start < -n) { + if (n < -start) { let v: errors.invalid; let e: io.error = v; return e; @@ -193,24 +210,35 @@ fn fixedwrite(s: io.stream, buf: []u8) (size | io.error) = { let space: i32 = m.len - m.pos; let n: i32 = buf.len; if (space < n) { n = space; }; - let i: i32 = 0; - for (i < n) { - m.ptr[m.pos + i] = buf[i]; - i += 1; - }; + memiomove(m.ptr + (m.pos: u64), buf.ptr, n); m.pos += n; return n: size; }; fn dynamicwrite(s: io.stream, buf: []u8) (size | io.error) = { let m: *stream = s: *stream; - let need: i32 = m.pos + buf.len; - if (need > m.cap) { dynamicgrow(m, need); }; - let i: i32 = 0; - for (i < buf.len) { - m.ptr[m.pos + i] = buf[i]; - i += 1; + if (buf.len > types.I32_MAX - m.pos) { + let nm: nomem; + let e: io.error = nm; + return e; }; + let need: i32 = m.pos + buf.len; + let aliased: bool = false; + let srcoff: i32 = 0; + if (m.cap > 0 && (buf.ptr: uintptr) >= (m.ptr: uintptr)) { + let delta: uintptr = (buf.ptr: uintptr) - (m.ptr: uintptr); + if (delta < (m.cap: uintptr)) { + assert(buf.len <= m.cap - (delta: i32), + "memio.dynamicwrite: aliased source exceeds buffer"); + aliased = true; + srcoff = delta: i32; + }; + }; + if (need > m.cap) { + dynamicgrow(m, need); + if (aliased) { buf.ptr = m.ptr + (srcoff: u64); }; + }; + memiomove(m.ptr + (m.pos: u64), buf.ptr, buf.len); m.pos += buf.len; if (m.pos > m.len) { m.len = m.pos; }; return buf.len: size; @@ -233,13 +261,15 @@ fn dynamicclose(s: io.stream) (void | io.error) = { fn dynamicgrow(d: *stream, need: i32) void = { let newcap: i32 = d.cap; if (newcap < 8) { newcap = 8; }; - for (newcap < need) { newcap *= 2; }; - let nbuf: *u8 = rt.malloc(newcap: u64): *u8; - let i: i32 = 0; - for (i < d.len) { - nbuf[i] = d.ptr[i]; - i += 1; + for (newcap < need) { + if (newcap > types.I32_MAX / 2) { + newcap = need; + break; + }; + newcap *= 2; }; + let nbuf: *u8 = rt.malloc(newcap: u64): *u8; + memiomove(nbuf, d.ptr, d.len); if (d.cap > 0) { os.free(d.ptr: *void, d.cap: u64); }; d.ptr = nbuf; d.cap = newcap; @@ -256,6 +286,7 @@ export fn string(s: *stream) str = { let r: str; r.ptr = s.ptr; r.len = s.pos; + r.cap = r.len; return r; }; @@ -266,6 +297,7 @@ export fn buffer(s: *stream) []u8 = { let r: []u8; r.ptr = s.ptr; r.len = s.pos; + r.cap = r.len; return r; }; @@ -285,6 +317,7 @@ export fn reset(s: *stream) void = { // Mirrors ref/hare/memio/stream.ha:94 borrowedread(st: *stream, amt). // `amt: i32` (not Hare's `size`) per the i32-index convention. export fn borrowedread(s: *stream, amt: i32) ([]u8 | io.eof) = { + assert(amt >= 0, "memio.borrowedread: amount must not be negative"); if (s.len - s.pos < amt) { let e: io.eof; return e; @@ -292,6 +325,7 @@ export fn borrowedread(s: *stream, amt: i32) ([]u8 | io.eof) = { let r: []u8; r.ptr = s.ptr + (s.pos: u64); r.len = amt; + r.cap = r.len; s.pos += amt; return r; }; diff --git a/lib/memio/memio_test.ww b/lib/memio/memio_test.ww index 81ec0ece..ecdf84ef 100644 --- a/lib/memio/memio_test.ww +++ b/lib/memio/memio_test.ww @@ -11,6 +11,7 @@ import bytes; import errors; import io; import memio; +import test; fn putstr(s: str, into: []u8, off: i32) i32 = { let i: i32 = 0; @@ -69,6 +70,21 @@ fn putstr(s: str, into: []u8, off: i32) i32 = { match (c) { case void => {}; case let e: io.error => abort(); }; }; +@test fn fixedreadoverlap() void = { + let arr: [4]u8; + let _: i32 = putstr("ABCD", arr[0:4], 0); + let st: memio.stream = memio.fixed(arr[0:4]); + let r: (size | io.eof | io.error) = io.read(&st.vt, arr[1:4]); + match (r) { + case let n: size => assert(n: i32 == 3); + case io.eof => abort(); + case let e: io.error => abort(); + }; + let want: [4]u8; + let _: i32 = putstr("AABC", want[0:4], 0); + assert(bytes.equal(arr[0:4], want[0:4])); +}; + @test fn fixedwritecases() void = { let dst: [16]u8; let st: memio.stream = memio.fixed(dst[0:16]); @@ -158,6 +174,22 @@ fn putstr(s: str, into: []u8, off: i32) i32 = { }; }; +@test fn fixedwriteoverlap() void = { + let arr: [4]u8; + let _: i32 = putstr("ABCD", arr[0:4], 0); + let st: memio.stream = memio.fixed(arr[0:4]); + let sk: (io.off | io.error) = io.seek(&st.vt, 1, io.whence.SET); + match (sk) { case let o: io.off => {}; case let e: io.error => abort(); }; + let r: (size | io.error) = io.write(&st.vt, arr[0:3]); + match (r) { + case let n: size => assert(n: i32 == 3); + case let e: io.error => abort(); + }; + let want: [4]u8; + let _: i32 = putstr("AABC", want[0:4], 0); + assert(bytes.equal(arr[0:4], want[0:4])); +}; + // Drive grow 0 → 8 → 16 → 32 by writing sized chunks. Verify // accumulated `pos` after each step. @test fn dynamicgrowcases() void = { @@ -198,6 +230,47 @@ fn putstr(s: str, into: []u8, off: i32) i32 = { match (c) { case void => {}; case let e: io.error => abort(); }; }; +@test fn dynamicgrowaliasedwrite() void = { + let seed: []u8; + seed.ptr = nil; seed.len = 0; seed.cap = 0; + append(seed, 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H'); + let st: memio.stream = memio.dynamicfrom(seed); + let sk: (io.off | io.error) = io.seek(&st.vt, 0, io.whence.END); + match (sk) { case let o: io.off => {}; case let e: io.error => abort(); }; + let alias: []u8 = memio.buffer(&st); + let r: (size | io.error) = io.write(&st.vt, alias); + match (r) { + case let n: size => assert(n: i32 == 8); + case let e: io.error => abort(); + }; + let want: [16]u8; + let _: i32 = putstr("ABCDEFGHABCDEFGH", want[0:16], 0); + assert(alias.ptr != memio.buffer(&st).ptr); + assert(bytes.equal(memio.buffer(&st), want[0:16])); + let c: (void | io.error) = io.close(&st.vt); + match (c) { case void => {}; case let e: io.error => abort(); }; +}; + +@test fn dynamicwriteoverflow() void = { + let st: memio.stream = memio.dynamic(); + let one: [1]u8; + one[0] = 1u8; + let first: (size | io.error) = io.write(&st.vt, one[0:1]); + match (first) { case let n: size => {}; case let e: io.error => abort(); }; + let huge: []u8; + huge.ptr = nil; + huge.len = 2147483647; + huge.cap = huge.len; + let r: (size | io.error) = io.write(&st.vt, huge); + match (r) { + case let n: size => abort(); + case let e: io.error => assert(e is nomem); + }; + assert(memio.buffer(&st).len == 1); + let c: (void | io.error) = io.close(&st.vt); + match (c) { case void => {}; case let e: io.error => abort(); }; +}; + // op=0 writes `ln` bytes from a rolling source; op=1 resets and ignores // ln. After each row the accumulated len must equal `want`. @test fn dynamicreset() void = { @@ -267,6 +340,7 @@ fn putstr(s: str, into: []u8, off: i32) i32 = { case let v: []u8 => { assert(!(weof[i] != 0)); assert(!(v.len != amt[i])); + assert(v.cap == v.len); assert(!(v.len > 0 && v[0] != wf[i])); assert(!(v.len > 0 && v[v.len - 1] != wl[i])); }; @@ -276,6 +350,13 @@ fn putstr(s: str, into: []u8, off: i32) i32 = { }; }; +@test fn borrowedreadnegativeaborts() void = { + test.expectabort(); + let arr: [1]u8; + let st: memio.stream = memio.fixed(arr[0:1]); + let r: ([]u8 | io.eof) = memio.borrowedread(&st, -1); +}; + @test fn stringview() void = { let st: memio.stream = memio.dynamic(); let s: io.stream = &st.vt; @@ -303,6 +384,8 @@ fn putstr(s: str, into: []u8, off: i32) i32 = { match (r) { case let n: size => {}; case let e: io.error => abort(); }; let v: str = memio.string(&st); assert(!(v.len != want[i])); + assert(v.cap == v.len); + assert(memio.buffer(&st).cap == v.len); assert(!(v[0] != wf[i])); assert(!(v[v.len - 1] != wl[i])); i += 1; @@ -448,6 +531,24 @@ fn putstr(s: str, into: []u8, off: i32) i32 = { }; }; +@test fn seekminleavescursor() void = { + let arr: [8]u8; + let st: memio.stream = memio.fixed(arr[0:8]); + let set: (io.off | io.error) = io.seek(&st.vt, 3, io.whence.SET); + match (set) { case let o: io.off => {}; case let e: io.error => abort(); }; + let min: i64 = -9223372036854775807i64 - 1i64; + let r: (io.off | io.error) = io.seek(&st.vt, min, io.whence.CUR); + match (r) { + case let o: io.off => abort(); + case let e: io.error => assert(e is errors.invalid); + }; + let pos: (io.off | io.error) = io.tell(&st.vt); + match (pos) { + case let o: io.off => assert((o: i64) == 3); + case let e: io.error => abort(); + }; +}; + @test fn emptyseek() void = { let arr: [1]u8; arr[0] = 0u8;