diff options
| author | omagdy <omar.professional8777@gmail.com> | 2025-07-16 06:42:03 +0300 |
|---|---|---|
| committer | omagdy <omar.professional8777@gmail.com> | 2025-07-16 06:42:03 +0300 |
| commit | a312e41beb06a59a3842ffe56a5d04c3cbef092c (patch) | |
| tree | 0d6b0e3d5b99bfc78a784e6d347b30d18995e6b1 /src | |
| parent | b11fa99cd8f7e68ef9b9bc320e1ccd90321c506f (diff) | |
| download | redis-rust-a312e41beb06a59a3842ffe56a5d04c3cbef092c.tar.xz redis-rust-a312e41beb06a59a3842ffe56a5d04c3cbef092c.zip | |
feat: Implemented a little top down parser for RESP
Diffstat (limited to 'src')
| -rw-r--r-- | src/resp_parser.rs | 1574 |
1 files changed, 1574 insertions, 0 deletions
diff --git a/src/resp_parser.rs b/src/resp_parser.rs new file mode 100644 index 0000000..a8400d3 --- /dev/null +++ b/src/resp_parser.rs @@ -0,0 +1,1574 @@ +#![allow(unused)] +use std::{ + borrow::Cow, + collections::{HashMap, HashSet}, + fmt::format, + io::Read, + isize, +}; + +pub const SIMPLE_STRING: u8 = b'+'; +pub const SIMPLE_ERROR: u8 = b'-'; +pub const INTEGER: u8 = b':'; +pub const BULK_STRING: u8 = b'$'; +pub const ARRAY: u8 = b'*'; +pub const NULL: u8 = b'_'; +pub const BOOLEAN: u8 = b'#'; +pub const DOUBLES: u8 = b','; +pub const BIG_NUMBERS: u8 = b'('; +pub const BULK_ERRORS: u8 = b'!'; +pub const VERBATIM_STRINGS: u8 = b'='; +pub const MAPS: u8 = b'%'; +pub const ATTRIBUTES: u8 = b'|'; +pub const SETS: u8 = b'~'; +pub const PUSHES: u8 = b'>'; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum RespError { + // Protocol errors + InvalidProtocol, + InvalidDataType, + InvalidLength, + UnexpectedEnd, + InvalidInteger, + InvalidBulkString, + InvalidArray, + MalformedCommand, + + // Command errors + UnknownCommand, + WrongNumberOfArguments, + InvalidCommandSyntax, + + // Data type errors + WrongType, + InvalidKey, + InvalidValue, + InvalidIndex, + IndexOutOfRange, + + // Memory and resource errors + OutOfMemory, + MaxClientsReached, + MaxDatabasesReached, + + // Authentication and authorization + NoAuth, + InvalidPassword, + NoPermission, + + // Database errors + InvalidDatabase, + DatabaseNotFound, + KeyNotFound, + KeyExists, + + // Transaction errors + MultiNotAllowed, + ExecWithoutMulti, + DiscardWithoutMulti, + WatchInMulti, + + // Replication errors + MasterDown, + SlaveNotConnected, + ReplicationError, + + // Scripting errors + ScriptError, + ScriptKilled, + ScriptTimeout, + NoScript, + + // Pub/Sub errors + InvalidChannel, + NotSubscribed, + + // Persistence errors + BackgroundSaveInProgress, + BackgroundSaveError, + + // Generic errors + InternalError, + Timeout, + ConnectionLost, + InvalidArgument, + OperationNotSupported, + Readonly, + Loading, + Busy, + + // Custom error with message + Custom(String), +} + +impl RespError { + pub fn message(&self) -> Cow<'static, str> { + match self { + // Protocol errors + RespError::InvalidProtocol => Cow::Borrowed("ERR Protocol error"), + RespError::InvalidDataType => Cow::Borrowed("ERR Invalid data type"), + RespError::InvalidLength => Cow::Borrowed("ERR Invalid length"), + RespError::UnexpectedEnd => Cow::Borrowed("ERR Unexpected end of input"), + RespError::InvalidInteger => Cow::Borrowed("ERR Invalid integer"), + RespError::InvalidBulkString => Cow::Borrowed("ERR Invalid bulk string"), + RespError::InvalidArray => Cow::Borrowed("ERR Invalid array"), + RespError::MalformedCommand => Cow::Borrowed("ERR Malformed command"), + + // Command errors + RespError::UnknownCommand => Cow::Borrowed("ERR unknown command"), + RespError::WrongNumberOfArguments => Cow::Borrowed("ERR wrong number of arguments"), + RespError::InvalidCommandSyntax => Cow::Borrowed("ERR syntax error"), + + // Data type errors + RespError::WrongType => { + Cow::Borrowed("WRONGTYPE Operation against a key holding the wrong kind of value") + } + RespError::InvalidKey => Cow::Borrowed("ERR invalid key"), + RespError::InvalidValue => Cow::Borrowed("ERR invalid value"), + RespError::InvalidIndex => Cow::Borrowed("ERR invalid index"), + RespError::IndexOutOfRange => Cow::Borrowed("ERR index out of range"), + + // Memory and resource errors + RespError::OutOfMemory => { + Cow::Borrowed("OOM command not allowed when used memory > 'maxmemory'") + } + RespError::MaxClientsReached => Cow::Borrowed("ERR max number of clients reached"), + RespError::MaxDatabasesReached => Cow::Borrowed("ERR max number of databases reached"), + + // Authentication and authorization + RespError::NoAuth => Cow::Borrowed("NOAUTH Authentication required"), + RespError::InvalidPassword => Cow::Borrowed("ERR invalid password"), + RespError::NoPermission => { + Cow::Borrowed("NOPERM this user has no permissions to run this command") + } + + // Database errors + RespError::InvalidDatabase => Cow::Borrowed("ERR invalid database"), + RespError::DatabaseNotFound => Cow::Borrowed("ERR database not found"), + RespError::KeyNotFound => Cow::Borrowed("ERR key not found"), + RespError::KeyExists => Cow::Borrowed("ERR key already exists"), + + // Transaction errors + RespError::MultiNotAllowed => Cow::Borrowed("ERR MULTI calls can not be nested"), + RespError::ExecWithoutMulti => Cow::Borrowed("ERR EXEC without MULTI"), + RespError::DiscardWithoutMulti => Cow::Borrowed("ERR DISCARD without MULTI"), + RespError::WatchInMulti => Cow::Borrowed("ERR WATCH inside MULTI is not allowed"), + + // Replication errors + RespError::MasterDown => Cow::Borrowed("ERR master is down"), + RespError::SlaveNotConnected => Cow::Borrowed("ERR slave not connected"), + RespError::ReplicationError => Cow::Borrowed("ERR replication error"), + + // Scripting errors + RespError::ScriptError => Cow::Borrowed("ERR script error"), + RespError::ScriptKilled => Cow::Borrowed("ERR script killed"), + RespError::ScriptTimeout => Cow::Borrowed("ERR script timeout"), + RespError::NoScript => Cow::Borrowed("NOSCRIPT No matching script"), + + // Pub/Sub errors + RespError::InvalidChannel => Cow::Borrowed("ERR invalid channel"), + RespError::NotSubscribed => Cow::Borrowed("ERR not subscribed"), + + // Persistence errors + RespError::BackgroundSaveInProgress => { + Cow::Borrowed("ERR Background save already in progress") + } + RespError::BackgroundSaveError => Cow::Borrowed("ERR Background save error"), + + // Generic errors + RespError::InternalError => Cow::Borrowed("ERR internal error"), + RespError::Timeout => Cow::Borrowed("ERR timeout"), + RespError::ConnectionLost => Cow::Borrowed("ERR connection lost"), + RespError::InvalidArgument => Cow::Borrowed("ERR invalid argument"), + RespError::OperationNotSupported => Cow::Borrowed("ERR operation not supported"), + RespError::Readonly => { + Cow::Borrowed("READONLY You can't write against a read only replica") + } + RespError::Loading => Cow::Borrowed("LOADING Redis is loading the dataset in memory"), + RespError::Busy => Cow::Borrowed("BUSY Redis is busy running a script"), + + // Custom error + RespError::Custom(msg) => Cow::Owned(format!("ERR {msg}")), + } + } +} + +fn parse_simple_strings(bytes: &[u8]) -> Result<(RespType, &[u8]), RespError> { + match bytes { + [first, rest @ ..] => { + if *first != SIMPLE_STRING { + return Err(RespError::WrongType); + } + + let (consumed, remained) = rest + .windows(2) + .position(|window| window == b"\r\n") + .map(|pos| (&rest[..pos], &rest[pos + 2..])) + .ok_or(RespError::UnexpectedEnd)?; + + if consumed.iter().any(|&byte| byte == b'\r' || byte == b'\n') { + return Err(RespError::InvalidValue); + } + + let consumed = RespType::SimpleString(String::from_utf8_lossy(consumed).to_string()); + return Ok((consumed, remained)); + } + [] => Err(RespError::Custom(String::from("Empty data"))), + } +} + +fn parse_simple_errors(bytes: &[u8]) -> Result<(RespType, &[u8]), RespError> { + match bytes { + [first, rest @ ..] => { + if *first != SIMPLE_ERROR { + return Err(RespError::InvalidDataType); + } + + let (consumed, remained) = rest + .windows(2) + .position(|window| window == b"\r\n") + .map(|pos| (&rest[..pos], &rest[pos + 2..])) + .ok_or(RespError::UnexpectedEnd)?; + + if consumed.iter().any(|&byte| byte == b'\r' || byte == b'\n') { + return Err(RespError::InvalidValue); + } + + let consumed = RespType::SimpleError(String::from_utf8_lossy(consumed).to_string()); + return Ok((consumed, remained)); + } + [] => Err(RespError::Custom(String::from("Empty data"))), + } +} + +fn parse_integers(bytes: &[u8]) -> Result<(RespType, &[u8]), RespError> { + match bytes { + [first, rest @ ..] => { + if *first != INTEGER { + return Err(RespError::InvalidDataType); + } + + let (consumed, remained) = rest + .windows(2) + .position(|window| window == b"\r\n") + .map(|pos| (&rest[..pos], &rest[pos + 2..])) + .ok_or(RespError::UnexpectedEnd)?; + + let parsed_int = String::from_utf8_lossy(consumed) + .parse::<u64>() + .map_err(|_| RespError::InvalidValue)?; + let consumed = RespType::Integer(parsed_int); + return Ok((consumed, remained)); + } + [] => Err(RespError::Custom(String::from("Empty data"))), + } +} + +pub fn parse(bytes: &[u8]) -> Result<(RespType, &[u8]), RespError> { + match bytes[0] { + SIMPLE_STRING => { + let (parsed, remain) = parse_simple_strings(bytes)?; + Ok((parsed, remain)) + } + SIMPLE_ERROR => { + let (parsed, remain) = parse_simple_errors(bytes)?; + Ok((parsed, remain)) + } + BULK_STRING => { + let (parsed, remain) = parse_bulk_strings(bytes)?; + Ok((parsed, remain)) + } + ARRAY => { + let (parsed, remain) = parse_array(bytes)?; + Ok((parsed, remain)) + } + INTEGER => { + let (parsed, remain) = parse_integers(bytes)?; + Ok((parsed, remain)) + } + DOUBLES => { + let (parsed, remain) = parse_doubles(bytes)?; + Ok((parsed, remain)) + } + BOOLEAN => { + let (parsed, remain) = parse_boolean(bytes)?; + Ok((parsed, remain)) + } + NULL => { + let (parsed, remain) = parse_nulls(bytes)?; + Ok((parsed, remain)) + } + + _ => Err(RespError::InvalidDataType), + } +} + +fn parse_array(bytes: &[u8]) -> Result<(RespType, &[u8]), RespError> { + match bytes { + [first, rest @ ..] => { + if *first != ARRAY { + return Err(RespError::InvalidDataType); + } + + let (consumed, mut remained) = rest + .windows(2) + .position(|window| window == b"\r\n") + .map(|pos| (&rest[..pos], &rest[pos + 2..])) + .ok_or(RespError::UnexpectedEnd)?; + + let length = String::from_utf8_lossy(consumed) + .parse::<u64>() + .map_err(|_| RespError::InvalidValue)?; + + let mut array: Vec<RespType> = Vec::with_capacity(length as usize); + + for _ in 0..length { + if !remained.is_empty() { + let (parsed, rest) = parse(remained)?; + remained = rest; + array.push(parsed); + } + } + + if array.len() != length as usize { + return Err(RespError::UnexpectedEnd); + } + + let consumed = RespType::Array(array); + + return Ok((consumed, remained)); + } + [] => Err(RespError::Custom(String::from("Empty data"))), + } +} + +fn parse_bulk_strings(bytes: &[u8]) -> Result<(RespType, &[u8]), RespError> { + match bytes { + [first, rest @ ..] => { + if *first != BULK_STRING { + return Err(RespError::InvalidDataType); + } + + let (consumed, remained) = rest + .windows(2) + .position(|window| window == b"\r\n") + .map(|pos| (&rest[..pos], &rest[pos + 2..])) + .ok_or(RespError::UnexpectedEnd)?; + + let length = String::from_utf8_lossy(consumed) + .parse::<isize>() + .map_err(|_| RespError::InvalidValue)?; + + if length == -1 { + return Ok((RespType::Null(), remained)); + } + + if length < 0 { + return Err(RespError::InvalidValue); + } + + if length as usize > remained.len() { + return Err(RespError::UnexpectedEnd); + } + + let mut bulk_string: Vec<u8> = Vec::with_capacity(length as usize); + + for i in 0..length { + bulk_string.push(remained[i as usize]); + } + + let consumed = RespType::BulkString(bulk_string); + + if !(&remained[length as usize..]).starts_with(b"\r\n") { + return Err(RespError::UnexpectedEnd); + } + return Ok((consumed, &remained[length as usize + 2..])); + } + [] => Err(RespError::Custom(String::from("Empty data"))), + } +} + +fn parse_nulls(bytes: &[u8]) -> Result<(RespType, &[u8]), RespError> { + match bytes { + [first, rest @ ..] => { + if *first != NULL { + return Err(RespError::WrongType); + } + + let (consumed, remained) = rest + .windows(2) + .position(|window| window == b"\r\n") + .map(|pos| (&rest[..pos], &rest[pos + 2..])) + .ok_or(RespError::UnexpectedEnd)?; + + if consumed.iter().any(|&byte| byte == b'\r' || byte == b'\n') { + return Err(RespError::InvalidValue); + } + + let consumed = RespType::Null(); + return Ok((consumed, remained)); + } + [] => Err(RespError::Custom(String::from("Empty data"))), + } +} + +fn parse_boolean(bytes: &[u8]) -> Result<(RespType, &[u8]), RespError> { + match bytes { + [first, rest @ ..] => { + if *first != BOOLEAN { + return Err(RespError::InvalidDataType); + } + + let (consumed, remained) = rest + .windows(2) + .position(|window| window == b"\r\n") + .map(|pos| (&rest[..pos], &rest[pos + 2..])) + .ok_or(RespError::UnexpectedEnd)?; + + let mut val = false; + if consumed.len() == 1 { + match consumed.first().unwrap() { + b't' => val = true, + b'f' => val = false, + _ => return Err(RespError::InvalidValue), + } + } else { + return Err(RespError::UnexpectedEnd); + } + + let consumed = RespType::Boolean(val); + return Ok((consumed, remained)); + } + [] => Err(RespError::Custom(String::from("Empty data"))), + } +} + +fn parse_doubles(bytes: &[u8]) -> Result<(RespType, &[u8]), RespError> { + match bytes { + [first, rest @ ..] => { + if *first != DOUBLES { + return Err(RespError::InvalidDataType); + } + + let (consumed, remained) = rest + .windows(2) + .position(|window| window == b"\r\n") + .map(|pos| (&rest[..pos], &rest[pos + 2..])) + .ok_or(RespError::UnexpectedEnd)?; + + let parsed_double = String::from_utf8_lossy(consumed) + .parse::<f64>() + .map_err(|_| RespError::InvalidValue)?; + let consumed = RespType::Doubles(parsed_double); + return Ok((consumed, remained)); + } + [] => Err(RespError::Custom(String::from("Empty data"))), + } +} + +fn parse_big_numbers() { + todo!() +} + +fn parse_sets() { + todo!() +} + +fn parse_maps() { + todo!() +} + +fn parse_verbatim_string() { + todo!() +} + +fn parse_bulk_errors() { + todo!() +} + +fn parse_attributes() { + todo!() +} + +fn parse_pushes() { + todo!() +} + +#[derive(Debug)] +pub enum RespType { + SimpleString(String), // + + SimpleError(String), // - + Integer(u64), // : + BulkString(Vec<u8>), // $ + Array(Vec<RespType>), // * + Null(), // _ + Boolean(bool), // # + Doubles(f64), // , + BigNumbers(String), // ( + BulkErrors(Vec<RespType>), // ! + VerbatimStrings(Vec<RespType>), // = + Maps(HashMap<RespType, RespType>), // % + Attributes(Vec<RespType>), // | + Sets(HashSet<RespType>), // ~ + Pushes(Vec<RespType>), // > +} + +impl PartialEq for RespType { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (RespType::SimpleString(a), RespType::SimpleString(b)) => a == b, + (RespType::SimpleError(a), RespType::SimpleError(b)) => a == b, + (RespType::Integer(a), RespType::Integer(b)) => a == b, + (RespType::BulkString(a), RespType::BulkString(b)) => a == b, + (RespType::Array(a), RespType::Array(b)) => a == b, + (RespType::Null(), RespType::Null()) => true, + (RespType::Boolean(a), RespType::Boolean(b)) => a == b, + (RespType::Doubles(a), RespType::Doubles(b)) => a == b, + (RespType::BigNumbers(a), RespType::BigNumbers(b)) => a == b, + (RespType::BulkErrors(a), RespType::BulkErrors(b)) => a == b, + (RespType::VerbatimStrings(a), RespType::VerbatimStrings(b)) => a == b, + // (RespType::Maps(a), RespType::Maps(b)) => a == b, + (RespType::Attributes(a), RespType::Attributes(b)) => a == b, + // (RespType::Sets(a), RespType::Sets(b)) => a == b, + (RespType::Pushes(a), RespType::Pushes(b)) => a == b, + _ => false, + } + } +} + +impl PartialEq<&str> for RespType { + fn eq(&self, other: &&str) -> bool { + match self { + RespType::SimpleString(s) => s == other, + RespType::SimpleError(s) => s == other, + RespType::BigNumbers(s) => s == other, + RespType::BulkString(bytes) => { + for (b1, b2) in bytes.iter().zip(other.as_bytes().iter()) { + if b1 != b2 { + return false; + } + } + return true; + } + _ => false, + } + } +} + +impl PartialEq<str> for RespType { + fn eq(&self, other: &str) -> bool { + match self { + RespType::SimpleString(s) => s == other, + RespType::SimpleError(s) => s == other, + RespType::BigNumbers(s) => s == other, + RespType::BulkString(bytes) => { + if let Ok(s) = std::str::from_utf8(bytes) { + *s == *other + } else { + false + } + } + _ => false, + } + } +} + +impl PartialEq<String> for RespType { + fn eq(&self, other: &String) -> bool { + match self { + RespType::SimpleString(s) => s == other, + RespType::SimpleError(s) => s == other, + RespType::BigNumbers(s) => s == other, + RespType::BulkString(bytes) => { + for (b1, b2) in bytes.iter().zip(other.as_bytes().iter()) { + if b1 != b2 { + return false; + } + } + return true; + } + _ => false, + } + } +} + +impl PartialEq<u64> for RespType { + fn eq(&self, other: &u64) -> bool { + match self { + RespType::Integer(i) => i == other, + _ => false, + } + } +} + +impl PartialEq<bool> for RespType { + fn eq(&self, other: &bool) -> bool { + match self { + RespType::Boolean(b) => b == other, + _ => false, + } + } +} + +impl PartialEq<f64> for RespType { + fn eq(&self, other: &f64) -> bool { + match self { + RespType::Doubles(d) => d == other, + _ => false, + } + } +} + +// Test module +#[cfg(test)] +mod tests { + use super::*; + + mod simple_strings_tests { + use super::*; + + #[test] + fn test_valid_simple_strings() { + // Basic valid cases + assert_eq!(parse_simple_strings(b"+OK\r\n").unwrap().0, "OK"); + assert_eq!(parse_simple_strings(b"+PONG\r\n").unwrap().0, "PONG"); + assert_eq!( + parse_simple_strings(b"+Hello World\r\n").unwrap().0, + "Hello World" + ); + + // Empty string + assert_eq!(parse_simple_strings(b"+\r\n").unwrap().0, ""); + + // String with spaces and special characters (but no \r or \n) + assert_eq!( + parse_simple_strings(b"+Hello, World! 123\r\n").unwrap().0, + "Hello, World! 123" + ); + + // String with various ASCII characters + assert_eq!( + parse_simple_strings(b"+!@#$%^&*()_+-={}[]|\\:;\"'<>?,./ \r\n") + .unwrap() + .0, + "!@#$%^&*()_+-={}[]|\\:;\"'<>?,./ " + ); + + // Unicode characters (should work with UTF-8) + assert_eq!( + parse_simple_strings(b"+\xc3\xa9\xc3\xa1\xc3\xb1\r\n") + .unwrap() + .0, + "éáñ" + ); + } + + #[test] + fn test_invalid_prefix() { + // Missing '+' prefix + assert_eq!( + parse_simple_strings(b"OK\r\n").err().unwrap().message(), + "WRONGTYPE Operation against a key holding the wrong kind of value" + ); + + // Wrong prefix + assert_eq!( + parse_simple_strings(b"-Error\r\n").err().unwrap().message(), + "WRONGTYPE Operation against a key holding the wrong kind of value" + ); + assert_eq!( + parse_simple_strings(b":123\r\n").err().unwrap().message(), + "WRONGTYPE Operation against a key holding the wrong kind of value" + ); + assert_eq!( + parse_simple_strings(b"$5\r\nhello\r\n") + .err() + .unwrap() + .message(), + "WRONGTYPE Operation against a key holding the wrong kind of value" + ); + } + + #[test] + fn test_missing_crlf_terminator() { + // No CRLF at all + assert_eq!( + parse_simple_strings(b"+OK").err().unwrap().message(), + "ERR Unexpected end of input" + ); + + // Only \r + assert_eq!( + parse_simple_strings(b"+OK\r").err().unwrap().message(), + "ERR Unexpected end of input" + ); + + // Only \n + assert_eq!( + parse_simple_strings(b"+OK\n").err().unwrap().message(), + "ERR Unexpected end of input" + ); + + // Wrong order (\n\r instead of \r\n) + assert_eq!( + parse_simple_strings(b"+OK\n\r").err().unwrap().message(), + "ERR Unexpected end of input" + ); + } + + #[test] + fn test_invalid_characters_in_content() { + // Contains \r in content + assert_eq!( + parse_simple_strings(b"+Hello\rWorld\r\n") + .err() + .unwrap() + .message(), + "ERR invalid value" + ); + + // Contains \n in content + assert_eq!( + parse_simple_strings(b"+Hello\nWorld\r\n") + .err() + .unwrap() + .message(), + "ERR invalid value" + ); + } + + #[test] + fn test_empty_input() { + assert_eq!( + parse_simple_strings(b"").err().unwrap().message(), + "ERR Empty data" + ); + } + + #[test] + fn test_with_trailing_data() { + // RESP simple string with extra data after CRLF (should be ignored) + assert_eq!(parse_simple_strings(b"+OK\r\nextra_data").unwrap().0, "OK"); + assert_eq!( + parse_simple_strings(b"+PONG\r\n+another_string\r\n") + .unwrap() + .0, + "PONG" + ); + } + + #[test] + fn test_real_world_redis_responses() { + // Common Redis simple string responses + assert_eq!(parse_simple_strings(b"+OK\r\n").unwrap().0, "OK"); + assert_eq!(parse_simple_strings(b"+PONG\r\n").unwrap().0, "PONG"); + assert_eq!(parse_simple_strings(b"+QUEUED\r\n").unwrap().0, "QUEUED"); + + // Redis status responses + assert_eq!( + parse_simple_strings(b"+Background saving started\r\n") + .unwrap() + .0, + "Background saving started" + ); + assert_eq!( + parse_simple_strings(b"+Background saving successfully finished\r\n") + .unwrap() + .0, + "Background saving successfully finished" + ); + } + + #[test] + fn test_edge_cases() { + // Just the prefix and CRLF + assert_eq!(parse_simple_strings(b"+\r\n").unwrap().0, ""); + + // Long string + let long_string = "a".repeat(1000); + let mut input = b"+".to_vec(); + input.extend_from_slice(long_string.as_bytes()); + input.extend_from_slice(b"\r\n"); + assert_eq!(parse_simple_strings(&input).unwrap().0, long_string); + + // String with only spaces + assert_eq!(parse_simple_strings(b"+ \r\n").unwrap().0, " "); + + // String with tabs and other whitespace + assert_eq!(parse_simple_strings(b"+\t \t\r\n").unwrap().0, "\t \t"); + } + + #[test] + fn test_binary_safety_within_limits() { + // Non-UTF8 bytes (but no \r or \n) + let mut input = b"+".to_vec(); + input.extend_from_slice(&[0xFF, 0xFE, 0xFD]); // Invalid UTF-8 + input.extend_from_slice(b"\r\n"); + + // Should handle invalid UTF-8 gracefully with replacement characters + if let RespType::SimpleString(data) = parse_simple_strings(&input).unwrap().0 { + assert!(!data.is_empty()); // Should contain replacement characters + } + } + } + + mod simple_errors_tests { + use super::*; + + #[test] + fn test_valid_simple_errors() { + // Basic valid cases + assert_eq!( + parse_simple_errors(b"-ERR unknown command\r\n").unwrap().0, + "ERR unknown command" + ); + assert_eq!( + parse_simple_errors(b"-WRONGTYPE\r\n").unwrap().0, + "WRONGTYPE" + ); + assert_eq!( + parse_simple_errors(b"-ERR syntax error\r\n").unwrap().0, + "ERR syntax error" + ); + + // Empty error string + assert_eq!(parse_simple_errors(b"-\r\n").unwrap().0, ""); + + // Error with spaces and special characters (but no \r or \n) + assert_eq!( + parse_simple_errors(b"-ERR invalid key: 'test123'\r\n") + .unwrap() + .0, + "ERR invalid key: 'test123'" + ); + + // Error with various ASCII characters + assert_eq!( + parse_simple_errors(b"-ERR !@#$%^&*()_+-={}[]|\\:;\"'<>?,./ \r\n") + .unwrap() + .0, + "ERR !@#$%^&*()_+-={}[]|\\:;\"'<>?,./ " + ); + + // Unicode characters in error message + assert_eq!( + parse_simple_errors(b"-ERR \xc3\xa9\xc3\xa1\xc3\xb1\r\n") + .unwrap() + .0, + "ERR éáñ" + ); + + // Common Redis error patterns + assert_eq!( + parse_simple_errors(b"-NOAUTH Authentication required\r\n") + .unwrap() + .0, + "NOAUTH Authentication required" + ); + assert_eq!( + parse_simple_errors( + b"-WRONGTYPE Operation against a key holding the wrong kind of value\r\n" + ) + .unwrap() + .0, + "WRONGTYPE Operation against a key holding the wrong kind of value" + ); + } + + #[test] + fn test_invalid_simple_errors() { + // Wrong data type marker + assert_eq!( + parse_simple_errors(b"+OK\r\n").err().unwrap().message(), + "ERR Invalid data type" + ); + + // Contains \r in content + assert_eq!( + parse_simple_errors(b"-ERR invalid\r character\r\n") + .err() + .unwrap() + .message(), + "ERR invalid value" + ); + + // Contains \n in content + assert_eq!( + parse_simple_errors(b"-ERR invalid\n character\r\n") + .err() + .unwrap() + .message(), + "ERR invalid value" + ); + + // Missing \r\n terminator + assert_eq!( + parse_simple_errors(b"-ERR no terminator") + .err() + .unwrap() + .message(), + "ERR Unexpected end of input" + ); + + // Only \r without \n + assert_eq!( + parse_simple_errors(b"-ERR only carriage return\r") + .err() + .unwrap() + .message(), + "ERR Unexpected end of input" + ); + + // Only \n without \r + assert_eq!( + parse_simple_errors(b"-ERR only newline\n") + .err() + .unwrap() + .message(), + "ERR Unexpected end of input" + ); + + // Empty input + assert_eq!( + parse_simple_errors(b"").err().unwrap().message(), + "ERR Empty data" + ); + + // Just the marker without content + assert_eq!( + parse_simple_errors(b"-").err().unwrap().message(), + "ERR Unexpected end of input" + ); + } + + #[test] + fn test_simple_error_remaining_bytes() { + // Test that remaining bytes are correctly returned + let (error, remaining) = parse_simple_errors(b"-ERR test\r\nnext data").unwrap(); + assert_eq!(error, "ERR test"); + assert_eq!(remaining, b"next data"); + + // Test with multiple commands + let (error, remaining) = parse_simple_errors(b"-WRONGTYPE\r\n+OK\r\n").unwrap(); + assert_eq!(error, "WRONGTYPE"); + assert_eq!(remaining, b"+OK\r\n"); + + // Test with no remaining data + let (error, remaining) = parse_simple_errors(b"-ERR final\r\n").unwrap(); + assert_eq!(error, "ERR final"); + assert_eq!(remaining, b""); + } + } + + mod integeres_tests { + use super::*; + + #[test] + fn test_valid_integers() { + // Basic valid cases + assert_eq!(parse_integers(b":0\r\n").unwrap().0, 0u64); + assert_eq!(parse_integers(b":1\r\n").unwrap().0, 1u64); + assert_eq!(parse_integers(b":42\r\n").unwrap().0, 42u64); + assert_eq!(parse_integers(b":1000\r\n").unwrap().0, 1000u64); + + assert_eq!(parse_integers(b":+42\r\n").unwrap().0, 42u64); + + // Large numbers + assert_eq!( + parse_integers(b":9223372036854775807\r\n").unwrap().0, + 9223372036854775807u64 + ); + assert_eq!( + parse_integers(b":18446744073709551615\r\n").unwrap().0, + 18446744073709551615u64 + ); // u64::MAX + + // Edge cases + assert_eq!(parse_integers(b":123456789\r\n").unwrap().0, 123456789u64); + + // Numbers with leading zeros (should still parse correctly) + assert_eq!(parse_integers(b":0000042\r\n").unwrap().0, 42u64); + assert_eq!(parse_integers(b":00000\r\n").unwrap().0, 0u64); + } + + #[test] + fn test_invalid_integers() { + // Wrong data type marker + assert_eq!( + parse_integers(b"+42\r\n").err().unwrap().message(), + "ERR Invalid data type" + ); + + // Negative numbers (not valid for u64) + assert_eq!( + parse_integers(b":-42\r\n").err().unwrap().message(), + "ERR invalid value" + ); + + // Non-numeric content + assert_eq!( + parse_integers(b":abc\r\n").err().unwrap().message(), + "ERR invalid value" + ); + + // Mixed numeric and non-numeric + assert_eq!( + parse_integers(b":42abc\r\n").err().unwrap().message(), + "ERR invalid value" + ); + + // Empty integer + assert_eq!( + parse_integers(b":\r\n").err().unwrap().message(), + "ERR invalid value" + ); + + // Contains \r in content + assert_eq!( + parse_integers(b":42\r23\r\n").err().unwrap().message(), + "ERR invalid value" + ); + + // Contains \n in content + assert_eq!( + parse_integers(b":42\n23\r\n").err().unwrap().message(), + "ERR invalid value" + ); |
