├── .gitignore ├── .rspec ├── Gemfile ├── src ├── memcached │ ├── mod.rs │ ├── store.rs │ ├── types.rs │ ├── stream.rs │ ├── handler_stream.rs │ └── parser.rs ├── socket_stream.rs ├── unpack.rs ├── copy_stream_to_write.rs ├── crlf_delimited_stream.rs └── main.rs ├── Cargo.toml ├── Gemfile.lock ├── spec ├── memcached_spec.rb └── spec_helper.rb ├── README.md └── Cargo.lock /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "memcached" 4 | gem "rspec" 5 | gem "pry" 6 | gem "dalli" 7 | -------------------------------------------------------------------------------- /src/memcached/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod types; 2 | pub mod parser; 3 | pub mod stream; 4 | pub mod store; 5 | pub mod handler_stream; 6 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "decachedmem" 3 | version = "0.1.0" 4 | authors = ["Sam Phippen "] 5 | 6 | [dependencies] 7 | tokio-core = { git = "https://github.com/tokio-rs/tokio-core/" } 8 | futures = "0.1" 9 | env_logger = "0.3.4" 10 | 11 | [profile.release] 12 | lto = true 13 | -------------------------------------------------------------------------------- /src/memcached/store.rs: -------------------------------------------------------------------------------- 1 | use memcached::types::*; 2 | use std::collections::HashMap; 3 | 4 | #[derive(Debug)] 5 | pub struct Store { 6 | kv: HashMap, 7 | } 8 | 9 | impl Store { 10 | pub fn new() -> Store { 11 | Store { kv: HashMap::new() } 12 | } 13 | 14 | pub fn set(&mut self, key: &MemcachedKey, value: MemcachedValue) { 15 | self.kv.insert(key.clone(), value); 16 | } 17 | 18 | pub fn get(&mut self, key: &MemcachedKey) -> Option<&MemcachedValue> { 19 | self.kv.get(key) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | coderay (1.1.1) 5 | dalli (2.7.6) 6 | diff-lcs (1.2.5) 7 | memcached (1.8.0) 8 | method_source (0.8.2) 9 | pry (0.10.4) 10 | coderay (~> 1.1.0) 11 | method_source (~> 0.8.1) 12 | slop (~> 3.4) 13 | rspec (3.5.0) 14 | rspec-core (~> 3.5.0) 15 | rspec-expectations (~> 3.5.0) 16 | rspec-mocks (~> 3.5.0) 17 | rspec-core (3.5.3) 18 | rspec-support (~> 3.5.0) 19 | rspec-expectations (3.5.0) 20 | diff-lcs (>= 1.2.0, < 2.0) 21 | rspec-support (~> 3.5.0) 22 | rspec-mocks (3.5.0) 23 | diff-lcs (>= 1.2.0, < 2.0) 24 | rspec-support (~> 3.5.0) 25 | rspec-support (3.5.0) 26 | slop (3.6.0) 27 | 28 | PLATFORMS 29 | ruby 30 | 31 | DEPENDENCIES 32 | dalli 33 | memcached 34 | pry 35 | rspec 36 | 37 | BUNDLED WITH 38 | 1.13.0 39 | -------------------------------------------------------------------------------- /spec/memcached_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | require "memcached" 3 | 4 | RSpec.describe "decachedmem" do 5 | let(:cache) { Memcached.new("localhost:11211") } 6 | let(:cache2) { Memcached.new("localhost:11211") } 7 | it "doesn't choke when setting a key" do 8 | expect { 9 | cache.set("foo", "37") 10 | }.not_to raise_error 11 | end 12 | 13 | it "doesn't choke when setting the key many times" do 14 | expect { 15 | cache.set("foo", "37") 16 | cache.set("foo", "37") 17 | }.not_to raise_error 18 | end 19 | 20 | it "can get a value it has set" do 21 | cache.set("foo", "37") 22 | expect(cache.get("foo")).to eq("37") 23 | end 24 | 25 | it "supports multiple connections" do 26 | cache.set("foo1", "37") 27 | cache2.set("foo2", "12") 28 | aggregate_failures do 29 | expect(cache.get("foo2")).to eq("12") 30 | expect(cache2.get("foo1")).to eq("37") 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Decachedmem 2 | 3 | An (bad, partial) implementation of memcached in Rust using 4 | [futures](https://github.com/alexcrichton/futures-rs) and 5 | [tokio](https://github.com/tokio-rs/tokio-core). 6 | 7 | At the moment it only supports the memcached `get` and `set` commands. To 8 | demonstrate that those work, you'll see a set of RSpec test files attached which 9 | manipulate the server. 10 | 11 | ## Making it go 12 | 13 | 1. You need both Ruby and Rust development tools on your local machine 14 | 2. Clone this repo 15 | 3. Run `cargo build --release && bundle install` 16 | 4. In one terminal run `cargo run` 17 | 5. In another terminal run `bundle exec rspec` 18 | 19 | When you run `cargo run` you're starting the memcached server bound to port 20 | `11211` (so you might get a conflict if you're running the real version of 21 | memcached in the background). Hypothetically, you can attach any basic memcached 22 | client to this server. As a note, the Ruby `dalli` gem doesn't work, because it 23 | actually asks for stats from the server when it connects, which this one isn't 24 | yet set up to do. 25 | -------------------------------------------------------------------------------- /src/memcached/types.rs: -------------------------------------------------------------------------------- 1 | pub type MemcachedKey = Vec; 2 | pub type MemcachedValue = Vec; 3 | pub type MemcachedFlags = u16; 4 | pub type MemcachedExpTime = i32; 5 | 6 | #[derive(Debug)] 7 | pub enum MemcachedCommandName { 8 | Get, 9 | Set, 10 | } 11 | 12 | #[derive(Debug)] 13 | pub struct MemcachedFrameHeader { 14 | pub command_name: MemcachedCommandName, 15 | pub key: MemcachedKey, 16 | pub flags: MemcachedFlags, 17 | pub exptime: MemcachedExpTime, 18 | pub byte_count: usize, 19 | } 20 | 21 | impl MemcachedFrameHeader { 22 | pub fn as_dataless_frame(self) -> MemcachedFrame { 23 | MemcachedFrame { 24 | header: self, 25 | bytes: vec![], 26 | } 27 | } 28 | } 29 | 30 | #[derive(Debug)] 31 | pub struct MemcachedFrame { 32 | pub header: MemcachedFrameHeader, 33 | pub bytes: MemcachedValue, 34 | } 35 | 36 | impl MemcachedFrame { 37 | pub fn new(header: MemcachedFrameHeader, bytes: MemcachedValue) -> MemcachedFrame { 38 | MemcachedFrame { 39 | header: header, 40 | bytes: bytes, 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/socket_stream.rs: -------------------------------------------------------------------------------- 1 | use std::io::{self, Read}; 2 | use futures::stream::Stream; 3 | use tokio_core::io::{ReadHalf, Io}; 4 | use futures::Poll; 5 | use futures::Async; 6 | 7 | pub struct SocketStream { 8 | read: ReadHalf, 9 | } 10 | 11 | impl SocketStream 12 | where T: Io 13 | { 14 | pub fn new(read: ReadHalf) -> SocketStream { 15 | SocketStream { read: read } 16 | } 17 | } 18 | 19 | 20 | impl Stream for SocketStream 21 | where T: Io 22 | { 23 | type Item = u8; 24 | type Error = io::Error; 25 | 26 | fn poll(&mut self) -> Poll, Self::Error> { 27 | let ready = self.read.poll_read(); 28 | match ready { 29 | Async::Ready(_) => { 30 | let mut buf = [0; 1]; 31 | let read_result = try_nb!(self.read.read(&mut buf)); 32 | match read_result { 33 | 0 => Ok(Async::Ready(None)), 34 | _ => Ok(Async::Ready(Some(buf[0]))), 35 | } 36 | } 37 | Async::NotReady => Ok(Async::NotReady), 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/unpack.rs: -------------------------------------------------------------------------------- 1 | use futures::stream::Stream; 2 | use futures::{Poll, Async}; 3 | 4 | pub struct Unpack>> { 5 | buffer: Vec, 6 | stream: T, 7 | } 8 | 9 | impl>> Unpack { 10 | pub fn new(stream: T) -> Self { 11 | Unpack { 12 | buffer: vec![], 13 | stream: stream, 14 | } 15 | } 16 | } 17 | 18 | impl>> Stream for Unpack { 19 | type Item = I; 20 | type Error = T::Error; 21 | 22 | fn poll(&mut self) -> Poll, Self::Error> { 23 | if self.buffer.len() == 0 { 24 | let poll = try!(self.stream.poll()); 25 | match poll { 26 | Async::Ready(Some(mut x)) => { 27 | self.buffer.append(&mut x); 28 | } 29 | Async::Ready(None) => return Ok(Async::Ready(None)), 30 | Async::NotReady => return Ok(Async::NotReady), 31 | } 32 | } 33 | 34 | let next_item = self.buffer.remove(0); 35 | Ok(Async::Ready(Some(next_item))) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/copy_stream_to_write.rs: -------------------------------------------------------------------------------- 1 | use futures::stream::Stream; 2 | use futures::{Poll, Async}; 3 | use std::io::{self, Write}; 4 | 5 | pub struct CopyStreamToWrite, W: Write> { 6 | stream: S, 7 | write: W, 8 | } 9 | 10 | impl, W: Write> CopyStreamToWrite { 11 | pub fn new(stream: S, write: W) -> CopyStreamToWrite { 12 | CopyStreamToWrite { 13 | stream: stream, 14 | write: write, 15 | } 16 | } 17 | } 18 | 19 | impl, W: Write> Stream for CopyStreamToWrite { 20 | type Item = (); 21 | type Error = io::Error; 22 | 23 | fn poll(&mut self) -> Poll, Self::Error> { 24 | let poll = try_nb!(self.stream.poll()); 25 | let result = match poll { 26 | Async::Ready(Some(x)) => { 27 | try_nb!(self.write.write(&[x])); 28 | Async::Ready(Some(())) 29 | } 30 | Async::Ready(None) => Async::Ready(None), 31 | Async::NotReady => Async::NotReady, 32 | }; 33 | Ok(result) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/crlf_delimited_stream.rs: -------------------------------------------------------------------------------- 1 | use std::{io, mem}; 2 | use futures::stream::Stream; 3 | use futures::{Poll, Async}; 4 | 5 | 6 | pub struct CarriageReturnLineFeedDelimitedStream> { 7 | stream: T, 8 | build: Vec, 9 | } 10 | 11 | impl> CarriageReturnLineFeedDelimitedStream { 12 | pub fn new(stream: T) -> CarriageReturnLineFeedDelimitedStream { 13 | CarriageReturnLineFeedDelimitedStream { 14 | stream: stream, 15 | build: vec![], 16 | } 17 | } 18 | } 19 | 20 | impl> Stream for CarriageReturnLineFeedDelimitedStream { 21 | type Item = Vec; 22 | type Error = io::Error; 23 | 24 | fn poll(&mut self) -> Poll, Self::Error> { 25 | while !self.build.ends_with(b"\r\n") { 26 | match try!(self.stream.poll()) { 27 | Async::Ready(Some(byte)) => self.build.push(byte), 28 | Async::NotReady | 29 | Async::Ready(None) => return Ok(Async::NotReady), 30 | } 31 | } 32 | let mut complete_frame = Vec::new(); 33 | mem::swap(&mut complete_frame, &mut self.build); 34 | Ok(Async::Ready(Some(complete_frame))) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/memcached/stream.rs: -------------------------------------------------------------------------------- 1 | use futures::stream::Stream; 2 | use futures::{Poll, Async}; 3 | use std::io; 4 | use std::error::Error; 5 | 6 | use memcached::parser::MemcachedParseStateMachine; 7 | use memcached::types::*; 8 | 9 | fn other_io_error(error: T) -> io::Error 10 | where T: Error + Sized 11 | { 12 | io::Error::new(io::ErrorKind::Other, error.description()) 13 | } 14 | 15 | 16 | pub struct MemcachedProtcolStream, Error = io::Error>> { 17 | stream: T, 18 | parser: MemcachedParseStateMachine, 19 | } 20 | 21 | impl, Error = io::Error>> MemcachedProtcolStream { 22 | pub fn new(stream: T) -> MemcachedProtcolStream { 23 | MemcachedProtcolStream { 24 | stream: stream, 25 | parser: MemcachedParseStateMachine::new(), 26 | } 27 | } 28 | } 29 | 30 | impl, Error = io::Error>> Stream for MemcachedProtcolStream { 31 | type Item = MemcachedFrame; 32 | type Error = io::Error; 33 | 34 | fn poll(&mut self) -> Poll, Self::Error> { 35 | loop { 36 | let poll = try!(self.stream.poll()); 37 | match poll { 38 | Async::Ready(Some(x)) => { 39 | let frame = try!(self.parser.add_bytes(&x).map_err(|e| other_io_error(e))); 40 | 41 | match frame { 42 | Some(x) => return Ok(Async::Ready(Some(x))), 43 | None => {} 44 | } 45 | } 46 | Async::NotReady | 47 | Async::Ready(None) => return Ok(Async::NotReady), 48 | }; 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![deny(warnings)] 2 | 3 | #[macro_use] 4 | extern crate tokio_core; 5 | extern crate futures; 6 | extern crate env_logger; 7 | 8 | use std::env; 9 | use std::net::SocketAddr; 10 | use std::iter::repeat; 11 | 12 | use futures::Future; 13 | use futures::stream::{self, Stream}; 14 | use tokio_core::io::Io; 15 | use tokio_core::reactor::Core; 16 | use tokio_core::net::TcpListener; 17 | 18 | mod socket_stream; 19 | mod crlf_delimited_stream; 20 | mod memcached; 21 | mod copy_stream_to_write; 22 | mod unpack; 23 | 24 | use socket_stream::SocketStream; 25 | use crlf_delimited_stream::CarriageReturnLineFeedDelimitedStream; 26 | use memcached::stream::MemcachedProtcolStream; 27 | use memcached::handler_stream::MemcachedHandlerStream; 28 | use memcached::store::Store; 29 | use std::rc::Rc; 30 | use std::cell::RefCell; 31 | use copy_stream_to_write::CopyStreamToWrite; 32 | use unpack::Unpack; 33 | 34 | fn main() { 35 | env_logger::init().unwrap(); 36 | let addr = env::args().nth(1).unwrap_or("127.0.0.1:11211".to_string()); 37 | let addr = addr.parse::().unwrap(); 38 | 39 | 40 | // Create the event loop that will drive this server 41 | let mut l = Core::new().unwrap(); 42 | let pin = l.handle(); 43 | let store = Store::new(); 44 | 45 | // Create a TCP listener which will listen for incoming connections 46 | let server = TcpListener::bind(&addr, &pin); 47 | 48 | match server { 49 | Ok(bound_socket) => { 50 | let store_stream = stream::iter(repeat(Ok(Rc::new(RefCell::new(store))))); 51 | let done = bound_socket.incoming() 52 | .map_err(|_| ()) 53 | .zip(store_stream) 54 | .for_each(|((socket, _addr), store)| { 55 | let pair = futures::lazy(|| Ok(socket.split())); 56 | let foo = pair.and_then(|(read_half, write_half)| { 57 | let stream = SocketStream::new(read_half); 58 | let crlf = CarriageReturnLineFeedDelimitedStream::new(stream); 59 | let memcached = MemcachedProtcolStream::new(crlf); 60 | let handler = MemcachedHandlerStream::new(store, memcached); 61 | let unpack = Unpack::new(handler); 62 | let output = CopyStreamToWrite::new(unpack, write_half); 63 | output.for_each(|_| Ok(())) 64 | }); 65 | 66 | pin.spawn(foo.map(|_| ()).map_err(|e| { 67 | println!("Done here!"); 68 | println!("{:?}", e); 69 | () 70 | })); 71 | Ok(()) 72 | }); 73 | 74 | l.run(done).unwrap(); 75 | } 76 | Err(e) => { 77 | println!("binding failed: {}", e); 78 | return; 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/memcached/handler_stream.rs: -------------------------------------------------------------------------------- 1 | use futures::stream::Stream; 2 | use futures::{Poll, Async}; 3 | use memcached::stream::MemcachedProtcolStream; 4 | use memcached::store::Store; 5 | use memcached::types::*; 6 | use std::io; 7 | use std::cell::RefCell; 8 | use std::rc::Rc; 9 | 10 | pub struct MemcachedHandlerStream, Error = io::Error>> { 11 | protocol_stream: MemcachedProtcolStream, 12 | store: Rc>, 13 | } 14 | 15 | impl, Error = io::Error>> MemcachedHandlerStream { 16 | pub fn new(store: Rc>, 17 | protocol_stream: MemcachedProtcolStream) 18 | -> MemcachedHandlerStream { 19 | MemcachedHandlerStream { 20 | store: store, 21 | protocol_stream: protocol_stream, 22 | } 23 | } 24 | } 25 | 26 | impl, Error = io::Error>> Stream for MemcachedHandlerStream { 27 | type Item = Vec; 28 | type Error = io::Error; 29 | 30 | fn poll(&mut self) -> Poll, Self::Error> { 31 | let poll = try!(self.protocol_stream.poll()); 32 | let result = match poll { 33 | Async::Ready(Some(message)) => { 34 | let mut store = self.store.borrow_mut(); 35 | Async::Ready(Some(handle_message(&mut store, message))) 36 | } 37 | Async::Ready(None) => Async::Ready(None), 38 | Async::NotReady => Async::NotReady, 39 | }; 40 | 41 | println!("handler {:?}", result); 42 | Ok(result) 43 | } 44 | } 45 | 46 | fn handle_message(store: &mut Store, message: MemcachedFrame) -> Vec { 47 | let key = message.header.key; 48 | let command = message.header.command_name; 49 | let bytes = message.bytes; 50 | let response = match command { 51 | MemcachedCommandName::Get => handle_get(&key, store), 52 | MemcachedCommandName::Set => handle_set(&key, bytes, store), 53 | }; 54 | 55 | response 56 | } 57 | 58 | 59 | fn handle_get(key: &MemcachedKey, store: &mut Store) -> Vec { 60 | let value = store.get(key); 61 | match value { 62 | Some(x) => found_response(key, x), 63 | None => not_found_response(), 64 | } 65 | } 66 | 67 | fn handle_set(key: &MemcachedKey, bytes: MemcachedValue, store: &mut Store) -> Vec { 68 | store.set(key, bytes); 69 | 70 | b"STORED\r\n".to_vec() 71 | } 72 | 73 | fn found_response(key: &MemcachedKey, value: &MemcachedValue) -> Vec { 74 | let mut build = vec![]; 75 | // header 76 | build.extend_from_slice(b"VALUE "); 77 | 78 | // key 79 | build.extend_from_slice(&key); 80 | build.extend_from_slice(b" "); 81 | 82 | // flags 83 | build.extend_from_slice(b"0 "); 84 | 85 | // value length 86 | build.extend_from_slice(value.len().to_string().as_bytes()); 87 | build.extend_from_slice(b" "); 88 | 89 | // end of header 90 | build.extend_from_slice(b"\r\n"); 91 | 92 | // value 93 | build.extend_from_slice(&value); 94 | build.extend_from_slice(b"\r\n"); 95 | 96 | // end frame 97 | build.extend_from_slice(b"END\r\n"); 98 | build 99 | } 100 | 101 | fn not_found_response() -> Vec { 102 | let mut build = vec![]; 103 | build.extend_from_slice(b"END\r\n"); 104 | build 105 | } 106 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # This file was generated by the `rspec --init` command. Conventionally, all 2 | # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. 3 | # The generated `.rspec` file contains `--require spec_helper` which will cause 4 | # this file to always be loaded, without a need to explicitly require it in any 5 | # files. 6 | # 7 | # Given that it is always loaded, you are encouraged to keep this file as 8 | # light-weight as possible. Requiring heavyweight dependencies from this file 9 | # will add to the boot time of your test suite on EVERY test run, even for an 10 | # individual file that may not need all of that loaded. Instead, consider making 11 | # a separate helper file that requires the additional dependencies and performs 12 | # the additional setup, and require it from the spec files that actually need 13 | # it. 14 | # 15 | # The `.rspec` file also contains a few flags that are not defaults but that 16 | # users commonly want. 17 | # 18 | # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration 19 | RSpec.configure do |config| 20 | # rspec-expectations config goes here. You can use an alternate 21 | # assertion/expectation library such as wrong or the stdlib/minitest 22 | # assertions if you prefer. 23 | config.expect_with :rspec do |expectations| 24 | # This option will default to `true` in RSpec 4. It makes the `description` 25 | # and `failure_message` of custom matchers include text for helper methods 26 | # defined using `chain`, e.g.: 27 | # be_bigger_than(2).and_smaller_than(4).description 28 | # # => "be bigger than 2 and smaller than 4" 29 | # ...rather than: 30 | # # => "be bigger than 2" 31 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 32 | end 33 | 34 | # rspec-mocks config goes here. You can use an alternate test double 35 | # library (such as bogus or mocha) by changing the `mock_with` option here. 36 | config.mock_with :rspec do |mocks| 37 | # Prevents you from mocking or stubbing a method that does not exist on 38 | # a real object. This is generally recommended, and will default to 39 | # `true` in RSpec 4. 40 | mocks.verify_partial_doubles = true 41 | end 42 | 43 | # This option will default to `:apply_to_host_groups` in RSpec 4 (and will 44 | # have no way to turn it off -- the option exists only for backwards 45 | # compatibility in RSpec 3). It causes shared context metadata to be 46 | # inherited by the metadata hash of host groups and examples, rather than 47 | # triggering implicit auto-inclusion in groups with matching metadata. 48 | config.shared_context_metadata_behavior = :apply_to_host_groups 49 | 50 | # The settings below are suggested to provide a good initial experience 51 | # with RSpec, but feel free to customize to your heart's content. 52 | =begin 53 | # This allows you to limit a spec run to individual examples or groups 54 | # you care about by tagging them with `:focus` metadata. When nothing 55 | # is tagged with `:focus`, all examples get run. RSpec also provides 56 | # aliases for `it`, `describe`, and `context` that include `:focus` 57 | # metadata: `fit`, `fdescribe` and `fcontext`, respectively. 58 | config.filter_run_when_matching :focus 59 | 60 | # Allows RSpec to persist some state between runs in order to support 61 | # the `--only-failures` and `--next-failure` CLI options. We recommend 62 | # you configure your source control system to ignore this file. 63 | config.example_status_persistence_file_path = "spec/examples.txt" 64 | 65 | # Limits the available syntax to the non-monkey patched syntax that is 66 | # recommended. For more details, see: 67 | # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/ 68 | # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ 69 | # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode 70 | config.disable_monkey_patching! 71 | 72 | # This setting enables warnings. It's recommended, but in some cases may 73 | # be too noisy due to issues in dependencies. 74 | config.warnings = true 75 | 76 | # Many RSpec users commonly either run the entire suite or an individual 77 | # file, and it's useful to allow more verbose output when running an 78 | # individual spec file. 79 | if config.files_to_run.one? 80 | # Use the documentation formatter for detailed output, 81 | # unless a formatter has already been configured 82 | # (e.g. via a command-line flag). 83 | config.default_formatter = 'doc' 84 | end 85 | 86 | # Print the 10 slowest examples and example groups at the 87 | # end of the spec run, to help surface which specs are running 88 | # particularly slow. 89 | config.profile_examples = 10 90 | 91 | # Run specs in random order to surface order dependencies. If you find an 92 | # order dependency and want to debug it, you can fix the order by providing 93 | # the seed, which is printed after each run. 94 | # --seed 1234 95 | config.order = :random 96 | 97 | # Seed global randomization in this process using the `--seed` CLI option. 98 | # Setting this allows you to use `--seed` to deterministically reproduce 99 | # test failures related to randomization by passing the same `--seed` value 100 | # as the one that triggered the failure. 101 | Kernel.srand config.seed 102 | =end 103 | end 104 | -------------------------------------------------------------------------------- /src/memcached/parser.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | use std::fmt::{self, Display, Formatter}; 3 | use std::str::{self, FromStr}; 4 | use std::mem; 5 | 6 | use memcached::types::*; 7 | 8 | #[derive(Debug)] 9 | pub struct MemcachedParseError { 10 | description: String, 11 | } 12 | 13 | impl MemcachedParseError { 14 | fn new(description: String) -> MemcachedParseError { 15 | MemcachedParseError { description: description } 16 | } 17 | 18 | fn from_err(e: T) -> MemcachedParseError 19 | where T: Error + Sized 20 | { 21 | Self::new(e.description().into()) 22 | } 23 | } 24 | 25 | impl Display for MemcachedParseError { 26 | fn fmt(&self, f: &mut Formatter) -> fmt::Result { 27 | write!(f, "MemcachedError({})", self.description) 28 | } 29 | } 30 | 31 | impl Error for MemcachedParseError { 32 | fn description(&self) -> &str { 33 | &self.description 34 | } 35 | } 36 | 37 | fn parse_header_part(part: &[u8]) -> Result 38 | where T: FromStr, 39 | T::Err: Error + Sized 40 | { 41 | let string = try!(String::from_utf8(part.to_vec()) 42 | .map_err(|e| MemcachedParseError::from_err(e))); 43 | let parsed = try!(string.parse().map_err(|e| MemcachedParseError::from_err(e))); 44 | Ok(parsed) 45 | } 46 | 47 | fn parse_memcached_header(head: &Vec) -> Result { 48 | let parts: Vec<&[u8]> = 49 | head[0..head.len() - 2].split(|&byte| byte == b' ').filter(|&part| part != b"").collect(); 50 | match parts.len() { 51 | 5 => parse_write(parts), 52 | 2 => parse_read(parts), 53 | _ => { 54 | Err(MemcachedParseError { 55 | description: "wrong number of space separated parts in header".into(), 56 | }) 57 | } 58 | } 59 | } 60 | 61 | fn parse_write(parts: Vec<&[u8]>) -> Result { 62 | let command = match parts[0] { 63 | b"set" => MemcachedCommandName::Set, 64 | _ => { 65 | return Err(MemcachedParseError::new(format!("invalid memcached write command name: \ 66 | {:?}", 67 | parts[0]) 68 | .into())) 69 | } 70 | }; 71 | let key = parts[1].to_vec(); 72 | let flags = try!(parse_header_part(parts[2])); 73 | let exptime = try!(parse_header_part(parts[3])); 74 | let byte_count = try!(parse_header_part(&parts[4])); 75 | 76 | Ok(MemcachedFrameHeader { 77 | command_name: command, 78 | key: key, 79 | flags: flags, 80 | exptime: exptime, 81 | byte_count: byte_count, 82 | }) 83 | } 84 | 85 | fn parse_read(parts: Vec<&[u8]>) -> Result { 86 | let command = match parts[0] { 87 | b"get" => MemcachedCommandName::Get, 88 | _ => { 89 | return Err(MemcachedParseError::new(format!("invalid memcached read command name: \ 90 | {:?}", 91 | parts[0]) 92 | .into())) 93 | } 94 | }; 95 | let key = parts[1].to_vec(); 96 | 97 | Ok(MemcachedFrameHeader { 98 | command_name: command, 99 | key: key, 100 | flags: 0, 101 | exptime: 0, 102 | byte_count: 0, 103 | }) 104 | } 105 | 106 | enum MemcachedParseState { 107 | NewHeader, 108 | AccumulatingSet, 109 | ErrorState, 110 | } 111 | 112 | pub struct MemcachedParseStateMachine { 113 | state: MemcachedParseState, 114 | buffer: Vec, 115 | partial_header: Option, 116 | } 117 | 118 | impl MemcachedParseStateMachine { 119 | pub fn new() -> MemcachedParseStateMachine { 120 | MemcachedParseStateMachine { 121 | state: MemcachedParseState::NewHeader, 122 | buffer: vec![], 123 | partial_header: None, 124 | } 125 | } 126 | 127 | // does not deal with splitting by CRLF, only pass in byte sequences that are already CRLF 128 | // delimited 129 | pub fn add_bytes(&mut self, 130 | bytes: &Vec) 131 | -> Result, MemcachedParseError> { 132 | match self.state { 133 | MemcachedParseState::ErrorState => { 134 | Err(MemcachedParseError::new("In error state from previous call".into())) 135 | } 136 | MemcachedParseState::NewHeader => self.parse_header(bytes), 137 | MemcachedParseState::AccumulatingSet => self.accumulate(bytes), 138 | } 139 | } 140 | 141 | fn parse_header(&mut self, 142 | bytes: &Vec) 143 | -> Result, MemcachedParseError> { 144 | let header = parse_memcached_header(bytes); 145 | match header { 146 | Ok(header) => Ok(self.transition_for(header)), 147 | Err(e) => { 148 | self.state = MemcachedParseState::ErrorState; 149 | Err(MemcachedParseError::from_err(e)) 150 | } 151 | } 152 | } 153 | 154 | fn accumulate(&mut self, 155 | bytes: &Vec) 156 | -> Result, MemcachedParseError> { 157 | self.buffer.extend_from_slice(bytes.as_slice()); 158 | 159 | if self.buffer.len() >= self.partial_header.as_mut().unwrap().byte_count { 160 | self.buffer.truncate(self.partial_header.as_mut().unwrap().byte_count); 161 | 162 | let frame = self.produce_frame_and_reset(); 163 | 164 | Ok(Some(frame)) 165 | } else { 166 | Ok(None) 167 | } 168 | } 169 | 170 | fn produce_frame_and_reset(&mut self) -> MemcachedFrame { 171 | let mut new_buffer = vec![]; 172 | mem::swap(&mut new_buffer, &mut self.buffer); 173 | 174 | let mut new_header = None; 175 | mem::swap(&mut new_header, &mut self.partial_header); 176 | 177 | self.state = MemcachedParseState::NewHeader; 178 | 179 | MemcachedFrame::new(new_header.unwrap(), new_buffer) 180 | } 181 | 182 | fn transition_for(&mut self, header: MemcachedFrameHeader) -> Option { 183 | match header.command_name { 184 | MemcachedCommandName::Get => Some(header.as_dataless_frame()), 185 | MemcachedCommandName::Set => { 186 | self.state = MemcachedParseState::AccumulatingSet; 187 | self.partial_header = Some(header); 188 | None 189 | } 190 | } 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | [root] 2 | name = "decachedmem" 3 | version = "0.1.0" 4 | dependencies = [ 5 | "env_logger 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", 6 | "futures 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", 7 | "tokio-core 0.1.0 (git+https://github.com/tokio-rs/tokio-core/)", 8 | ] 9 | 10 | [[package]] 11 | name = "aho-corasick" 12 | version = "0.5.3" 13 | source = "registry+https://github.com/rust-lang/crates.io-index" 14 | dependencies = [ 15 | "memchr 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", 16 | ] 17 | 18 | [[package]] 19 | name = "bitflags" 20 | version = "0.4.0" 21 | source = "registry+https://github.com/rust-lang/crates.io-index" 22 | 23 | [[package]] 24 | name = "cfg-if" 25 | version = "0.1.0" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | 28 | [[package]] 29 | name = "env_logger" 30 | version = "0.3.5" 31 | source = "registry+https://github.com/rust-lang/crates.io-index" 32 | dependencies = [ 33 | "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", 34 | "regex 0.1.77 (registry+https://github.com/rust-lang/crates.io-index)", 35 | ] 36 | 37 | [[package]] 38 | name = "futures" 39 | version = "0.1.1" 40 | source = "registry+https://github.com/rust-lang/crates.io-index" 41 | dependencies = [ 42 | "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", 43 | ] 44 | 45 | [[package]] 46 | name = "kernel32-sys" 47 | version = "0.2.2" 48 | source = "registry+https://github.com/rust-lang/crates.io-index" 49 | dependencies = [ 50 | "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", 51 | "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", 52 | ] 53 | 54 | [[package]] 55 | name = "lazycell" 56 | version = "0.4.0" 57 | source = "registry+https://github.com/rust-lang/crates.io-index" 58 | 59 | [[package]] 60 | name = "libc" 61 | version = "0.2.16" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | 64 | [[package]] 65 | name = "log" 66 | version = "0.3.6" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | 69 | [[package]] 70 | name = "memchr" 71 | version = "0.1.11" 72 | source = "registry+https://github.com/rust-lang/crates.io-index" 73 | dependencies = [ 74 | "libc 0.2.16 (registry+https://github.com/rust-lang/crates.io-index)", 75 | ] 76 | 77 | [[package]] 78 | name = "mio" 79 | version = "0.6.0" 80 | source = "registry+https://github.com/rust-lang/crates.io-index" 81 | dependencies = [ 82 | "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", 83 | "lazycell 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", 84 | "libc 0.2.16 (registry+https://github.com/rust-lang/crates.io-index)", 85 | "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", 86 | "miow 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", 87 | "net2 0.2.26 (registry+https://github.com/rust-lang/crates.io-index)", 88 | "nix 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", 89 | "slab 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", 90 | "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", 91 | ] 92 | 93 | [[package]] 94 | name = "miow" 95 | version = "0.1.3" 96 | source = "registry+https://github.com/rust-lang/crates.io-index" 97 | dependencies = [ 98 | "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", 99 | "net2 0.2.26 (registry+https://github.com/rust-lang/crates.io-index)", 100 | "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", 101 | "ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", 102 | ] 103 | 104 | [[package]] 105 | name = "net2" 106 | version = "0.2.26" 107 | source = "registry+https://github.com/rust-lang/crates.io-index" 108 | dependencies = [ 109 | "cfg-if 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", 110 | "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", 111 | "libc 0.2.16 (registry+https://github.com/rust-lang/crates.io-index)", 112 | "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", 113 | "ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", 114 | ] 115 | 116 | [[package]] 117 | name = "nix" 118 | version = "0.6.0" 119 | source = "registry+https://github.com/rust-lang/crates.io-index" 120 | dependencies = [ 121 | "bitflags 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", 122 | "cfg-if 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", 123 | "libc 0.2.16 (registry+https://github.com/rust-lang/crates.io-index)", 124 | "rustc_version 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", 125 | "semver 0.1.20 (registry+https://github.com/rust-lang/crates.io-index)", 126 | "void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", 127 | ] 128 | 129 | [[package]] 130 | name = "regex" 131 | version = "0.1.77" 132 | source = "registry+https://github.com/rust-lang/crates.io-index" 133 | dependencies = [ 134 | "aho-corasick 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)", 135 | "memchr 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", 136 | "regex-syntax 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", 137 | "thread_local 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", 138 | "utf8-ranges 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", 139 | ] 140 | 141 | [[package]] 142 | name = "regex-syntax" 143 | version = "0.3.5" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | 146 | [[package]] 147 | name = "rustc_version" 148 | version = "0.1.7" 149 | source = "registry+https://github.com/rust-lang/crates.io-index" 150 | dependencies = [ 151 | "semver 0.1.20 (registry+https://github.com/rust-lang/crates.io-index)", 152 | ] 153 | 154 | [[package]] 155 | name = "scoped-tls" 156 | version = "0.1.0" 157 | source = "registry+https://github.com/rust-lang/crates.io-index" 158 | 159 | [[package]] 160 | name = "semver" 161 | version = "0.1.20" 162 | source = "registry+https://github.com/rust-lang/crates.io-index" 163 | 164 | [[package]] 165 | name = "slab" 166 | version = "0.3.0" 167 | source = "registry+https://github.com/rust-lang/crates.io-index" 168 | 169 | [[package]] 170 | name = "thread-id" 171 | version = "2.0.0" 172 | source = "registry+https://github.com/rust-lang/crates.io-index" 173 | dependencies = [ 174 | "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", 175 | "libc 0.2.16 (registry+https://github.com/rust-lang/crates.io-index)", 176 | ] 177 | 178 | [[package]] 179 | name = "thread_local" 180 | version = "0.2.6" 181 | source = "registry+https://github.com/rust-lang/crates.io-index" 182 | dependencies = [ 183 | "thread-id 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)", 184 | ] 185 | 186 | [[package]] 187 | name = "tokio-core" 188 | version = "0.1.0" 189 | source = "git+https://github.com/tokio-rs/tokio-core/#418a973520bbd485af60f13aef28f1c20d51b888" 190 | dependencies = [ 191 | "futures 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", 192 | "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", 193 | "mio 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", 194 | "scoped-tls 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", 195 | "slab 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", 196 | ] 197 | 198 | [[package]] 199 | name = "utf8-ranges" 200 | version = "0.1.3" 201 | source = "registry+https://github.com/rust-lang/crates.io-index" 202 | 203 | [[package]] 204 | name = "void" 205 | version = "1.0.2" 206 | source = "registry+https://github.com/rust-lang/crates.io-index" 207 | 208 | [[package]] 209 | name = "winapi" 210 | version = "0.2.8" 211 | source = "registry+https://github.com/rust-lang/crates.io-index" 212 | 213 | [[package]] 214 | name = "winapi-build" 215 | version = "0.1.1" 216 | source = "registry+https://github.com/rust-lang/crates.io-index" 217 | 218 | [[package]] 219 | name = "ws2_32-sys" 220 | version = "0.2.1" 221 | source = "registry+https://github.com/rust-lang/crates.io-index" 222 | dependencies = [ 223 | "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", 224 | "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", 225 | ] 226 | 227 | [metadata] 228 | "checksum aho-corasick 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ca972c2ea5f742bfce5687b9aef75506a764f61d37f8f649047846a9686ddb66" 229 | "checksum bitflags 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8dead7461c1127cf637931a1e50934eb6eee8bff2f74433ac7909e9afcee04a3" 230 | "checksum cfg-if 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "de1e760d7b6535af4241fca8bd8adf68e2e7edacc6b29f5d399050c5e48cf88c" 231 | "checksum env_logger 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "15abd780e45b3ea4f76b4e9a26ff4843258dd8a3eed2775a0e7368c2e7936c2f" 232 | "checksum futures 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "62af3ebbb8916ecf7ebcc4c130aacc33cc7f48d8b6e74fc9ed010bfa4f359794" 233 | "checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" 234 | "checksum lazycell 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ce12306c4739d86ee97c23139f3a34ddf0387bbf181bc7929d287025a8c3ef6b" 235 | "checksum libc 0.2.16 (registry+https://github.com/rust-lang/crates.io-index)" = "408014cace30ee0f767b1c4517980646a573ec61a57957aeeabcac8ac0a02e8d" 236 | "checksum log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "ab83497bf8bf4ed2a74259c1c802351fcd67a65baa86394b6ba73c36f4838054" 237 | "checksum memchr 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)" = "d8b629fb514376c675b98c1421e80b151d3817ac42d7c667717d282761418d20" 238 | "checksum mio 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2dadd39d4b47343e10513ac2a731c979517a4761224ecb6bbd243602300c9537" 239 | "checksum miow 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "d5bfc6782530ac8ace97af10a540054a37126b63b0702ddaaa243b73b5745b9a" 240 | "checksum net2 0.2.26 (registry+https://github.com/rust-lang/crates.io-index)" = "5edf9cb6be97212423aed9413dd4729d62b370b5e1c571750e882cebbbc1e3e2" 241 | "checksum nix 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7a7bb1da2be7da3cbffda73fc681d509ffd9e665af478d2bee1907cee0bc64b2" 242 | "checksum regex 0.1.77 (registry+https://github.com/rust-lang/crates.io-index)" = "64b03446c466d35b42f2a8b203c8e03ed8b91c0f17b56e1f84f7210a257aa665" 243 | "checksum regex-syntax 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "279401017ae31cf4e15344aa3f085d0e2e5c1e70067289ef906906fdbe92c8fd" 244 | "checksum rustc_version 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "c5f5376ea5e30ce23c03eb77cbe4962b988deead10910c372b226388b594c084" 245 | "checksum scoped-tls 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f417c22df063e9450888a7561788e9bd46d3bb3c1466435b4eccb903807f147d" 246 | "checksum semver 0.1.20 (registry+https://github.com/rust-lang/crates.io-index)" = "d4f410fedcf71af0345d7607d246e7ad15faaadd49d240ee3b24e5dc21a820ac" 247 | "checksum slab 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "17b4fcaed89ab08ef143da37bc52adbcc04d4a69014f4c1208d6b51f0c47bc23" 248 | "checksum thread-id 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a9539db560102d1cef46b8b78ce737ff0bb64e7e18d35b2a5688f7d097d0ff03" 249 | "checksum thread_local 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "55dd963dbaeadc08aa7266bf7f91c3154a7805e32bb94b820b769d2ef3b4744d" 250 | "checksum tokio-core 0.1.0 (git+https://github.com/tokio-rs/tokio-core/)" = "" 251 | "checksum utf8-ranges 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a1ca13c08c41c9c3e04224ed9ff80461d97e121589ff27c753a16cb10830ae0f" 252 | "checksum void 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" 253 | "checksum winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" 254 | "checksum winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" 255 | "checksum ws2_32-sys 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e" 256 | --------------------------------------------------------------------------------