Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions packages/bun-types/bun.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -943,6 +943,10 @@ declare module "bun" {
* @param options Optional behavior flags (such as `ellipsis` for truncation)
* @returns The sliced string with ANSI codes intact
*
* @throws A `RangeError` (out of memory) when the result passes the maximum
* string length (2^31 - 1 characters). The result can be longer than the
* input, because the slice re-opens the active styles and adds the ellipsis.
*
* @example
* ```ts
* import { sliceAnsi } from "bun";
Expand Down
25 changes: 25 additions & 0 deletions src/jsc/bindings/StringSizeLimit.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#pragma once

#include "root.h"
#include <wtf/text/StringImpl.h>

extern "C" size_t Bun__stringSyntheticAllocationLimit;

namespace Bun {

// A string this long cannot be built. The synthetic limit lets tests lower the bound.
inline bool exceedsStringLimit(size_t length)
{
return length > Bun__stringSyntheticAllocationLimit || length > WTF::StringImpl::MaxLength;
}

// Half of the longest 16-bit string: the first 16-bit character doubles the reserve (oven-sh/WebKit#631).
inline unsigned cappedStringBuilderReserve(size_t length)
{
constexpr size_t max16BitLength = (std::numeric_limits<unsigned>::max() - sizeof(WTF::StringImpl)) / sizeof(char16_t);
static_assert(WTF::StringImpl::isValidLength<char16_t>(max16BitLength));
static_assert(!WTF::StringImpl::isValidLength<char16_t>(max16BitLength + 1));
return static_cast<unsigned>(std::min(length, max16BitLength / 2));
Comment thread
robobun marked this conversation as resolved.
}

} // namespace Bun
77 changes: 50 additions & 27 deletions src/jsc/bindings/sliceAnsi.cpp
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
#include "root.h"
#include "sliceAnsi.h"
#include "ANSIHelpers.h"
#include "StringSizeLimit.h"

#include <wtf/text/WTFString.h>
#include <wtf/text/StringBuilder.h>
#include <wtf/text/MakeString.h>
#include <wtf/Vector.h>
#include <optional>

// Native exports (implemented in stringWidth.cpp) for visible width and grapheme break
extern "C" uint8_t Bun__codepointWidth(uint32_t cp, bool ambiguous_as_wide);
Expand Down Expand Up @@ -530,9 +533,9 @@ static const Char* parseEscapeSequence(const Char* start, const Char* end)
return nullptr;
}

// Parse hyperlink: ESC]8;...;url TERMINATOR
// Parse hyperlink: ESC]8;...;url TERMINATOR. `code` is the whole sequence, a view of the input.
template<typename Char>
static const Char* parseHyperlink(const Char* start, const Char* end, bool& isOpen, StringBuilder& codeBuilder, String& closePrefix, String& terminator)
static const Char* parseHyperlink(const Char* start, const Char* end, bool& isOpen, std::span<const Char>& code, String& closePrefix, String& terminator)
{
const Char* it = start;
bool isEscOsc = false;
Expand Down Expand Up @@ -568,8 +571,7 @@ static const Char* parseHyperlink(const Char* start, const Char* end, bool& isOp
if (*p == 0x07) {
// BEL terminator
isOpen = (p > uriStart); // empty URI = close
for (const Char* q = start; q <= p; ++q)
codeBuilder.append(static_cast<UChar>(*q));
code = std::span { start, static_cast<size_t>(p + 1 - start) };
if (isEscOsc) {
closePrefix = "\x1b]8;;"_s;
} else {
Expand All @@ -587,8 +589,7 @@ static const Char* parseHyperlink(const Char* start, const Char* end, bool& isOp
if (*p == 0x1b && p + 1 < end && *(p + 1) == '\\') {
// ESC\ terminator (ST)
isOpen = (p > uriStart);
for (const Char* q = start; q <= p + 1; ++q)
codeBuilder.append(static_cast<UChar>(*q));
code = std::span { start, static_cast<size_t>(p + 2 - start) };
if (isEscOsc) {
closePrefix = "\x1b]8;;"_s;
} else {
Expand All @@ -603,8 +604,7 @@ static const Char* parseHyperlink(const Char* start, const Char* end, bool& isOp
if (*p == 0x9c) {
// C1 ST terminator
isOpen = (p > uriStart);
for (const Char* q = start; q <= p; ++q)
codeBuilder.append(static_cast<UChar>(*q));
code = std::span { start, static_cast<size_t>(p + 1 - start) };
if (isEscOsc) {
closePrefix = "\x1b]8;;"_s;
} else {
Expand Down Expand Up @@ -705,13 +705,13 @@ static const Char* parseControlString(const Char* start, const Char* end)

// Try to parse an ANSI sequence at position. Returns type and end pointer.
template<typename Char>
static const Char* tryParseAnsi(const Char* start, const Char* end, TokenType& type, bool& isSgr, bool& isCanonicalSgr, bool& isHyperlinkOpen, StringBuilder& hyperlinkCodeBuilder, String& hyperlinkClosePrefix, String& hyperlinkTerminator)
static const Char* tryParseAnsi(const Char* start, const Char* end, TokenType& type, bool& isSgr, bool& isCanonicalSgr, bool& isHyperlinkOpen, std::span<const Char>& hyperlinkCode, String& hyperlinkClosePrefix, String& hyperlinkTerminator)
{
Char c = *start;

// Try hyperlink first (for ESC and C1 OSC)
if (c == 0x1b || c == 0x9d) {
const Char* hlEnd = parseHyperlink(start, end, isHyperlinkOpen, hyperlinkCodeBuilder, hyperlinkClosePrefix, hyperlinkTerminator);
const Char* hlEnd = parseHyperlink(start, end, isHyperlinkOpen, hyperlinkCode, hyperlinkClosePrefix, hyperlinkTerminator);
if (hlEnd) {
type = TokenType::Hyperlink;
return hlEnd;
Expand Down Expand Up @@ -803,7 +803,7 @@ static size_t computeTotalWidth(std::span<const Char> input, size_t asciiPrefix,
if (ANSI::isEscapeCharacter(*p) || *p == 0x9c) {
TokenType type;
bool a, b, c;
StringBuilder d;
std::span<const Char> d;
String e, f;
if (const Char* after = tryParseAnsi(p, dataEnd, type, a, b, c, d, e, f)) {
p = after;
Expand Down Expand Up @@ -859,8 +859,9 @@ static size_t computeTotalWidth(std::span<const Char> input, size_t asciiPrefix,
// close-only). The buffer holds at most a few short spans (typically 0-1).
//
// `end == SIZE_MAX` means unbounded (endD was +Inf) — we emit to EOF.
// std::nullopt: out of memory (the result passes the string length limit).
template<typename Char>
static WTF::String emitSliceStreaming(
static std::optional<WTF::String> emitSliceStreaming(
std::span<const Char> input, size_t asciiPrefix,
size_t start, size_t end,
StringView ellipsis, size_t ellipsisWidth,
Expand All @@ -872,12 +873,15 @@ static WTF::String emitSliceStreaming(
const Char* const dataEnd = data + input.size();
const bool endUnbounded = (end == SIZE_MAX);

StringBuilder result;
result.reserveCapacity(input.size());
// Only the input and the ellipsis can put a 16-bit character into the result.
const bool canUpconvert = sizeof(Char) == 2 || !ellipsis.is8Bit();
StringBuilder result { OverflowPolicy::RecordOverflow };
result.reserveCapacity(canUpconvert ? cappedStringBuilderReserve(input.size()) : static_cast<unsigned>(input.size()));

SgrStyleState activeStyles;
bool activeHyperlink = false;
String activeHyperlinkClosePrefix, activeHyperlinkTerminator, activeHyperlinkCode;
String activeHyperlinkClosePrefix, activeHyperlinkTerminator;
std::span<const Char> activeHyperlinkCode;

// Column where the NEXT new cluster starts. Correct at all breaks.
size_t position = 0;
Expand All @@ -901,7 +905,7 @@ static WTF::String emitSliceStreaming(
};
Vector<Pending, 4> pending;
// Captured hyperlink parse state per pending Hyperlink entry (indexed separately).
Vector<std::tuple<String, String, String>, 2> pendingHl; // code, closePrefix, terminator
Vector<std::tuple<std::span<const Char>, String, String>, 2> pendingHl; // code, closePrefix, terminator

auto flushPending = [&](bool filterCloseOnly) {
size_t hlIdx = 0;
Expand Down Expand Up @@ -1232,7 +1236,7 @@ static WTF::String emitSliceStreaming(
if (ANSI::isEscapeCharacter(*p) || *p == 0x9c) {
TokenType type = TokenType::Character;
bool isSgr = false, isCanon = false, hlOpen = false;
StringBuilder hlCode;
std::span<const Char> hlCode;
String hlCP, hlTerm;
const Char* after = tryParseAnsi(p, dataEnd, type, isSgr, isCanon, hlOpen, hlCode, hlCP, hlTerm);
if (after) {
Expand All @@ -1244,7 +1248,7 @@ static WTF::String emitSliceStreaming(
case TokenType::Hyperlink:
if (hlOpen) {
activeHyperlink = true;
activeHyperlinkCode = hlCode.toString();
activeHyperlinkCode = hlCode;
activeHyperlinkClosePrefix = hlCP;
activeHyperlinkTerminator = hlTerm;
} else
Expand All @@ -1256,7 +1260,7 @@ static WTF::String emitSliceStreaming(
} else {
pending.append(Pending { p, after, type, hlOpen });
if (type == TokenType::Hyperlink)
pendingHl.append(std::make_tuple(hlCode.toString(), hlCP, hlTerm));
pendingHl.append(std::make_tuple(hlCode, hlCP, hlTerm));
}
p = after;
continue;
Expand Down Expand Up @@ -1337,11 +1341,14 @@ walkDone:;
}
if (needEndEllipsis) result.append(ellipsis);
activeStyles.emitCloseCodes(result);
if (result.hasOverflowed() || exceedsStringLimit(result.length())) [[unlikely]]
return std::nullopt;
Comment thread
robobun marked this conversation as resolved.
return result.toString();
}

// std::nullopt: out of memory. A null String: the caller reuses the input string.
template<typename Char>
static WTF::String sliceAnsiImpl(std::span<const Char> input, double startD, double endD, StringView ellipsis, size_t ellipsisWidth, bool ambiguousIsWide)
static std::optional<WTF::String> sliceAnsiImpl(std::span<const Char> input, double startD, double endD, StringView ellipsis, size_t ellipsisWidth, bool ambiguousIsWide)
{
if (input.empty())
return emptyString();
Expand Down Expand Up @@ -1397,9 +1404,21 @@ static WTF::String sliceAnsiImpl(std::span<const Char> input, double startD, dou
if (doEnd) en -= ellipsisWidth;
if (!doStart && !doEnd) return ellipsis.toString();
StringView content(std::span { data + st, en - st });
if (doStart && doEnd) return makeString(ellipsis, content, ellipsis);
if (doStart) return makeString(ellipsis, content);
return makeString(content, ellipsis);
// The ellipsis makes the result longer than the input.
const size_t ellipsisCount = (doStart ? 1 : 0) + (doEnd ? 1 : 0);
const size_t resultLength = content.length() + ellipsisCount * ellipsis.length();
if (exceedsStringLimit(resultLength)) [[unlikely]]
return std::nullopt;
WTF::String joined;
if (doStart && doEnd)
joined = tryMakeString(ellipsis, content, ellipsis);
else if (doStart)
joined = tryMakeString(ellipsis, content);
else
joined = tryMakeString(content, ellipsis);
if (joined.isNull()) [[unlikely]]
return std::nullopt;
return joined;
}
return WTF::String(std::span { data + st, en - st });
}
Expand Down Expand Up @@ -1537,19 +1556,23 @@ JSC_DEFINE_HOST_FUNCTION(jsFunctionBunSliceAnsi, (JSC::JSGlobalObject * globalOb
: Bun__visibleWidthExcludeANSI_utf16(reinterpret_cast<const uint16_t*>(ellipsis.span16().data()), ellipsis.length(), ambiguousIsWide);
}

WTF::String result;
std::optional<WTF::String> result;
if (view->is8Bit()) {
result = sliceAnsiImpl<Latin1Character>(view->span8(), startD, endD, ellipsis, ellipsisWidth, ambiguousIsWide);
} else {
result = sliceAnsiImpl<UChar>(view->span16(), startD, endD, ellipsis, ellipsisWidth, ambiguousIsWide);
}

if (!result) [[unlikely]] {
throwOutOfMemoryError(globalObject, scope);
return {};
}
// null → no-op fast path hit: return the input JSString unchanged (zero-copy).
if (result.isNull())
if (result->isNull())
return JSC::JSValue::encode(jsString);
if (result.isEmpty())
if (result->isEmpty())
return JSC::JSValue::encode(JSC::jsEmptyString(vm));
return JSC::JSValue::encode(JSC::jsString(vm, result));
return JSC::JSValue::encode(JSC::jsString(vm, WTF::move(*result)));
}

} // namespace Bun
11 changes: 3 additions & 8 deletions src/jsc/bindings/webcore/streams/WebStreamsInternals.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
#include <wtf/Vector.h>
#include <wtf/text/WTFString.h>
#include "helpers.h"
#include "StringSizeLimit.h"
#include <wtf/text/StringImpl.h>

namespace WebCore {
Expand All @@ -41,14 +42,8 @@ class AbortSignal;
namespace Bun {
namespace WebStreams {

// Building a string past this limit would abort the process inside WTF; text consumers
// check it and throw a catchable out-of-memory error instead. Mirrors the predicate
// Bun's string constructors use (helpers.h), including the synthetic limit that
// `bun:internal-for-testing` can lower.
inline bool exceedsStringLimit(size_t length)
{
return length > Bun__stringSyntheticAllocationLimit || length > WTF::StringImpl::MaxLength;
}
// Text consumers check the string length limit and throw a catchable out-of-memory error.
using Bun::exceedsStringLimit;

// Reduce noise: every class name below is a WebCore JS cell (StreamsForward.h).
using WebCore::JSCrossRealmTransformState;
Expand Down
Loading
Loading