Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
75 commits
Select commit Hold shift + click to select a range
dc88940
install: tighten dependency name validation before install
Jarred-Sumner May 21, 2026
b8dbf3f
install: tighten trusted dependency resolution checks
Jarred-Sumner May 21, 2026
a450aab
http3: guard stream header callback state
Jarred-Sumner May 21, 2026
d17368a
install: tighten lockfile field validation on load
Jarred-Sumner May 21, 2026
5a01667
install: track created symlinks during streaming extraction
Jarred-Sumner May 21, 2026
6c02ed5
libarchive: tighten file open flags during extraction
Jarred-Sumner May 21, 2026
696ce1a
install: validate bin entry targets before linking
Jarred-Sumner May 21, 2026
0edb723
spawn: tighten argument handling for script interpreters
Jarred-Sumner May 21, 2026
5bafdf8
mysql: align ssl mode fallback behavior with postgres driver
Jarred-Sumner May 21, 2026
23afa36
bunx: tighten bin name extraction from package manifests
Jarred-Sumner May 21, 2026
41d69dc
bake: normalize error report strings before terminal output
Jarred-Sumner May 21, 2026
cacf731
valkey: scan for complete replies before parsing buffered data
Jarred-Sumner May 21, 2026
703b3b6
http: bound decompressed response body growth
Jarred-Sumner May 21, 2026
e805175
http2: account for stream table size in session memory usage
Jarred-Sumner May 21, 2026
089d047
mysql: bound result set column count
Jarred-Sumner May 21, 2026
aae8a44
bundler: escape source path comments in css output
Jarred-Sumner May 21, 2026
f2f2179
valkey: bound aggregate preallocation while parsing replies
Jarred-Sumner May 21, 2026
1c0bbfb
blob: tighten content type validation
Jarred-Sumner May 21, 2026
ff45183
node:http: refine link header value pattern
Jarred-Sumner May 21, 2026
e017fce
node:http: tighten link header value validation
Jarred-Sumner May 21, 2026
1d80d2c
yaml: bound merge key expansion work
Jarred-Sumner May 21, 2026
ef2fb4f
shell: track literal brace metacharacters during expansion
Jarred-Sumner May 21, 2026
c9076cd
bake: validate websocket upgrade origin on dev server
Jarred-Sumner May 21, 2026
e98cb62
node:wasi: resolve guest paths against the preopen directory
Jarred-Sumner May 21, 2026
cc3ef59
postgres: tighten authentication state transitions
Jarred-Sumner May 21, 2026
95068a0
mysql: harden reader capacity check
Jarred-Sumner May 21, 2026
9553f61
crypto: normalize password hash comparison
Jarred-Sumner May 21, 2026
ae403f7
formdata: anchor boundary parameter parsing
Jarred-Sumner May 21, 2026
b33f506
shell: treat interpolated values as literal text
Jarred-Sumner May 21, 2026
91c2433
server: apply host allowlist to devtools metadata route
Jarred-Sumner May 21, 2026
5dfb809
create: align postinstall task gating with preinstall
Jarred-Sumner May 21, 2026
5185d97
libarchive: normalize extracted file permissions
Jarred-Sumner May 21, 2026
4ad9a29
structured-clone: validate regexp flags during deserialization
Jarred-Sumner May 21, 2026
a4945ad
node:http: propagate parser callback errors when header buffer flushes
Jarred-Sumner May 21, 2026
383e5ed
node:tls: validate checkServerIdentity option type
Jarred-Sumner May 21, 2026
5be79a3
sourcemap: fix vlq decode table size
Jarred-Sumner May 21, 2026
0125b78
http: tighten request line separator handling
Jarred-Sumner May 21, 2026
ef74590
semver: drop range chains iteratively
Jarred-Sumner May 21, 2026
238ef0f
sqlite: fix buffer ownership on deserialize failure
Jarred-Sumner May 21, 2026
30728af
inspector: check upgrade request origin
Jarred-Sumner May 21, 2026
deb169a
ini: bound section header segment depth
Jarred-Sumner May 21, 2026
e32ce67
webcore: bound rsa prime count during key deserialization
Jarred-Sumner May 21, 2026
dd8e4f2
structured-clone: validate bigint length against remaining input
Jarred-Sumner May 21, 2026
6f2dbf1
structured-clone: manage bio lifetime with raii during key deserializ…
Jarred-Sumner May 21, 2026
fbc1243
bundler: escape source path comments in js output
Jarred-Sumner May 21, 2026
305ca88
websocket: bound buffered handshake response size
Jarred-Sumner May 21, 2026
9f824de
[autofix.ci] apply automated fixes
autofix-ci[bot] May 21, 2026
9a20bac
address review: compare normalized origin in inspector allowlist
Jarred-Sumner May 21, 2026
e36f02d
address review: grow prime info vector incrementally during deseriali…
Jarred-Sumner May 21, 2026
38dd86d
address review: clarify merge-key comparison cap scope in docs
Jarred-Sumner May 21, 2026
6fe1a52
address review: reject malformed authorities in dev server host parsing
Jarred-Sumner May 21, 2026
762303d
address review: filter encoded C1 controls from error report output
Jarred-Sumner May 21, 2026
f3df66a
address review: normalize trailing characters before batch file check
Jarred-Sumner May 21, 2026
39fe081
http2: count only live streams toward session memory
Jarred-Sumner May 21, 2026
d5266a0
bunx: derive command name from unscoped package name in string-bin fa…
Jarred-Sumner May 21, 2026
4e0ca3e
create: only skip postinstall tasks on explicit opt-out
Jarred-Sumner May 21, 2026
adc0535
install: reject drive-relative bin targets
Jarred-Sumner May 21, 2026
14b73f3
address review: only reject colons in the leading bin target component
Jarred-Sumner May 21, 2026
6f4dbd5
install: align streaming extractor file-open flags with buffered extr…
Jarred-Sumner May 21, 2026
a3be819
wasi: re-check containment after path resolution
Jarred-Sumner May 21, 2026
5819f76
[autofix.ci] apply automated fixes
autofix-ci[bot] May 21, 2026
7d3faf9
address review: blank the lead byte of filtered two-byte sequences
Jarred-Sumner May 21, 2026
b182ccd
yaml: rely on the parser's existing stack guard instead of a merge wo…
Jarred-Sumner May 22, 2026
64ae772
address review: enforce decode output cap after each chunk
Jarred-Sumner May 22, 2026
471cc1c
address review: anchor multipart boundary parameter parsing
Jarred-Sumner May 22, 2026
a775854
address review: include argv0 in batch-file argument validation
Jarred-Sumner May 22, 2026
542585f
address review: reject empty port suffix in host parsing
Jarred-Sumner May 22, 2026
3671277
address review: validate negative aggregate lengths in reply scanner
Jarred-Sumner May 22, 2026
fa6e400
address review: enforce inflate output cap after each write
Jarred-Sumner May 22, 2026
39b6631
address review: enforce decompress output cap after each write
Jarred-Sumner May 22, 2026
cefd8c9
[autofix.ci] apply automated fixes
autofix-ci[bot] May 22, 2026
f681139
shell: keep literal tilde after interpolation literal
Jarred-Sumner May 22, 2026
ec84518
sqlite: close database handle on deserialize error paths
Jarred-Sumner May 22, 2026
8860b85
[autofix.ci] apply automated fixes
autofix-ci[bot] May 22, 2026
c041aa6
wasi: resolve parent directory before containment check when target d…
Jarred-Sumner May 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/bun-uws/src/Http3Context.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ struct Http3Context {
Http3ContextData *cd = (Http3ContextData *) us_quic_socket_context_ext(us_quic_stream_context(s));
Http3Response *res = (Http3Response *) s;
Http3ResponseData *rd = res->getHttpResponseData();
/* lsquic re-fires on_stream_headers for every HEADERS block on
* the stream; only the first one is the request. Re-running
* reset()/route() for trailers would wipe the live response's
* onAborted/userData and dispatch the handler a second time.
* state is 0 only before the first reset() on this stream. */
if (rd->state != 0) return;
rd->reset();

Http3Request req(s);
Expand Down
5 changes: 3 additions & 2 deletions packages/bun-uws/src/HttpParser.h
Original file line number Diff line number Diff line change
Expand Up @@ -570,8 +570,9 @@ namespace uWS
}


