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
17 changes: 13 additions & 4 deletions src/bun.js/bindings/JSValue.zig
Original file line number Diff line number Diff line change
Expand Up @@ -405,17 +405,26 @@ pub const JSValue = enum(i64) {
return bun.jsc.fromJSHostCallGeneric(globalObject, @src(), JSC__JSValue__push, .{ value, globalObject, out });
}

extern fn JSC__JSValue__toISOString(*jsc.JSGlobalObject, jsc.JSValue, *[28]u8) c_int;
pub fn toISOString(this: JSValue, globalObject: *jsc.JSGlobalObject, buf: *[28]u8) []const u8 {
/// Buffer type for `toISOString` / `getDateNowISOString`. Must match the
/// `char in[64]` parameter of `Bun::toISOString` in `wtf-bindings.cpp`.
pub const ISOStringBuffer = [64]u8;

extern fn JSC__JSValue__toISOString(*jsc.JSGlobalObject, jsc.JSValue, *ISOStringBuffer) c_int;
/// Serializes a JavaScript `Date` value as an ISO 8601 / RFC 3339 string
/// (the same format as `Date.prototype.toISOString`). Returns an empty
/// slice on failure (e.g. the value is not a finite `Date`).
pub fn toISOString(this: JSValue, globalObject: *jsc.JSGlobalObject, buf: *ISOStringBuffer) []const u8 {
const count = JSC__JSValue__toISOString(globalObject, this, buf);
if (count < 0) {
return "";
Comment thread
robobun marked this conversation as resolved.
}

return buf[0..@as(usize, @intCast(count))];
}
extern fn JSC__JSValue__DateNowISOString(*JSGlobalObject, f64) JSValue;
pub fn getDateNowISOString(globalObject: *jsc.JSGlobalObject, buf: *[28]u8) []const u8 {
extern fn JSC__JSValue__DateNowISOString(*jsc.JSGlobalObject, *ISOStringBuffer) c_int;
/// Writes `new Date(Date.now()).toISOString()` into `buf` and returns
/// the written slice. Empty slice on failure.
pub fn getDateNowISOString(globalObject: *jsc.JSGlobalObject, buf: *ISOStringBuffer) []const u8 {
const count = JSC__JSValue__DateNowISOString(globalObject, buf);
if (count < 0) {
return "";
Expand Down
32 changes: 4 additions & 28 deletions src/bun.js/bindings/bindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5801,7 +5801,6 @@ extern "C" EncodedJSValue JSC__JSValue__dateInstanceFromNullTerminatedString(JSC
// this is largely copied from dateProtoFuncToISOString
extern "C" int JSC__JSValue__toISOString(JSC::JSGlobalObject* globalObject, EncodedJSValue dateValue, char* buf)
{
char buffer[64];
JSC::DateInstance* thisDateObj = dynamicDowncast<JSC::DateInstance>(JSC::JSValue::decode(dateValue));
if (!thisDateObj)
return -1;
Expand All @@ -5811,41 +5810,18 @@ extern "C" int JSC__JSValue__toISOString(JSC::JSGlobalObject* globalObject, Enco

auto& vm = JSC::getVM(globalObject);

return static_cast<int>(Bun::toISOString(vm, thisDateObj->internalNumber(), buffer));
return static_cast<int>(Bun::toISOString(vm, thisDateObj->internalNumber(), buf));
}

extern "C" int JSC__JSValue__DateNowISOString(JSC::JSGlobalObject* globalObject, char* buf)
Comment thread
claude[bot] marked this conversation as resolved.
{
char buffer[29];
JSC::DateInstance* thisDateObj = JSC::DateInstance::create(globalObject->vm(), globalObject->dateStructure(), globalObject->jsDateNow());

if (!std::isfinite(thisDateObj->internalNumber()))
return -1;

auto& vm = JSC::getVM(globalObject);
double now = globalObject->jsDateNow();

const GregorianDateTime* gregorianDateTime = thisDateObj->gregorianDateTimeUTC(vm.dateCache);
if (!gregorianDateTime)
return -1;

// If the year is outside the bounds of 0 and 9999 inclusive we want to use the extended year format (ES 15.9.1.15.1).
int ms = static_cast<int>(fmod(thisDateObj->internalNumber(), msPerSecond));
if (ms < 0)
ms += msPerSecond;

int charactersWritten;
if (gregorianDateTime->year() > 9999 || gregorianDateTime->year() < 0)
charactersWritten = snprintf(buffer, sizeof(buffer), "%+07d-%02d-%02dT%02d:%02d:%02d.%03dZ", gregorianDateTime->year(), gregorianDateTime->month() + 1, gregorianDateTime->monthDay(), gregorianDateTime->hour(), gregorianDateTime->minute(), gregorianDateTime->second(), ms);
else
charactersWritten = snprintf(buffer, sizeof(buffer), "%04d-%02d-%02dT%02d:%02d:%02d.%03dZ", gregorianDateTime->year(), gregorianDateTime->month() + 1, gregorianDateTime->monthDay(), gregorianDateTime->hour(), gregorianDateTime->minute(), gregorianDateTime->second(), ms);

memcpy(buf, buffer, charactersWritten);

ASSERT(charactersWritten > 0 && static_cast<unsigned>(charactersWritten) < sizeof(buffer));
if (static_cast<unsigned>(charactersWritten) >= sizeof(buffer))
if (!std::isfinite(now))
return -1;

return charactersWritten;
return static_cast<int>(Bun::toISOString(vm, now, buf));
}

#pragma mark - WebCore::DOMFormData
Expand Down
2 changes: 1 addition & 1 deletion src/s3/credentials.zig
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,7 @@ pub const S3Credentials = struct {

fn getAMZDate(allocator: std.mem.Allocator) DateResult {
// We can also use Date.now() but would be slower and would add jsc dependency
// var buffer: [28]u8 = undefined;
// var buffer: jsc.JSValue.ISOStringBuffer = undefined;
// the code bellow is the same as new Date(Date.now()).toISOString()
// jsc.JSValue.getDateNowISOString(globalObject, &buffer);

Expand Down
36 changes: 28 additions & 8 deletions src/sql/postgres/PostgresRequest.zig
Original file line number Diff line number Diff line change
Expand Up @@ -151,14 +151,34 @@ pub fn writeBind(
},

else => {
const str = try String.fromJS(value, globalObject);
if (str.tag == .Dead) return error.OutOfMemory;
defer str.deref();
const slice = str.toUTF8WithoutRef(bun.default_allocator);
defer slice.deinit();
const l = try writer.length();
try writer.write(slice.slice());
try l.writeExcludingSelf();
// JS `Date` objects reach this branch whenever the server
// hasn't told us the parameter type is `.timestamp` /
// `.timestamptz` — e.g. under `prepare: false`, or on the
// first execution of a fresh prepared statement before
// Describe returns. `bun.String.fromJS(date)` would emit
// `Date.prototype.toString()` output (a locale-dependent
// string like "Mon Jan 15 2024 12:30:45 GMT+0000 ..."),
// which PostgreSQL rejects. Serialize as ISO 8601 instead.
// On failure propagate a bind error rather than falling
// back to the legacy text path — the fallback would just
// re-emit the same broken locale string.
if (value.isDate()) {
var iso_buf: JSValue.ISOStringBuffer = undefined;
const iso = value.toISOString(globalObject, &iso_buf);
if (iso.len == 0) return error.InvalidQueryBinding;
Comment thread
robobun marked this conversation as resolved.
const l = try writer.length();
try writer.write(iso);
try l.writeExcludingSelf();
} else {
const str = try String.fromJS(value, globalObject);
if (str.tag == .Dead) return error.OutOfMemory;
defer str.deref();
const slice = str.toUTF8WithoutRef(bun.default_allocator);
defer slice.deinit();
const l = try writer.length();
try writer.write(slice.slice());
try l.writeExcludingSelf();
Comment thread
robobun marked this conversation as resolved.
}
},
}
Comment thread
robobun marked this conversation as resolved.
}
Comment thread
robobun marked this conversation as resolved.
Expand Down
42 changes: 42 additions & 0 deletions src/sql/postgres/PostgresSQLConnection.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1017,6 +1017,35 @@ pub const Writer = struct {
}
};

/// Snapshot of `write_buffer` length for rollback on a failed write.
///
/// `PostgresRequest.*` helpers append directly to `write_buffer` and can
/// fail mid-message — e.g. `writeBind` returns `error.InvalidQueryBinding`
/// for `new Date(NaN)` *after* emitting the 'B' tag, a zeroed length
/// placeholder, names, format codes and earlier parameter values. If that
/// partial frame is left in the buffer, the next query's bytes are appended
/// after it and `flushData()` ships a protocol stream PostgreSQL rejects
/// with "invalid message length", killing the connection.
///
/// Capture with `.take(connection)` immediately before a `PostgresRequest`
/// call and `.restore(connection)` at the top of its `catch |err|` block so
/// the connection remains usable after a bind error the user catches.
/// `head` is not captured — it only advances in `flushData()`, which is not
/// called between the snapshot and the catch.
pub const WriteBufferSnapshot = struct {
len: u32,

pub fn take(connection: *const PostgresSQLConnection) WriteBufferSnapshot {
return .{ .len = connection.write_buffer.byte_list.len };
}

pub fn restore(self: WriteBufferSnapshot, connection: *PostgresSQLConnection) void {
bun.debugAssert(self.len <= connection.write_buffer.byte_list.len);
bun.debugAssert(connection.write_buffer.head <= self.len);
connection.write_buffer.byte_list.len = self.len;
}
};

pub fn writer(this: *PostgresSQLConnection) protocol.NewWriter(Writer) {
return .{
.wrapped = .{
Expand Down Expand Up @@ -1139,7 +1168,9 @@ fn advance(this: *PostgresSQLConnection) void {
var query_str = req.query.toUTF8(bun.default_allocator);
defer query_str.deinit();
debug("execute simple query: {s}", .{query_str.slice()});
const write_start = WriteBufferSnapshot.take(this);
PostgresRequest.executeQuery(query_str.slice(), PostgresSQLConnection.Writer, this.writer()) catch |err| {
write_start.restore(this);
if (this.globalObject.tryTakeException()) |err_| {
req.onJSError(err_, this.globalObject);
} else {
Expand Down Expand Up @@ -1202,7 +1233,9 @@ fn advance(this: *PostgresSQLConnection) void {
debug("parse, bind and execute unnamed stmt", .{});
var query_str = req.query.toUTF8(bun.default_allocator);
defer query_str.deinit();
const write_start = WriteBufferSnapshot.take(this);
PostgresRequest.parseAndBindAndExecute(this.globalObject, query_str.slice(), statement, binding_value, columns_value, false, PostgresSQLConnection.Writer, this.writer()) catch |err| {
write_start.restore(this);
if (this.globalObject.tryTakeException()) |err_| {
req.onJSError(err_, this.globalObject);
} else {
Expand All @@ -1221,7 +1254,9 @@ fn advance(this: *PostgresSQLConnection) void {
};
} else {
debug("binding and executing stmt", .{});
const write_start = WriteBufferSnapshot.take(this);
PostgresRequest.bindAndExecute(this.globalObject, statement, binding_value, columns_value, PostgresSQLConnection.Writer, this.writer()) catch |err| {
write_start.restore(this);
if (this.globalObject.tryTakeException()) |err_| {
req.onJSError(err_, this.globalObject);
} else {
Expand Down Expand Up @@ -1280,7 +1315,9 @@ fn advance(this: *PostgresSQLConnection) void {
// prepareAndQueryWithSignature will write + bind + execute, it will change to running after binding is complete
const binding_value = PostgresSQLQuery.js.bindingGetCached(thisValue) orelse .zero;
debug("prepareAndQueryWithSignature", .{});
const write_start = WriteBufferSnapshot.take(this);
PostgresRequest.prepareAndQueryWithSignature(this.globalObject, query_str.slice(), binding_value, PostgresSQLConnection.Writer, this.writer(), &statement.signature) catch |err| {
write_start.restore(this);
if (this.globalObject.tryTakeException()) |err_| {
req.onJSError(err_, this.globalObject);
} else {
Expand Down Expand Up @@ -1323,7 +1360,9 @@ fn advance(this: *PostgresSQLConnection) void {
const binding_value = PostgresSQLQuery.js.bindingGetCached(thisValue) orelse .zero;
const columns_value = PostgresSQLQuery.js.columnsGetCached(thisValue) orelse .zero;
debug("parseAndBindAndExecute (unnamed, first execution)", .{});
const write_start = WriteBufferSnapshot.take(this);
PostgresRequest.parseAndBindAndExecute(this.globalObject, query_str.slice(), statement, binding_value, columns_value, true, PostgresSQLConnection.Writer, this.writer()) catch |err| {
write_start.restore(this);
if (this.globalObject.tryTakeException()) |err_| {
req.onJSError(err_, this.globalObject);
} else {
Expand Down Expand Up @@ -1353,7 +1392,9 @@ fn advance(this: *PostgresSQLConnection) void {
const connection_writer = this.writer();
debug("writing query", .{});
// write query and wait for it to be prepared
const write_start = WriteBufferSnapshot.take(this);
PostgresRequest.writeQuery(query_str.slice(), statement.signature.prepared_statement_name, statement.signature.fields, PostgresSQLConnection.Writer, connection_writer) catch |err| {
write_start.restore(this);
Comment thread
robobun marked this conversation as resolved.
if (this.globalObject.tryTakeException()) |err_| {
req.onJSError(err_, this.globalObject);
} else {
Expand All @@ -1368,6 +1409,7 @@ fn advance(this: *PostgresSQLConnection) void {
continue;
};
connection_writer.write(&protocol.Sync) catch |err| {
write_start.restore(this);
if (this.globalObject.tryTakeException()) |err_| {
req.onJSError(err_, this.globalObject);
} else {
Expand Down
9 changes: 9 additions & 0 deletions src/sql/postgres/PostgresSQLQuery.zig
Original file line number Diff line number Diff line change
Expand Up @@ -296,7 +296,9 @@ pub fn doRun(this: *PostgresSQLQuery, globalObject: *jsc.JSGlobalObject, callfra

const can_execute = !connection.hasQueryRunning();
if (can_execute) {
const write_start = PostgresSQLConnection.WriteBufferSnapshot.take(connection);
PostgresRequest.executeQuery(query_str.slice(), PostgresSQLConnection.Writer, writer) catch |err| {
write_start.restore(connection);
// fail to run do cleanup
this.statement = null;
bun.default_allocator.destroy(stmt);
Expand Down Expand Up @@ -371,7 +373,9 @@ pub fn doRun(this: *PostgresSQLQuery, globalObject: *jsc.JSGlobalObject, callfra
debug("bindAndExecute", .{});

// bindAndExecute will bind + execute, it will change to running after binding is complete
const write_start = PostgresSQLConnection.WriteBufferSnapshot.take(connection);
PostgresRequest.bindAndExecute(globalObject, this.statement.?, binding_value, columns_value, PostgresSQLConnection.Writer, writer) catch |err| {
write_start.restore(connection);
// fail to run do cleanup
this.statement = null;
stmt.deref();
Expand Down Expand Up @@ -402,7 +406,9 @@ pub fn doRun(this: *PostgresSQLQuery, globalObject: *jsc.JSGlobalObject, callfra
if (!has_params) {
debug("prepareAndQueryWithSignature", .{});
// prepareAndQueryWithSignature will write + bind + execute, it will change to running after binding is complete
const write_start = PostgresSQLConnection.WriteBufferSnapshot.take(connection);
PostgresRequest.prepareAndQueryWithSignature(globalObject, query_str.slice(), binding_value, PostgresSQLConnection.Writer, writer, &signature) catch |err| {
write_start.restore(connection);
if (connection_entry_value != null) {
_ = connection.statements.remove(signature_hash);
}
Expand All @@ -425,7 +431,9 @@ pub fn doRun(this: *PostgresSQLQuery, globalObject: *jsc.JSGlobalObject, callfra
// for ParameterDescription before sending Bind+Execute in advance().
debug("writeQuery", .{});

const write_start = PostgresSQLConnection.WriteBufferSnapshot.take(connection);
PostgresRequest.writeQuery(query_str.slice(), signature.prepared_statement_name, signature.fields, PostgresSQLConnection.Writer, writer) catch |err| {
write_start.restore(connection);
if (connection_entry_value != null) {
_ = connection.statements.remove(signature_hash);
}
Expand All @@ -440,6 +448,7 @@ pub fn doRun(this: *PostgresSQLQuery, globalObject: *jsc.JSGlobalObject, callfra
return error.JSError;
};
writer.write(&protocol.Sync) catch |err| {
write_start.restore(connection);
if (connection_entry_value != null) {
_ = connection.statements.remove(signature_hash);
}
Expand Down
11 changes: 10 additions & 1 deletion src/sql/postgres/types/date.zig
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ pub fn fromBinary(bytes: []const u8) f64 {
return (double_microseconds / std.time.us_per_ms) + POSTGRES_EPOCH_DATE;
}

pub fn fromJS(globalObject: *jsc.JSGlobalObject, value: JSValue) bun.JSError!i64 {
pub fn fromJS(globalObject: *jsc.JSGlobalObject, value: JSValue) AnyPostgresError!i64 {
const double_value = if (value.isDate())
value.getUnixTimestamp()
else if (value.isNumber())
Expand All @@ -22,6 +22,14 @@ pub fn fromJS(globalObject: *jsc.JSGlobalObject, value: JSValue) bun.JSError!i64
break :brk try str.parseDate(globalObject);
} else return 0;

// `@intFromFloat` on a non-finite value is Illegal Behavior. Invalid
// `Date` objects (e.g. `new Date("bad")` / `new Date(NaN)`) are real
// `DateInstance`s whose internal value is NaN, so `getUnixTimestamp()`
// — and likewise `parseDate` on a bad string or `asNumber` on `NaN` —
// can return NaN / ±Infinity here. The text path (`toISOString`) already
// rejects these via `std::isfinite`; mirror that for the binary path.
if (!std.math.isFinite(double_value)) return error.InvalidQueryBinding;
Comment thread
robobun marked this conversation as resolved.

const unix_timestamp: i64 = @intFromFloat(double_value);
return (unix_timestamp - POSTGRES_EPOCH_DATE) * std.time.us_per_ms;
}
Expand All @@ -46,6 +54,7 @@ pub fn toJS(

const bun = @import("bun");
const std = @import("std");
const AnyPostgresError = @import("../AnyPostgresError.zig").AnyPostgresError;
const Data = @import("../../shared/Data.zig").Data;

const int_types = @import("./int_types.zig");
Expand Down
Loading
Loading