Skip to content

Commit 5e61ccd

Browse files
committed
fix(rivetkit-sqlite): preserve text with embedded nul bytes
1 parent a70e758 commit 5e61ccd

5 files changed

Lines changed: 110 additions & 10 deletions

File tree

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
22
"code": "incoming_too_long",
33
"group": "message",
4-
"message": "Incoming message too long."
4+
"message": "Incoming message too long"
55
}

rivetkit-rust/packages/rivetkit-sqlite/src/query.rs

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -209,12 +209,15 @@ fn bind_params(
209209
BindParam::Null => unsafe { sqlite3_bind_null(stmt, bind_index) },
210210
BindParam::Integer(value) => unsafe { sqlite3_bind_int64(stmt, bind_index, *value) },
211211
BindParam::Float(value) => unsafe { sqlite3_bind_double(stmt, bind_index, *value) },
212-
BindParam::Text(value) => {
213-
let text = CString::new(value.as_str()).map_err(|err| anyhow!(err.to_string()))?;
214-
unsafe {
215-
sqlite3_bind_text(stmt, bind_index, text.as_ptr(), -1, SQLITE_TRANSIENT())
216-
}
217-
}
212+
BindParam::Text(value) => unsafe {
213+
sqlite3_bind_text(
214+
stmt,
215+
bind_index,
216+
value.as_ptr() as *const c_char,
217+
value.len() as i32,
218+
SQLITE_TRANSIENT(),
219+
)
220+
},
218221
BindParam::Blob(value) => unsafe {
219222
sqlite3_bind_blob(
220223
stmt,
@@ -258,9 +261,11 @@ fn column_value(stmt: *mut libsqlite3_sys::sqlite3_stmt, index: i32) -> ColumnVa
258261
if text_ptr.is_null() {
259262
ColumnValue::Null
260263
} else {
261-
let text = unsafe { CStr::from_ptr(text_ptr as *const c_char) }
262-
.to_string_lossy()
263-
.into_owned();
264+
let text_len = unsafe { sqlite3_column_bytes(stmt, index) } as usize;
265+
let text = String::from_utf8_lossy(unsafe {
266+
std::slice::from_raw_parts(text_ptr as *const u8, text_len)
267+
})
268+
.into_owned();
264269
ColumnValue::Text(text)
265270
}
266271
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
use std::ffi::CString;
2+
use std::ptr;
3+
4+
use libsqlite3_sys::{SQLITE_OK, sqlite3, sqlite3_close, sqlite3_open};
5+
use rivetkit_sqlite::query::{
6+
BindParam, ColumnValue, exec_statements, execute_statement, query_statement,
7+
};
8+
9+
struct MemoryDb(*mut sqlite3);
10+
11+
impl MemoryDb {
12+
fn open() -> Self {
13+
let name = CString::new(":memory:").unwrap();
14+
let mut db = ptr::null_mut();
15+
let rc = unsafe { sqlite3_open(name.as_ptr(), &mut db) };
16+
assert_eq!(rc, SQLITE_OK);
17+
Self(db)
18+
}
19+
20+
fn as_ptr(&self) -> *mut sqlite3 {
21+
self.0
22+
}
23+
}
24+
25+
impl Drop for MemoryDb {
26+
fn drop(&mut self) {
27+
unsafe {
28+
sqlite3_close(self.0);
29+
}
30+
}
31+
}
32+
33+
#[test]
34+
fn text_with_embedded_nul_round_trips() {
35+
let db = MemoryDb::open();
36+
exec_statements(
37+
db.as_ptr(),
38+
"CREATE TABLE items(id INTEGER PRIMARY KEY, label TEXT);",
39+
)
40+
.unwrap();
41+
42+
execute_statement(
43+
db.as_ptr(),
44+
"INSERT INTO items(label) VALUES (?);",
45+
Some(&[BindParam::Text("a\0b".to_owned())]),
46+
)
47+
.unwrap();
48+
49+
let rows = query_statement(
50+
db.as_ptr(),
51+
"SELECT label, hex(label), length(label) FROM items;",
52+
None,
53+
)
54+
.unwrap();
55+
56+
assert_eq!(
57+
rows.rows,
58+
vec![vec![
59+
ColumnValue::Text("a\0b".to_owned()),
60+
ColumnValue::Text("610062".to_owned()),
61+
ColumnValue::Integer(1),
62+
]]
63+
);
64+
}

rivetkit-typescript/packages/rivetkit/fixtures/driver-test-suite/actor-db-raw.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,23 @@ export const dbActorRaw = actor({
9898
);
9999
return { id: results[0].id };
100100
},
101+
insertValueAndReadBack: async (c, value: string) => {
102+
await c.db.execute(
103+
"INSERT INTO test_data (value, payload, created_at) VALUES (?, ?, ?)",
104+
value,
105+
"",
106+
Date.now(),
107+
);
108+
const inserted = await c.db.execute<{
109+
id: number;
110+
value: string;
111+
hex_value: string;
112+
sqlite_length: number;
113+
}>(
114+
`SELECT id, value, hex(value) as hex_value, length(value) as sqlite_length FROM test_data ORDER BY id DESC LIMIT 1`,
115+
);
116+
return inserted[0] ?? null;
117+
},
101118
getValues: async (c) => {
102119
const results = await c.db.execute<{
103120
id: number;

rivetkit-typescript/packages/rivetkit/tests/driver/actor-db-raw.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,20 @@ describeDriverMatrix("Actor Db Raw", (driverTestConfig) => {
2323
expect(values[1].value).toBe("Bob");
2424
});
2525

26+
test("round-trips text containing an embedded nul byte", async (c) => {
27+
const { client } = await setupDriverTest(c, driverTestConfig);
28+
29+
const instance = client.dbActorRaw.getOrCreate(["nul-text"]);
30+
const input = "a\0b";
31+
32+
const row = await instance.insertValueAndReadBack(input);
33+
34+
expect(row).not.toBeNull();
35+
expect(row!.value).toBe(input);
36+
expect(row!.hex_value).toBe("610062");
37+
expect(row!.sqlite_length).toBe(1);
38+
});
39+
2640
test("persists data across actor instances", async (c) => {
2741
const { client } = await setupDriverTest(c, driverTestConfig);
2842

0 commit comments

Comments
 (0)