Repository: bearcove/loona (language: rust)

Issue:
Introduce Method enum
Most methods are standard, we can use an enum to make the API easier to use.


Discussion / hints:

Fix patch:
diff --git a/src/buffet/io_chunk.rs b/src/buffet/io_chunk.rs
deleted file mode 100644
index ddf22556..00000000
--- a/src/buffet/io_chunk.rs
+++ /dev/null
@@ -1,126 +0,0 @@
-//! Types for performing vectored I/O.
-
-use tokio_uring::buf::IoBuf;
-
-use crate::Roll;
-
-pub enum IoChunk {
-    Static(&'static [u8]),
-    Vec(Vec<u8>),
-    Roll(Roll),
-}
-
-impl From<&'static [u8]> for IoChunk {
-    fn from(slice: &'static [u8]) -> Self {
-        IoChunk::Static(slice)
-    }
-}
-
-impl From<&'static str> for IoChunk {
-    fn from(slice: &'static str) -> Self {
-        IoChunk::Static(slice.as_bytes())
-    }
-}
-
-impl From<Vec<u8>> for IoChunk {
-    fn from(vec: Vec<u8>) -> Self {
-        IoChunk::Vec(vec)
-    }
-}
-
-impl From<Roll> for IoChunk {
-    fn from(roll: Roll) -> Self {
-        IoChunk::Roll(roll)
-    }
-}
-
-impl AsRef<[u8]> for IoChunk {
-    fn as_ref(&self) -> &[u8] {
-        match self {
-            IoChunk::Static(slice) => slice,
-            IoChunk::Vec(vec) => vec.as_ref(),
-            IoChunk::Roll(roll) => roll.as_ref(),
-        }
-    }
-}
-
-impl IoChunk {
-    #[inline(always)]
-    pub fn as_io_buf(&self) -> &dyn IoBuf {
-        match self {
-            IoChunk::Static(slice) => slice,
-            IoChunk::Vec(vec) => vec,
-            IoChunk::Roll(roll) => roll,
-        }
-    }
-}
-
-unsafe impl IoBuf for IoChunk {
-    #[inline(always)]
-    fn stable_ptr(&self) -> *const u8 {
-        IoBuf::stable_ptr(self.as_io_buf())
-    }
-
-    fn bytes_init(&self) -> usize {
-        IoBuf::bytes_init(self.as_io_buf())
-    }
-
-    fn bytes_total(&self) -> usize {
-        IoBuf::bytes_total(self.as_io_buf())
-    }
-}
-
-impl IoChunk {
-    #[inline(always)]
-    pub fn len(&self) -> usize {
-        match self {
-            IoChunk::Static(slice) => slice.len(),
-            IoChunk::Vec(vec) => vec.len(),
-            IoChunk::Roll(roll) => roll.len(),
-        }
-    }
-
-    #[inline(always)]
-    pub fn is_empty(&self) -> bool {
-        self.len() == 0
-    }
-}
-
-#[derive(Default)]
-pub struct IoChunkList {
-    chunks: Vec<IoChunk>,
-}
-
-impl IoChunkList {
-    /// Add a single chunk to the list
-    pub fn push(&mut self, chunk: impl Into<IoChunk>) {
-        self.chunks.push(chunk.into());
-    }
-
-    /// Returns total length
-    pub fn len(&self) -> usize {
-        self.chunks.iter().map(|c| c.len()).sum()
-    }
-
-    pub fn num_chunks(&self) -> usize {
-        self.chunks.len()
-    }
-
-    pub fn is_empty(&self) -> bool {
-        self.chunks.is_empty() || self.len() == 0
-    }
-
-    pub fn clear(&mut self) {
-        self.chunks.clear();
-    }
-
-    pub fn into_vec(self) -> Vec<IoChunk> {
-        self.chunks
-    }
-}
-
-impl From<Vec<IoChunk>> for IoChunkList {
-    fn from(chunks: Vec<IoChunk>) -> Self {
-        Self { chunks }
-    }
-}
diff --git a/src/buffet/mod.rs b/src/buffet/mod.rs
index fb140ea2..e4c416f7 100644
--- a/src/buffet/mod.rs
+++ b/src/buffet/mod.rs
@@ -1,8 +1,8 @@
 mod roll;
 pub use roll::*;
 
