Skip to main content

memcache/
value.rs

1use bytes::Bytes;
2
3use crate::error::{Error, Result};
4
5/// Flag bits used to tag stored values.
6///
7/// These match the constants used by the Java clients (`cn.vika.memcached`
8/// `NativeHandler` and `com.schooner.MemCached`), so values written by this
9/// crate can be read back by those clients and vice versa.
10pub mod flags {
11    /// Value body is QuickLZ-compressed (this crate cannot decode it).
12    pub const F_COMPRESSED: u32 = 2;
13    /// Value body is a Java-serialized object (this crate cannot decode it).
14    pub const F_SERIALIZED: u32 = 8;
15
16    /// A single byte.
17    pub const MARKER_BYTE: u32 = 1;
18    /// A 32-bit integer, big-endian.
19    pub const MARKER_INTEGER: u32 = 4;
20    /// A character, stored as a 32-bit integer.
21    pub const MARKER_CHARACTER: u32 = 16;
22    /// A `String`, stored as UTF-8.
23    pub const MARKER_STRING: u32 = 32;
24    /// A `StringBuffer`, stored as UTF-8.
25    pub const MARKER_STRINGBUFFER: u32 = 64;
26    /// A 16-bit short, big-endian.
27    pub const MARKER_SHORT: u32 = 256;
28    /// A 64-bit double, big-endian IEEE-754 bits.
29    pub const MARKER_DOUBLE: u32 = 512;
30    /// A date, stored as an 8-byte big-endian millisecond timestamp.
31    pub const MARKER_DATE: u32 = 1024;
32    /// A `StringBuilder`, stored as UTF-8.
33    pub const MARKER_STRINGBUILDER: u32 = 2048;
34    /// A raw byte array.
35    pub const MARKER_BYTEARR: u32 = 4096;
36    /// A boolean, stored as a single byte (`0`/`1`).
37    pub const MARKER_BOOLEAN: u32 = 8192;
38    /// A 64-bit long, big-endian.
39    pub const MARKER_LONG: u32 = 16384;
40}
41
42/// A value stored in / retrieved from memcached: opaque bytes plus flags.
43#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct Value {
45    data: Bytes,
46    flags: u32,
47}
48
49impl Value {
50    /// Construct a value from raw bytes and flags.
51    pub fn new(data: impl Into<Bytes>, flags: u32) -> Self {
52        Value {
53            data: data.into(),
54            flags,
55        }
56    }
57
58    /// The raw stored bytes.
59    pub fn as_bytes(&self) -> &[u8] {
60        &self.data
61    }
62
63    /// Consume the value and return the raw bytes.
64    pub fn into_bytes(self) -> Bytes {
65        self.data
66    }
67
68    /// The stored flags.
69    pub fn flags(&self) -> u32 {
70        self.flags
71    }
72
73    fn reject_opaque(&self) -> Result<()> {
74        if self.flags & flags::F_COMPRESSED != 0 {
75            return Err(Error::Unsupported("QuickLZ compressed value"));
76        }
77        if self.flags & flags::F_SERIALIZED != 0 {
78            return Err(Error::Unsupported("Java-serialized value"));
79        }
80        Ok(())
81    }
82
83    /// Decode the value as a UTF-8 string.
84    ///
85    /// Works for values written as `String`/`StringBuffer`/`StringBuilder`
86    /// (marker bits) as well as plain untagged UTF-8 blobs.
87    pub fn as_string(&self) -> Result<String> {
88        self.reject_opaque()?;
89        std::str::from_utf8(&self.data)
90            .map(str::to_owned)
91            .map_err(|err| Error::Decode(format!("value is not valid UTF-8: {err}")))
92    }
93
94    /// Decode the value as a signed 64-bit integer.
95    ///
96    /// Accepts 8-byte (`long`/`date`), 4-byte (`int`/`char`), 2-byte (`short`)
97    /// and 1-byte (`byte`/`boolean`) big-endian encodings.
98    pub fn as_i64(&self) -> Result<i64> {
99        self.reject_opaque()?;
100        match self.data.len() {
101            8 => Ok(i64::from_be_bytes(self.data[..8].try_into().unwrap())),
102            4 => Ok(i32::from_be_bytes(self.data[..4].try_into().unwrap()) as i64),
103            2 => Ok(i16::from_be_bytes(self.data[..2].try_into().unwrap()) as i64),
104            1 => Ok(self.data[0] as i64),
105            len => Err(Error::Decode(format!(
106                "cannot decode {len}-byte value as integer"
107            ))),
108        }
109    }
110
111    /// Decode the value as an unsigned 64-bit integer (e.g. a counter).
112    pub fn as_u64(&self) -> Result<u64> {
113        self.reject_opaque()?;
114        // Counters set via the text protocol are stored as ASCII decimal.
115        if let Ok(text) = std::str::from_utf8(&self.data)
116            && let Ok(parsed) = text.trim().parse::<u64>()
117        {
118            return Ok(parsed);
119        }
120        match self.data.len() {
121            8 => Ok(u64::from_be_bytes(self.data[..8].try_into().unwrap())),
122            4 => Ok(u32::from_be_bytes(self.data[..4].try_into().unwrap()) as u64),
123            _ => Err(Error::Decode(
124                "cannot decode value as unsigned integer".into(),
125            )),
126        }
127    }
128
129    /// Decode the value as a boolean.
130    pub fn as_bool(&self) -> Result<bool> {
131        self.reject_opaque()?;
132        match self.data.first() {
133            Some(&byte) => Ok(byte != 0),
134            None => Err(Error::Decode("empty value cannot decode as bool".into())),
135        }
136    }
137}
138
139/// A value together with its CAS (compare-and-swap) token.
140#[derive(Debug, Clone, PartialEq, Eq)]
141pub struct CasValue {
142    /// The stored value.
143    pub value: Value,
144    /// The CAS token to be supplied to a subsequent `cas` call.
145    pub cas: u64,
146}
147
148impl CasValue {
149    /// Construct a CAS value pair.
150    pub fn new(value: Value, cas: u64) -> Self {
151        CasValue { value, cas }
152    }
153}
154
155/// Types that can be stored as a memcached value.
156///
157/// Numeric and boolean types are tagged with the same marker flags the Java
158/// clients use, so they can be read back as the corresponding Java type. Raw
159/// byte containers and strings are stored untagged (flags `0`) as generic
160/// blobs, which any memcached client can read.
161pub trait ToMemcacheValue {
162    /// Encode `self` into a [`Value`].
163    fn to_memcache_value(self) -> Value;
164}
165
166impl ToMemcacheValue for Value {
167    fn to_memcache_value(self) -> Value {
168        self
169    }
170}
171
172impl ToMemcacheValue for &[u8] {
173    fn to_memcache_value(self) -> Value {
174        Value::new(Bytes::copy_from_slice(self), 0)
175    }
176}
177
178impl ToMemcacheValue for Vec<u8> {
179    fn to_memcache_value(self) -> Value {
180        Value::new(self, 0)
181    }
182}
183
184impl ToMemcacheValue for Bytes {
185    fn to_memcache_value(self) -> Value {
186        Value::new(self, 0)
187    }
188}
189
190impl ToMemcacheValue for &str {
191    fn to_memcache_value(self) -> Value {
192        Value::new(Bytes::copy_from_slice(self.as_bytes()), 0)
193    }
194}
195
196impl ToMemcacheValue for String {
197    fn to_memcache_value(self) -> Value {
198        Value::new(self.into_bytes(), 0)
199    }
200}
201
202impl ToMemcacheValue for i32 {
203    fn to_memcache_value(self) -> Value {
204        Value::new(self.to_be_bytes().to_vec(), flags::MARKER_INTEGER)
205    }
206}
207
208impl ToMemcacheValue for i64 {
209    fn to_memcache_value(self) -> Value {
210        Value::new(self.to_be_bytes().to_vec(), flags::MARKER_LONG)
211    }
212}
213
214impl ToMemcacheValue for u64 {
215    fn to_memcache_value(self) -> Value {
216        Value::new((self as i64).to_be_bytes().to_vec(), flags::MARKER_LONG)
217    }
218}
219
220impl ToMemcacheValue for bool {
221    fn to_memcache_value(self) -> Value {
222        Value::new(vec![self as u8], flags::MARKER_BOOLEAN)
223    }
224}
225
226#[cfg(test)]
227mod tests {
228    use super::*;
229
230    #[test]
231    fn string_roundtrip() {
232        let value = "hello".to_memcache_value();
233        assert_eq!(value.as_string().unwrap(), "hello");
234        assert_eq!(value.as_bytes(), b"hello");
235    }
236
237    #[test]
238    fn integer_roundtrip_with_marker() {
239        let value = 42_i64.to_memcache_value();
240        assert_eq!(value.flags(), flags::MARKER_LONG);
241        assert_eq!(value.as_i64().unwrap(), 42);
242    }
243
244    #[test]
245    fn bool_roundtrip_with_marker() {
246        let value = true.to_memcache_value();
247        assert_eq!(value.flags(), flags::MARKER_BOOLEAN);
248        assert!(value.as_bool().unwrap());
249    }
250
251    #[test]
252    fn compressed_and_serialized_are_unsupported() {
253        let compressed = Value::new(vec![0, 1, 2], flags::F_COMPRESSED);
254        assert!(matches!(compressed.as_bytes().len(), 3));
255        assert!(compressed.as_string().is_err());
256        let serialized = Value::new(vec![0, 1, 2], flags::F_SERIALIZED);
257        assert!(serialized.as_i64().is_err());
258    }
259
260    #[test]
261    fn counter_decimal_decodes_as_u64() {
262        let value = Value::new(b"12345".to_vec(), 0);
263        assert_eq!(value.as_u64().unwrap(), 12345);
264    }
265}