mirror of
https://github.com/aljazceru/turso.git
synced 2026-01-17 15:14:20 +01:00
Merge 'sim: fix singlequote escaping and unescaping' from Jussi Saurio
Fixes https://github.com/tursodatabase/limbo/issues/1801 by preventing the generation of strings like `'foo''` that contain an improper unescaped sequence of quotes Closes #1819
This commit is contained in:
@@ -185,10 +185,58 @@ impl From<ast::Literal> for SimValue {
|
||||
}
|
||||
}
|
||||
|
||||
/// Sanitaizes a string literal by removing single quote at front and back
|
||||
/// and escaping double single quotes
|
||||
fn sanitize_string(input: &str) -> String {
|
||||
input[1..input.len() - 1].replace("''", "'").to_string()
|
||||
/// Converts a SQL string literal with already-escaped single quotes to a regular string by:
|
||||
/// - Removing the enclosing single quotes
|
||||
/// - Converting sequences of 2N single quotes ('''''') to N single quotes (''')
|
||||
///
|
||||
/// Assumes:
|
||||
/// - The input starts and ends with a single quote
|
||||
/// - The input contains a valid amount of single quotes inside the enclosing quotes;
|
||||
/// i.e. any ' is escaped as a double ''
|
||||
fn unescape_singlequotes(input: &str) -> String {
|
||||
assert!(
|
||||
input.starts_with('\'') && input.ends_with('\''),
|
||||
"Input string must be wrapped in single quotes"
|
||||
);
|
||||
// Skip first and last characters (the enclosing quotes)
|
||||
let inner = &input[1..input.len() - 1];
|
||||
|
||||
let mut result = String::with_capacity(inner.len());
|
||||
let mut chars = inner.chars().peekable();
|
||||
|
||||
while let Some(c) = chars.next() {
|
||||
if c == '\'' {
|
||||
// Count consecutive single quotes
|
||||
let mut quote_count = 1;
|
||||
while chars.peek() == Some(&'\'') {
|
||||
quote_count += 1;
|
||||
chars.next();
|
||||
}
|
||||
assert!(
|
||||
quote_count % 2 == 0,
|
||||
"Expected even number of quotes, got {} in string {}",
|
||||
quote_count,
|
||||
input
|
||||
);
|
||||
// For every pair of quotes, output one quote
|
||||
for _ in 0..(quote_count / 2) {
|
||||
result.push('\'');
|
||||
}
|
||||
} else {
|
||||
result.push(c);
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Escapes a string by doubling contained single quotes and then wrapping it in single quotes.
|
||||
fn escape_singlequotes(input: &str) -> String {
|
||||
let mut result = String::with_capacity(input.len() + 2);
|
||||
result.push('\'');
|
||||
result.push_str(&input.replace("'", "''"));
|
||||
result.push('\'');
|
||||
result
|
||||
}
|
||||
|
||||
impl From<&ast::Literal> for SimValue {
|
||||
@@ -196,8 +244,7 @@ impl From<&ast::Literal> for SimValue {
|
||||
let new_value = match value {
|
||||
ast::Literal::Null => types::Value::Null,
|
||||
ast::Literal::Numeric(number) => Numeric::from(number).into(),
|
||||
// TODO: see how to avoid sanitizing here
|
||||
ast::Literal::String(string) => types::Value::build_text(sanitize_string(string)),
|
||||
ast::Literal::String(string) => types::Value::build_text(unescape_singlequotes(string)),
|
||||
ast::Literal::Blob(blob) => types::Value::Blob(
|
||||
blob.as_bytes()
|
||||
.chunks_exact(2)
|
||||
@@ -227,7 +274,7 @@ impl From<&SimValue> for ast::Literal {
|
||||
types::Value::Null => Self::Null,
|
||||
types::Value::Integer(i) => Self::Numeric(i.to_string()),
|
||||
types::Value::Float(f) => Self::Numeric(f.to_string()),
|
||||
text @ types::Value::Text(..) => Self::String(format!("'{}'", text)),
|
||||
text @ types::Value::Text(..) => Self::String(escape_singlequotes(&text.to_string())),
|
||||
types::Value::Blob(blob) => Self::Blob(hex::encode(blob)),
|
||||
}
|
||||
}
|
||||
@@ -266,3 +313,32 @@ impl From<&limbo_core::types::Value> for SimValue {
|
||||
Self(value.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::model::table::{escape_singlequotes, unescape_singlequotes};
|
||||
|
||||
#[test]
|
||||
fn test_unescape_singlequotes() {
|
||||
assert_eq!(unescape_singlequotes("'hello'"), "hello");
|
||||
assert_eq!(unescape_singlequotes("'O''Reilly'"), "O'Reilly");
|
||||
assert_eq!(
|
||||
unescape_singlequotes("'multiple''single''quotes'"),
|
||||
"multiple'single'quotes"
|
||||
);
|
||||
assert_eq!(unescape_singlequotes("'test''''test'"), "test''test");
|
||||
assert_eq!(unescape_singlequotes("'many''''''quotes'"), "many'''quotes");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_escape_singlequotes() {
|
||||
assert_eq!(escape_singlequotes("hello"), "'hello'");
|
||||
assert_eq!(escape_singlequotes("O'Reilly"), "'O''Reilly'");
|
||||
assert_eq!(
|
||||
escape_singlequotes("multiple'single'quotes"),
|
||||
"'multiple''single''quotes'"
|
||||
);
|
||||
assert_eq!(escape_singlequotes("test''test"), "'test''''test'");
|
||||
assert_eq!(escape_singlequotes("many'''quotes"), "'many''''''quotes'");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user