|
3 | 3 |
|
4 | 4 | use crate::redis_tokenizer::{RedisTokenType, RedisTokenizer}; |
5 | 5 |
|
| 6 | +const REDIS_TRUNCATION_MARK: &str = "..."; |
| 7 | +const MAX_REDIS_NB_COMMANDS: usize = 3; |
| 8 | + |
| 9 | +/// Returns a quantized version of a Redis query, keeping only up to 3 command names. |
| 10 | +pub fn quantize_redis_string(query: &str) -> String { |
| 11 | + let mut commands: Vec<String> = Vec::with_capacity(MAX_REDIS_NB_COMMANDS); |
| 12 | + let mut truncated = false; |
| 13 | + |
| 14 | + for raw_line in query.lines() { |
| 15 | + if commands.len() >= MAX_REDIS_NB_COMMANDS { |
| 16 | + break; |
| 17 | + } |
| 18 | + |
| 19 | + let line = raw_line.trim(); |
| 20 | + if line.is_empty() { |
| 21 | + continue; |
| 22 | + } |
| 23 | + |
| 24 | + let mut tokens = line.split_whitespace(); |
| 25 | + let Some(first) = tokens.next() else { continue }; |
| 26 | + |
| 27 | + if first.ends_with(REDIS_TRUNCATION_MARK) { |
| 28 | + truncated = true; |
| 29 | + continue; |
| 30 | + } |
| 31 | + |
| 32 | + let cmd = first.to_ascii_uppercase(); |
| 33 | + let command = match cmd.as_bytes() { |
| 34 | + b"CLIENT" | b"CLUSTER" | b"COMMAND" | b"CONFIG" | b"DEBUG" | b"SCRIPT" => { |
| 35 | + match tokens.next() { |
| 36 | + Some(sub) if sub.ends_with(REDIS_TRUNCATION_MARK) => { |
| 37 | + truncated = true; |
| 38 | + continue; |
| 39 | + } |
| 40 | + Some(sub) => format!("{cmd} {}", sub.to_ascii_uppercase()), |
| 41 | + None => cmd, |
| 42 | + } |
| 43 | + } |
| 44 | + _ => cmd, |
| 45 | + }; |
| 46 | + |
| 47 | + commands.push(command); |
| 48 | + truncated = false; |
| 49 | + } |
| 50 | + |
| 51 | + let mut result = commands.join(" "); |
| 52 | + if commands.len() == MAX_REDIS_NB_COMMANDS || truncated { |
| 53 | + if !result.is_empty() { |
| 54 | + result.push(' '); |
| 55 | + } |
| 56 | + result.push_str("..."); |
| 57 | + } |
| 58 | + result |
| 59 | +} |
| 60 | + |
6 | 61 | pub fn obfuscate_redis_string(cmd: &str) -> String { |
7 | 62 | let mut tokenizer = RedisTokenizer::new(cmd); |
8 | 63 | let s = &mut String::new(); |
@@ -79,8 +134,8 @@ fn obfuscate_redis_cmd<'a>(str: &mut String, cmd: &'a str, mut args: Vec<&'a str |
79 | 134 | // • ZSCORE key member |
80 | 135 | args = obfuscate_redis_args_n(args, 1); |
81 | 136 | } |
82 | | - b"HSET" | b"HSETNX" | b"LREM" | b"LSET" | b"SETBIT" | b"SETEX" | b"PSETEX" |
83 | | - | b"SETRANGE" | b"ZINCRBY" | b"SMOVE" | b"RESTORE" => { |
| 137 | + b"HSETNX" | b"LREM" | b"LSET" | b"SETBIT" | b"SETEX" | b"PSETEX" | b"SETRANGE" |
| 138 | + | b"ZINCRBY" | b"SMOVE" | b"RESTORE" => { |
84 | 139 | // Obfuscate 3rd argument: |
85 | 140 | // • HSET key field value |
86 | 141 | // • HSETNX key field value |
@@ -120,7 +175,7 @@ fn obfuscate_redis_cmd<'a>(str: &mut String, cmd: &'a str, mut args: Vec<&'a str |
120 | 175 | // • GEOADD key longitude latitude member [longitude latitude member ...] |
121 | 176 | args = obfuscate_redis_args_step(args, 1, 3) |
122 | 177 | } |
123 | | - b"HMSET" => { |
| 178 | + b"HMSET" | b"HSET" => { |
124 | 179 | // Every 2nd argument starting from first. |
125 | 180 | // • HMSET key field value [field value ...] |
126 | 181 | args = obfuscate_redis_args_step(args, 1, 2) |
@@ -196,7 +251,7 @@ fn obfuscate_redis_args_step(mut args: Vec<&str>, start: usize, step: usize) -> |
196 | 251 | args |
197 | 252 | } |
198 | 253 |
|
199 | | -pub(crate) fn remove_all_redis_args(redis_cmd: &str) -> String { |
| 254 | +pub fn remove_all_redis_args(redis_cmd: &str) -> String { |
200 | 255 | let mut redis_cmd_iter = redis_cmd.split_whitespace().peekable(); |
201 | 256 | let mut obfuscated_cmd = String::new(); |
202 | 257 |
|
@@ -267,7 +322,110 @@ fn ascii_uppercase<'a>(s: &str, dest: &'a mut [u8]) -> Option<&'a [u8]> { |
267 | 322 | mod tests { |
268 | 323 | use duplicate::duplicate_item; |
269 | 324 |
|
270 | | - use super::{obfuscate_redis_string, remove_all_redis_args}; |
| 325 | + use super::{obfuscate_redis_string, quantize_redis_string, remove_all_redis_args}; |
| 326 | + |
| 327 | + #[duplicate_item( |
| 328 | + [ |
| 329 | + test_name [test_quantize_redis_string_client] |
| 330 | + input ["CLIENT"] |
| 331 | + expected ["CLIENT"]; |
| 332 | + ] |
| 333 | + [ |
| 334 | + test_name [test_quantize_redis_string_client_list] |
| 335 | + input ["CLIENT LIST"] |
| 336 | + expected ["CLIENT LIST"]; |
| 337 | + ] |
| 338 | + [ |
| 339 | + test_name [test_quantize_redis_string_get_lowercase] |
| 340 | + input ["get my_key"] |
| 341 | + expected ["GET"]; |
| 342 | + ] |
| 343 | + [ |
| 344 | + test_name [test_quantize_redis_string_set] |
| 345 | + input ["SET le_key le_value"] |
| 346 | + expected ["SET"]; |
| 347 | + ] |
| 348 | + [ |
| 349 | + test_name [test_quantize_redis_string_set_with_newlines] |
| 350 | + input ["\n\n \nSET foo bar \n \n\n "] |
| 351 | + expected ["SET"]; |
| 352 | + ] |
| 353 | + [ |
| 354 | + test_name [test_quantize_redis_string_config_set] |
| 355 | + input ["CONFIG SET parameter value"] |
| 356 | + expected ["CONFIG SET"]; |
| 357 | + ] |
| 358 | + [ |
| 359 | + test_name [test_quantize_redis_string_two_cmds] |
| 360 | + input ["SET toto tata \n \n EXPIRE toto 15 "] |
| 361 | + expected ["SET EXPIRE"]; |
| 362 | + ] |
| 363 | + [ |
| 364 | + test_name [test_quantize_redis_string_mset] |
| 365 | + input ["MSET toto tata toto tata toto tata \n "] |
| 366 | + expected ["MSET"]; |
| 367 | + ] |
| 368 | + [ |
| 369 | + test_name [test_quantize_redis_string_max_cmds] |
| 370 | + input ["MULTI\nSET k1 v1\nSET k2 v2\nSET k3 v3\nSET k4 v4\nDEL to_del\nEXEC"] |
| 371 | + expected ["MULTI SET SET ..."]; |
| 372 | + ] |
| 373 | + [ |
| 374 | + test_name [test_quantize_redis_string_truncation_first] |
| 375 | + input ["GET..."] |
| 376 | + expected ["..."]; |
| 377 | + ] |
| 378 | + [ |
| 379 | + test_name [test_quantize_redis_string_truncation_arg] |
| 380 | + input ["GET k..."] |
| 381 | + expected ["GET"]; |
| 382 | + ] |
| 383 | + [ |
| 384 | + test_name [test_quantize_redis_string_truncation_third] |
| 385 | + input ["GET k1\nGET k2\nG..."] |
| 386 | + expected ["GET GET ..."]; |
| 387 | + ] |
| 388 | + [ |
| 389 | + test_name [test_quantize_redis_string_truncation_after_max] |
| 390 | + input ["GET k1\nGET k2\nDEL k3\nGET k..."] |
| 391 | + expected ["GET GET DEL ..."]; |
| 392 | + ] |
| 393 | + [ |
| 394 | + test_name [test_quantize_redis_string_truncation_hdel] |
| 395 | + input ["GET k1\nGET k2\nHDEL k3 a\nG..."] |
| 396 | + expected ["GET GET HDEL ..."]; |
| 397 | + ] |
| 398 | + [ |
| 399 | + test_name [test_quantize_redis_string_truncation_mid] |
| 400 | + input ["GET k...\nDEL k2\nMS..."] |
| 401 | + expected ["GET DEL ..."]; |
| 402 | + ] |
| 403 | + [ |
| 404 | + test_name [test_quantize_redis_string_truncation_early] |
| 405 | + input ["GET k...\nDE...\nMS..."] |
| 406 | + expected ["GET ..."]; |
| 407 | + ] |
| 408 | + [ |
| 409 | + test_name [test_quantize_redis_string_truncation_then_cmd] |
| 410 | + input ["GET k1\nDE...\nGET k2"] |
| 411 | + expected ["GET GET"]; |
| 412 | + ] |
| 413 | + [ |
| 414 | + test_name [test_quantize_redis_string_truncation_complex] |
| 415 | + input ["GET k1\nDE...\nGET k2\nHDEL k3 a\nGET k4\nDEL k5"] |
| 416 | + expected ["GET GET HDEL ..."]; |
| 417 | + ] |
| 418 | + [ |
| 419 | + test_name [test_quantize_redis_string_unknown] |
| 420 | + input ["UNKNOWN 123"] |
| 421 | + expected ["UNKNOWN"]; |
| 422 | + ] |
| 423 | + )] |
| 424 | + #[test] |
| 425 | + fn test_name() { |
| 426 | + let result = quantize_redis_string(input); |
| 427 | + assert_eq!(result, expected); |
| 428 | + } |
271 | 429 |
|
272 | 430 | #[duplicate_item( |
273 | 431 | [ |
@@ -684,6 +842,11 @@ SET k v |
684 | 842 | expected [r#"CONFIG command |
685 | 843 | SET k ?"#]; |
686 | 844 | ] |
| 845 | + [ |
| 846 | + test_name [test_obfuscate_redis_string_67] |
| 847 | + input ["HSET key field value field value"] |
| 848 | + expected ["HSET key field ? field ?"]; |
| 849 | + ] |
687 | 850 | )] |
688 | 851 | #[test] |
689 | 852 | fn test_name() { |
|
0 commit comments