diff --git a/Cargo.lock b/Cargo.lock index 032a31d..8b907ed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -49,7 +49,7 @@ checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "meta-memcache-socket" -version = "0.1.5" +version = "0.1.6" dependencies = [ "atoi", "base64", diff --git a/Cargo.toml b/Cargo.toml index f9cf699..0df9388 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "meta-memcache-socket" -version = "0.1.5" +version = "0.1.6" edition = "2021" [lib] diff --git a/src/impl_build_cmd.rs b/src/impl_build_cmd.rs index 2fa3c0d..e898a92 100644 --- a/src/impl_build_cmd.rs +++ b/src/impl_build_cmd.rs @@ -12,18 +12,10 @@ pub fn impl_build_cmd( request_flags: Option<&RequestFlags>, legacy_size_format: bool, ) -> Option> { - if key.len() >= MAX_KEY_LEN { - // Key is too long + if key.is_empty() || key.len() >= MAX_KEY_LEN { return None; } - let mut binary = false; - for c in key.iter() { - if *c <= b' ' || *c > b'~' { - // Not ascii or containing spaces - binary = true; - break; - } - } + let binary = key.iter().any(|&c| c <= b' ' || c > b'~'); if binary && key.len() >= MAX_BIN_KEY_LEN { // Key is too long return None; @@ -40,7 +32,7 @@ pub fn impl_build_cmd( if binary { // If the key contains binary or spaces, it will be send in b64 let result = general_purpose::STANDARD.encode(key); - buf.extend_from_slice(&result.as_bytes()); + buf.extend_from_slice(result.as_bytes()); } else { // Otherwise, it will be send as is buf.extend_from_slice(key); @@ -52,7 +44,8 @@ pub fn impl_build_cmd( if legacy_size_format { buf.push(b'S'); } - buf.extend_from_slice(&size.to_string().as_bytes()); + let mut itoa_buf = itoa::Buffer::new(); + buf.extend_from_slice(itoa_buf.format(size).as_bytes()); } // Add request flags diff --git a/src/impl_build_cmd_tests.rs b/src/impl_build_cmd_tests.rs index 86be468..6ce2cf4 100644 --- a/src/impl_build_cmd_tests.rs +++ b/src/impl_build_cmd_tests.rs @@ -27,7 +27,7 @@ mod tests { Some(666), // ma_delta_value, Some(777), // cas_token Some(b"opaque".to_vec()), // opaque - Some(65 as u8), // mode + Some(b'A'), // mode (APPEND) ); let result = impl_build_cmd(cmd, key, None, Some(&request_flags), false).unwrap(); @@ -138,12 +138,44 @@ mod tests { assert_eq!(result, b"mg S2V5IHdpdGggc3BhY2Vz b\r\n"); } + #[test] + fn test_empty_key_rejected() { + assert!(impl_build_cmd(b"mg", b"", None, None, false).is_none()); + } + + #[test] + fn test_key_at_max_length() { + // 249 bytes is OK (< 250) + let key = &vec![b'X'; 249]; + assert!(impl_build_cmd(b"mg", key, None, None, false).is_some()); + } + + #[test] + fn test_key_at_exact_max_length() { + // 250 bytes is rejected (>= MAX_KEY_LEN) + let key = &vec![b'X'; 250]; + assert!(impl_build_cmd(b"mg", key, None, None, false).is_none()); + } + + #[test] + fn test_binary_key_at_max_length() { + // 186 binary bytes is OK (< 187 = MAX_BIN_KEY_LEN) + let key = &vec![0x00u8; 186]; + assert!(impl_build_cmd(b"mg", key, None, None, false).is_some()); + } + + #[test] + fn test_binary_key_at_exact_max_length() { + // 187 binary bytes is rejected (>= MAX_BIN_KEY_LEN) + let key = &vec![0x00u8; 187]; + assert!(impl_build_cmd(b"mg", key, None, None, false).is_none()); + } + #[test] fn test_impl_build_cmd_large_key() { let cmd = b"mg"; let key = &vec![b'X'; 250]; - let no_result = impl_build_cmd(cmd, key, None, None, false); - assert!(no_result.is_none()); + assert!(impl_build_cmd(cmd, key, None, None, false).is_none()); } #[test] diff --git a/src/impl_parse_header.rs b/src/impl_parse_header.rs index 531d7b2..e7298c1 100644 --- a/src/impl_parse_header.rs +++ b/src/impl_parse_header.rs @@ -1,3 +1,5 @@ +use memchr::memmem; + use crate::{constants::*, ResponseFlags}; pub fn impl_parse_header( @@ -9,43 +11,32 @@ pub fn impl_parse_header( return None; } let end = end.min(data.len()); - let mut n = start + 2; - while n < end - 1 { - if data[n] == b'\r' && data[n + 1] == b'\n' { - let endl_pos = n + 2; - match &data[start..start + 2] { - b"VA" => { - match ResponseFlags::from_value_header(&data[start..n]) { - Some((size, flags)) => { - return Some((endl_pos, Some(RESPONSE_VALUE), Some(size), Some(flags))); - } - None => { - return Some((endl_pos, None, None, None)); - } - }; - } - b"HD" | b"OK" => { - let flags = ResponseFlags::from_success_header(&data[start..n]); - return Some((endl_pos, Some(RESPONSE_SUCCESS), None, Some(flags))); - } - b"NS" => { - return Some((endl_pos, Some(RESPONSE_NOT_STORED), None, None)); - } - b"EX" => { - return Some((endl_pos, Some(RESPONSE_CONFLICT), None, None)); - } - b"EN" | b"NF" => { - return Some((endl_pos, Some(RESPONSE_MISS), None, None)); - } - b"MN" => { - return Some((endl_pos, Some(RESPONSE_NOOP), None, None)); - } - _ => { - return Some((endl_pos, None, None, None)); + let search_start = start + 2; + if search_start >= end { + return None; + } + // Use memmem SIMD-accelerated search for \r\n + if let Some(pos) = memmem::find(&data[search_start..end], b"\r\n") { + let n = search_start + pos; + let endl_pos = n + 2; + match &data[start..start + 2] { + b"VA" => match ResponseFlags::from_value_header(&data[start..n]) { + Some((size, flags)) => { + Some((endl_pos, Some(RESPONSE_VALUE), Some(size), Some(flags))) } + None => Some((endl_pos, None, None, None)), + }, + b"HD" | b"OK" => { + let flags = ResponseFlags::from_success_header(&data[start..n]); + Some((endl_pos, Some(RESPONSE_SUCCESS), None, Some(flags))) } + b"NS" => Some((endl_pos, Some(RESPONSE_NOT_STORED), None, None)), + b"EX" => Some((endl_pos, Some(RESPONSE_CONFLICT), None, None)), + b"EN" | b"NF" => Some((endl_pos, Some(RESPONSE_MISS), None, None)), + b"MN" => Some((endl_pos, Some(RESPONSE_NOOP), None, None)), + _ => Some((endl_pos, None, None, None)), } - n += 1; + } else { + None } - None } diff --git a/src/lib.rs b/src/lib.rs index 48fb3c4..1a2b628 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,7 +4,9 @@ mod impl_build_cmd_tests; mod impl_parse_header; mod impl_parse_header_tests; mod request_flags; +mod request_flags_tests; mod response_flags; +mod response_flags_tests; pub use constants::*; use impl_build_cmd::impl_build_cmd; use impl_parse_header::impl_parse_header; diff --git a/src/request_flags.rs b/src/request_flags.rs index 378b474..1b27e8e 100644 --- a/src/request_flags.rs +++ b/src/request_flags.rs @@ -50,6 +50,7 @@ pub struct RequestFlags { impl RequestFlags { pub fn push_bytes(&self, buf: &mut Vec) { + let mut itoa_buf = itoa::Buffer::new(); if self.no_reply { buf.push(b' '); buf.push(b'q'); @@ -97,42 +98,42 @@ impl RequestFlags { if let Some(v) = self.cache_ttl { buf.push(b' '); buf.push(b'T'); - buf.extend_from_slice(&v.to_string().as_bytes()); + buf.extend_from_slice(itoa_buf.format(v).as_bytes()); } if let Some(v) = self.recache_ttl { buf.push(b' '); buf.push(b'R'); - buf.extend_from_slice(&v.to_string().as_bytes()); + buf.extend_from_slice(itoa_buf.format(v).as_bytes()); } if let Some(v) = self.vivify_on_miss_ttl { buf.push(b' '); buf.push(b'N'); - buf.extend_from_slice(&v.to_string().as_bytes()); + buf.extend_from_slice(itoa_buf.format(v).as_bytes()); } if let Some(v) = self.client_flag { buf.push(b' '); buf.push(b'F'); - buf.extend_from_slice(&v.to_string().as_bytes()); + buf.extend_from_slice(itoa_buf.format(v).as_bytes()); } if let Some(v) = self.ma_initial_value { buf.push(b' '); buf.push(b'J'); - buf.extend_from_slice(&v.to_string().as_bytes()); + buf.extend_from_slice(itoa_buf.format(v).as_bytes()); } if let Some(v) = self.ma_delta_value { buf.push(b' '); buf.push(b'D'); - buf.extend_from_slice(&v.to_string().as_bytes()); + buf.extend_from_slice(itoa_buf.format(v).as_bytes()); } if let Some(v) = self.cas_token { buf.push(b' '); buf.push(b'C'); - buf.extend_from_slice(&v.to_string().as_bytes()); + buf.extend_from_slice(itoa_buf.format(v).as_bytes()); } if let Some(v) = &self.opaque { buf.push(b' '); buf.push(b'O'); - buf.extend_from_slice(&v); + buf.extend_from_slice(v); } if let Some(v) = self.mode { if v != SET_MODE_SET && v != MA_MODE_INC { @@ -242,28 +243,7 @@ impl RequestFlags { } pub fn copy(&self) -> Self { - RequestFlags { - no_reply: self.no_reply, - return_client_flag: self.return_client_flag, - return_cas_token: self.return_cas_token, - return_value: self.return_value, - return_ttl: self.return_ttl, - return_size: self.return_size, - return_last_access: self.return_last_access, - return_fetched: self.return_fetched, - return_key: self.return_key, - no_update_lru: self.no_update_lru, - mark_stale: self.mark_stale, - cache_ttl: self.cache_ttl, - recache_ttl: self.recache_ttl, - vivify_on_miss_ttl: self.vivify_on_miss_ttl, - client_flag: self.client_flag, - ma_initial_value: self.ma_initial_value, - ma_delta_value: self.ma_delta_value, - cas_token: self.cas_token, - opaque: self.opaque.clone(), - mode: self.mode, - } + self.clone() } pub fn __str__(&self) -> String { diff --git a/src/request_flags_tests.rs b/src/request_flags_tests.rs new file mode 100644 index 0000000..d7e7fdb --- /dev/null +++ b/src/request_flags_tests.rs @@ -0,0 +1,562 @@ +#[cfg(test)] +mod tests { + use crate::request_flags::RequestFlags; + use crate::{MA_MODE_DEC, MA_MODE_INC, SET_MODE_ADD, SET_MODE_APPEND, SET_MODE_SET}; + + fn default_flags() -> RequestFlags { + RequestFlags::new( + false, false, false, false, false, false, false, false, false, false, false, None, + None, None, None, None, None, None, None, None, + ) + } + + fn push_to_vec(flags: &RequestFlags) -> Vec { + let mut buf = Vec::new(); + flags.push_bytes(&mut buf); + buf + } + + #[test] + fn test_empty_flags() { + let flags = default_flags(); + assert_eq!(push_to_vec(&flags), b""); + } + + #[test] + fn test_no_reply() { + let flags = RequestFlags::new( + true, false, false, false, false, false, false, false, false, false, false, None, None, + None, None, None, None, None, None, None, + ); + assert_eq!(push_to_vec(&flags), b" q"); + } + + #[test] + fn test_return_client_flag() { + let flags = RequestFlags::new( + false, true, false, false, false, false, false, false, false, false, false, None, None, + None, None, None, None, None, None, None, + ); + assert_eq!(push_to_vec(&flags), b" f"); + } + + #[test] + fn test_return_cas_token() { + let flags = RequestFlags::new( + false, false, true, false, false, false, false, false, false, false, false, None, None, + None, None, None, None, None, None, None, + ); + assert_eq!(push_to_vec(&flags), b" c"); + } + + #[test] + fn test_return_value() { + let flags = RequestFlags::new( + false, false, false, true, false, false, false, false, false, false, false, None, None, + None, None, None, None, None, None, None, + ); + assert_eq!(push_to_vec(&flags), b" v"); + } + + #[test] + fn test_return_ttl() { + let flags = RequestFlags::new( + false, false, false, false, true, false, false, false, false, false, false, None, None, + None, None, None, None, None, None, None, + ); + assert_eq!(push_to_vec(&flags), b" t"); + } + + #[test] + fn test_return_size() { + let flags = RequestFlags::new( + false, false, false, false, false, true, false, false, false, false, false, None, None, + None, None, None, None, None, None, None, + ); + assert_eq!(push_to_vec(&flags), b" s"); + } + + #[test] + fn test_return_last_access() { + let flags = RequestFlags::new( + false, false, false, false, false, false, true, false, false, false, false, None, None, + None, None, None, None, None, None, None, + ); + assert_eq!(push_to_vec(&flags), b" l"); + } + + #[test] + fn test_return_fetched() { + let flags = RequestFlags::new( + false, false, false, false, false, false, false, true, false, false, false, None, None, + None, None, None, None, None, None, None, + ); + assert_eq!(push_to_vec(&flags), b" h"); + } + + #[test] + fn test_return_key() { + let flags = RequestFlags::new( + false, false, false, false, false, false, false, false, true, false, false, None, None, + None, None, None, None, None, None, None, + ); + assert_eq!(push_to_vec(&flags), b" k"); + } + + #[test] + fn test_no_update_lru() { + let flags = RequestFlags::new( + false, false, false, false, false, false, false, false, false, true, false, None, None, + None, None, None, None, None, None, None, + ); + assert_eq!(push_to_vec(&flags), b" u"); + } + + #[test] + fn test_mark_stale() { + let flags = RequestFlags::new( + false, false, false, false, false, false, false, false, false, false, true, None, None, + None, None, None, None, None, None, None, + ); + assert_eq!(push_to_vec(&flags), b" I"); + } + + #[test] + fn test_cache_ttl() { + let flags = RequestFlags::new( + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + Some(300), + None, + None, + None, + None, + None, + None, + None, + None, + ); + assert_eq!(push_to_vec(&flags), b" T300"); + } + + #[test] + fn test_recache_ttl() { + let flags = RequestFlags::new( + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + None, + Some(60), + None, + None, + None, + None, + None, + None, + None, + ); + assert_eq!(push_to_vec(&flags), b" R60"); + } + + #[test] + fn test_vivify_on_miss_ttl() { + let flags = RequestFlags::new( + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + None, + None, + Some(120), + None, + None, + None, + None, + None, + None, + ); + assert_eq!(push_to_vec(&flags), b" N120"); + } + + #[test] + fn test_client_flag() { + let flags = RequestFlags::new( + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + None, + None, + None, + Some(42), + None, + None, + None, + None, + None, + ); + assert_eq!(push_to_vec(&flags), b" F42"); + } + + #[test] + fn test_ma_initial_value() { + let flags = RequestFlags::new( + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + None, + None, + None, + None, + Some(100), + None, + None, + None, + None, + ); + assert_eq!(push_to_vec(&flags), b" J100"); + } + + #[test] + fn test_ma_delta_value() { + let flags = RequestFlags::new( + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + None, + None, + None, + None, + None, + Some(5), + None, + None, + None, + ); + assert_eq!(push_to_vec(&flags), b" D5"); + } + + #[test] + fn test_cas_token() { + let flags = RequestFlags::new( + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + None, + None, + None, + None, + None, + None, + Some(999), + None, + None, + ); + assert_eq!(push_to_vec(&flags), b" C999"); + } + + #[test] + fn test_opaque() { + let flags = RequestFlags::new( + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + None, + None, + None, + None, + None, + None, + None, + Some(b"token123".to_vec()), + None, + ); + assert_eq!(push_to_vec(&flags), b" Otoken123"); + } + + // Mode optimization: SET_MODE_SET and MA_MODE_INC are defaults, not sent + #[test] + fn test_mode_set_not_sent() { + let flags = RequestFlags::new( + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + None, + None, + None, + None, + None, + None, + None, + None, + Some(SET_MODE_SET), + ); + assert_eq!(push_to_vec(&flags), b""); + } + + #[test] + fn test_mode_inc_not_sent() { + let flags = RequestFlags::new( + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + None, + None, + None, + None, + None, + None, + None, + None, + Some(MA_MODE_INC), + ); + assert_eq!(push_to_vec(&flags), b""); + } + + #[test] + fn test_mode_add_sent() { + let flags = RequestFlags::new( + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + None, + None, + None, + None, + None, + None, + None, + None, + Some(SET_MODE_ADD), + ); + assert_eq!(push_to_vec(&flags), b" ME"); + } + + #[test] + fn test_mode_append_sent() { + let flags = RequestFlags::new( + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + None, + None, + None, + None, + None, + None, + None, + None, + Some(SET_MODE_APPEND), + ); + assert_eq!(push_to_vec(&flags), b" MA"); + } + + #[test] + fn test_mode_dec_sent() { + let flags = RequestFlags::new( + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + None, + None, + None, + None, + None, + None, + None, + None, + Some(MA_MODE_DEC), + ); + assert_eq!(push_to_vec(&flags), b" M-"); + } + + #[test] + fn test_large_u64_values() { + let flags = RequestFlags::new( + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + None, + None, + None, + None, + Some(u64::MAX), + Some(u64::MAX), + None, + None, + None, + ); + let result = push_to_vec(&flags); + let expected = format!(" J{} D{}", u64::MAX, u64::MAX); + assert_eq!(result, expected.as_bytes()); + } + + #[test] + fn test_zero_values() { + let flags = RequestFlags::new( + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + Some(0), + Some(0), + Some(0), + Some(0), + Some(0), + Some(0), + Some(0), + None, + None, + ); + assert_eq!(push_to_vec(&flags), b" T0 R0 N0 F0 J0 D0 C0"); + } + + #[test] + fn test_flag_ordering() { + // Verify flags are emitted in the correct order + let flags = RequestFlags::new( + true, // q + true, // f + true, // c + true, // v + true, // t + true, // s + true, // l + true, // h + true, // k + true, // u + true, // I + Some(1), // T + Some(2), // R + Some(3), // N + Some(4), // F + Some(5), // J + Some(6), // D + Some(7), // C + Some(b"op".to_vec()), // O + Some(SET_MODE_ADD), // M + ); + assert_eq!( + push_to_vec(&flags), + b" q f c v t s l h k u I T1 R2 N3 F4 J5 D6 C7 Oop ME" + ); + } +} diff --git a/src/response_flags.rs b/src/response_flags.rs index 5352df4..5b6b9a8 100644 --- a/src/response_flags.rs +++ b/src/response_flags.rs @@ -1,16 +1,13 @@ use atoi::FromRadix10Checked; +use memchr::memchr; use pyo3::prelude::*; #[inline] fn find_space_or_end(header: &[u8], start: usize) -> usize { - let mut n = start; - while n < header.len() { - if header[n] == b' ' { - break; - } - n += 1; + match memchr(b' ', &header[start..]) { + Some(pos) => start + pos, + None => header.len(), } - n } #[inline] diff --git a/src/response_flags_tests.rs b/src/response_flags_tests.rs new file mode 100644 index 0000000..6a577b6 --- /dev/null +++ b/src/response_flags_tests.rs @@ -0,0 +1,240 @@ +#[cfg(test)] +mod tests { + use crate::response_flags::ResponseFlags; + + #[test] + fn test_parse_flags_empty() { + let flags = ResponseFlags::parse_flags(b"HD", 2); + assert_eq!(flags.cas_token, None); + assert_eq!(flags.fetched, None); + assert_eq!(flags.last_access, None); + assert_eq!(flags.ttl, None); + assert_eq!(flags.client_flag, None); + assert_eq!(flags.win, None); + assert!(!flags.stale); + assert_eq!(flags.size, None); + assert_eq!(flags.opaque, None); + } + + #[test] + fn test_parse_cas_token() { + let flags = ResponseFlags::parse_flags(b"HD c12345", 2); + assert_eq!(flags.cas_token, Some(12345)); + } + + #[test] + fn test_parse_fetched_true() { + let flags = ResponseFlags::parse_flags(b"HD h1", 2); + assert_eq!(flags.fetched, Some(true)); + } + + #[test] + fn test_parse_fetched_false() { + let flags = ResponseFlags::parse_flags(b"HD h0", 2); + assert_eq!(flags.fetched, Some(false)); + } + + #[test] + fn test_parse_fetched_invalid() { + let flags = ResponseFlags::parse_flags(b"HD hX", 2); + assert_eq!(flags.fetched, None); + } + + #[test] + fn test_parse_last_access() { + let flags = ResponseFlags::parse_flags(b"HD l999", 2); + assert_eq!(flags.last_access, Some(999)); + } + + #[test] + fn test_parse_ttl_positive() { + let flags = ResponseFlags::parse_flags(b"HD t3600", 2); + assert_eq!(flags.ttl, Some(3600)); + } + + #[test] + fn test_parse_ttl_negative_one() { + // -1 means no expiration + let flags = ResponseFlags::parse_flags(b"HD t-1", 2); + assert_eq!(flags.ttl, Some(-1)); + } + + #[test] + fn test_parse_ttl_negative_other() { + // Any negative value is treated as -1 + let flags = ResponseFlags::parse_flags(b"HD t-999", 2); + assert_eq!(flags.ttl, Some(-1)); + } + + #[test] + fn test_parse_ttl_just_dash() { + let flags = ResponseFlags::parse_flags(b"HD t-", 2); + assert_eq!(flags.ttl, Some(-1)); + } + + #[test] + fn test_parse_client_flag() { + let flags = ResponseFlags::parse_flags(b"HD f42", 2); + assert_eq!(flags.client_flag, Some(42)); + } + + #[test] + fn test_parse_win() { + let flags = ResponseFlags::parse_flags(b"HD W", 2); + assert_eq!(flags.win, Some(true)); + } + + #[test] + fn test_parse_lose() { + let flags = ResponseFlags::parse_flags(b"HD Z", 2); + assert_eq!(flags.win, Some(false)); + } + + #[test] + fn test_parse_stale() { + let flags = ResponseFlags::parse_flags(b"HD X", 2); + assert!(flags.stale); + } + + #[test] + fn test_parse_size() { + let flags = ResponseFlags::parse_flags(b"HD s4096", 2); + assert_eq!(flags.size, Some(4096)); + } + + #[test] + fn test_parse_opaque() { + let flags = ResponseFlags::parse_flags(b"HD Otoken123", 2); + assert_eq!(flags.opaque, Some(b"token123".to_vec())); + } + + #[test] + fn test_parse_opaque_with_more_flags() { + let flags = ResponseFlags::parse_flags(b"HD Otoken X", 2); + assert_eq!(flags.opaque, Some(b"token".to_vec())); + assert!(flags.stale); + } + + #[test] + fn test_parse_unknown_flag_skipped() { + let flags = ResponseFlags::parse_flags(b"HD Q123 c99", 2); + assert_eq!(flags.cas_token, Some(99)); + } + + #[test] + fn test_parse_multiple_spaces() { + let flags = ResponseFlags::parse_flags(b"HD c1 h1 ", 2); + assert_eq!(flags.cas_token, Some(1)); + assert_eq!(flags.fetched, Some(true)); + } + + #[test] + fn test_parse_all_flags() { + let flags = ResponseFlags::parse_flags(b"HD c100 h1 l200 t300 f400 W X s500 Odata", 2); + assert_eq!(flags.cas_token, Some(100)); + assert_eq!(flags.fetched, Some(true)); + assert_eq!(flags.last_access, Some(200)); + assert_eq!(flags.ttl, Some(300)); + assert_eq!(flags.client_flag, Some(400)); + assert_eq!(flags.win, Some(true)); + assert!(flags.stale); + assert_eq!(flags.size, Some(500)); + assert_eq!(flags.opaque, Some(b"data".to_vec())); + } + + #[test] + fn test_parse_u32_overflow() { + // u32 max is 4294967295, this overflows + let flags = ResponseFlags::parse_flags(b"HD c99999999999", 2); + assert_eq!(flags.cas_token, None); + } + + #[test] + fn test_parse_zero_values() { + let flags = ResponseFlags::parse_flags(b"HD c0 l0 t0 f0 s0", 2); + assert_eq!(flags.cas_token, Some(0)); + assert_eq!(flags.last_access, Some(0)); + assert_eq!(flags.ttl, Some(0)); + assert_eq!(flags.client_flag, Some(0)); + assert_eq!(flags.size, Some(0)); + } + + // from_value_header tests + #[test] + fn test_from_value_header_basic() { + let result = ResponseFlags::from_value_header(b"VA 100 c1"); + assert!(result.is_some()); + let (size, flags) = result.unwrap(); + assert_eq!(size, 100); + assert_eq!(flags.cas_token, Some(1)); + } + + #[test] + fn test_from_value_header_no_flags() { + let result = ResponseFlags::from_value_header(b"VA 42"); + assert!(result.is_some()); + let (size, flags) = result.unwrap(); + assert_eq!(size, 42); + assert_eq!(flags.cas_token, None); + } + + #[test] + fn test_from_value_header_too_short() { + assert!(ResponseFlags::from_value_header(b"VA").is_none()); + assert!(ResponseFlags::from_value_header(b"VA ").is_none()); + } + + #[test] + fn test_from_value_header_no_size() { + // No numeric size after "VA " + assert!(ResponseFlags::from_value_header(b"VA c1").is_none()); + } + + #[test] + fn test_from_value_header_size_overflow() { + assert!(ResponseFlags::from_value_header(b"VA 99999999999").is_none()); + } + + // from_success_header tests + #[test] + fn test_from_success_header_basic() { + let flags = ResponseFlags::from_success_header(b"HD c42 X"); + assert_eq!(flags.cas_token, Some(42)); + assert!(flags.stale); + } + + #[test] + fn test_from_success_header_empty() { + let flags = ResponseFlags::from_success_header(b"HD"); + assert_eq!(flags.cas_token, None); + assert!(!flags.stale); + } + + // Last opaque wins when multiple are present + #[test] + fn test_last_opaque_wins() { + let flags = ResponseFlags::parse_flags(b"HD Ofirst Osecond", 2); + assert_eq!(flags.opaque, Some(b"second".to_vec())); + } + + // Stale + lose combination + #[test] + fn test_stale_and_lose() { + let flags = ResponseFlags::parse_flags(b"HD X Z", 2); + assert!(flags.stale); + assert_eq!(flags.win, Some(false)); + } + + // Win overrides lose (last one wins) + #[test] + fn test_win_after_lose() { + let flags = ResponseFlags::parse_flags(b"HD Z W", 2); + assert_eq!(flags.win, Some(true)); + } + + #[test] + fn test_lose_after_win() { + let flags = ResponseFlags::parse_flags(b"HD W Z", 2); + assert_eq!(flags.win, Some(false)); + } +}