fix(imessage): replace sqlite CLI path with rusqlite parameterized reads

- use rusqlite with SQLITE_OPEN_READ_ONLY | SQLITE_OPEN_NO_MUTEX
- run sync sqlite reads via spawn_blocking
- bind since_rowid with ?1 parameter to avoid SQL interpolation
- add comprehensive edge-case tests for message fetch and rowid helpers

Fixes #50
This commit is contained in:
argenis de la rosa 2026-02-14 18:10:39 -05:00
parent 860b6acc31
commit 7c3f2f565f

View file

@ -210,9 +210,7 @@ async fn get_max_rowid(db_path: &Path) -> anyhow::Result<i64> {
&path, &path,
OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_NO_MUTEX, OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_NO_MUTEX,
)?; )?;
let mut stmt = conn.prepare( let mut stmt = conn.prepare("SELECT MAX(ROWID) FROM message WHERE is_from_me = 0")?;
"SELECT MAX(ROWID) FROM message WHERE is_from_me = 0"
)?;
let rowid: Option<i64> = stmt.query_row([], |row| row.get(0))?; let rowid: Option<i64> = stmt.query_row([], |row| row.get(0))?;
Ok(rowid.unwrap_or(0)) Ok(rowid.unwrap_or(0))
}) })
@ -228,31 +226,32 @@ async fn fetch_new_messages(
since_rowid: i64, since_rowid: i64,
) -> anyhow::Result<Vec<(i64, String, String)>> { ) -> anyhow::Result<Vec<(i64, String, String)>> {
let path = db_path.to_path_buf(); let path = db_path.to_path_buf();
let results = tokio::task::spawn_blocking(move || -> anyhow::Result<Vec<(i64, String, String)>> { let results =
let conn = Connection::open_with_flags( tokio::task::spawn_blocking(move || -> anyhow::Result<Vec<(i64, String, String)>> {
&path, let conn = Connection::open_with_flags(
OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_NO_MUTEX, &path,
)?; OpenFlags::SQLITE_OPEN_READ_ONLY | OpenFlags::SQLITE_OPEN_NO_MUTEX,
let mut stmt = conn.prepare( )?;
"SELECT m.ROWID, h.id, m.text \ let mut stmt = conn.prepare(
"SELECT m.ROWID, h.id, m.text \
FROM message m \ FROM message m \
JOIN handle h ON m.handle_id = h.ROWID \ JOIN handle h ON m.handle_id = h.ROWID \
WHERE m.ROWID > ?1 \ WHERE m.ROWID > ?1 \
AND m.is_from_me = 0 \ AND m.is_from_me = 0 \
AND m.text IS NOT NULL \ AND m.text IS NOT NULL \
ORDER BY m.ROWID ASC \ ORDER BY m.ROWID ASC \
LIMIT 20" LIMIT 20",
)?; )?;
let rows = stmt.query_map([since_rowid], |row| { let rows = stmt.query_map([since_rowid], |row| {
Ok(( Ok((
row.get::<_, i64>(0)?, row.get::<_, i64>(0)?,
row.get::<_, String>(1)?, row.get::<_, String>(1)?,
row.get::<_, String>(2)?, row.get::<_, String>(2)?,
)) ))
})?; })?;
rows.collect::<Result<Vec<_>, _>>().map_err(Into::into) rows.collect::<Result<Vec<_>, _>>().map_err(Into::into)
}) })
.await??; .await??;
Ok(results) Ok(results)
} }
@ -551,8 +550,9 @@ mod tests {
text TEXT, text TEXT,
is_from_me INTEGER DEFAULT 0, is_from_me INTEGER DEFAULT 0,
FOREIGN KEY (handle_id) REFERENCES handle(ROWID) FOREIGN KEY (handle_id) REFERENCES handle(ROWID)
);" );",
).unwrap(); )
.unwrap();
(dir, db_path) (dir, db_path)
} }
@ -573,7 +573,11 @@ mod tests {
// Insert test data // Insert test data
{ {
let conn = Connection::open(&db_path).unwrap(); let conn = Connection::open(&db_path).unwrap();
conn.execute("INSERT INTO handle (ROWID, id) VALUES (1, '+1234567890')", []).unwrap(); conn.execute(
"INSERT INTO handle (ROWID, id) VALUES (1, '+1234567890')",
[],
)
.unwrap();
conn.execute( conn.execute(
"INSERT INTO message (ROWID, handle_id, text, is_from_me) VALUES (100, 1, 'Hello', 0)", "INSERT INTO message (ROWID, handle_id, text, is_from_me) VALUES (100, 1, 'Hello', 0)",
[] []
@ -616,8 +620,16 @@ mod tests {
// Insert test data // Insert test data
{ {
let conn = Connection::open(&db_path).unwrap(); let conn = Connection::open(&db_path).unwrap();
conn.execute("INSERT INTO handle (ROWID, id) VALUES (1, '+1234567890')", []).unwrap(); conn.execute(
conn.execute("INSERT INTO handle (ROWID, id) VALUES (2, 'user@example.com')", []).unwrap(); "INSERT INTO handle (ROWID, id) VALUES (1, '+1234567890')",
[],
)
.unwrap();
conn.execute(
"INSERT INTO handle (ROWID, id) VALUES (2, 'user@example.com')",
[],
)
.unwrap();
conn.execute( conn.execute(
"INSERT INTO message (ROWID, handle_id, text, is_from_me) VALUES (10, 1, 'First message', 0)", "INSERT INTO message (ROWID, handle_id, text, is_from_me) VALUES (10, 1, 'First message', 0)",
[] []
@ -630,8 +642,18 @@ mod tests {
let result = fetch_new_messages(&db_path, 0).await.unwrap(); let result = fetch_new_messages(&db_path, 0).await.unwrap();
assert_eq!(result.len(), 2); assert_eq!(result.len(), 2);
assert_eq!(result[0], (10, "+1234567890".to_string(), "First message".to_string())); assert_eq!(
assert_eq!(result[1], (20, "user@example.com".to_string(), "Second message".to_string())); result[0],
(10, "+1234567890".to_string(), "First message".to_string())
);
assert_eq!(
result[1],
(
20,
"user@example.com".to_string(),
"Second message".to_string()
)
);
} }
#[tokio::test] #[tokio::test]
@ -641,7 +663,11 @@ mod tests {
// Insert test data // Insert test data
{ {
let conn = Connection::open(&db_path).unwrap(); let conn = Connection::open(&db_path).unwrap();
conn.execute("INSERT INTO handle (ROWID, id) VALUES (1, '+1234567890')", []).unwrap(); conn.execute(
"INSERT INTO handle (ROWID, id) VALUES (1, '+1234567890')",
[],
)
.unwrap();
conn.execute( conn.execute(
"INSERT INTO message (ROWID, handle_id, text, is_from_me) VALUES (10, 1, 'Old message', 0)", "INSERT INTO message (ROWID, handle_id, text, is_from_me) VALUES (10, 1, 'Old message', 0)",
[] []
@ -666,7 +692,11 @@ mod tests {
// Insert test data // Insert test data
{ {
let conn = Connection::open(&db_path).unwrap(); let conn = Connection::open(&db_path).unwrap();
conn.execute("INSERT INTO handle (ROWID, id) VALUES (1, '+1234567890')", []).unwrap(); conn.execute(
"INSERT INTO handle (ROWID, id) VALUES (1, '+1234567890')",
[],
)
.unwrap();
conn.execute( conn.execute(
"INSERT INTO message (ROWID, handle_id, text, is_from_me) VALUES (10, 1, 'Received', 0)", "INSERT INTO message (ROWID, handle_id, text, is_from_me) VALUES (10, 1, 'Received', 0)",
[] []
@ -689,15 +719,20 @@ mod tests {
// Insert test data // Insert test data
{ {
let conn = Connection::open(&db_path).unwrap(); let conn = Connection::open(&db_path).unwrap();
conn.execute("INSERT INTO handle (ROWID, id) VALUES (1, '+1234567890')", []).unwrap(); conn.execute(
"INSERT INTO handle (ROWID, id) VALUES (1, '+1234567890')",
[],
)
.unwrap();
conn.execute( conn.execute(
"INSERT INTO message (ROWID, handle_id, text, is_from_me) VALUES (10, 1, 'Has text', 0)", "INSERT INTO message (ROWID, handle_id, text, is_from_me) VALUES (10, 1, 'Has text', 0)",
[] []
).unwrap(); ).unwrap();
conn.execute( conn.execute(
"INSERT INTO message (ROWID, handle_id, text, is_from_me) VALUES (20, 1, NULL, 0)", "INSERT INTO message (ROWID, handle_id, text, is_from_me) VALUES (20, 1, NULL, 0)",
[] [],
).unwrap(); )
.unwrap();
} }
let result = fetch_new_messages(&db_path, 0).await.unwrap(); let result = fetch_new_messages(&db_path, 0).await.unwrap();
@ -712,7 +747,11 @@ mod tests {
// Insert 25 messages (limit is 20) // Insert 25 messages (limit is 20)
{ {
let conn = Connection::open(&db_path).unwrap(); let conn = Connection::open(&db_path).unwrap();
conn.execute("INSERT INTO handle (ROWID, id) VALUES (1, '+1234567890')", []).unwrap(); conn.execute(
"INSERT INTO handle (ROWID, id) VALUES (1, '+1234567890')",
[],
)
.unwrap();
for i in 1..=25 { for i in 1..=25 {
conn.execute( conn.execute(
&format!("INSERT INTO message (ROWID, handle_id, text, is_from_me) VALUES ({i}, 1, 'Message {i}', 0)"), &format!("INSERT INTO message (ROWID, handle_id, text, is_from_me) VALUES ({i}, 1, 'Message {i}', 0)"),
@ -734,7 +773,11 @@ mod tests {
// Insert messages out of order // Insert messages out of order
{ {
let conn = Connection::open(&db_path).unwrap(); let conn = Connection::open(&db_path).unwrap();
conn.execute("INSERT INTO handle (ROWID, id) VALUES (1, '+1234567890')", []).unwrap(); conn.execute(
"INSERT INTO handle (ROWID, id) VALUES (1, '+1234567890')",
[],
)
.unwrap();
conn.execute( conn.execute(
"INSERT INTO message (ROWID, handle_id, text, is_from_me) VALUES (30, 1, 'Third', 0)", "INSERT INTO message (ROWID, handle_id, text, is_from_me) VALUES (30, 1, 'Third', 0)",
[] []
@ -770,7 +813,11 @@ mod tests {
// Insert message with special characters (potential SQL injection patterns) // Insert message with special characters (potential SQL injection patterns)
{ {
let conn = Connection::open(&db_path).unwrap(); let conn = Connection::open(&db_path).unwrap();
conn.execute("INSERT INTO handle (ROWID, id) VALUES (1, '+1234567890')", []).unwrap(); conn.execute(
"INSERT INTO handle (ROWID, id) VALUES (1, '+1234567890')",
[],
)
.unwrap();
conn.execute( conn.execute(
"INSERT INTO message (ROWID, handle_id, text, is_from_me) VALUES (10, 1, 'Hello \"world'' OR 1=1; DROP TABLE message;--', 0)", "INSERT INTO message (ROWID, handle_id, text, is_from_me) VALUES (10, 1, 'Hello \"world'' OR 1=1; DROP TABLE message;--', 0)",
[] []
@ -789,7 +836,11 @@ mod tests {
{ {
let conn = Connection::open(&db_path).unwrap(); let conn = Connection::open(&db_path).unwrap();
conn.execute("INSERT INTO handle (ROWID, id) VALUES (1, '+1234567890')", []).unwrap(); conn.execute(
"INSERT INTO handle (ROWID, id) VALUES (1, '+1234567890')",
[],
)
.unwrap();
conn.execute( conn.execute(
"INSERT INTO message (ROWID, handle_id, text, is_from_me) VALUES (10, 1, 'Hello 🦀 世界 مرحبا', 0)", "INSERT INTO message (ROWID, handle_id, text, is_from_me) VALUES (10, 1, 'Hello 🦀 世界 مرحبا', 0)",
[] []
@ -807,11 +858,16 @@ mod tests {
{ {
let conn = Connection::open(&db_path).unwrap(); let conn = Connection::open(&db_path).unwrap();
conn.execute("INSERT INTO handle (ROWID, id) VALUES (1, '+1234567890')", []).unwrap(); conn.execute(
"INSERT INTO handle (ROWID, id) VALUES (1, '+1234567890')",
[],
)
.unwrap();
conn.execute( conn.execute(
"INSERT INTO message (ROWID, handle_id, text, is_from_me) VALUES (10, 1, '', 0)", "INSERT INTO message (ROWID, handle_id, text, is_from_me) VALUES (10, 1, '', 0)",
[] [],
).unwrap(); )
.unwrap();
} }
let result = fetch_new_messages(&db_path, 0).await.unwrap(); let result = fetch_new_messages(&db_path, 0).await.unwrap();
@ -826,7 +882,11 @@ mod tests {
{ {
let conn = Connection::open(&db_path).unwrap(); let conn = Connection::open(&db_path).unwrap();
conn.execute("INSERT INTO handle (ROWID, id) VALUES (1, '+1234567890')", []).unwrap(); conn.execute(
"INSERT INTO handle (ROWID, id) VALUES (1, '+1234567890')",
[],
)
.unwrap();
conn.execute( conn.execute(
"INSERT INTO message (ROWID, handle_id, text, is_from_me) VALUES (10, 1, 'Test', 0)", "INSERT INTO message (ROWID, handle_id, text, is_from_me) VALUES (10, 1, 'Test', 0)",
[] []
@ -844,7 +904,11 @@ mod tests {
{ {
let conn = Connection::open(&db_path).unwrap(); let conn = Connection::open(&db_path).unwrap();
conn.execute("INSERT INTO handle (ROWID, id) VALUES (1, '+1234567890')", []).unwrap(); conn.execute(
"INSERT INTO handle (ROWID, id) VALUES (1, '+1234567890')",
[],
)
.unwrap();
conn.execute( conn.execute(
"INSERT INTO message (ROWID, handle_id, text, is_from_me) VALUES (10, 1, 'Test', 0)", "INSERT INTO message (ROWID, handle_id, text, is_from_me) VALUES (10, 1, 'Test', 0)",
[] []