contextual_encoder/
sql.rs1use std::fmt;
58
59use crate::engine::{encode_loop, is_unicode_noncharacter};
60
61pub fn for_sql(input: &str) -> String {
81 let mut out = String::with_capacity(input.len());
82 write_sql(&mut out, input).expect("writing to string cannot fail");
83 out
84}
85
86pub fn write_sql<W: fmt::Write>(out: &mut W, input: &str) -> fmt::Result {
90 encode_loop(out, input, needs_sql_encoding, write_sql_encoded)
91}
92
93fn needs_sql_encoding(c: char) -> bool {
94 c == '\'' || c == '\0' || is_unicode_noncharacter(c as u32)
95}
96
97fn write_sql_encoded<W: fmt::Write>(out: &mut W, c: char, _next: Option<char>) -> fmt::Result {
98 match c {
99 '\'' => out.write_str("''"),
100 '\0' => Ok(()), _ if is_unicode_noncharacter(c as u32) => out.write_char(' '),
102 _ => out.write_char(c),
103 }
104}
105
106pub fn for_sql_backslash(input: &str) -> String {
124 let mut out = String::with_capacity(input.len());
125 write_sql_backslash(&mut out, input).expect("writing to string cannot fail");
126 out
127}
128
129pub fn write_sql_backslash<W: fmt::Write>(out: &mut W, input: &str) -> fmt::Result {
133 encode_loop(
134 out,
135 input,
136 needs_sql_backslash_encoding,
137 write_sql_backslash_encoded,
138 )
139}
140
141fn needs_sql_backslash_encoding(c: char) -> bool {
142 matches!(c, '\0' | '\x08' | '\t' | '\n' | '\r' | '\x1A' | '\'' | '\\')
143 || is_unicode_noncharacter(c as u32)
144}
145
146fn write_sql_backslash_encoded<W: fmt::Write>(
147 out: &mut W,
148 c: char,
149 _next: Option<char>,
150) -> fmt::Result {
151 match c {
152 '\0' => out.write_str("\\0"),
153 '\x08' => out.write_str("\\b"),
154 '\t' => out.write_str("\\t"),
155 '\n' => out.write_str("\\n"),
156 '\r' => out.write_str("\\r"),
157 '\x1A' => out.write_str("\\Z"),
158 '\'' => out.write_str("\\'"),
159 '\\' => out.write_str("\\\\"),
160 _ if is_unicode_noncharacter(c as u32) => out.write_char(' '),
161 _ => out.write_char(c),
162 }
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168
169 #[test]
172 fn sql_passthrough() {
173 assert_eq!(for_sql("hello world"), "hello world");
174 assert_eq!(for_sql(""), "");
175 assert_eq!(for_sql("SELECT 1"), "SELECT 1");
176 assert_eq!(for_sql("café"), "café");
177 assert_eq!(for_sql("日本語"), "日本語");
178 assert_eq!(for_sql("\u{1F600}"), "\u{1F600}");
179 }
180
181 #[test]
182 fn sql_doubles_single_quote() {
183 assert_eq!(for_sql("it's"), "it''s");
184 assert_eq!(for_sql("'quoted'"), "''quoted''");
185 assert_eq!(for_sql("a''b"), "a''''b");
186 }
187
188 #[test]
189 fn sql_backslash_passes_through() {
190 assert_eq!(for_sql(r"back\slash"), r"back\slash");
191 assert_eq!(for_sql(r"a\\b"), r"a\\b");
192 }
193
194 #[test]
195 fn sql_double_quote_passes_through() {
196 assert_eq!(for_sql(r#"a"b"#), r#"a"b"#);
197 }
198
199 #[test]
200 fn sql_backtick_passes_through() {
201 assert_eq!(for_sql("a`b"), "a`b");
202 }
203
204 #[test]
205 fn sql_removes_nul() {
206 assert_eq!(for_sql("before\x00after"), "beforeafter");
207 assert_eq!(for_sql("\x00"), "");
208 assert_eq!(for_sql("\x00\x00"), "");
209 }
210
211 #[test]
212 fn sql_control_chars_pass_through() {
213 assert_eq!(for_sql("\t"), "\t");
216 assert_eq!(for_sql("\n"), "\n");
217 assert_eq!(for_sql("\r"), "\r");
218 assert_eq!(for_sql("\x08"), "\x08");
219 }
220
221 #[test]
222 fn sql_nonchars_replaced() {
223 assert_eq!(for_sql("\u{FDD0}"), " ");
224 assert_eq!(for_sql("\u{FFFE}"), " ");
225 assert_eq!(for_sql("\u{1FFFE}"), " ");
226 }
227
228 #[test]
229 fn sql_injection_attempt() {
230 assert_eq!(
231 for_sql("'; DROP TABLE users; --"),
232 "''; DROP TABLE users; --"
233 );
234 }
235
236 #[test]
237 fn sql_writer_matches() {
238 let input = "test\x00'escape' café\u{FDD0}";
239 let mut w = String::new();
240 write_sql(&mut w, input).unwrap();
241 assert_eq!(for_sql(input), w);
242 }
243
244 #[test]
247 fn backslash_passthrough() {
248 assert_eq!(for_sql_backslash("hello world"), "hello world");
249 assert_eq!(for_sql_backslash(""), "");
250 assert_eq!(for_sql_backslash("SELECT 1"), "SELECT 1");
251 assert_eq!(for_sql_backslash("café"), "café");
252 assert_eq!(for_sql_backslash("日本語"), "日本語");
253 assert_eq!(for_sql_backslash("\u{1F600}"), "\u{1F600}");
254 }
255
256 #[test]
257 fn backslash_escapes_single_quote() {
258 assert_eq!(for_sql_backslash("it's"), r"it\'s");
259 assert_eq!(for_sql_backslash("'quoted'"), r"\'quoted\'");
260 }
261
262 #[test]
263 fn backslash_escapes_backslash() {
264 assert_eq!(for_sql_backslash(r"a\b"), r"a\\b");
265 assert_eq!(for_sql_backslash(r"a\\b"), r"a\\\\b");
266 }
267
268 #[test]
269 fn backslash_escapes_nul() {
270 assert_eq!(for_sql_backslash("before\x00after"), r"before\0after");
271 assert_eq!(for_sql_backslash("\x00"), r"\0");
272 }
273
274 #[test]
275 fn backslash_escapes_newline() {
276 assert_eq!(for_sql_backslash("line\nbreak"), r"line\nbreak");
277 }
278
279 #[test]
280 fn backslash_escapes_carriage_return() {
281 assert_eq!(for_sql_backslash("line\rbreak"), r"line\rbreak");
282 }
283
284 #[test]
285 fn backslash_escapes_tab() {
286 assert_eq!(for_sql_backslash("col\tcol"), r"col\tcol");
287 }
288
289 #[test]
290 fn backslash_escapes_backspace() {
291 assert_eq!(for_sql_backslash("a\x08b"), r"a\bb");
292 }
293
294 #[test]
295 fn backslash_escapes_control_z() {
296 assert_eq!(for_sql_backslash("a\x1Ab"), r"a\Zb");
297 }
298
299 #[test]
300 fn backslash_double_quote_passes_through() {
301 assert_eq!(for_sql_backslash(r#"a"b"#), r#"a"b"#);
302 }
303
304 #[test]
305 fn backslash_backtick_passes_through() {
306 assert_eq!(for_sql_backslash("a`b"), "a`b");
307 }
308
309 #[test]
310 fn backslash_other_controls_pass_through() {
311 assert_eq!(for_sql_backslash("\x01"), "\x01");
313 assert_eq!(for_sql_backslash("\x7F"), "\x7F");
314 }
315
316 #[test]
317 fn backslash_nonchars_replaced() {
318 assert_eq!(for_sql_backslash("\u{FDD0}"), " ");
319 assert_eq!(for_sql_backslash("\u{FFFE}"), " ");
320 }
321
322 #[test]
323 fn backslash_injection_attempt() {
324 assert_eq!(
325 for_sql_backslash("'; DROP TABLE users; --"),
326 r"\'; DROP TABLE users; --"
327 );
328 }
329
330 #[test]
331 fn backslash_injection_via_backslash() {
332 assert_eq!(for_sql_backslash("\\'"), r"\\\'");
334 }
335
336 #[test]
337 fn backslash_writer_matches() {
338 let input = "test\x00\x08\t\n\r\x1A'\\café\u{FDD0}";
339 let mut w = String::new();
340 write_sql_backslash(&mut w, input).unwrap();
341 assert_eq!(for_sql_backslash(input), w);
342 }
343}