Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
3 changes: 2 additions & 1 deletion docs/runtime/redis.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -340,8 +340,9 @@ The client automatically converts Redis responses to JavaScript values:
- Integer responses are returned as JavaScript numbers
- Bulk strings are returned as JavaScript strings
- Simple strings are returned as JavaScript strings
- Null bulk strings are returned as `null`
- Null bulk strings and null arrays are returned as `null`
- Array responses are returned as JavaScript arrays
- Big number responses (RESP3) are returned as `BigInt`. A payload that is not an integer literal is returned as a string. `getBuffer` returns the payload as a `Buffer`.
- Error responses throw JavaScript errors with appropriate error codes
- Boolean responses (RESP3) are returned as JavaScript booleans
- Map responses (RESP3) are returned as JavaScript objects
Expand Down
23 changes: 23 additions & 0 deletions src/jsc/JSValue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -666,6 +666,24 @@ impl JSValue {
pub fn from_uint64_no_truncate(global: &JSGlobalObject, i: u64) -> JsResult<JSValue> {
host_fn::from_js_host_call(global, || JSC__JSValue__fromUInt64NoTruncate(global, i))
}
/// A BigInt from a decimal integer literal (optional `-`, then digits).
/// Returns `Ok(None)` when `digits` is not such a literal.
#[track_caller]
pub fn big_int_from_decimal(
global: &JSGlobalObject,
digits: &[u8],
) -> JsResult<Option<JSValue>> {
let unsigned = digits.strip_prefix(b"-").unwrap_or(digits);
if unsigned.is_empty() || !unsigned.iter().all(u8::is_ascii_digit) {
return Ok(None);
}
// Only text StringToBigInt accepts gets here, so empty means it threw (too large).
let value = host_fn::from_js_host_call(global, || {
// SAFETY: `digits` is a live slice for the duration of the call.
unsafe { JSC__JSValue__bigIntFromLatin1(global, digits.as_ptr(), digits.len()) }
})?;
Ok(Some(value))
}
/// `JSValue.fromTimevalNoTruncate` — encode a `struct timeval`
/// as a BigInt (`sec * 1_000_000 + nsec`) without precision loss. May allocate
/// a heap BigInt, so wrapped in `from_js_host_call` for exception checking.
Expand Down Expand Up @@ -1984,6 +2002,11 @@ unsafe extern "C" {
safe fn JSC__JSValue__dateInstanceFromNumber(global: &JSGlobalObject, n: f64) -> JSValue;
safe fn JSC__JSValue__fromInt64NoTruncate(global: &JSGlobalObject, i: i64) -> JSValue;
safe fn JSC__JSValue__fromUInt64NoTruncate(global: &JSGlobalObject, i: u64) -> JSValue;
fn JSC__JSValue__bigIntFromLatin1(
global: &JSGlobalObject,
ptr: *const u8,
len: usize,
) -> JSValue;
safe fn JSC__JSValue__fromTimevalNoTruncate(
global: &JSGlobalObject,
nsec: i64,
Expand Down
7 changes: 7 additions & 0 deletions src/jsc/bindings/bindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4487,6 +4487,13 @@ JSC::EncodedJSValue JSC__JSValue__fromUInt64NoTruncate(JSC::JSGlobalObject* glob
return JSC::JSValue::encode(JSC::JSBigInt::createFrom(globalObject, val));
}

// Decimal integer literal (Latin-1) -> BigInt. Returns the empty value when
// the text is not a valid StringToBigInt input.
JSC::EncodedJSValue JSC__JSValue__bigIntFromLatin1(JSC::JSGlobalObject* globalObject, const uint8_t* ptr, size_t len)
{
return JSC::JSValue::encode(JSC::JSBigInt::stringToBigInt(globalObject, WTF::StringView(std::span { reinterpret_cast<const char*>(ptr), len })));
}

uint64_t JSC__JSValue__toUInt64NoTruncate(JSC::EncodedJSValue val)
{
JSC::JSValue value = JSC::JSValue::decode(val);
Expand Down
1 change: 1 addition & 0 deletions src/jsc/bindings/headers.h

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 7 additions & 11 deletions src/runtime/valkey_jsc/protocol_jsc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,11 +112,6 @@ pub(crate) fn resp_value_to_js_with_options(
RESPValue::Null => Ok(JSValue::NULL),
RESPValue::Double(d) => Ok(JSValue::js_number(*d)),
RESPValue::Boolean(b) => Ok(JSValue::from(*b)),
RESPValue::BlobError(str) => Ok(valkey_error_to_js(
global,
&**str,
RedisError::InvalidBlobError,
)),
RESPValue::VerbatimString(verbatim) => {
valkey_str_to_js_value(global, &mut verbatim.content, options)
}
Expand Down Expand Up @@ -158,14 +153,15 @@ pub(crate) fn resp_value_to_js_with_options(

Ok(js_obj)
}
// BigInt when the payload is an integer literal; modules and Lua can
// put anything after `(`, so other text stays a string.
RESPValue::BigNumber(str) => {
// Try to parse as number if possible
if let Ok(int) = bun_core::fmt::parse_int::<i64>(str, 10) {
Ok(JSValue::js_number(int as f64))
} else {
// If it doesn't fit in an i64, return as string
bun_string_jsc::create_utf8_for_js(global, str)
if !options.return_as_buffer
&& let Some(big) = JSValue::big_int_from_decimal(global, str)?
{
return Ok(big);
}
valkey_str_to_js_value(global, str, options)
}
}
}
19 changes: 13 additions & 6 deletions src/valkey/valkey_protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ impl RESPType {
pub enum RESPValue {
// RESP2 types
SimpleString(Box<[u8]>),
/// A `-` simple error or a `!` blob error reply, holding the server's message.
Error(Box<[u8]>),
Integer(i64),
BulkString(Option<Box<[u8]>>),
Expand All @@ -97,7 +98,6 @@ pub enum RESPValue {
Null,
Double(f64),
Boolean(bool),
BlobError(Box<[u8]>),
VerbatimString(VerbatimString),
Map(Vec<MapEntry>),
Set(Vec<RESPValue>),
Expand Down Expand Up @@ -134,7 +134,6 @@ impl fmt::Display for RESPValue {
RESPValue::Null => writer.write_str("(nil)"),
RESPValue::Double(d) => write!(writer, "{}", d),
RESPValue::Boolean(b) => write!(writer, "{}", b),
RESPValue::BlobError(str) => write!(writer, "Error: {}", BStr::new(str)),
RESPValue::VerbatimString(verbatim) => {
write!(
writer,
Expand Down Expand Up @@ -398,7 +397,8 @@ impl<'a> ValkeyReader<'a> {
}
let len = self.read_integer()?;
if len < 0 {
return Ok(RESPValue::Array(Vec::new()));
// RESP2 null array.
return Ok(RESPValue::Null);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
let len = usize::try_from(len).expect("int cast");
let mut array =
Expand All @@ -414,7 +414,9 @@ impl<'a> ValkeyReader<'a> {

// RESP3 types
RESPType::Null => {
let _ = self.read_until_crlf()?; // Read and discard CRLF
if !self.read_until_crlf()?.is_empty() {
return Err(RedisError::InvalidNull);
}
Ok(RESPValue::Null)
}
RESPType::Double => {
Expand All @@ -441,7 +443,7 @@ impl<'a> ValkeyReader<'a> {
return Err(RedisError::InvalidBlobError);
}
let owned = Box::<[u8]>::from(str);
Ok(RESPValue::BlobError(owned))
Ok(RESPValue::Error(owned))
}
RESPType::VerbatimString => Ok(RESPValue::VerbatimString(self.read_verbatim_string()?)),
RESPType::Map => {
Expand Down Expand Up @@ -668,13 +670,18 @@ impl ReplyScanner {
RESPType::SimpleString
| RESPType::Error
| RESPType::Integer
| RESPType::Null
| RESPType::Double
| RESPType::Boolean
| RESPType::BigNumber => {
let _ = reader.read_until_crlf()?;
Ok(None)
}
RESPType::Null => {
if !reader.read_until_crlf()?.is_empty() {
return Err(RedisError::InvalidNull);
}
Ok(None)
}
RESPType::BulkString | RESPType::BlobError | RESPType::VerbatimString => {
let invalid = match ty {
RESPType::BlobError => RedisError::InvalidBlobError,
Expand Down
Loading
Loading