-mod io_chunk;
-pub use io_chunk::*;
+mod piece;
+pub use piece::*;
 
 use std::{
     cell::{RefCell, RefMut},
diff --git a/src/buffet/piece.rs b/src/buffet/piece.rs
new file mode 100644
index 00000000..ad3abfff
--- /dev/null
+++ b/src/buffet/piece.rs
@@ -0,0 +1,223 @@
+//! Types for performing vectored I/O.
+
+use std::{
+    fmt::{Display, Formatter},
+    ops::Deref,
+    str::Utf8Error,
+};
+
+use tokio_uring::buf::IoBuf;
+
+use crate::{Roll, RollStr};
+
+/// A piece of data (arbitrary bytes) with a stable address, suitable for
+/// passing to the kernel (io_uring writes).
+#[derive(Clone)]
+pub enum Piece {
+    Static(&'static [u8]),
+    Vec(Vec<u8>),
+    Roll(Roll),
+}
+
+impl From<&'static [u8]> for Piece {
+    fn from(slice: &'static [u8]) -> Self {
+        Piece::Static(slice)
+    }
+}
+
+impl From<&'static str> for Piece {
+    fn from(slice: &'static str) -> Self {
+        Piece::Static(slice.as_bytes())
+    }
+}
+
+impl From<Vec<u8>> for Piece {
+    fn from(vec: Vec<u8>) -> Self {
+        Piece::Vec(vec)
+    }
+}
+
+impl From<Roll> for Piece {
+    fn from(roll: Roll) -> Self {
+        Piece::Roll(roll)
+    }
+}
+
+impl From<PieceStr> for Piece {
+    fn from(s: PieceStr) -> Self {
+        s.piece
+    }
+}
+
+impl Deref for Piece {
+    type Target = [u8];
+
+    fn deref(&self) -> &Self::Target {
+        self.as_ref()
+    }
+}
+
+impl AsRef<[u8]> for Piece {
+    fn as_ref(&self) -> &[u8] {
+        match self {
+            Piece::Static(slice) => slice,
+            Piece::Vec(vec) => vec.as_ref(),
+            Piece::Roll(roll) => roll.as_ref(),
+        }
+    }
+}
+
+impl Piece {
+    #[inline(always)]
+    pub fn as_io_buf(&self) -> &dyn IoBuf {
+        match self {
+            Piece::Static(slice) => slice,
+            Piece::Vec(vec) => vec,
+            Piece::Roll(roll) => roll,
+        }
+    }
+
+    /// Decode as utf-8
+    pub fn to_string(self) -> Result<PieceStr, Utf8Error> {
+        _ = std::str::from_utf8(&self)?;
+        Ok(PieceStr { piece: self })
+    }
+
+    /// Convert to [PieceStr].
+    ///
+    /// # Safety
+    /// UB if not utf-8. Typically only used in parsers.
+    pub unsafe fn to_string_unchecked(self) -> PieceStr {
+        PieceStr { piece: self }
+    }
+}
+
+unsafe impl IoBuf for Piece {
+    #[inline(always)]
+    fn stable_ptr(&self) -> *const u8 {
+        IoBuf::stable_ptr(self.as_io_buf())
+    }
+
+    fn bytes_init(&self) -> usize {
+        IoBuf::bytes_init(self.as_io_buf())
+    }
+
+    fn bytes_total(&self) -> usize {
+        IoBuf::bytes_total(self.as_io_buf())
+    }
+}
+
+impl Piece {
+    #[inline(always)]
+    pub fn len(&self) -> usize {
+        self.as_ref().len()
+    }
+
+    #[inline(always)]
+    pub fn is_empty(&self) -> bool {
+        self.len() == 0
+    }
+}
+
+/// A list of [Piece], suitable for issuing vectored writes via io_uring.
+#[derive(Default)]
+pub struct PieceList {
+    pieces: Vec<Piece>,
+}
+
+impl PieceList {
+    /// Add a single chunk to the list
+    pub fn push(&mut self, chunk: impl Into<Piece>) {
+        self.pieces.push(chunk.into());
+    }
+
+    /// Returns total length
+    pub fn len(&self) -> usize {
+        self.pieces.iter().map(|c| c.len()).sum()
+    }
+
+    pub fn num_pieces(&self) -> usize {
+        self.pieces.len()
+    }
+
+    pub fn is_empty(&self) -> bool {
+        self.pieces.is_empty() || self.len() == 0
+    }
+
+    pub fn clear(&mut self) {
+        self.pieces.clear();
+    }
+
+    pub fn into_vec(self) -> Vec<Piece> {
+        self.pieces
+    }
+}
+
+impl From<Vec<Piece>> for PieceList {
+    fn from(chunks: Vec<Piece>) -> Self {
+        Self { pieces: chunks }
+    }
+}
+
+/// A piece of data with a stablea ddress that's _also_
+/// valid utf-8.
+#[derive(Clone)]
+pub struct PieceStr {
+    piece: Piece,
+}
+
+impl Display for PieceStr {
+    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+        f.pad(self)
+    }
+}
+
+impl Deref for PieceStr {
+    type Target = str;
+
+    fn deref(&self) -> &Self::Target {
+        unsafe { std::str::from_utf8_unchecked(&self.piece) }
+    }
+}
+
+impl AsRef<str> for PieceStr {
+    fn as_ref(&self) -> &str {
+        self
+    }
+}
+
+impl PieceStr {
+    /// Returns the underlying bytes (borrowed)
+    pub fn as_bytes(&self) -> &[u8] {
+        self.piece.as_ref()
+    }
+
+    /// Returns the underlying bytes (owned)
+    pub fn into_inner(self) -> Piece {
+        self.piece
+    }
+}
+
+impl From<&'static str> for PieceStr {
+    fn from(s: &'static str) -> Self {
+        PieceStr {
+            piece: Piece::Static(s.as_bytes()),
+        }
+    }
+}
+
+impl From<String> for PieceStr {
+    fn from(s: String) -> Self {
+        PieceStr {
+            piece: Piece::Vec(s.into_bytes()),
+        }
+    }
+}
+
+impl From<RollStr> for PieceStr {
+    fn from(s: RollStr) -> Self {
+        PieceStr {
+            piece: Piece::Roll(s.into_inner()),
+        }
+    }
+}
diff --git a/src/buffet/roll.rs b/src/buffet/roll.rs
index 69441a69..508fd449 100644
--- a/src/buffet/roll.rs
+++ b/src/buffet/roll.rs
@@ -5,6 +5,7 @@ use std::{
     iter::Enumerate,
     ops::{Bound, Deref, RangeBounds},
     rc::Rc,
+    str::Utf8Error,
 };
 
 use nom::{
@@ -124,9 +125,8 @@ impl RollMut {
             buf: Rc::new(UnsafeCell::new(b)),
             off: 0,
         };
-        let src_slice = self.borrow_filled();
-        let dst_slice = bs.slice_mut(src_slice.len() as u32);
-        dst_slice.copy_from_slice(src_slice);
+        let dst_slice = bs.slice_mut(self.len() as u32);
+        dst_slice.copy_from_slice(&self[..]);
         let next_storage = StorageMut::Box(bs);
 
         self.storage = next_storage;
@@ -138,18 +138,17 @@ impl RollMut {
     pub fn realloc(&mut self) -> eyre::Result<()> {
         assert!(self.len() != self.storage_size());
 
-        let src_slice = self.borrow_filled();
         let next_storage = match &self.storage {
             StorageMut::Buf(_) => {
                 let mut next_b = BufMut::alloc()?;
-                next_b[..src_slice.len()].copy_from_slice(src_slice);
+                next_b[..self.len()].copy_from_slice(&self[..]);
                 StorageMut::Buf(next_b)
             }
             StorageMut::Box(b) => {
-                if src_slice.len() > BUF_SIZE as usize {
+                if self.len() > BUF_SIZE as usize {
                     // TODO: optimize via `MaybeUninit`?
                     let mut next_b = vec![0; b.cap()].into_boxed_slice();
-                    next_b[..src_slice.len()].copy_from_slice(src_slice);
+                    next_b[..self.len()].copy_from_slice(&self[..]);
                     let next_b = BoxStorage {
                         buf: Rc::new(UnsafeCell::new(next_b)),
                         off: 0,
@@ -157,7 +156,7 @@ impl RollMut {
                     StorageMut::Box(next_b)
                 } else {
                     let mut next_b = BufMut::alloc()?;
-                    next_b[..src_slice.len()].copy_from_slice(src_slice);
+                    next_b[..self.len()].copy_from_slice(&self[..]);
                     StorageMut::Buf(next_b)
                 }
             }
@@ -267,14 +266,6 @@ impl RollMut {
         }
     }
 
-    /// Borrow the filled portion of this buffer as a slice
-    pub fn borrow_filled(&self) -> &[u8] {
-        match &self.storage {
-            StorageMut::Buf(b) => &b[..self.len as usize],
-            StorageMut::Box(b) => b.slice(self.len),
-        }
-    }
-
     /// Split this [RollMut] at the given index.
     /// Panics if `at > len()`. If `at < len()`, the filled portion will carry
     /// over in the new [RollMut].
@@ -348,6 +339,18 @@ impl RollMut {
     }
 }
 
+impl Deref for RollMut {
+    type Target = [u8];
+
+    #[inline(always)]
+    fn deref(&self) -> &Self::Target {
+        match &self.storage {
+            StorageMut::Buf(b) => &b[..self.len as usize],
+            StorageMut::Box(b) => b.slice(self.len),
+        }
+    }
+}
+
 struct ReadInto {
     buf: RollMut,
     off: u32,
@@ -586,6 +589,20 @@ impl Roll {
     pub fn to_string_lossy(&self) -> Cow<'_, str> {
         String::from_utf8_lossy(self)
     }
+
+    /// Decode as utf-8
+    pub fn to_string(self) -> Result<RollStr, Utf8Error> {
+        _ = std::str::from_utf8(&self)?;
+        Ok(RollStr { roll: self })
+    }
+
+    /// Convert to [RollStr].
+    ///
+    /// # Safety
+    /// UB if not utf-8. Typically only used in parsers.
+    pub unsafe fn to_string_unchecked(self) -> RollStr {
+        RollStr { roll: self }
+    }
 }
 
 impl InputIter for Roll {
@@ -802,6 +819,32 @@ impl InputLength for Roll {
     }
 }
 
+/// A [Roll] that's also a valid utf-8 string.
+#[derive(Clone)]
+pub struct RollStr {
+    roll: Roll,
+}
+
+impl Debug for RollStr {
+    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+        Debug::fmt(&self[..], f)
+    }
+}
+
+impl Deref for RollStr {
+    type Target = str;
+
+    fn deref(&self) -> &Self::Target {
+        unsafe { std::str::from_utf8_unchecked(&self.roll) }
+    }
+}
+
+impl RollStr {
+    pub fn into_inner(self) -> Roll {
+        self.roll
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use nom::IResult;
@@ -880,7 +923,7 @@ mod tests {
 
         assert_eq!(rm.storage_size(), BUF_SIZE as usize * 2);
         assert_eq!(rm.len(), put.len());
-        assert_eq!(rm.borrow_filled(), put.as_bytes());
+        assert_eq!(&rm[..], put.as_bytes());
     }
 
     #[test]
@@ -925,16 +968,16 @@ mod tests {
 
         rm.put(input).unwrap();
         assert_eq!(rm.len(), input.len());
-        assert_eq!(rm.borrow_filled(), input);
+        assert_eq!(&rm[..], input);
 
         assert_eq!(rm.cap(), BUF_SIZE as usize - input.len());
 
         rm.grow();
         assert_eq!(rm.cap(), 2 * (BUF_SIZE as usize) - input.len());
-        assert_eq!(rm.borrow_filled(), input);
+        assert_eq!(&rm[..], input);
 
         rm.skip(5);
-        assert_eq!(rm.borrow_filled(), b"pretty long");
+        assert_eq!(&rm[..], b"pretty long");
     }
 
     #[test]
@@ -966,7 +1009,7 @@ mod tests {
     fn test_roll_keep() {
         fn test_roll_keep_inner(mut rm: RollMut) {
             rm.put(b"helloworld").unwrap();
-            assert_eq!(rm.borrow_filled(), b"helloworld");
+            assert_eq!(&rm[..], b"helloworld");
 
             {
                 let roll = rm.filled().slice(3..=5);
@@ -982,7 +1025,7 @@ mod tests {
             assert_eq!(roll, b"world");
 
             rm.keep(roll);
-            assert_eq!(rm.borrow_filled(), b"world");
+            assert_eq!(&rm[..], b"world");
         }
 
         let rm = RollMut::alloc().unwrap();
diff --git a/src/h1/body.rs b/src/h1/body.rs
index 03e96e7e..e67eb9d2 100644
--- a/src/h1/body.rs
+++ b/src/h1/body.rs
@@ -4,7 +4,7 @@ use tracing::debug;
 
 use crate::{
     util::{read_and_parse, write_all_list},
-    Body, BodyChunk, BodyErrorReason, IoChunk, IoChunkList, ReadOwned, RollMut, WriteOwned,
+    Body, BodyChunk, BodyErrorReason, Piece, PieceList, ReadOwned, RollMut, WriteOwned,
 };
 
 /// An HTTP/1.1 body, either chunked or content-length.
@@ -265,12 +265,12 @@ pub(crate) async fn write_h1_body(
 
 pub(crate) async fn write_h1_body_chunk(
     transport: &impl WriteOwned,
-    chunk: IoChunk,
+    chunk: Piece,
     mode: BodyWriteMode,
 ) -> eyre::Result<()> {
     match mode {
         BodyWriteMode::Chunked => {
-            let mut list = IoChunkList::default();
+            let mut list = PieceList::default();
             list.push(format!("{:x}\r\n", chunk.len()).into_bytes());
             list.push(chunk);
             list.push("\r\n");
@@ -292,7 +292,7 @@ pub(crate) async fn write_h1_body_end(
 ) -> eyre::Result<()> {
     match mode {
         BodyWriteMode::Chunked => {
-            let mut list = IoChunkList::default();
+            let mut list = PieceList::default();
             list.push("0\r\n\r\n");
             _ = write_all_list(transport, list).await?;
         }
diff --git a/src/h1/client.rs b/src/h1/client.rs
index 662dce02..7cb10c91 100644
--- a/src/h1/client.rs
+++ b/src/h1/client.rs
@@ -4,7 +4,7 @@ use eyre::Context;
 use tracing::debug;
 
 use crate::{
-    buffet::IoChunkList,
+    buffet::PieceList,
     io::ReadWriteOwned,
     types::Request,
     util::{read_and_parse, write_all_list},
@@ -49,7 +49,7 @@ where
         BodyWriteMode::ContentLength
     };
 
-    let mut list = IoChunkList::default();
+    let mut list = PieceList::default();
     encode_request(req, &mut list)?;
     _ = write_all_list(transport.as_ref(), list)
         .await
diff --git a/src/h1/encode.rs b/src/h1/encode.rs
index e103b527..62bc83f5 100644
--- a/src/h1/encode.rs
+++ b/src/h1/encode.rs
@@ -1,10 +1,10 @@
 use crate::{
-    buffet::IoChunkList,
+    buffet::PieceList,
     types::{Headers, Request, Response},
 };
 
-pub(crate) fn encode_request(req: Request, list: &mut IoChunkList) -> eyre::Result<()> {
-    list.push(req.method);
+pub(crate) fn encode_request(req: Request, list: &mut PieceList) -> eyre::Result<()> {
+    list.push(req.method.into_chunk());
     list.push(" ");
     list.push(req.path);
     match req.version {
@@ -21,7 +21,7 @@ pub(crate) fn encode_request(req: Request, list: &mut IoChunkList) -> eyre::Resu
     Ok(())
 }
 
-pub(crate) fn encode_response(res: Response, list: &mut IoChunkList) -> eyre::Result<()> {
+pub(crate) fn encode_response(res: Response, list: &mut PieceList) -> eyre::Result<()> {
     match res.version {
         1 => list.push(&b"HTTP/1.1 "[..]),
         _ => return Err(eyre::eyre!("unsupported HTTP version 1.{}", res.version)),
@@ -37,7 +37,7 @@ pub(crate) fn encode_response(res: Response, list: &mut IoChunkList) -> eyre::Re
     Ok(())
 }
 
-pub(crate) fn encode_headers(headers: Headers, list: &mut IoChunkList) -> eyre::Result<()> {
+pub(crate) fn encode_headers(headers: Headers, list: &mut PieceList) -> eyre::Result<()> {
     for header in headers {
         list.push(header.name);
         list.push(": ");
diff --git a/src/h1/parse.rs b/src/h1/parse.rs
index e4cbda7d..a3b6d563 100644
--- a/src/h1/parse.rs
+++ b/src/h1/parse.rs
@@ -12,7 +12,7 @@ use nom::{
 
 use crate::{
     types::{Header, Headers, Request, Response},
-    Roll,
+    Method, PieceStr, Roll, RollStr,
 };
 
 const CRLF: &[u8] = b"\r\n";
@@ -29,20 +29,60 @@ pub fn crlf(i: Roll) -> IResult<Roll, ()> {
 
 // Looks like `GET /path HTTP/1.1\r\n`, then headers
 pub fn request(i: Roll) -> IResult<Roll, Request> {
-    let (i, method) = take_until_and_consume(b" ")(i)?;
-    let (i, path) = take_until_and_consume(b" ")(i)?;
+    let (i, method) = terminated(method, space1)(i)?;
+    let (i, path) = terminated(path, space1)(i)?;
     let (i, version) = terminated(http_version, tag(CRLF))(i)?;
     let (i, headers) = headers_and_crlf(i)?;
 
     let request = Request {
         method,
-        path,
+        path: path.into(),
         version,
         headers,
     };
     Ok((i, request))
 }
 
+pub fn method(i: Roll) -> IResult<Roll, Method> {
+    let (i, method) = token(i)?;
+    let method: PieceStr = method.into();
+    Ok((i, method.into()))
+}
+
+/// A short textual identifier that does not include whitspace or delimiters,
+/// cf. https://httpwg.org/specs/rfc9110.html#rule.token.separators
+pub fn token(i: Roll) -> IResult<Roll, RollStr> {
+    let (i, token) = take_while1(is_tchar)(i)?;
+    let token = unsafe { token.to_string_unchecked() };
+    Ok((i, token))
+}
+
+/// cf. https://httpwg.org/specs/rfc9110.html#rule.token.separators
+fn is_tchar(c: u8) -> bool {
+    c.is_ascii_graphic() && !is_delimiter(c)
+}
+
+/// cf. https://httpwg.org/specs/rfc9110.html#rule.token.separators
+fn is_delimiter(c: u8) -> bool {
+    memchr::memchr(c, br#"(),/:;<=>?@[\]{}""#).is_some()
+}
+
+fn path(i: Roll) -> IResult<Roll, RollStr> {
+    let (i, path) = take_while1(is_uri_char)(i)?;
+    let path = unsafe { path.to_string_unchecked() };
+    Ok((i, path))
+}
+
+/// Returns true if `c` is a character that can be found in an URI
+/// cf. https://stackoverflow.com/a/7109208
+fn is_uri_char(c: u8) -> bool {
+    memchr::memchr(
+        c,
+        br#"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~:/?#[]@!$&'()*+,;%="#,
+    )
+    .is_some()
+}
+
 // Looks like `HTTP/1.1 200 OK\r\n` or `HTTP/1.1 404 Not Found\r\n`, then headers
 pub fn response(i: Roll) -> IResult<Roll, Response> {
     let (i, version) = terminated(http_version, space1)(i)?;
@@ -129,3 +169,16 @@ fn space1(i: Roll) -> IResult<Roll, ()> {
 fn take_until_and_consume(needle: &[u8]) -> impl FnMut(Roll) -> IResult<Roll, Roll> + '_ {
     terminated(take_until(needle), tag(needle))
 }
+
+#[cfg(test)]
+mod tests {
+    use crate::h1::parse::is_delimiter;
+
+    #[test]
+    fn test_h1_parse_various_lowlevel_functions() {
+        assert!(is_delimiter(b'('));
+        assert!(is_delimiter(b'"'));
+        assert!(is_delimiter(b'\\'));
+        assert!(!is_delimiter(b'B'));
+    }
+}
diff --git a/src/h1/server.rs b/src/h1/server.rs
index da87f419..9f367cbb 100644
--- a/src/h1/server.rs
+++ b/src/h1/server.rs
@@ -4,10 +4,10 @@ use eyre::Context;
 use tracing::debug;
 
 use crate::{
-    buffet::IoChunkList,
+    buffet::PieceList,
     io::WriteOwned,
     util::{read_and_parse, write_all_list, SemanticError},
-    Body, Headers, IoChunk, ReadWriteOwned, Request, Response, RollMut,
+    Body, Headers, Piece, ReadWriteOwned, Request, Response, RollMut,
 };
 
 use super::{
@@ -203,7 +203,7 @@ where
     }
 
     async fn write_response_internal(self, res: Response) -> eyre::Result<Self> {
-        let mut list = IoChunkList::default();
+        let mut list = PieceList::default();
         encode_response(res, &mut list)?;
 
         let list = write_all_list(self.transport.as_ref(), list)
@@ -223,10 +223,7 @@ where
 {
     /// Send a response body chunk. Errors out if sending more than the
     /// announced content-length.
-    pub async fn write_chunk(
-        self,
-        chunk: IoChunk,
-    ) -> eyre::Result<Responder<T, ExpectResponseBody>> {
+    pub async fn write_chunk(self, chunk: Piece) -> eyre::Result<Responder<T, ExpectResponseBody>> {
         super::body::write_h1_body_chunk(self.transport.as_ref(), chunk, self.state.mode).await?;
         Ok(self)
     }
@@ -244,7 +241,7 @@ where
 
         if let Some(trailers) = trailers {
             // TODO: check all preconditions
-            let mut list = IoChunkList::default();
+            let mut list = PieceList::default();
             encode_headers(*trailers, &mut list)?;
 
             let list = write_all_list(self.transport.as_ref(), list)
diff --git a/src/types/method.rs b/src/types/method.rs
new file mode 100644
index 00000000..7ab1e9ed
--- /dev/null
+++ b/src/types/method.rs
@@ -0,0 +1,74 @@
+use std::fmt;
+
+use crate::{Piece, PieceStr};
+
+/// An HTTP method, see https://httpwg.org/specs/rfc9110.html#methods
+#[derive(Clone)]
+pub enum Method {
+    Get,
+    Head,
+    Post,
+    Put,
+    Delete,
+    Connect,
+    Options,
+    Trace,
+    Other(PieceStr),
+}
+
+impl fmt::Debug for Method {
+    // forward to display
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        fmt::Display::fmt(self, f)
+    }
+}
+
+impl fmt::Display for Method {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        let s = match self {
+            Method::Get => "GET",
+            Method::Head => "HEAD",
+            Method::Post => "POST",
+            Method::Put => "PUT",
+            Method::Delete => "DELETE",
+            Method::Connect => "CONNECT",
+            Method::Options => "OPTIONS",
+            Method::Trace => "TRACE",
+            Method::Other(s) => s,
+        };
+        f.pad(s)
+    }
+}
+
+impl Method {
+    pub fn into_chunk(self) -> Piece {
+        let s = match self {
+            Method::Get => "GET",
+            Method::Head => "HEAD",
+            Method::Post => "POST",
+            Method::Put => "PUT",
+            Method::Delete => "DELETE",
+            Method::Connect => "CONNECT",
+            Method::Options => "OPTIONS",
+            Method::Trace => "TRACE",
+            Method::Other(roll) => return roll.into_inner(),
+        };
+        s.into()
+    }
+}
+
+impl From<PieceStr> for Method {
+    fn from(s: PieceStr) -> Self {
+        match &s[..] {
+            "GET" => Method::Get,
+            "HEAD" => Method::Head,
+            "POST" => Method::Post,
+            "PUT" => Method::Put,
+            "DELETE" => Method::Delete,
+            "CONNECT" => Method::Connect,
+            "OPTIONS" => Method::Options,
+            "TRACE" => Method::Trace,
+            _ => Method::Other(s),
+        }
+    }
+}
diff --git a/src/types/mod.rs b/src/types/mod.rs
index 0c38726b..67f07b65 100644
--- a/src/types/mod.rs
+++ b/src/types/mod.rs
@@ -1,17 +1,21 @@
-mod headers;
 use std::fmt::{self, Debug};
 
-pub use headers::*;
 use tracing::debug;
 
-use crate::{IoChunk, Roll};
+use crate::{Piece, PieceStr, Roll};
+
+mod headers;
+pub use headers::*;
+
+mod method;
+pub use method::*;
 
 /// An HTTP request
 pub struct Request {
-    pub method: Roll,
+    pub method: Method,
 
     /// Requested entity
-    pub path: Roll,
+    pub path: PieceStr,
 
     /// The 'b' in 'HTTP/1.b'
     pub version: u8,
@@ -22,7 +26,7 @@ pub struct Request {
 
 impl Request {
     pub(crate) fn debug_print(&self) {
-        debug!(method = %self.method.to_string_lossy(), path = %self.path.to_string_lossy(), version = %self.version, "got request");
+        debug!(method = %self.method, path = %self.path, version = %self.version, "got request");
         for h in &self.headers {
             debug!(name = %h.name.to_string_lossy(), value = %h.value.to_string_lossy(), "got header");
         }
@@ -55,7 +59,7 @@ impl Response {
 
 /// A body chunk
 pub enum BodyChunk {
-    Chunk(IoChunk),
+    Chunk(Piece),
 
     /// The body finished, and it matched the announced content-length,
     /// or we were using a framed protocol
diff --git a/src/util.rs b/src/util.rs
index f5b7919f..ee0ce589 100644
--- a/src/util.rs
+++ b/src/util.rs
@@ -4,7 +4,7 @@ use pretty_hex::PrettyHex;
 use tracing::debug;
 
 use crate::{
-    buffet::IoChunkList,
+    buffet::PieceList,
     io::{ReadOwned, WriteOwned},
     Roll, RollMut,
 };
@@ -31,10 +31,9 @@ where
             Err(err) => {
                 if err.is_incomplete() {
                     {
-                        let filled = buf.borrow_filled();
                         debug!(
                             "incomplete request, need more data. start of buffer: {:?}",
-                            filled[0..std::cmp::min(filled.len(), 128)].hex_dump()
+                            &buf[..std::cmp::min(buf.len(), 128)].hex_dump()
                         );
                     }
 
@@ -72,10 +71,10 @@ where
 /// buffer re-using the remaining space.
 pub(crate) async fn write_all_list(
     stream: &impl WriteOwned,
-    list: IoChunkList,
-) -> eyre::Result<IoChunkList> {
+    list: PieceList,
+) -> eyre::Result<PieceList> {
     let len = list.len();
-    let num_chunks = list.num_chunks();
+    let num_chunks = list.num_pieces();
     let list = list.into_vec();
     debug!("writing {len} bytes in {num_chunks} chunks");
 


Test patch:
diff --git a/tests/integration_test.rs b/tests/integration_test.rs
index fd3c0fa0..c8c70fcd 100644
--- a/tests/integration_test.rs
+++ b/tests/integration_test.rs
@@ -6,8 +6,8 @@ mod helpers;
 
 use bytes::BytesMut;
 use hring::{
-    h1, Body, BodyChunk, ChanRead, ChanWrite, Headers, ReadWritePair, Request, Response, RollMut,
-    WriteOwned,
+    h1, Body, BodyChunk, ChanRead, ChanWrite, Headers, Method, ReadWritePair, Request, Response,
+    RollMut, WriteOwned,
 };
 use httparse::{Status, EMPTY_HEADER};
 use pretty_assertions::assert_eq;
@@ -143,19 +143,9 @@ fn request_api() {
         let (mut rx, write) = ChanWrite::new();
         let transport = ReadWritePair(read, write);
 
-        let mut buf = RollMut::alloc()?;
-
-        buf.put(b"GET")?;
-        let method = buf.take_all();
-
-        buf.put(b"/")?;
-        let path = buf.take_all();
-
-        _ = buf;
-
         let req = Request {
-            method,
-            path,
+            method: Method::Get,
+            path: "/".into(),
             version: 1,
             headers: Headers::default(),
         };