bool isHTTPMethod = (__builtin_expect(data[1] == '/', 1));
bool isConnect = !isHTTPMethod && ((data - start) == 7 && memcmp(start, "CONNECT", 7) == 0);
/* RFC 9112 3: exactly one SP separates method and request-target */
bool isHTTPMethod = (__builtin_expect(data[0] == 32 && data[1] == '/', 1));
bool isConnect = !isHTTPMethod && ((data - start) == 7 && data[0] == 32 && memcmp(start, "CONNECT", 7) == 0);
/* Also accept proxy-style absolute URLs (http://... or https://...) as valid request targets */
bool isProxyStyleURL = !isHTTPMethod && !isConnect && data[0] == 32 && isHTTPorHTTPSPrefixForProxies(data + 1, end) == 1;
if (isHTTPMethod || isConnect || isProxyStyleURL) [[likely]] {
Expand Down
17 changes: 11 additions & 6 deletions src/ast/e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -618,7 +618,7 @@
pub ref_: Ref,
}

/// In development mode, the new JSX transform has a few special props

Check warning on line 621 in src/ast/e.rs

View workflow job for this annotation

GitHub Actions / cargo miri test

unclosed attribute block (`{}`): missing `}` at the end
/// - `React.jsxDEV(type, arguments, key, isStaticChildren, source, self)`
/// - `arguments`:
/// ```{ ...props, children: children, }```
Expand Down Expand Up @@ -902,17 +902,22 @@
}
impl Rope {
pub fn append(&mut self, expr: Expr, bump: &Bump) -> Result<*mut Rope, AllocError> {
if let Some(mut next) = core::ptr::NonNull::new(self.next).map(StoreRef::from_non_null) {
// Arena-allocated Rope nodes are uniquely owned by the chain at this
// point in TOML parsing; route through `StoreRef::DerefMut` (the
// arena-backed handle whose deref is centralised in `nodes.rs`).
return next.append(expr, bump);
// Walk to the tail iteratively: recursing once per node overflows the
// native stack on adversarially deep ropes (e.g. an `.npmrc` section
// header with thousands of dot-separated segments).
//
// Arena-allocated Rope nodes are uniquely owned by the chain at this
// point; route through `StoreRef::DerefMut` (the arena-backed handle
// whose deref is centralised in `nodes.rs`).
let mut tail = StoreRef::from_bump(self);
while let Some(next) = core::ptr::NonNull::new(tail.next).map(StoreRef::from_non_null) {
tail = next;
}
let rope: *mut Rope = bump.alloc(Rope {
head: expr,
next: core::ptr::null_mut(),
});
self.next = rope;
tail.next = rope;
Ok(rope)
}

Expand Down
4 changes: 2 additions & 2 deletions src/base64/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -303,8 +303,8 @@ pub mod vlq {
const U7_MAX: u8 = 127;

// base64 stores values up to 7 bits
const BASE64_LUT: [u8; U7_MAX as usize] = {
let mut bytes = [U7_MAX; U7_MAX as usize];
const BASE64_LUT: [u8; U7_MAX as usize + 1] = {
let mut bytes = [U7_MAX; U7_MAX as usize + 1];
let mut i = 0;
while i < BASE64.len() {
bytes[BASE64[i] as usize] = i as u8;
Expand Down
15 changes: 15 additions & 0 deletions src/brotli/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ pub struct BrotliReaderArrayList<'a> {
pub state: ReaderState,
pub total_out: usize,
pub total_in: usize,
/// Decompression-bomb guard: `read_all` errors instead of growing the
/// output past this many bytes. Defaults to unbounded.
pub max_output_size: usize,
pub flush_op: c::BrotliEncoderOperation,
pub finish_flush_op: c::BrotliEncoderOperation,
pub full_flush_op: c::BrotliEncoderOperation,
Expand Down Expand Up @@ -152,6 +155,7 @@ impl<'a> BrotliReaderArrayList<'a> {
state: ReaderState::Uninitialized,
total_out: 0,
total_in: 0,
max_output_size: usize::MAX,
flush_op,
finish_flush_op,
full_flush_op,
Expand Down Expand Up @@ -204,6 +208,13 @@ impl<'a> BrotliReaderArrayList<'a> {
unsafe { bun_core::vec::commit_spare(self.list_ptr, bytes_written) };
self.total_in += bytes_read;

// Enforce the cap after every write so a chunk that ends the
// stream (`success`) cannot push the output past the limit.
if self.list_ptr.len() > self.max_output_size {
self.state = ReaderState::Error;
return Err(err!("BrotliDecompressionError"));
}

match result {
c::BrotliDecoderResult::success => {
debug_assert!(BrotliDecoder::is_finished(self.brotli()));
Expand Down Expand Up @@ -237,6 +248,10 @@ impl<'a> BrotliReaderArrayList<'a> {
return Err(err!("ShortRead"));
}
c::BrotliDecoderResult::needs_more_output => {
if self.list_ptr.len() >= self.max_output_size {
self.state = ReaderState::Error;
return Err(err!("BrotliDecompressionError"));
}
Comment thread
Jarred-Sumner marked this conversation as resolved.
let target = self.list_ptr.capacity() + 4096;
self.list_ptr
.reserve(target.saturating_sub(self.list_ptr.len()));
Expand Down
54 changes: 43 additions & 11 deletions src/bun_core/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5786,21 +5786,53 @@ pub mod form_data {

/// `FormData.getBoundary` — borrow the `boundary=` value out of a
/// `Content-Type` header. Returns `None` on malformed quoting.
///
/// Parameters are `;`-delimited per RFC 7231 and the parameter *name* must
/// be exactly `boundary`, so a different parameter (`xboundary=FAKE`) or a
/// `boundary=` substring inside another parameter's value is not picked up
/// by an unanchored substring search. A `;` inside a quoted parameter
/// value (RFC 7230 quoted-string, `\` escapes the next byte) does not
/// delimit parameters.
pub fn get_boundary(content_type: &[u8]) -> Option<&[u8]> {
let idx = ::bstr::ByteSlice::find(content_type, b"boundary=")?;
let begin = &content_type[idx + b"boundary=".len()..];
if begin.is_empty() {
return None;
let mut rest = content_type;
loop {
let semi = index_of_unquoted_semicolon(rest)?;
rest = &rest[semi + 1..];
let Some(begin) =
crate::strings_impl::trim_left(rest, b" \t").strip_prefix(b"boundary=")
else {
continue;
};
if begin.is_empty() {
return None;
}
let end = crate::strings_impl::index_of_char(begin, b';').unwrap_or(begin.len());
if begin[0] == b'"' {
if end > 1 && begin[end - 1] == b'"' {
return Some(&begin[1..end - 1]);
}
// Opening quote with no matching closing quote — malformed.
return None;
}
return Some(&begin[..end]);
Comment thread
Jarred-Sumner marked this conversation as resolved.
}
let end = crate::strings_impl::index_of_char(begin, b';').unwrap_or(begin.len());
if begin[0] == b'"' {
if end > 1 && begin[end - 1] == b'"' {
return Some(&begin[1..end - 1]);
}

/// Index of the next `;` in `s` that is not inside an RFC 7230
/// quoted-string (`\` escapes the following byte inside quotes).
fn index_of_unquoted_semicolon(s: &[u8]) -> Option<usize> {
let mut in_quotes = false;
let mut i = 0;
while i < s.len() {
match s[i] {
b'"' => in_quotes = !in_quotes,
b'\\' if in_quotes => i += 1,
b';' if !in_quotes => return Some(i),
_ => {}
}
// Opening quote with no matching closing quote — malformed.
return None;
i += 1;
}
Some(&begin[..end])
None
}

/// `FormData.AsyncFormData` — heap-allocated, owns its `Encoding`.
Expand Down
18 changes: 16 additions & 2 deletions src/bundler/linker_context/postProcessCSSChunk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,22 @@ pub fn post_process_css_chunk(
j.push_static(b"/* ");
line_offset.advance(b"/* ");

j.push_static(pretty);
line_offset.advance(pretty);
// A `*/` in the path would terminate the comment early and let the
// rest of the path be parsed as CSS in the bundled output.
if bun_core::strings::contains(pretty, b"*/") {
let mut escaped = Vec::with_capacity(pretty.len() + 1);
for &byte in pretty {
if byte == b'/' && escaped.last() == Some(&b'*') {
escaped.push(b'\\');
}
escaped.push(byte);
}
line_offset.advance(&escaped);
j.push_owned(escaped.into_boxed_slice());
} else {
j.push_static(pretty);
line_offset.advance(pretty);
}

j.push_static(b" */\n");
line_offset.advance(b" */\n");
Expand Down
15 changes: 13 additions & 2 deletions src/bundler/linker_context/postProcessJSChunk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -680,8 +680,19 @@ pub fn post_process_js_chunk(
}
}

j.push_static(pretty);
line_offset.advance(pretty);
// A `*/` in the path would terminate the block comment early and
// turn the rest of the path into generated JavaScript.
if matches!(comment_type, CommentType::Multiline) && strings::contains(pretty, b"*/") {
let mut sanitized = pretty.to_vec();
while let Some(i) = strings::index_of(&sanitized, b"*/") {
sanitized[i + 1] = b'_';
}
line_offset.advance(&sanitized);
j.push_owned(sanitized.into_boxed_slice());
} else {
j.push_static(pretty);
line_offset.advance(pretty);
}

if emit_targets_in_commands {
j.push_static(b" (");
Expand Down
14 changes: 11 additions & 3 deletions src/http/Decompressor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ unsafe fn seat<'a>(input: &'a [u8], out: &'a mut Vec<u8>) -> (&'static [u8], &'s
}
}

/// Decompression-bomb guard for response bodies inflated on the HTTP thread:
/// a hostile server must not be able to expand a tiny compressed payload into
/// an unbounded allocation.
const MAX_DECOMPRESSED_BODY_SIZE: usize = 1024 * 1024 * 1024;

impl Decompressor {
// PORT NOTE: Zig `deinit` called `that.deinit()` on the active reader and
// reset to `.none`. The boxed readers' `Drop` impls call `end()`, so an
Expand All @@ -77,7 +82,7 @@ impl Decompressor {
let (input, out) = unsafe { seat(buffer, &mut body_out_str.list) };
match encoding {
Encoding::Gzip | Encoding::Deflate => {
let reader = ZlibReaderArrayList::init_with_options_and_list_allocator(
let mut reader = ZlibReaderArrayList::init_with_options_and_list_allocator(
input,
out,
// PORT NOTE: Zig passed `body_out_str.allocator` and
Expand All @@ -97,26 +102,29 @@ impl Decompressor {
..Default::default()
},
)?;
reader.max_output_size = MAX_DECOMPRESSED_BODY_SIZE;
*self = Decompressor::Zlib(reader);
return Ok(());
}
Encoding::Brotli => {
let reader = BrotliReaderArrayList::new_with_options(
let mut reader = BrotliReaderArrayList::new_with_options(
input,
out,
// PORT NOTE: Zig passed `body_out_str.allocator`; dropped per §Allocators.
&Default::default(),
)?;
reader.max_output_size = MAX_DECOMPRESSED_BODY_SIZE;
*self = Decompressor::Brotli(reader);
return Ok(());
}
Encoding::Zstd => {
let reader = ZstdReaderArrayList::init_with_list_allocator(
let mut reader = ZstdReaderArrayList::init_with_list_allocator(
input,
out,
// PORT NOTE: Zig passed `body_out_str.allocator` and
// `bun.http.default_allocator`; dropped per §Allocators.
)?;
reader.max_output_size = MAX_DECOMPRESSED_BODY_SIZE;
*self = Decompressor::Zstd(reader);
return Ok(());
}
Expand Down
20 changes: 20 additions & 0 deletions src/http_jsc/websocket_client/WebSocketUpgradeClient.rs
Original file line number Diff line number Diff line change
Expand Up @@ -993,6 +993,11 @@ impl<const SSL: bool> HTTPClient<SSL> {
let me = unsafe { &mut *this };
let mut body = data;
if !me.body.is_empty() {
if me.body.len().saturating_add(data.len()) > bun_http::max_http_header_size() {
// SAFETY: `me`'s last use is above; no `&mut Self` spans this call.
unsafe { Self::terminate(this, ErrorCode::InvalidResponse) };
return;
}
me.body.extend_from_slice(data);
body = &me.body;
}
Expand Down Expand Up @@ -1020,6 +1025,11 @@ impl<const SSL: bool> HTTPClient<SSL> {
}
Err(picohttp::ParseResponseError::ShortRead) => {
if me.body.is_empty() {
if data.len() > bun_http::max_http_header_size() {
// SAFETY: `me`'s last use is above; no `&mut Self` spans this call.
unsafe { Self::terminate(this, ErrorCode::InvalidResponse) };
return;
}
me.body.extend_from_slice(data);
}
return;
Expand Down Expand Up @@ -1233,6 +1243,11 @@ impl<const SSL: bool> HTTPClient<SSL> {
// Process as if it came directly from the socket
let mut body = data;
if !me.body.is_empty() {
if me.body.len().saturating_add(data.len()) > bun_http::max_http_header_size() {
// SAFETY: `me`'s last use is above; no `&mut Self` spans this call.
unsafe { Self::terminate(this, ErrorCode::InvalidResponse) };
return;
}
me.body.extend_from_slice(data);
body = &me.body;
}
Expand All @@ -1257,6 +1272,11 @@ impl<const SSL: bool> HTTPClient<SSL> {
}
Err(picohttp::ParseResponseError::ShortRead) => {
if me.body.is_empty() {
if data.len() > bun_http::max_http_header_size() {
// SAFETY: `me`'s last use is above; no `&mut Self` spans this call.
unsafe { Self::terminate(this, ErrorCode::InvalidResponse) };
return;
}
me.body.extend_from_slice(data);
}
return;
Expand Down
18 changes: 15 additions & 3 deletions src/ini/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,13 @@ mod draft {

type OOM<T> = Result<T, AllocError>;

/// Hard cap on dot-separated segments in a section-header rope. The rope is
/// consumed by `E::Object::get_or_put_object`, which recurses once per
/// `rope.next` link, so an unbounded header overflows the stack. Past the
/// cap the remainder of the header (dots included) becomes the final
/// segment. Mirrors `MAX_DOTTED_KEY_SEGMENTS` in the TOML parser.
const MAX_SECTION_ROPE_SEGMENTS: usize = 512;

// ──────────────────────────────────────────────────────────────────────────
// Parser
// ──────────────────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -692,6 +699,7 @@ mod draft {
// RopeT is *Rope when usage==Section, else unit. In Rust we just
// keep an Option<&mut Rope> and ignore it for non-section usages.
let mut rope: Option<&'a mut Rope> = None;
let mut rope_parts: usize = 0;

let mut i: usize = 0;
'walk: while i < val.len() {
Expand Down Expand Up @@ -779,8 +787,10 @@ mod draft {
did_any_escape = true;
}
b'.' => {
if usage == Usage::Section {
if usage == Usage::Section && rope_parts < MAX_SECTION_ROPE_SEGMENTS
{
self.commit_rope_part(bump, ropealloc, &mut unesc, &mut rope)?;
rope_parts += 1;
} else {
unesc.push(b'.');
}
Expand Down Expand Up @@ -1069,10 +1079,11 @@ mod draft {
next: ptr::null_mut(),
});

let mut segments: usize = 1;
while dot_idx + 1 < key.len() {
let next_dot_idx = match next_dot(&key[dot_idx + 1..]) {
Some(n) => dot_idx + 1 + n,
None => {
Some(n) if segments < MAX_SECTION_ROPE_SEGMENTS => dot_idx + 1 + n,
_ => {
let rest = &key[dot_idx + 1..];
let _ = rope_head
.append(Expr::init(E::EString::init(rest), Loc::EMPTY), ropealloc)?;
Expand All @@ -1082,6 +1093,7 @@ mod draft {
let part = &key[dot_idx + 1..next_dot_idx];
let _ =
rope_head.append(Expr::init(E::EString::init(part), Loc::EMPTY), ropealloc)?;
segments += 1;
dot_idx = next_dot_idx;
}

Expand Down
Loading
Loading