diff --git a/ext/src/ruby_api/memory.rs b/ext/src/ruby_api/memory.rs index 14dd4103..f7417e2a 100644 --- a/ext/src/ruby_api/memory.rs +++ b/ext/src/ruby_api/memory.rs @@ -352,6 +352,57 @@ impl<'a> Memory<'a> { self.write_fixed(offset, value.to_le_bytes()) } + /// @yard + /// Read a NUL-terminated C string starting at +offset+ as an ASCII-8BIT + /// (binary) +String+. + /// + /// @def read_cstring(offset) + /// @param offset [Integer] + /// @return [String] + pub fn read_cstring(ruby: &Ruby, rb_self: Obj, offset: usize) -> Result { + let context = rb_self.store.context()?; + let data = rb_self.get_wasmtime_memory().data(context); + + let bytes: &[u8] = match data.get(offset..) { + Some(slice) => { + let end = slice.iter().position(|&b| b == 0).unwrap_or(slice.len()); + &slice[..end] + } + None => &[], + }; + + Ok(ruby.str_from_slice(bytes)) + } + + /// @yard + /// Write +value+'s bytes followed by a NUL terminator at +offset+. + /// + /// @def write_cstring(offset, value) + /// @param offset [Integer] + /// @param value [String] + /// @return [void] + pub fn write_cstring(&self, offset: usize, value: RString) -> Result<(), Error> { + let slice = unsafe { value.as_slice() }; + if slice.contains(&0) { + return Err(Error::new( + Ruby::get_with(value).exception_arg_error(), + "string contains null byte", + )); + } + let len = slice.len(); + let mut context = self.store.context_mut()?; + let dst = self + .get_wasmtime_memory() + .data_mut(&mut context) + .get_mut(offset..) + .and_then(|s| s.get_mut(..len + 1)) + .ok_or_else(|| error!("out of bounds memory access"))?; + + dst[..len].copy_from_slice(slice); + dst[len] = 0; + Ok(()) + } + /// @yard /// Grows a memory by +delta+ pages. /// Raises if the memory grows beyond its limit. @@ -443,6 +494,8 @@ pub fn init(ruby: &Ruby) -> Result<(), Error> { class.define_method("size", method!(Memory::size, 0))?; class.define_method("data_size", method!(Memory::data_size, 0))?; class.define_method("read_unsafe_slice", method!(Memory::read_unsafe_slice, 2))?; + class.define_method("read_cstring", method!(Memory::read_cstring, 1))?; + class.define_method("write_cstring", method!(Memory::write_cstring, 2))?; unsafe_slice::init(ruby)?; diff --git a/spec/unit/memory_spec.rb b/spec/unit/memory_spec.rb index bffde274..9e1aba40 100644 --- a/spec/unit/memory_spec.rb +++ b/spec/unit/memory_spec.rb @@ -117,6 +117,49 @@ module Wasmtime end end + describe "#read_cstring" do + it "reads NUL-terminated bytes as a binary string" do + mem = Memory.new(store, min_size: 1) + mem.write(0, "héllo\x00trailing garbage") + str = mem.read_cstring(0) + expect(str).to eq("héllo".b) + expect(str.encoding).to eq(Encoding::BINARY) + end + + it "returns an empty string at a leading NUL byte" do + mem = Memory.new(store, min_size: 1) + mem.write(0, "\x00") + expect(mem.read_cstring(0)).to eq("") + end + + it "returns an empty string when offset is at/past the end of memory" do + mem = Memory.new(store, min_size: 1) + expect(mem.read_cstring(mem.data_size)).to eq("") + expect(mem.read_cstring(mem.data_size + 100)).to eq("") + end + end + + describe "#write_cstring" do + it "writes the bytes plus a NUL terminator and round-trips via read_cstring" do + mem = Memory.new(store, min_size: 1) + expect(mem.write_cstring(3, "héllo")).to be_nil + expect(mem.read_cstring(3)).to eq("héllo".b) + expect(mem.read(3, "héllo".bytesize + 1)).to eq("héllo\x00".b) + end + + it "raises when writing past the end of the buffer" do + mem = Memory.new(store, min_size: 1) + expect { mem.write_cstring(mem.data_size, "x") } + .to raise_error(Wasmtime::Error, "out of bounds memory access") + end + + it "raises when the value contains a NUL byte" do + mem = Memory.new(store, min_size: 1) + expect { mem.write_cstring(0, "foo\x00bar") } + .to raise_error(ArgumentError, "string contains null byte") + end + end + describe "#read_i64, #write_i64" do it "round-trips a signed 64-bit integer" do mem = Memory.new(store, min_size: 1)