From 497a75859995df641c4cbf093e1fe9672058300c Mon Sep 17 00:00:00 2001 From: Drew Pirrone-Brusse Date: Tue, 13 Jan 2026 18:10:25 -0500 Subject: [PATCH] feat: Extend HeaderValue to provide access to/semantics of backing Bytes This change adds, - `HeaderValue::from_shared[_unchecked]` which directly accept a `Bytes` object as an argument. This should allow for lwss copying when constructing `HeaderValue`s from pre-existing `Bytes` buffers. - `HeaderValue::as_shared` which returns a clone of the `Bytes` object that backs the given `HeaderValue`. - `HeaderValue::[try_]slice` which mirrors the `Bytes::slice` interface, and provides copy-free construction of sub-sliced `HeaderValue`s. - `HeaderName::as_shared` which is a rename of the previously `pub(crate)` `.into_bytes` method. Allows access to a `Bytes` representation of a HeaderName, though it will usually copy via `Bytes::from_static`. Add .slice to HeaderValue, provide access to the backing Bytes objects --- src/header/name.rs | 19 ++++- src/header/value.rs | 183 ++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 196 insertions(+), 6 deletions(-) diff --git a/src/header/name.rs b/src/header/name.rs index 02af57e1..c108c01a 100644 --- a/src/header/name.rs +++ b/src/header/name.rs @@ -1270,7 +1270,24 @@ impl HeaderName { } } - pub(super) fn into_bytes(self) -> Bytes { + /// Creates a [`Bytes`] object from this `HeaderName`. + /// + /// This function will _likely_ copy the standard string representation of + /// this `HeaderName` into a new `Bytes` object. If this `HeaderName` is a + /// non-standard header name (that just so happens to be backed by a `Bytes` + /// object), the copy may be elided in favor of claiming a reference-counted + /// borrow of the shared memory region that backs this `HeaderName`. Expect + /// the former behavior. + /// + /// # Examples + /// + /// ``` + /// # use http::header::*; + /// # use bytes::Bytes; + /// let hdr = HeaderName::from_static("content-length"); + /// assert_eq!(hdr.as_shared(), Bytes::from_static(b"content-length")); + /// ``` + pub fn as_shared(self) -> Bytes { self.inner.into() } } diff --git a/src/header/value.rs b/src/header/value.rs index abd5d036..d50f06c5 100644 --- a/src/header/value.rs +++ b/src/header/value.rs @@ -1,5 +1,6 @@ use bytes::{Bytes, BytesMut}; +use core::ops::RangeBounds; use std::convert::TryFrom; use std::error::Error; use std::fmt::Write; @@ -152,6 +153,61 @@ impl HeaderValue { HeaderValue::try_from_generic(src, Bytes::copy_from_slice) } + /// Convert a [`Bytes`] into a `HeaderValue`. + /// + /// This function will not perform any copying, however if the argument + /// contains invalid header value bytes, an error is returned. Only byte + /// values between 32 and 255 (inclusive) are permitted, excluding byte + /// 127 (DEL). + /// + /// # Examples + /// + /// ``` + /// # use http::header::HeaderValue; + /// # use bytes::Bytes; + /// let bytes = Bytes::from_static(b"hello\xfa"); + /// let val = HeaderValue::from_shared(bytes).unwrap(); + /// assert_eq!(val, &b"hello\xfa"[..]); + /// ``` + pub fn from_shared(src: Bytes) -> Result { + for &b in src.as_ref() { + if !is_valid(b) { + return Err(InvalidHeaderValue { _priv: () }); + } + } + Ok(HeaderValue { + inner: src, + is_sensitive: false, + }) + } + + /// Convert a [`Bytes`] directly into a `HeaderValue` without validating. + /// + /// This function does NOT validate that illegal bytes are not contained + /// within the buffer. + /// + /// ## Panics + /// In a debug build this will panic if `src` is not valid UTF-8. + /// + /// ## Safety + /// `src` must contain valid UTF-8. In a release build it is undefined + /// behaviour to call this with `src` that is not valid UTF-8. + pub unsafe fn from_shared_unchecked(src: Bytes) -> HeaderValue { + if cfg!(debug_assertions) { + match HeaderValue::from_shared(src) { + Ok(val) => val, + Err(_err) => { + panic!("HeaderValue::from_shared_unchecked() with invalid bytes"); + } + } + } else { + return HeaderValue { + inner: src, + is_sensitive: false, + }; + } + } + /// Attempt to convert a `Bytes` buffer to a `HeaderValue`. /// /// This will try to prevent a copy if the type passed is the type used @@ -205,10 +261,6 @@ impl HeaderValue { } } - fn from_shared(src: Bytes) -> Result { - HeaderValue::try_from_generic(src, std::convert::identity) - } - fn try_from_generic, F: FnOnce(T) -> Bytes>( src: T, into: F, @@ -296,6 +348,127 @@ impl HeaderValue { self.as_ref() } + /// Creates a [`Bytes`] object from this `HeaderValue`. + /// + /// This function will not perform any copying. Rather, it allocates a new, + /// owned `Bytes` object (which will be, at the time of writing, roughly 4 + /// pointers in size) which will claim a reference-counted borrow of the + /// shared memory region that backs this `HeaderValue`. Because of the + /// Arc-like semantics of `Bytes` and `HeaderValue`s, this `HeaderValue` and + /// the returned `Bytes` can be dropped in any order and the shared memory + /// region will remain valid for the other. + /// + /// # Examples + /// + /// ``` + /// # use http::header::HeaderValue; + /// # use bytes::Bytes; + /// let val = HeaderValue::from_static("hello"); + /// assert_eq!(val.as_shared(), Bytes::from_static(b"hello")); + /// ``` + #[inline] + pub fn as_shared(&self) -> Bytes { + self.inner.clone() + } + + /// Returns a slice of `self` over the provided range. + /// + /// + /// This function will not perform any copying. Rather, it allocates a new, + /// owned `HeaderValue` object (which will be, at the time of writing, + /// roughly 5 pointers in size) which will claim a reference-counted borrow + /// of the shared memory region that backs this `HeaderValue`. Because of the + /// Arc-like semantics of`HeaderValue`s, this and the resulting + /// `HeaderValue` can be dropped in any order and the shared memory region + /// will remain valid for the other. + /// + /// If the `range` would be out of bounds, this function will return `None`. + /// + /// # Examples + /// + /// ``` + /// # use http::header::HeaderValue; + /// let val = HeaderValue::from_static(r#"W/"67ab43", "54ed21", "7892dd""#); + /// let slice_1 = val.try_slice(0..10).unwrap(); + /// let slice_3 = val.try_slice(22..30).unwrap(); + /// assert_eq!(slice_1, r#"W/"67ab43""#); + /// assert_eq!(slice_3, "\"7892dd\""); + /// assert!(val.try_slice(0..31).is_none()); + /// ``` + #[inline] + pub fn try_slice(&self, range: impl RangeBounds) -> Option { + use core::ops::Bound; + + let len = self.len(); + + let begin = match range.start_bound() { + Bound::Included(&n) => n, + Bound::Excluded(&n) => { + if let Some(n) = n.checked_add(1) { + n + } else { + return None; + } + } + Bound::Unbounded => 0, + }; + + let end = match range.end_bound() { + Bound::Included(&n) => { + if let Some(n) = n.checked_add(1) { + n + } else { + return None; + } + } + Bound::Excluded(&n) => n, + Bound::Unbounded => len, + }; + + if begin > end { + return None; + } + if end > len { + return None; + } + + Some(self.slice(range)) + } + + /// Returns a slice of `self` over the provided range. + /// + /// + /// This function will not perform any copying. Rather, it allocates a new, + /// owned `HeaderValue` object (which will be, at the time of writing, + /// roughly 5 pointers in size) which will claim a reference-counted borrow + /// of the shared memory region that backs this `HeaderValue`. Because of the + /// Arc-like semantics of`HeaderValue`s, this and the resulting + /// `HeaderValue` can be dropped in any order and the shared memory region + /// will remain valid for the other. + /// + /// # Panics + /// + /// Requires that `begin <= end` and `end <= self.len()`, otherwise slicing + /// will panic. + /// + /// # Examples + /// + /// ``` + /// # use http::header::HeaderValue; + /// let val = HeaderValue::from_static(r#"W/"67ab43", "54ed21", "7892dd""#); + /// let slice_1 = val.slice(0..10); + /// let slice_3 = val.slice(22..30); + /// assert_eq!(slice_1, r#"W/"67ab43""#); + /// assert_eq!(slice_3, "\"7892dd\""); + /// ``` + #[inline] + pub fn slice(&self, range: impl RangeBounds) -> HeaderValue { + let inner_slice = self.inner.slice(range); + // SAFETY: Any subslice of `self.inner` should be just as valid for use + // as a HeaderValue as the full `self.inner` buffer. + unsafe { HeaderValue::from_shared_unchecked(inner_slice) } + } + /// Mark that the header value represents sensitive information. /// /// # Examples @@ -386,7 +559,7 @@ impl From for HeaderValue { #[inline] fn from(h: HeaderName) -> HeaderValue { HeaderValue { - inner: h.into_bytes(), + inner: h.as_shared(), is_sensitive: false, } }