Skip to content

Commit d28df4b

Browse files
committed
Add escape_identifier() and escape_literal().
1 parent 83ffcf8 commit d28df4b

File tree

4 files changed

+109
-0
lines changed

4 files changed

+109
-0
lines changed

tokio-postgres/src/escape.rs

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
//! Provides functions for escaping literals and identifiers for use
2+
//! in SQL queries.
3+
//!
4+
//! Prefer parameterized queries where possible; see
5+
//! [`Client::query`](crate::Client::query). Do not escape parameters.
6+
7+
/// Escape a literal and surround result with single quotes. Not
8+
/// recommended in most cases.
9+
///
10+
/// If input contains backslashes, result will be of the form `
11+
/// E'...'` so it is safe to use regardless of the setting of
12+
/// standard_conforming_strings.
13+
pub fn escape_literal(input: &str) -> String {
14+
escape_internal(input, false)
15+
}
16+
17+
/// Escape an identifier and surround result with double quotes.
18+
pub fn escape_identifier(input: &str) -> String {
19+
escape_internal(input, true)
20+
}
21+
22+
// Translation of PostgreSQL libpq's PQescapeInternal(). Does not
23+
// require a connection because input string is known to be valid
24+
// UTF-8.
25+
//
26+
// Escape arbitrary strings. If as_ident is true, we escape the
27+
// result as an identifier; if false, as a literal. The result is
28+
// returned in a newly allocated buffer. If we fail due to an
29+
// encoding violation or out of memory condition, we return NULL,
30+
// storing an error message into conn.
31+
fn escape_internal(input: &str, as_ident: bool) -> String {
32+
let mut num_backslashes = 0;
33+
let mut num_quotes = 0;
34+
let quote_char = if as_ident { '"' } else { '\'' };
35+
36+
// Scan the string for characters that must be escaped.
37+
for ch in input.chars() {
38+
if ch == quote_char {
39+
num_quotes += 1;
40+
} else if ch == '\\' {
41+
num_backslashes += 1;
42+
}
43+
}
44+
45+
// Allocate output String.
46+
let mut result_size = input.len() + num_quotes + 3; // two quotes, plus a NUL
47+
if !as_ident && num_backslashes > 0 {
48+
result_size += num_backslashes + 2;
49+
}
50+
51+
let mut output = String::with_capacity(result_size);
52+
53+
// If we are escaping a literal that contains backslashes, we use
54+
// the escape string syntax so that the result is correct under
55+
// either value of standard_conforming_strings. We also emit a
56+
// leading space in this case, to guard against the possibility
57+
// that the result might be interpolated immediately following an
58+
// identifier.
59+
if !as_ident && num_backslashes > 0 {
60+
output.push(' ');
61+
output.push('E');
62+
}
63+
64+
// Opening quote.
65+
output.push(quote_char);
66+
67+
// Use fast path if possible.
68+
//
69+
// We've already verified that the input string is well-formed in
70+
// the current encoding. If it contains no quotes and, in the
71+
// case of literal-escaping, no backslashes, then we can just copy
72+
// it directly to the output buffer, adding the necessary quotes.
73+
//
74+
// If not, we must rescan the input and process each character
75+
// individually.
76+
if num_quotes == 0 && (num_backslashes == 0 || as_ident) {
77+
output.push_str(input);
78+
} else {
79+
for ch in input.chars() {
80+
if ch == quote_char || (!as_ident && ch == '\\') {
81+
output.push(ch);
82+
}
83+
output.push(ch);
84+
}
85+
}
86+
87+
output.push(quote_char);
88+
89+
return output;
90+
}

tokio-postgres/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ mod connection;
158158
mod copy_in;
159159
mod copy_out;
160160
pub mod error;
161+
pub mod escape;
161162
mod generic_client;
162163
mod maybe_tls_stream;
163164
mod portal;

tokio-postgres/tests/test/escape.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
use tokio_postgres::escape::{escape_identifier, escape_literal};
2+
3+
#[test]
4+
fn test_escape_idenifier() {
5+
assert_eq!(escape_identifier("foo"), String::from("\"foo\""));
6+
assert_eq!(escape_identifier("f\\oo"), String::from("\"f\\oo\""));
7+
assert_eq!(escape_identifier("f'oo"), String::from("\"f'oo\""));
8+
assert_eq!(escape_identifier("f\"oo"), String::from("\"f\"\"oo\""));
9+
}
10+
11+
#[test]
12+
fn test_escape_literal() {
13+
assert_eq!(escape_literal("foo"), String::from("'foo'"));
14+
assert_eq!(escape_literal("f\\oo"), String::from(" E'f\\\\oo'"));
15+
assert_eq!(escape_literal("f'oo"), String::from("'f''oo'"));
16+
assert_eq!(escape_literal("f\"oo"), String::from("'f\"oo'"));
17+
}

tokio-postgres/tests/test/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ use tokio_postgres::{
1717
};
1818

1919
mod binary_copy;
20+
mod escape;
2021
mod parse;
2122
#[cfg(feature = "runtime")]
2223
mod runtime;

0 commit comments

Comments
 (0)