1pub fn escape_postgres_jsonb_segment(segment: &str) -> String {
22 segment.replace('\'', "''")
23}
24
25pub fn escape_postgres_jsonb_path(path: &[String]) -> Vec<String> {
35 path.iter().map(|segment| escape_postgres_jsonb_segment(segment)).collect()
36}
37
38pub fn escape_mysql_json_path(path: &[String]) -> String {
52 let json_path = path.join(".");
53 format!("$.{}", json_path.replace('\'', "''"))
55}
56
57pub fn escape_sqlite_json_path(path: &[String]) -> String {
63 let json_path = path.join(".");
64 format!("$.{}", json_path.replace('\'', "''"))
67}
68
69pub fn escape_sqlserver_json_path(path: &[String]) -> String {
74 let json_path = path.join(".");
75 format!("$.{}", json_path.replace('\'', "''"))
76}
77
78#[cfg(test)]
79mod tests {
80 use super::*;
81
82 #[test]
83 fn test_postgres_single_quote() {
84 assert_eq!(escape_postgres_jsonb_segment("user'admin"), "user''admin");
85 }
86
87 #[test]
88 fn test_postgres_multiple_quotes() {
89 assert_eq!(escape_postgres_jsonb_segment("it's"), "it''s");
90 }
91
92 #[test]
93 fn test_postgres_no_quote() {
94 assert_eq!(escape_postgres_jsonb_segment("username"), "username");
95 }
96
97 #[test]
98 fn test_postgres_path_vector() {
99 let path = vec!["user'name".to_string(), "id".to_string()];
100 let result = escape_postgres_jsonb_path(&path);
101 assert_eq!(result[0], "user''name");
102 assert_eq!(result[1], "id");
103 }
104
105 #[test]
106 fn test_mysql_single_quote() {
107 let result = escape_mysql_json_path(&["user'admin".to_string()]);
108 assert_eq!(result, "$.user''admin");
109 }
110
111 #[test]
112 fn test_sqlite_single_quote() {
113 let result = escape_sqlite_json_path(&["user'admin".to_string()]);
114 assert_eq!(result, "$.user''admin");
115 }
116
117 #[test]
118 fn test_sqlserver_single_quote() {
119 let result = escape_sqlserver_json_path(&["user'admin".to_string()]);
120 assert_eq!(result, "$.user''admin");
121 }
122
123 #[test]
124 fn test_all_databases_empty_path() {
125 let empty_path: Vec<String> = vec![];
126 let pg_result = escape_postgres_jsonb_path(&empty_path);
127 let mysql_result = escape_mysql_json_path(&empty_path);
128 let sqlite_result = escape_sqlite_json_path(&empty_path);
129 let sqlserver_result = escape_sqlserver_json_path(&empty_path);
130
131 assert_eq!(pg_result.len(), 0);
132 assert_eq!(mysql_result, "$.");
133 assert_eq!(sqlite_result, "$.");
134 assert_eq!(sqlserver_result, "$.");
135 }
136
137 #[test]
144 fn test_postgres_injection_drop_table() {
145 let payload = "'; DROP TABLE users; --";
146 let escaped = escape_postgres_jsonb_segment(payload);
147 assert!(escaped.starts_with("''"), "Opening single quote must be doubled for PostgreSQL");
150 assert!(!escaped.starts_with("'\""), "Must not produce an unescaped sequence");
151 }
152
153 #[test]
154 fn test_postgres_injection_or_1_eq_1() {
155 let payload = "' OR '1'='1";
156 let escaped = escape_postgres_jsonb_segment(payload);
157 let original_quote_count = payload.chars().filter(|&c| c == '\'').count();
159 let doubled_count = escaped.matches("''").count();
160 assert_eq!(doubled_count, original_quote_count, "Every single quote must be doubled");
161 }
162
163 #[test]
164 fn test_postgres_injection_double_quote_or() {
165 let payload = r#"" OR "1"="1"#;
166 let escaped = escape_postgres_jsonb_segment(payload);
167 assert_eq!(escaped, payload);
170 }
171
172 #[test]
173 fn test_postgres_injection_backslash() {
174 let payload = r"\";
175 let escaped = escape_postgres_jsonb_segment(payload);
176 assert_eq!(escaped, payload);
179 }
180
181 #[test]
182 fn test_postgres_injection_like_percent() {
183 let payload = "%";
184 let escaped = escape_postgres_jsonb_segment(payload);
185 assert_eq!(escaped, payload);
187 }
188
189 #[test]
190 fn test_postgres_injection_like_underscore() {
191 let payload = "_";
192 let escaped = escape_postgres_jsonb_segment(payload);
193 assert_eq!(escaped, payload);
194 }
195
196 #[test]
197 fn test_postgres_injection_xss_script_tag() {
198 let payload = "<script>alert(1)</script>";
199 let escaped = escape_postgres_jsonb_segment(payload);
200 assert_eq!(escaped, payload);
202 }
203
204 #[test]
205 fn test_postgres_injection_null_literal() {
206 let payload = "NULL";
207 let escaped = escape_postgres_jsonb_segment(payload);
208 assert_eq!(escaped, "NULL");
209 }
210
211 #[test]
212 fn test_postgres_injection_empty_string() {
213 let payload = "";
214 let escaped = escape_postgres_jsonb_segment(payload);
215 assert_eq!(escaped, "");
216 }
217
218 #[test]
219 fn test_postgres_injection_unicode_accents() {
220 let payload = "François";
221 let escaped = escape_postgres_jsonb_segment(payload);
222 assert_eq!(escaped, "François");
224 }
225
226 #[test]
229 fn test_mysql_injection_drop_table() {
230 let payload = "'; DROP TABLE users; --";
231 let result = escape_mysql_json_path(&[payload.to_string()]);
232 assert!(result.contains("''"), "Single quote must be doubled for MySQL");
235 assert!(!result.contains("\\'"), "Must not use backslash escaping");
236 assert!(result.starts_with("$."), "MySQL path must start with $.");
238 }
239
240 #[test]
241 fn test_mysql_injection_or_1_eq_1() {
242 let payload = "' OR '1'='1";
243 let result = escape_mysql_json_path(&[payload.to_string()]);
244 let original_quote_count = payload.chars().filter(|&c| c == '\'').count();
246 let doubled_count = result.matches("''").count();
247 assert_eq!(
248 doubled_count, original_quote_count,
249 "Every single quote must be doubled in MySQL"
250 );
251 }
252
253 #[test]
254 fn test_mysql_injection_double_quote_or() {
255 let payload = r#"" OR "1"="1"#;
256 let result = escape_mysql_json_path(&[payload.to_string()]);
257 assert!(result.starts_with("$."), "MySQL path must start with '$.'");
260 }
261
262 #[test]
263 fn test_mysql_injection_backslash() {
264 let payload = r"\";
265 let result = escape_mysql_json_path(&[payload.to_string()]);
266 assert!(result.starts_with("$."), "MySQL path must start with '$.'");
267 }
268
269 #[test]
270 fn test_mysql_injection_like_percent() {
271 let payload = "%";
272 let result = escape_mysql_json_path(&[payload.to_string()]);
273 assert_eq!(result, "$.%");
274 }
275
276 #[test]
277 fn test_mysql_injection_like_underscore() {
278 let payload = "_";
279 let result = escape_mysql_json_path(&[payload.to_string()]);
280 assert_eq!(result, "$._");
281 }
282
283 #[test]
284 fn test_mysql_injection_xss_script_tag() {
285 let payload = "<script>alert(1)</script>";
286 let result = escape_mysql_json_path(&[payload.to_string()]);
287 assert!(result.starts_with("$."), "MySQL path must start with '$.'");
288 assert!(!result.contains("'; "), "Should not contain unescaped quotes");
289 }
290
291 #[test]
292 fn test_mysql_injection_null_literal() {
293 let payload = "NULL";
294 let result = escape_mysql_json_path(&[payload.to_string()]);
295 assert_eq!(result, "$.NULL");
296 }
297
298 #[test]
299 fn test_mysql_injection_empty_segment() {
300 let result = escape_mysql_json_path(&[String::new()]);
302 assert_eq!(result, "$.");
303 }
304
305 #[test]
306 fn test_mysql_injection_unicode_accents() {
307 let payload = "François";
308 let result = escape_mysql_json_path(&[payload.to_string()]);
309 assert_eq!(result, "$.François");
310 }
311
312 #[test]
315 fn test_sqlite_injection_drop_table() {
316 let payload = "'; DROP TABLE users; --";
317 let result = escape_sqlite_json_path(&[payload.to_string()]);
318 assert!(result.contains("''"), "Single quote must be doubled for SQLite");
321 assert!(!result.contains("\\'"), "Must not use backslash escaping");
322 assert!(result.starts_with("$."), "SQLite path must start with $.");
323 }
324
325 #[test]
326 fn test_sqlite_injection_or_1_eq_1() {
327 let payload = "' OR '1'='1";
328 let result = escape_sqlite_json_path(&[payload.to_string()]);
329 let original_quote_count = payload.chars().filter(|&c| c == '\'').count();
330 let doubled_count = result.matches("''").count();
331 assert_eq!(
332 doubled_count, original_quote_count,
333 "Every single quote must be doubled in SQLite"
334 );
335 }
336
337 #[test]
338 fn test_sqlite_injection_double_quote_or() {
339 let payload = r#"" OR "1"="1"#;
340 let result = escape_sqlite_json_path(&[payload.to_string()]);
341 assert!(result.starts_with("$."), "SQLite path must start with '$.'");
342 }
343
344 #[test]
345 fn test_sqlite_injection_backslash() {
346 let payload = r"\";
347 let result = escape_sqlite_json_path(&[payload.to_string()]);
348 assert!(result.starts_with("$."), "SQLite path must start with '$.'");
349 }
350
351 #[test]
352 fn test_sqlite_injection_like_percent() {
353 let payload = "%";
354 let result = escape_sqlite_json_path(&[payload.to_string()]);
355 assert_eq!(result, "$.%");
356 }
357
358 #[test]
359 fn test_sqlite_injection_like_underscore() {
360 let payload = "_";
361 let result = escape_sqlite_json_path(&[payload.to_string()]);
362 assert_eq!(result, "$._");
363 }
364
365 #[test]
366 fn test_sqlite_injection_xss_script_tag() {
367 let payload = "<script>alert(1)</script>";
368 let result = escape_sqlite_json_path(&[payload.to_string()]);
369 assert!(result.starts_with("$."), "SQLite path must start with '$.'");
370 }
371
372 #[test]
373 fn test_sqlite_injection_null_literal() {
374 let payload = "NULL";
375 let result = escape_sqlite_json_path(&[payload.to_string()]);
376 assert_eq!(result, "$.NULL");
377 }
378
379 #[test]
380 fn test_sqlite_injection_empty_segment() {
381 let result = escape_sqlite_json_path(&[String::new()]);
382 assert_eq!(result, "$.");
383 }
384
385 #[test]
386 fn test_sqlite_injection_unicode_accents() {
387 let payload = "François";
388 let result = escape_sqlite_json_path(&[payload.to_string()]);
389 assert_eq!(result, "$.François");
390 }
391
392 #[test]
395 fn test_sqlserver_injection_drop_table() {
396 let payload = "'; DROP TABLE users; --";
397 let result = escape_sqlserver_json_path(&[payload.to_string()]);
398 assert!(result.contains("''"), "Single quote must be doubled in SQL Server");
400 assert!(result.starts_with("$."), "SQL Server path must start with $.");
401 }
402
403 #[test]
404 fn test_sqlserver_injection_or_1_eq_1() {
405 let payload = "' OR '1'='1";
406 let result = escape_sqlserver_json_path(&[payload.to_string()]);
407 let original_quote_count = payload.chars().filter(|&c| c == '\'').count();
408 let doubled_count = result.matches("''").count();
409 assert_eq!(
410 doubled_count, original_quote_count,
411 "Every single quote must be doubled in SQL Server"
412 );
413 }
414
415 #[test]
416 fn test_sqlserver_injection_double_quote_or() {
417 let payload = r#"" OR "1"="1"#;
418 let result = escape_sqlserver_json_path(&[payload.to_string()]);
419 assert!(result.starts_with("$."), "SQL Server path must start with '$.'");
420 }
421
422 #[test]
423 fn test_sqlserver_injection_backslash() {
424 let payload = r"\";
425 let result = escape_sqlserver_json_path(&[payload.to_string()]);
426 assert!(result.starts_with("$."), "SQL Server path must start with '$.'");
427 }
428
429 #[test]
430 fn test_sqlserver_injection_like_percent() {
431 let payload = "%";
432 let result = escape_sqlserver_json_path(&[payload.to_string()]);
433 assert_eq!(result, "$.%");
434 }
435
436 #[test]
437 fn test_sqlserver_injection_like_underscore() {
438 let payload = "_";
439 let result = escape_sqlserver_json_path(&[payload.to_string()]);
440 assert_eq!(result, "$._");
441 }
442
443 #[test]
444 fn test_sqlserver_injection_xss_script_tag() {
445 let payload = "<script>alert(1)</script>";
446 let result = escape_sqlserver_json_path(&[payload.to_string()]);
447 assert!(result.starts_with("$."), "SQL Server path must start with '$.'");
448 }
449
450 #[test]
451 fn test_sqlserver_injection_null_literal() {
452 let payload = "NULL";
453 let result = escape_sqlserver_json_path(&[payload.to_string()]);
454 assert_eq!(result, "$.NULL");
455 }
456
457 #[test]
458 fn test_sqlserver_injection_empty_segment() {
459 let result = escape_sqlserver_json_path(&[String::new()]);
460 assert_eq!(result, "$.");
461 }
462
463 #[test]
464 fn test_sqlserver_injection_unicode_accents() {
465 let payload = "François";
466 let result = escape_sqlserver_json_path(&[payload.to_string()]);
467 assert_eq!(result, "$.François");
468 }
469
470 #[test]
473 fn test_postgres_segment_double_single_quote_roundtrip() {
474 let input = "it's";
476 let escaped = escape_postgres_jsonb_segment(input);
477 assert_eq!(escaped, "it''s");
478 }
479
480 #[test]
481 fn test_mysql_vs_sqlite_same_escaping_for_single_quote() {
482 let payload = "user'name";
483 let mysql_result = escape_mysql_json_path(&[payload.to_string()]);
484 let sqlite_result = escape_sqlite_json_path(&[payload.to_string()]);
485 assert_eq!(
487 mysql_result, sqlite_result,
488 "MySQL and SQLite should escape single quotes identically"
489 );
490 assert!(mysql_result.contains("''"), "MySQL must use double-single-quote escaping");
491 assert!(!mysql_result.contains("\\'"), "MySQL must not use backslash escaping");
492 }
493
494 #[test]
495 fn test_sqlserver_vs_postgres_same_doubling_strategy() {
496 let payload = "user'name";
497 let pg_seg = escape_postgres_jsonb_segment(payload);
498 let ss_result = escape_sqlserver_json_path(&[payload.to_string()]);
499 assert!(pg_seg.contains("''"), "PostgreSQL should double the quote");
501 assert!(ss_result.contains("''"), "SQL Server should double the quote");
502 }
503
504 #[test]
505 fn test_postgres_path_multi_segment_escaping() {
506 let path = vec!["user'name".to_string(), "field's".to_string()];
507 let result = escape_postgres_jsonb_path(&path);
508 assert_eq!(result[0], "user''name");
509 assert_eq!(result[1], "field''s");
510 }
511
512 #[test]
513 fn test_mysql_multi_segment_path_joins_with_dot() {
514 let path = vec![
515 "user".to_string(),
516 "address".to_string(),
517 "city".to_string(),
518 ];
519 let result = escape_mysql_json_path(&path);
520 assert_eq!(result, "$.user.address.city");
521 }
522
523 #[test]
526 fn mysql_escape_single_quote_no_backslash_mode() {
527 let result = escape_mysql_json_path(&["user'name".to_string()]);
530 assert!(
531 !result.contains('\\'),
532 "Must not contain backslash (breaks under NO_BACKSLASH_ESCAPES)"
533 );
534 assert!(result.contains("''"), "Must double single quotes");
535 assert_eq!(result, "$.user''name");
536 }
537
538 #[test]
539 fn sqlite_escape_single_quote_no_backslash_mode() {
540 let result = escape_sqlite_json_path(&["user'name".to_string()]);
543 assert!(!result.contains('\\'), "Must not contain backslash");
544 assert!(result.contains("''"), "Must double single quotes");
545 assert_eq!(result, "$.user''name");
546 }
547}