diff --git a/buffer/buffer.mbt b/buffer/buffer.mbt index 9b9bd4517..33afa765d 100644 --- a/buffer/buffer.mbt +++ b/buffer/buffer.mbt @@ -41,24 +41,44 @@ struct Buffer { } ///| -/// Expand the buffer size if capacity smaller than required space. -fn Buffer::grow_if_necessary(self : Buffer, required : Int) -> Unit { - let start = if self.data.length() <= 0 { 1 } else { self.data.length() } +/// Compute the next capacity without allocating. Since appends never shrink the +/// buffer, `required < len` means the required-size calculation overflowed. +fn buffer_growth_capacity(current : Int, len : Int, required : Int) -> Int { + if required < len { + abort("Buffer capacity overflow") + } + let start = if current <= 0 { 1 } else { current } let enough_space = for space = start { if space >= required { break space } - continue space * 2 - } - if enough_space != self.data.length() { - let new_data = FixedArray::make_and_blit( - self.data, - allocate_len=enough_space, - init=b'\x00', - len=self.len, - ) - self.data = new_data + let next = space * 2 + if next <= space { + break required + } + continue next } + enough_space +} + +///| +/// Grow the buffer to at least `required`. Callers keep the capacity check on +/// their fast path and enter here only when growth or overflow handling is +/// needed. The buffer invariant `0 <= len <= data.length()` lets fixed-size +/// appends compare against the remaining capacity without overflowing. +fn Buffer::grow(self : Buffer, required : Int) -> Unit { + let new_capacity = buffer_growth_capacity( + self.data.length(), + self.len, + required, + ) + let new_data = FixedArray::make_and_blit( + self.data, + allocate_len=new_capacity, + init=b'\x00', + len=self.len, + ) + self.data = new_data } ///| @@ -164,7 +184,7 @@ pub fn Buffer::Buffer(size_hint? : Int = 0) -> Buffer { pub fn from_bytes(bytes : BytesView) -> Buffer { let val_len = bytes.length() let buf = Buffer(size_hint=val_len) - // inline write_bytes, skip grow_if_necessary check + // Inline write_bytes because the exact capacity is known. // SAFETY: known bytes size buf.data.blit_from_bytes(0, bytes.data(), bytes.start_offset(), val_len) buf.len = val_len @@ -176,7 +196,7 @@ pub fn from_bytes(bytes : BytesView) -> Buffer { pub fn from_array(arr : ArrayView[Byte]) -> Buffer { let buf = Buffer(size_hint=arr.length()) for byte in arr { - // inline write_byte, skip grow_if_necessary check + // Inline write_byte because the exact capacity is known. // SAFETY: known array size buf.data[buf.len] = byte buf.len += 1 @@ -189,10 +209,9 @@ pub fn from_array(arr : ArrayView[Byte]) -> Buffer { pub fn from_iter(iter : Iter[Byte]) -> Buffer { let buf = Buffer() for byte in iter; capacity = buf.data.length() { - // inline write_byte and grow_if_necessary - // only call grow_if_necessary when necessary + // Inline write_byte and keep growth off the fast path. let capacity = if buf.len == capacity { - buf.grow_if_necessary(capacity + 1) + buf.grow(capacity + 1) buf.data.length() } else { capacity @@ -220,7 +239,10 @@ pub impl Logger for Buffer ///| pub impl Logger for Buffer with fn write_string(self, value) { - self.grow_if_necessary(self.len + value.length() * 2) + let required = self.len + value.length() * 2 + if required > self.data.length() || required < self.len { + self.grow(required) + } self.data.blit_from_string(self.len, value, 0, value.length()) self.len += value.length() * 2 } @@ -250,7 +272,9 @@ pub impl Logger for Buffer with fn write_string(self, value) { /// } /// ``` pub fn Buffer::write_uint64_be(self : Buffer, value : UInt64) -> Unit { - self.grow_if_necessary(self.len + 8) + if self.data.length() - self.len < 8 { + self.grow(self.len + 8) + } let offset = self.len self.data[offset] = (value >> 56).to_byte() self.data[offset + 1] = (value >> 48).to_byte() @@ -287,7 +311,9 @@ pub fn Buffer::write_uint64_be(self : Buffer, value : UInt64) -> Unit { /// } /// ``` pub fn Buffer::write_uint64_le(self : Buffer, value : UInt64) -> Unit { - self.grow_if_necessary(self.len + 8) + if self.data.length() - self.len < 8 { + self.grow(self.len + 8) + } let offset = self.len self.data[offset] = value.to_byte() self.data[offset + 1] = (value >> 8).to_byte() @@ -377,7 +403,9 @@ pub fn Buffer::write_int64_le(self : Buffer, value : Int64) -> Unit { /// } /// ``` pub fn Buffer::write_uint_be(self : Buffer, value : UInt) -> Unit { - self.grow_if_necessary(self.len + 4) + if self.data.length() - self.len < 4 { + self.grow(self.len + 4) + } let offset = self.len self.data[offset] = (value >> 24).to_byte() self.data[offset + 1] = (value >> 16).to_byte() @@ -411,7 +439,9 @@ pub fn Buffer::write_uint_be(self : Buffer, value : UInt) -> Unit { /// } /// ``` pub fn Buffer::write_uint_le(self : Buffer, value : UInt) -> Unit { - self.grow_if_necessary(self.len + 4) + if self.data.length() - self.len < 4 { + self.grow(self.len + 4) + } let offset = self.len self.data[offset] = value.to_byte() self.data[offset + 1] = (value >> 8).to_byte() @@ -494,7 +524,9 @@ pub fn Buffer::write_int_le(self : Buffer, value : Int) -> Unit { /// } /// ``` pub fn Buffer::write_uint16_be(self : Buffer, value : UInt16) -> Unit { - self.grow_if_necessary(self.len + 2) + if self.data.length() - self.len < 2 { + self.grow(self.len + 2) + } let offset = self.len self.data[offset] = (value.to_int() >> 8).to_byte() self.data[offset + 1] = value.to_byte() @@ -526,7 +558,9 @@ pub fn Buffer::write_uint16_be(self : Buffer, value : UInt16) -> Unit { /// } /// ``` pub fn Buffer::write_uint16_le(self : Buffer, value : UInt16) -> Unit { - self.grow_if_necessary(self.len + 2) + if self.data.length() - self.len < 2 { + self.grow(self.len + 2) + } let offset = self.len self.data[offset] = value.to_byte() self.data[offset + 1] = (value.to_int() >> 8).to_byte() @@ -557,7 +591,9 @@ pub fn Buffer::write_uint16_le(self : Buffer, value : UInt16) -> Unit { /// } /// ``` pub fn Buffer::write_int16_be(self : Buffer, value : Int16) -> Unit { - self.grow_if_necessary(self.len + 2) + if self.data.length() - self.len < 2 { + self.grow(self.len + 2) + } let offset = self.len self.data[offset] = (value.to_int() >> 8).to_byte() self.data[offset + 1] = value.to_byte() @@ -588,7 +624,9 @@ pub fn Buffer::write_int16_be(self : Buffer, value : Int16) -> Unit { /// } /// ``` pub fn Buffer::write_int16_le(self : Buffer, value : Int16) -> Unit { - self.grow_if_necessary(self.len + 2) + if self.data.length() - self.len < 2 { + self.grow(self.len + 2) + } let offset = self.len self.data[offset] = value.to_byte() self.data[offset + 1] = (value.to_int() >> 8).to_byte() @@ -801,7 +839,10 @@ pub fn Buffer::write_bytes(self : Buffer, value : BytesView) -> Unit { /// ``` pub fn Buffer::write_bytesview(self : Buffer, value : BytesView) -> Unit { let val_len = value.length() - self.grow_if_necessary(self.len + val_len) + let required = self.len + val_len + if required > self.data.length() || required < self.len { + self.grow(required) + } self.data.blit_from_bytes( self.len, value.data(), @@ -817,25 +858,33 @@ pub fn Buffer::write_char_utf8(buf : Self, value : Char) -> Unit { let code = value.to_uint() match code { _..<0x80 => { - buf.grow_if_necessary(buf.len + 1) + if buf.len >= buf.data.length() { + buf.grow(buf.len + 1) + } buf.data[buf.len] = ((code & 0x7F) | 0x00).to_byte() buf.len += 1 } _..<0x0800 => { - buf.grow_if_necessary(buf.len + 2) + if buf.data.length() - buf.len < 2 { + buf.grow(buf.len + 2) + } buf.data[buf.len] = (((code >> 6) & 0x1F) | 0xC0).to_byte() buf.data[buf.len + 1] = ((code & 0x3F) | 0x80).to_byte() buf.len += 2 } _..<0x010000 => { - buf.grow_if_necessary(buf.len + 3) + if buf.data.length() - buf.len < 3 { + buf.grow(buf.len + 3) + } buf.data[buf.len] = (((code >> 12) & 0x0F) | 0xE0).to_byte() buf.data[buf.len + 1] = (((code >> 6) & 0x3F) | 0x80).to_byte() buf.data[buf.len + 2] = ((code & 0x3F) | 0x80).to_byte() buf.len += 3 } _..<0x110000 => { - buf.grow_if_necessary(buf.len + 4) + if buf.data.length() - buf.len < 4 { + buf.grow(buf.len + 4) + } buf.data[buf.len] = (((code >> 18) & 0x07) | 0xF0).to_byte() buf.data[buf.len + 1] = (((code >> 12) & 0x3F) | 0x80).to_byte() buf.data[buf.len + 2] = (((code >> 6) & 0x3F) | 0x80).to_byte() @@ -851,7 +900,9 @@ pub fn Buffer::write_char_utf8(buf : Self, value : Char) -> Unit { pub fn Buffer::write_char_utf16le(buf : Self, value : Char) -> Unit { let code = value.to_uint() if code < 0x10000 { - buf.grow_if_necessary(buf.len + 2) + if buf.data.length() - buf.len < 2 { + buf.grow(buf.len + 2) + } buf.data[buf.len + 0] = (code & 0xFF).to_byte() buf.data[buf.len + 1] = (code >> 8).to_byte() buf.len += 2 @@ -859,7 +910,9 @@ pub fn Buffer::write_char_utf16le(buf : Self, value : Char) -> Unit { let cp = code - 0x10000 let high = (cp >> 10) | 0xD800 let low = (cp & 0x3FF) | 0xDC00 - buf.grow_if_necessary(buf.len + 4) + if buf.data.length() - buf.len < 4 { + buf.grow(buf.len + 4) + } buf.data[buf.len + 0] = (high & 0xFF).to_byte() buf.data[buf.len + 1] = (high >> 8).to_byte() buf.data[buf.len + 2] = (low & 0xFF).to_byte() @@ -875,12 +928,16 @@ pub fn Buffer::write_char_utf16le(buf : Self, value : Char) -> Unit { pub fn Buffer::write_char_utf16be(buf : Self, value : Char) -> Unit { let code = value.to_uint() if code < 0x10000 { - buf.grow_if_necessary(buf.len + 2) + if buf.data.length() - buf.len < 2 { + buf.grow(buf.len + 2) + } buf.data[buf.len + 0] = (code >> 8).to_byte() buf.data[buf.len + 1] = (code & 0xFF).to_byte() buf.len += 2 } else if code < 0x110000 { - buf.grow_if_necessary(buf.len + 4) + if buf.data.length() - buf.len < 4 { + buf.grow(buf.len + 4) + } let cp = code - 0x10000 let high = (cp >> 10) | 0xD800 let low = (cp & 0x3FF) | 0xDC00 @@ -937,7 +994,10 @@ pub fn Buffer::write_string_utf8(buf : Self, string : StringView) -> Unit { #alias(write_stringview, deprecated="use write_string_utf16le instead") pub fn Buffer::write_string_utf16le(buf : Self, string : StringView) -> Unit { let len = string.length() - buf.grow_if_necessary(buf.len + len * 2) + let required = buf.len + len * 2 + if required > buf.data.length() || required < buf.len { + buf.grow(required) + } for code_unit in string.code_units(); j = buf.len { let c = code_unit.to_int().reinterpret_as_uint() buf.data[j] = (c & 0xff).to_byte() @@ -966,7 +1026,10 @@ pub fn Buffer::write_string_utf16le(buf : Self, string : StringView) -> Unit { /// ``` pub fn Buffer::write_string_utf16be(buf : Self, string : StringView) -> Unit { let len = string.length() - buf.grow_if_necessary(buf.len + len * 2) + let required = buf.len + len * 2 + if required > buf.data.length() || required < buf.len { + buf.grow(required) + } for code_unit in string.code_units(); j = buf.len { let c = code_unit.to_int().reinterpret_as_uint() buf.data[j + 1] = (c & 0xff).to_byte() @@ -986,7 +1049,10 @@ pub fn Buffer::write_string_utf16be(buf : Self, string : StringView) -> Unit { /// * `count` : The number of characters to write. Must be non-negative and /// `offset + count` must not exceed the length of the source string. pub impl Logger for Buffer with fn write_view(self : Buffer, value : StringView) -> Unit { - self.grow_if_necessary(self.len + value.length() * 2) + let required = self.len + value.length() * 2 + if required > self.data.length() || required < self.len { + self.grow(required) + } self.data.blit_from_string( self.len, value.data(), @@ -1005,7 +1071,9 @@ pub impl Logger for Buffer with fn write_view(self : Buffer, value : StringView) /// * `buffer` : The buffer to write to. /// * `char` : The character to be written. pub impl Logger for Buffer with fn write_char(self : Buffer, value : Char) -> Unit { - self.grow_if_necessary(self.len + 4) + if self.data.length() - self.len < 4 { + self.grow(self.len + 4) + } let inc = self.data.set_utf16le_char(self.len, value) self.len += inc } @@ -1034,7 +1102,9 @@ pub impl Logger for Buffer with fn write_char(self : Buffer, value : Char) -> Un /// } /// ``` pub fn Buffer::write_byte(self : Buffer, value : Byte) -> Unit { - self.grow_if_necessary(self.len + 1) + if self.len >= self.data.length() { + self.grow(self.len + 1) + } self.data[self.len] = value self.len += 1 } diff --git a/buffer/buffer_growth_wbtest.mbt b/buffer/buffer_growth_wbtest.mbt new file mode 100644 index 000000000..0ff58a966 --- /dev/null +++ b/buffer/buffer_growth_wbtest.mbt @@ -0,0 +1,36 @@ +// Copyright 2026 International Digital Economy Academy +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +///| +test "buffer growth capacity doubles while representable" { + inspect(buffer_growth_capacity(8, 8, 9), content="16") + inspect(buffer_growth_capacity(8, 8, 33), content="64") +} + +///| +test "buffer growth capacity falls back to the exact requirement" { + inspect( + buffer_growth_capacity(0x40000000, 0x40000000, 0x40000001), + content="1073741825", + ) + inspect( + buffer_growth_capacity(0x20000000, 0x20000000, 0x40000001), + content="1073741825", + ) +} + +///| +test "panic buffer growth rejects a wrapped required size" { + ignore(buffer_growth_capacity(16, 16, -1)) +} diff --git a/buffer/buffer_test.mbt b/buffer/buffer_test.mbt index d414589da..15dce1033 100644 --- a/buffer/buffer_test.mbt +++ b/buffer/buffer_test.mbt @@ -63,7 +63,7 @@ test "expect method with matching content" { } ///| -test "grow_if_necessary method" { +test "buffer grows beyond its initial capacity" { let buf = Buffer(size_hint=10) buf.write_string_utf16le( "This is a test string that is longer than the initial capacity", diff --git a/buffer/sleb128.mbt b/buffer/sleb128.mbt index 1832fe570..97f813dd5 100644 --- a/buffer/sleb128.mbt +++ b/buffer/sleb128.mbt @@ -21,7 +21,10 @@ trait Leb128 { pub impl Leb128 for Int with fn output(self, buffer) { // A 32-bit LEB128 value needs at most 5 bytes. Reserving them before // caching `data` makes every unsafe write below stay within the buffer. - buffer.grow_if_necessary(buffer.len + 5) + let required = buffer.len + 5 + if required > buffer.data.length() || required < buffer.len { + buffer.grow(required) + } let data = buffer.data let mut len = buffer.len for value = self { @@ -50,7 +53,10 @@ pub impl Leb128 for Int with fn output(self, buffer) { pub impl Leb128 for Int64 with fn output(self, buffer) { // A 64-bit LEB128 value needs at most 10 bytes. Reserving them before // caching `data` makes every unsafe write below stay within the buffer. - buffer.grow_if_necessary(buffer.len + 10) + let required = buffer.len + 10 + if required > buffer.data.length() || required < buffer.len { + buffer.grow(required) + } let data = buffer.data let mut len = buffer.len for value = self { diff --git a/buffer/uleb128.mbt b/buffer/uleb128.mbt index 95ffb58cd..cd7aada72 100644 --- a/buffer/uleb128.mbt +++ b/buffer/uleb128.mbt @@ -16,7 +16,10 @@ pub impl Leb128 for UInt with fn output(self, buffer) { // A 32-bit LEB128 value needs at most 5 bytes. Reserving them before // caching `data` makes every unsafe write below stay within the buffer. - buffer.grow_if_necessary(buffer.len + 5) + let required = buffer.len + 5 + if required > buffer.data.length() || required < buffer.len { + buffer.grow(required) + } let data = buffer.data let mut len = buffer.len for value = self { @@ -39,7 +42,10 @@ pub impl Leb128 for UInt with fn output(self, buffer) { pub impl Leb128 for UInt64 with fn output(self, buffer) { // A 64-bit LEB128 value needs at most 10 bytes. Reserving them before // caching `data` makes every unsafe write below stay within the buffer. - buffer.grow_if_necessary(buffer.len + 10) + let required = buffer.len + 10 + if required > buffer.data.length() || required < buffer.len { + buffer.grow(required) + } let data = buffer.data let mut len = buffer.len for value = self { diff --git a/builtin/moon.pkg b/builtin/moon.pkg index 84cc23996..540f77f60 100644 --- a/builtin/moon.pkg +++ b/builtin/moon.pkg @@ -53,6 +53,7 @@ options( "panic_test.mbt": [ "not", "native", "llvm" ], "panic_wbtest.mbt": [ "not", "native", "llvm" ], "stringbuilder_buffer.mbt": [ "not", "js" ], + "stringbuilder_buffer_wbtest.mbt": [ "not", "js" ], "stringbuilder_concat.mbt": [ "js" ], }, ) diff --git a/builtin/stringbuilder_buffer.mbt b/builtin/stringbuilder_buffer.mbt index 839ab542c..9d79ad1d6 100644 --- a/builtin/stringbuilder_buffer.mbt +++ b/builtin/stringbuilder_buffer.mbt @@ -43,24 +43,43 @@ pub fn StringBuilder::is_empty(self : StringBuilder) -> Bool { } ///| -fn StringBuilder::grow_if_necessary( - self : StringBuilder, +/// Compute the next capacity without allocating. Since appends never shrink the +/// builder, `required < len` means the required-size calculation overflowed. +#inline +fn stringbuilder_growth_capacity( + current : Int, + len : Int, required : Int, -) -> Unit { - let current_len = self.data.length() - if required <= current_len { - return +) -> Int { + if required < len { + abort("StringBuilder capacity overflow") } - // current_len is at least 1 - // double the enough_space until it larger than required - let enough_space = for enough_space = current_len; enough_space < required; { - continue enough_space * 2 + let enough_space = for space = current; space < required; { + let next = space * 2 + if next <= space { + break required + } + continue next } nobreak { - enough_space + space } + enough_space +} + +///| +/// Grow the builder to at least `required`. Callers keep the capacity check on +/// their fast path and enter here only when growth or overflow handling is +/// needed. The builder invariant `0 <= len <= data.length()` lets fixed-size +/// appends compare against the remaining capacity without overflowing. +fn StringBuilder::grow(self : StringBuilder, required : Int) -> Unit { + let new_capacity = stringbuilder_growth_capacity( + self.data.length(), + self.len, + required, + ) let new_data = FixedArray::make_and_blit( self.data, - allocate_len=enough_space, + allocate_len=new_capacity, init=(Default::default() : UInt16), len=self.len, ) @@ -88,7 +107,10 @@ pub impl Logger for StringBuilder with fn write_string(self, str) { if str_len == 0 { return } - self.grow_if_necessary(self.len + str_len) + let required = self.len + str_len + if required > self.data.length() || required < self.len { + self.grow(required) + } self.data.unsafe_blit_from_string(self.len, str, 0, str_len) self.len += str_len } @@ -98,11 +120,15 @@ pub impl Logger for StringBuilder with fn write_string(self, str) { pub impl Logger for StringBuilder with fn write_char(self, ch) { let code = ch.to_uint() if code <= 0xFFFFU { - self.grow_if_necessary(self.len + 1) + if self.len >= self.data.length() { + self.grow(self.len + 1) + } self.data[self.len] = code.to_uint16() self.len += 1 } else if code <= 0x10FFFFU { - self.grow_if_necessary(self.len + 2) + if self.data.length() - self.len < 2 { + self.grow(self.len + 2) + } let code = code - 0x10000U self.data[self.len] = (0xD800U + (code >> 10)).to_uint16() self.data[self.len + 1] = (0xDC00U + code.land(0x3FFU)).to_uint16() @@ -139,7 +165,10 @@ pub impl Logger for StringBuilder with fn write_view( if str_len == 0 { return } - self.grow_if_necessary(self.len + str_len) + let required = self.len + str_len + if required > self.data.length() || required < self.len { + self.grow(required) + } self.data.unsafe_blit_from_string( self.len, str.data(), diff --git a/builtin/stringbuilder_buffer_wbtest.mbt b/builtin/stringbuilder_buffer_wbtest.mbt new file mode 100644 index 000000000..9fb330304 --- /dev/null +++ b/builtin/stringbuilder_buffer_wbtest.mbt @@ -0,0 +1,36 @@ +// Copyright 2026 International Digital Economy Academy +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +///| +test "StringBuilder growth capacity doubles while representable" { + inspect(stringbuilder_growth_capacity(8, 8, 9), content="16") + inspect(stringbuilder_growth_capacity(8, 8, 33), content="64") +} + +///| +test "StringBuilder growth capacity falls back to the exact requirement" { + inspect( + stringbuilder_growth_capacity(0x40000000, 0x40000000, 0x40000001), + content="1073741825", + ) + inspect( + stringbuilder_growth_capacity(0x20000000, 0x20000000, 0x40000001), + content="1073741825", + ) +} + +///| +test "panic StringBuilder growth rejects a wrapped required size" { + ignore(stringbuilder_growth_capacity(16, 16, -1)) +}