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