mirror of
https://github.com/oven-sh/bun
synced 2026-02-10 02:48:50 +00:00
Replace `catch bun.outOfMemory()`, which can accidentally catch non-OOM-related errors, with either `bun.handleOom` or a manual `catch |err| switch (err)`. (For internal tracking: fixes STAB-1070) --------- Co-authored-by: Dylan Conway <dylan.conway567@gmail.com>
181 lines
4.6 KiB
Zig
181 lines
4.6 KiB
Zig
//! Rope-like data structure for joining many small strings into one big string.
|
|
//! Implemented as a linked list of potentially-owned slices and a length.
|
|
|
|
const StringJoiner = @This();
|
|
|
|
/// Temporary allocator used for nodes and duplicated strings.
|
|
/// It is recommended to use a stack-fallback allocator for this.
|
|
allocator: Allocator,
|
|
|
|
/// Total length of all nodes
|
|
len: usize = 0,
|
|
|
|
head: ?*Node = null,
|
|
tail: ?*Node = null,
|
|
|
|
/// Avoid an extra pass over the list when joining
|
|
watcher: Watcher = .{},
|
|
|
|
const Node = struct {
|
|
allocator: NullableAllocator = .{},
|
|
slice: []const u8 = "",
|
|
next: ?*Node = null,
|
|
|
|
pub fn init(joiner_alloc: Allocator, slice: []const u8, slice_alloc: ?Allocator) *Node {
|
|
const node = bun.handleOom(joiner_alloc.create(Node));
|
|
node.* = .{
|
|
.slice = slice,
|
|
.allocator = NullableAllocator.init(slice_alloc),
|
|
};
|
|
return node;
|
|
}
|
|
|
|
pub fn deinit(node: *Node, joiner_alloc: Allocator) void {
|
|
node.allocator.free(node.slice);
|
|
joiner_alloc.destroy(node);
|
|
}
|
|
};
|
|
|
|
pub const Watcher = struct {
|
|
input: []const u8 = "",
|
|
estimated_count: u32 = 0,
|
|
needs_newline: bool = false,
|
|
};
|
|
|
|
/// `data` is expected to live until `.done` is called
|
|
pub fn pushStatic(this: *StringJoiner, data: []const u8) void {
|
|
this.push(data, null);
|
|
}
|
|
|
|
/// `data` is cloned
|
|
pub fn pushCloned(this: *StringJoiner, data: []const u8) void {
|
|
if (data.len == 0) return;
|
|
this.push(
|
|
bun.handleOom(this.allocator.dupe(u8, data)),
|
|
this.allocator,
|
|
);
|
|
}
|
|
|
|
pub fn push(this: *StringJoiner, data: []const u8, allocator: ?Allocator) void {
|
|
if (data.len == 0) return;
|
|
this.len += data.len;
|
|
|
|
const new_tail = Node.init(this.allocator, data, allocator);
|
|
|
|
if (data.len > 0) {
|
|
this.watcher.estimated_count += @intFromBool(
|
|
this.watcher.input.len > 0 and
|
|
bun.strings.contains(data, this.watcher.input),
|
|
);
|
|
this.watcher.needs_newline = data[data.len - 1] != '\n';
|
|
}
|
|
|
|
if (this.tail) |current_tail| {
|
|
current_tail.next = new_tail;
|
|
} else {
|
|
assert(this.head == null);
|
|
this.head = new_tail;
|
|
}
|
|
this.tail = new_tail;
|
|
}
|
|
|
|
/// This deinits the string joiner on success, the new string is owned by `allocator`
|
|
pub fn done(this: *StringJoiner, allocator: Allocator) ![]u8 {
|
|
var current: ?*Node = this.head orelse {
|
|
assert(this.tail == null);
|
|
assert(this.len == 0);
|
|
return &.{};
|
|
};
|
|
|
|
const slice = try allocator.alloc(u8, this.len);
|
|
|
|
var remaining = slice;
|
|
while (current) |node| {
|
|
@memcpy(remaining[0..node.slice.len], node.slice);
|
|
remaining = remaining[node.slice.len..];
|
|
|
|
const prev = node;
|
|
current = node.next;
|
|
prev.deinit(this.allocator);
|
|
}
|
|
|
|
bun.assert(remaining.len == 0);
|
|
|
|
return slice;
|
|
}
|
|
|
|
pub fn deinit(this: *StringJoiner) void {
|
|
var current: ?*Node = this.head orelse {
|
|
assert(this.tail == null);
|
|
assert(this.len == 0);
|
|
return;
|
|
};
|
|
|
|
while (current) |node| {
|
|
const prev = node;
|
|
current = node.next;
|
|
prev.deinit(this.allocator);
|
|
}
|
|
}
|
|
|
|
/// Same as `.done`, but appends extra slice `end`
|
|
pub fn doneWithEnd(this: *StringJoiner, allocator: Allocator, end: []const u8) ![]u8 {
|
|
var current: ?*Node = this.head orelse {
|
|
assert(this.tail == null);
|
|
assert(this.len == 0);
|
|
|
|
if (end.len > 0) {
|
|
return allocator.dupe(u8, end);
|
|
}
|
|
|
|
return &.{};
|
|
};
|
|
|
|
const slice = try allocator.alloc(u8, this.len + end.len);
|
|
|
|
var remaining = slice;
|
|
while (current) |node| {
|
|
@memcpy(remaining[0..node.slice.len], node.slice);
|
|
remaining = remaining[node.slice.len..];
|
|
|
|
const prev = node;
|
|
current = node.next;
|
|
prev.deinit(this.allocator);
|
|
}
|
|
|
|
bun.assert(remaining.len == end.len);
|
|
@memcpy(remaining, end);
|
|
|
|
return slice;
|
|
}
|
|
|
|
pub fn lastByte(this: *const StringJoiner) u8 {
|
|
const slice = (this.tail orelse return 0).slice;
|
|
assert(slice.len > 0);
|
|
return slice[slice.len - 1];
|
|
}
|
|
|
|
pub fn ensureNewlineAtEnd(this: *StringJoiner) void {
|
|
if (this.watcher.needs_newline) {
|
|
this.watcher.needs_newline = false;
|
|
this.pushStatic("\n");
|
|
}
|
|
}
|
|
|
|
pub fn contains(this: *const StringJoiner, slice: []const u8) bool {
|
|
var el = this.head;
|
|
while (el) |node| {
|
|
el = node.next;
|
|
if (bun.strings.contains(node.slice, slice)) return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
const std = @import("std");
|
|
const Allocator = std.mem.Allocator;
|
|
|
|
const bun = @import("bun");
|
|
const NullableAllocator = bun.NullableAllocator;
|
|
const assert = bun.assert;
|