diff --git a/grpc/Cargo.toml b/grpc/Cargo.toml index 46313252f..9188537a5 100644 --- a/grpc/Cargo.toml +++ b/grpc/Cargo.toml @@ -41,11 +41,13 @@ tls-rustls = [ ] [dependencies] +base64 = "0.22" bytes = "1.10.1" hickory-resolver = { version = "0.25.1", optional = true } http = "1.1.0" http-body = "1.0.1" hyper = { version = "1.6.0", features = ["client", "http2"] } +itoa = "1.0" parking_lot = "0.12.4" pin-project-lite = "0.2.16" rand = "0.9" @@ -80,6 +82,7 @@ url = "2.5.0" [dev-dependencies] async-stream = "0.3.6" +criterion = "0.5" hickory-server = "0.25.2" prost = "0.14.0" rustls = { version = "0.23", default-features = false, features = ["ring"] } @@ -90,3 +93,7 @@ tonic = { version = "0.14.0", path = "../tonic", default-features = false, featu "tls-ring", ] } tonic-prost = { version = "0.14.0", path = "../tonic-prost" } + +[[bench]] +name = "metadata" +harness = false diff --git a/grpc/benches/metadata.rs b/grpc/benches/metadata.rs new file mode 100644 index 000000000..658bc4714 --- /dev/null +++ b/grpc/benches/metadata.rs @@ -0,0 +1,197 @@ +/* + * + * Copyright 2026 gRPC authors. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + * + */ + +use criterion::Criterion; +use criterion::black_box; +use criterion::criterion_group; +use criterion::criterion_main; +use grpc::metadata::MetadataKey; +use grpc::metadata::MetadataMap; +use grpc::metadata::MetadataValue; +use tonic::metadata::MetadataMap as TonicMetadataMap; + +fn bench_metadata_map_insert(c: &mut Criterion) { + let mut group = c.benchmark_group("metadata_map_insert"); + + for size in [5, 10, 20].iter() { + group.bench_with_input(format!("grpc_metadata_map_{}", size), size, |b, &size| { + b.iter(|| { + let mut map = MetadataMap::with_capacity(size); + for i in 0..size { + let key_str = format!("x-header-{}", i); + let key = MetadataKey::from_bytes(key_str.as_bytes()).unwrap(); + let val = MetadataValue::try_from("value").unwrap(); + map.insert(key, val); + } + black_box(map); + }); + }); + + group.bench_with_input(format!("tonic_metadata_map_{}", size), size, |b, &size| { + b.iter(|| { + let mut map = TonicMetadataMap::with_capacity(size); + for i in 0..size { + let key_str = format!("x-header-{}", i); + let key = key_str + .parse::>() + .unwrap(); + let val = "value" + .parse::>() + .unwrap(); + map.insert(key, val); + } + black_box(map); + }); + }); + } + group.finish(); +} + +fn bench_metadata_map_append(c: &mut Criterion) { + let mut group = c.benchmark_group("metadata_map_append"); + + for size in [5, 10, 20].iter() { + group.bench_with_input(format!("grpc_metadata_map_{}", size), size, |b, &size| { + b.iter(|| { + let mut map = MetadataMap::with_capacity(size); + for i in 0..size { + let key_str = format!("x-header-{}", i); + let key = MetadataKey::from_bytes(key_str.as_bytes()).unwrap(); + let val = MetadataValue::try_from("value").unwrap(); + map.append(key, val); + } + black_box(map); + }); + }); + + group.bench_with_input(format!("tonic_metadata_map_{}", size), size, |b, &size| { + b.iter(|| { + let mut map = TonicMetadataMap::with_capacity(size); + for i in 0..size { + let key_str = format!("x-header-{}", i); + let key = key_str + .parse::>() + .unwrap(); + let val = "value" + .parse::>() + .unwrap(); + map.append(key, val); + } + black_box(map); + }); + }); + } + group.finish(); +} + +fn bench_metadata_map_get(c: &mut Criterion) { + let mut group = c.benchmark_group("metadata_map_get"); + + for size in [5, 10, 20].iter() { + let mut map = MetadataMap::with_capacity(*size); + let mut keys = Vec::new(); + for i in 0..*size { + let key_str = format!("x-header-{}", i); + let key = MetadataKey::from_bytes(key_str.as_bytes()).unwrap(); + map.insert(key.clone(), MetadataValue::try_from("value").unwrap()); + keys.push(key); + } + + group.bench_with_input(format!("grpc_metadata_map_{}", size), size, |b, _| { + b.iter(|| { + for key in &keys { + black_box(map.get(key)); + } + }); + }); + + let mut tonic_map = TonicMetadataMap::with_capacity(*size); + let mut tonic_keys = Vec::new(); + for i in 0..*size { + let key_str = format!("x-header-{}", i); + let key = key_str + .parse::>() + .unwrap(); + tonic_map.insert(key.clone(), "value".parse().unwrap()); + tonic_keys.push(key); + } + + group.bench_with_input(format!("tonic_metadata_map_{}", size), size, |b, _| { + b.iter(|| { + for key in &tonic_keys { + black_box(tonic_map.get(key)); + } + }); + }); + } + group.finish(); +} + +fn bench_metadata_map_iter(c: &mut Criterion) { + let mut group = c.benchmark_group("metadata_map_iter"); + + for size in [5, 10, 20].iter() { + let mut map = MetadataMap::with_capacity(*size); + for i in 0..*size { + let key_str = format!("x-header-{}", i); + let key = MetadataKey::from_bytes(key_str.as_bytes()).unwrap(); + map.insert(key, MetadataValue::try_from("value").unwrap()); + } + + group.bench_with_input(format!("grpc_metadata_map_{}", size), size, |b, _| { + b.iter(|| { + for entry in map.iter() { + black_box(entry); + } + }); + }); + + let mut tonic_map = TonicMetadataMap::with_capacity(*size); + for i in 0..*size { + let key_str = format!("x-header-{}", i); + let key = key_str + .parse::>() + .unwrap(); + tonic_map.insert(key, "value".parse().unwrap()); + } + + group.bench_with_input(format!("tonic_metadata_map_{}", size), size, |b, _| { + b.iter(|| { + for entry in tonic_map.iter() { + black_box(entry); + } + }); + }); + } + group.finish(); +} + +criterion_group!( + benches, + bench_metadata_map_insert, + bench_metadata_map_append, + bench_metadata_map_get, + bench_metadata_map_iter, +); +criterion_main!(benches); diff --git a/grpc/src/lib.rs b/grpc/src/lib.rs index ecfd9259a..9e6b8495e 100644 --- a/grpc/src/lib.rs +++ b/grpc/src/lib.rs @@ -35,6 +35,7 @@ pub mod client; pub mod core; pub mod credentials; pub mod inmemory; +pub mod metadata; pub mod server; mod macros; diff --git a/grpc/src/metadata/encoding.rs b/grpc/src/metadata/encoding.rs new file mode 100644 index 000000000..bd1297d1e --- /dev/null +++ b/grpc/src/metadata/encoding.rs @@ -0,0 +1,372 @@ +/* + * + * Copyright 2026 gRPC authors. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + * + */ + +use std::error::Error; +use std::fmt; +use std::hash::Hash; + +use base64::Engine as _; +use bytes::Bytes; + +use crate::metadata::value::UnencodedHeaderValue; +use crate::private; + +/// A possible error when converting a `MetadataValue` from a string or byte +/// slice. +#[derive(Debug, Hash)] +#[non_exhaustive] +pub struct InvalidMetadataValue {} + +pub trait ValueEncoding: Clone + Eq + PartialEq + Hash { + /// Returns true if the provided key is valid for this ValueEncoding type. + /// For example, `Ascii::is_valid_key("a") == true`, + /// `Ascii::is_valid_key("a-bin") == false`. + fn is_valid_key(key: &str) -> bool; + + #[doc(hidden)] + fn from_bytes( + value: &[u8], + _: private::Internal, + ) -> Result; + + #[doc(hidden)] + fn from_shared( + value: Bytes, + _: private::Internal, + ) -> Result; + + #[doc(hidden)] + fn from_static(value: &'static str, _: private::Internal) -> UnencodedHeaderValue; + + #[doc(hidden)] + fn decode(value: &[u8], _: private::Internal) -> Result; + + #[doc(hidden)] + fn encode(value: Bytes, _: private::Internal) -> Bytes; + + #[doc(hidden)] + fn equals(a: &UnencodedHeaderValue, b: &[u8], _: private::Internal) -> bool; + + #[doc(hidden)] + fn values_equal( + a: &UnencodedHeaderValue, + b: &UnencodedHeaderValue, + _: private::Internal, + ) -> bool; + + #[doc(hidden)] + fn fmt( + value: &UnencodedHeaderValue, + f: &mut fmt::Formatter<'_>, + _: private::Internal, + ) -> fmt::Result; +} + +/// gRPC metadata values can be either ASCII strings or binary. Note that only +/// visible ASCII characters (32-127) are permitted. +/// This type should never be instantiated -- in fact, it's impossible +/// to, because there are no variants to instantiate. Instead, it's just used as +/// a type parameter for [`MetadataKey`] and [`MetadataValue`]. +/// +/// [`MetadataKey`]: crate::metadata::MetadataKey +/// [`MetadataValue`]: crate::metadata::MetadataValue +#[derive(Clone, Debug, Eq, PartialEq, Hash)] +#[non_exhaustive] +pub enum Ascii {} + +impl Ascii { + pub(crate) fn is_valid_value(key: impl AsRef<[u8]>) -> bool { + // This array maps every byte (0-255) to a boolean (valid/invalid). + static VALID_METADATA_VALUE_CHARS: [bool; 256] = { + let mut table = [false; 256]; + + let mut i = 0x20; + while i <= 0x7E { + table[i as usize] = true; + i += 1; + } + table + }; + let bytes = key.as_ref(); + + for &b in bytes { + if !VALID_METADATA_VALUE_CHARS[b as usize] { + return false; + } + } + true + } +} + +/// gRPC metadata values can be either ASCII strings or binary. +/// This type should never be instantiated -- in fact, it's impossible +/// to, because there are no variants to instantiate. Instead, it's just used as +/// a type parameter for [`MetadataKey`] and [`MetadataValue`]. +/// +/// [`MetadataKey`]: crate::metadata::MetadataKey +/// [`MetadataValue`]: crate::metadata::MetadataValue +#[derive(Clone, Debug, Eq, PartialEq, Hash)] +#[non_exhaustive] +pub enum Binary {} + +// ===== impl ValueEncoding ===== + +impl ValueEncoding for Ascii { + fn is_valid_key(key: &str) -> bool { + !key.ends_with("-bin") && is_valid_key(key) + } + + fn from_bytes( + value: &[u8], + _: private::Internal, + ) -> Result { + let value = value.trim_ascii(); + + if value.is_empty() || !Ascii::is_valid_value(value) { + return Err(InvalidMetadataValueBytes::new()); + } + Ok(UnencodedHeaderValue::from_bytes(Bytes::copy_from_slice( + value, + ))) + } + + fn from_shared( + value: Bytes, + _: private::Internal, + ) -> Result { + let slice = value.as_ref(); + let trimmed = slice.trim_ascii(); + if !Ascii::is_valid_value(trimmed) { + return Err(InvalidMetadataValueBytes::new()); + } + + // If the length hasn't changed, we don't need to slice (saves a ref-count bump). + if trimmed.len() == slice.len() { + return Ok(UnencodedHeaderValue::from_bytes(value)); + } + + // Since 'trimmed' is a sub-slice of 'slice', we can calculate indices instantly. + let start = trimmed.as_ptr() as usize - slice.as_ptr() as usize; + let end = start + trimmed.len(); + + // This creates a new 'Bytes' pointing to the same memory region. + Ok(UnencodedHeaderValue::from_bytes(value.slice(start..end))) + } + + fn from_static(value: &'static str, _: private::Internal) -> UnencodedHeaderValue { + let value = value.trim_ascii(); + if !Ascii::is_valid_value(value) { + panic!("Invalid ASCII metadata value: {}", value) + } + UnencodedHeaderValue::from_bytes(Bytes::from_static(value.as_bytes())) + } + + fn decode(value: &[u8], _: private::Internal) -> Result { + let value = value.trim_ascii(); + + if value.is_empty() || !Ascii::is_valid_value(value) { + return Err(InvalidMetadataValueBytes::new()); + } + Ok(Bytes::copy_from_slice(value)) + } + + fn equals(a: &UnencodedHeaderValue, b: &[u8], _: private::Internal) -> bool { + a.as_bytes().as_ref() == b + } + + fn values_equal( + a: &UnencodedHeaderValue, + b: &UnencodedHeaderValue, + _: private::Internal, + ) -> bool { + a == b + } + + fn fmt( + value: &UnencodedHeaderValue, + f: &mut fmt::Formatter<'_>, + _: private::Internal, + ) -> fmt::Result { + fmt::Debug::fmt(value, f) + } + + fn encode(value: Bytes, _: private::Internal) -> Bytes { + value + } +} + +fn is_valid_key(key: impl AsRef<[u8]>) -> bool { + // This array maps every byte (0-255) to a boolean (valid/invalid). + static VALID_METADATA_KEY_CHARS: [bool; 256] = { + let mut table = [false; 256]; + + // Valid: 0-9 + let mut i = b'0'; + while i <= b'9' { + table[i as usize] = true; + i += 1; + } + + // Valid: a-z + let mut i = b'a'; + while i <= b'z' { + table[i as usize] = true; + i += 1; + } + + // Valid: special chars + table[b'_' as usize] = true; + table[b'-' as usize] = true; + table[b'.' as usize] = true; + + table + }; + let bytes = key.as_ref(); + if bytes.is_empty() { + return false; + } + + for &b in bytes { + if !VALID_METADATA_KEY_CHARS[b as usize] { + return false; + } + } + true +} + +impl ValueEncoding for Binary { + fn is_valid_key(key: &str) -> bool { + key.ends_with("-bin") && is_valid_key(key) + } + + fn from_bytes( + value: &[u8], + _: private::Internal, + ) -> Result { + Ok(UnencodedHeaderValue::from_bytes(Bytes::copy_from_slice( + value, + ))) + } + + fn from_shared( + value: Bytes, + _: private::Internal, + ) -> Result { + Ok(UnencodedHeaderValue::from_bytes(value)) + } + + fn from_static(value: &'static str, _: private::Internal) -> UnencodedHeaderValue { + UnencodedHeaderValue::from_bytes(Bytes::from_static(value.as_ref())) + } + + fn decode(value: &[u8], _: private::Internal) -> Result { + base64_util::STANDARD + .decode(value) + .map(|bytes_vec| bytes_vec.into()) + .map_err(|_| InvalidMetadataValueBytes::new()) + } + + fn equals(a: &UnencodedHeaderValue, b: &[u8], _: private::Internal) -> bool { + a.as_bytes().as_ref() == b + } + + fn values_equal( + a: &UnencodedHeaderValue, + b: &UnencodedHeaderValue, + _: private::Internal, + ) -> bool { + a.as_bytes() == b.as_bytes() + } + + fn fmt( + value: &UnencodedHeaderValue, + f: &mut fmt::Formatter<'_>, + _: private::Internal, + ) -> fmt::Result { + write!(f, "{:?}", value.as_bytes()) + } + + fn encode(value: Bytes, _: private::Internal) -> Bytes { + let encoded_value: String = base64_util::STANDARD_NO_PAD.encode(value); + Bytes::from(encoded_value) + } +} + +// ===== impl InvalidMetadataValue ===== + +impl InvalidMetadataValue { + pub(crate) fn new() -> Self { + InvalidMetadataValue {} + } +} + +impl fmt::Display for InvalidMetadataValue { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("failed to parse metadata value") + } +} + +impl Error for InvalidMetadataValue {} + +/// A possible error when converting a `MetadataValue` from a string or byte +/// slice. +#[derive(Debug, Hash)] +pub struct InvalidMetadataValueBytes(InvalidMetadataValue); + +// ===== impl InvalidMetadataValueBytes ===== + +impl InvalidMetadataValueBytes { + pub(crate) fn new() -> Self { + InvalidMetadataValueBytes(InvalidMetadataValue::new()) + } +} + +impl fmt::Display for InvalidMetadataValueBytes { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) + } +} + +impl Error for InvalidMetadataValueBytes {} + +mod base64_util { + use base64::alphabet; + use base64::engine::DecodePaddingMode; + use base64::engine::general_purpose::GeneralPurpose; + use base64::engine::general_purpose::GeneralPurposeConfig; + + pub(super) const STANDARD: GeneralPurpose = GeneralPurpose::new( + &alphabet::STANDARD, + GeneralPurposeConfig::new() + .with_encode_padding(true) + .with_decode_padding_mode(DecodePaddingMode::Indifferent), + ); + + pub(super) const STANDARD_NO_PAD: GeneralPurpose = GeneralPurpose::new( + &alphabet::STANDARD, + GeneralPurposeConfig::new() + .with_encode_padding(false) + .with_decode_padding_mode(DecodePaddingMode::Indifferent), + ); +} diff --git a/grpc/src/metadata/key.rs b/grpc/src/metadata/key.rs new file mode 100644 index 000000000..8a786235a --- /dev/null +++ b/grpc/src/metadata/key.rs @@ -0,0 +1,311 @@ +/* + * + * Copyright 2026 gRPC authors. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + * + */ + +use std::borrow::Borrow; +use std::error::Error; +use std::fmt; +use std::marker::PhantomData; +use std::str::FromStr; + +use http::header::HeaderName; + +use super::encoding::Ascii; +use super::encoding::Binary; +use super::encoding::ValueEncoding; + +/// Represents a custom metadata field name. +/// +/// `MetadataKey` is used as the [`MetadataMap`] key. +/// +/// [`MetadataMap`]: crate::metadata::MetadataMap +#[derive(Clone, Eq, PartialEq, Hash)] +#[repr(transparent)] +pub struct MetadataKey { + // Note: There are unsafe transmutes that assume that the memory layout + // of MetadataKey is identical to HeaderName + pub(crate) inner: HeaderName, + _phantom: PhantomData, +} + +/// A possible error when converting a `MetadataKey` from another type. +#[derive(Debug, Default)] +#[non_exhaustive] +pub struct InvalidMetadataKey {} + +/// An ascii metadata key. +pub type AsciiMetadataKey = MetadataKey; +/// A binary metadata key. +pub type BinaryMetadataKey = MetadataKey; + +impl MetadataKey { + /// Converts a slice of bytes to a `MetadataKey`. + /// + /// This function normalizes the input. + pub fn from_bytes(src: &[u8]) -> Result { + match HeaderName::from_bytes(src) { + Ok(name) => { + if !VE::is_valid_key(name.as_str()) { + return Err(InvalidMetadataKey::new()); + } + + Ok(MetadataKey { + inner: name, + _phantom: PhantomData, + }) + } + Err(_) => Err(InvalidMetadataKey::new()), + } + } + + /// Converts a static string to a `MetadataKey`. + /// + /// This function panics when the static string is a invalid metadata key. + /// + /// This function requires the static string to only contain lowercase + /// characters, numerals and symbols, as per the HTTP/2.0 specification + /// and metadata key names internal representation within this library. + /// + /// + /// # Examples + /// + /// ``` + /// # use grpc::metadata::*; + /// // Parsing a metadata key + /// let CUSTOM_KEY: &'static str = "custom-key"; + /// + /// let a = AsciiMetadataKey::from_bytes(b"custom-key").unwrap(); + /// let b = AsciiMetadataKey::from_static(CUSTOM_KEY); + /// assert_eq!(a, b); + /// ``` + /// + /// ```should_panic + /// # use grpc::metadata::*; + /// // Parsing a metadata key that contains invalid symbols(s): + /// AsciiMetadataKey::from_static("content{}{}length"); // This line panics! + /// ``` + /// + /// ```should_panic + /// # use grpc::metadata::*; + /// // Parsing a metadata key that contains invalid uppercase characters. + /// let a = AsciiMetadataKey::from_static("foobar"); + /// let b = AsciiMetadataKey::from_static("FOOBAR"); // This line panics! + /// ``` + /// + /// ```should_panic + /// # use grpc::metadata::*; + /// // Parsing a -bin metadata key as an Ascii key. + /// let b = AsciiMetadataKey::from_static("hello-bin"); // This line panics! + /// ``` + /// + /// ```should_panic + /// # use grpc::metadata::*; + /// // Parsing a non-bin metadata key as an Binary key. + /// let b = BinaryMetadataKey::from_static("hello"); // This line panics! + /// ``` + pub fn from_static(src: &'static str) -> Self { + let name = HeaderName::from_static(src); + if !VE::is_valid_key(name.as_str()) { + panic!("invalid metadata key") + } + + MetadataKey { + inner: name, + _phantom: PhantomData, + } + } + + /// Returns a `str` representation of the metadata key. + /// + /// The returned string will always be lower case. + #[inline] + pub fn as_str(&self) -> &str { + self.inner.as_str() + } + + /// Converts a HeaderName reference to a MetadataKey. This method assumes + /// that the caller has made sure that the metadata key name has the correct + /// "-bin" or non-"-bin" suffix, it does not validate its input. + #[inline] + pub(crate) fn unchecked_from_header_name_ref(header_name: &HeaderName) -> &Self { + unsafe { &*(header_name as *const HeaderName as *const Self) } + } +} + +impl FromStr for MetadataKey { + type Err = InvalidMetadataKey; + + fn from_str(s: &str) -> Result { + MetadataKey::from_bytes(s.as_bytes()).map_err(|_| InvalidMetadataKey::new()) + } +} + +impl AsRef for MetadataKey { + fn as_ref(&self) -> &str { + self.as_str() + } +} + +impl AsRef<[u8]> for MetadataKey { + fn as_ref(&self) -> &[u8] { + self.as_str().as_bytes() + } +} + +impl Borrow for MetadataKey { + fn borrow(&self) -> &str { + self.as_str() + } +} + +impl fmt::Debug for MetadataKey { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Debug::fmt(self.as_str(), fmt) + } +} + +impl fmt::Display for MetadataKey { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(self.as_str(), fmt) + } +} + +impl InvalidMetadataKey { + fn new() -> InvalidMetadataKey { + Self::default() + } +} + +impl<'a, VE: ValueEncoding> From<&'a MetadataKey> for MetadataKey { + fn from(src: &'a MetadataKey) -> MetadataKey { + src.clone() + } +} + +impl<'a, VE: ValueEncoding> PartialEq<&'a MetadataKey> for MetadataKey { + #[inline] + fn eq(&self, other: &&'a MetadataKey) -> bool { + *self == **other + } +} + +impl PartialEq> for &MetadataKey { + #[inline] + fn eq(&self, other: &MetadataKey) -> bool { + *other == *self + } +} + +impl PartialEq for MetadataKey { + /// Performs a case-insensitive comparison of the string against the + /// metadata key name. + /// + /// # Examples + /// + /// ``` + /// # use grpc::metadata::*; + /// let content_length = AsciiMetadataKey::from_static("content-length"); + /// + /// assert_eq!(content_length, "content-length"); + /// assert_eq!(content_length, "Content-Length"); + /// assert_ne!(content_length, "content length"); + /// ``` + #[inline] + fn eq(&self, other: &str) -> bool { + self.inner.eq(other) + } +} + +impl PartialEq> for str { + /// Performs a case-insensitive comparison of the string against the + /// metadata key name. + /// + /// # Examples + /// + /// ``` + /// # use grpc::metadata::*; + /// let content_length = AsciiMetadataKey::from_static("content-length"); + /// + /// assert_eq!(content_length, "content-length"); + /// assert_eq!(content_length, "Content-Length"); + /// assert_ne!(content_length, "content length"); + /// ``` + #[inline] + fn eq(&self, other: &MetadataKey) -> bool { + other.inner == *self + } +} + +impl<'a, VE: ValueEncoding> PartialEq<&'a str> for MetadataKey { + /// Performs a case-insensitive comparison of the string against the + /// metadata key name. + #[inline] + fn eq(&self, other: &&'a str) -> bool { + *self == **other + } +} + +impl PartialEq> for &str { + /// Performs a case-insensitive comparison of the string against the + /// metadata key name. + #[inline] + fn eq(&self, other: &MetadataKey) -> bool { + *other == *self + } +} + +impl fmt::Display for InvalidMetadataKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("invalid gRPC metadata key name") + } +} + +impl Error for InvalidMetadataKey {} + +#[cfg(test)] +mod tests { + use super::AsciiMetadataKey; + use super::BinaryMetadataKey; + + #[test] + fn test_from_bytes_binary() { + assert!(BinaryMetadataKey::from_bytes(b"").is_err()); + assert!(BinaryMetadataKey::from_bytes(b"\xFF").is_err()); + assert!(BinaryMetadataKey::from_bytes(b"abc").is_err()); + assert_eq!( + BinaryMetadataKey::from_bytes(b"abc-bin").unwrap().as_str(), + "abc-bin" + ); + } + + #[test] + fn test_from_bytes_ascii() { + assert!(AsciiMetadataKey::from_bytes(b"").is_err()); + assert!(AsciiMetadataKey::from_bytes(b"\xFF").is_err()); + assert_eq!( + AsciiMetadataKey::from_bytes(b"abc").unwrap().as_str(), + "abc" + ); + assert!(AsciiMetadataKey::from_bytes(b"abc-bin").is_err()); + } +} diff --git a/grpc/src/metadata/map.rs b/grpc/src/metadata/map.rs new file mode 100644 index 000000000..79fd06038 --- /dev/null +++ b/grpc/src/metadata/map.rs @@ -0,0 +1,1568 @@ +/* + * + * Copyright 2026 gRPC authors. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + * + */ + +use core::fmt; +use std::fmt::Debug; + +use http::HeaderMap; +use http::HeaderName; +use http::HeaderValue; + +use super::encoding::Ascii; +use super::encoding::Binary; +use super::encoding::ValueEncoding; +use super::key::MetadataKey; +use super::value::MetadataValue; +use crate::metadata::value::UnencodedHeaderValue; +use crate::private; + +/// A set of gRPC custom metadata entries. +/// +/// # Examples +/// +/// Basic usage +/// +/// ``` +/// # use grpc::metadata::*; +/// let mut map = MetadataMap::new(); +/// +/// map.insert("x-host", "example.com".parse().unwrap()); +/// map.insert("x-number", "123".parse().unwrap()); +/// map.insert_bin("trace-proto-bin", MetadataValue::from_bytes(b"[binary data]")); +/// +/// assert!(map.contains_key("x-host")); +/// assert!(!map.contains_key("x-location")); +/// +/// assert_eq!(map.get("x-host").unwrap(), "example.com"); +/// +/// map.remove("x-host"); +/// +/// assert!(!map.contains_key("x-host")); +/// ``` +#[derive(Clone, Debug, Default)] +pub struct MetadataMap { + headers: Vec<(HeaderName, UnencodedHeaderValue)>, +} + +/// `MetadataMap` entry iterator. +/// +/// Yields `KeyAndValueRef` values. The same metadata key name may be yielded +/// more than once if it has more than one associated value. +#[derive(Debug)] +pub struct Iter<'a> { + inner: std::slice::Iter<'a, (HeaderName, UnencodedHeaderValue)>, +} + +/// Reference to a key and an associated value in a `MetadataMap`. It can point +/// to either an ascii or a binary ("*-bin") key. +#[derive(Debug)] +pub enum KeyAndValueRef<'a> { + /// An ascii metadata key and value. + Ascii(&'a MetadataKey, &'a MetadataValue), + /// A binary metadata key and value. + Binary(&'a MetadataKey, &'a MetadataValue), +} + +/// Reference to a key in a `MetadataMap`. It can point +/// to either an ascii or a binary ("*-bin") key. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum Key { + /// An ascii metadata key and value. + Ascii(MetadataKey), + /// A binary metadata key and value. + Binary(MetadataKey), +} + +/// An iterator of all values associated with a single metadata key. +pub struct ValueIter<'a, VE> +where + MetadataKey: Debug, +{ + inner: std::slice::Iter<'a, (HeaderName, UnencodedHeaderValue)>, + key: Option>, +} + +impl<'a, VE> Debug for ValueIter<'a, VE> +where + VE: ValueEncoding, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("ValueIter") + .field("inner", &self.inner) + .field("key", &self.key) + .finish() + } +} + +/// A view to all values stored in a single entry. +/// +/// This struct is returned by `MetadataMap::get_all` and +/// `MetadataMap::get_all_bin`. +pub struct GetAll<'a, VE> { + map: &'a MetadataMap, + key: Option>, +} + +impl<'a, VE> std::fmt::Debug for GetAll<'a, VE> +where + VE: ValueEncoding, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("GetAll") + .field("map", &self.map) + .field("key", &self.key) + .finish() + } +} + +// ===== impl MetadataMap ===== + +impl MetadataMap { + /// Create an empty `MetadataMap`. + /// + /// The map will be created without any capacity. This function will not + /// allocate. + /// + /// # Examples + /// + /// ``` + /// # use grpc::metadata::*; + /// let map = MetadataMap::new(); + /// + /// assert!(map.is_empty()); + /// assert_eq!(0, map.capacity()); + /// ``` + pub fn new() -> Self { + MetadataMap::with_capacity(0) + } + + /// Convert an HTTP HeaderMap to a MetadataMap + pub(crate) fn from_headers(headers: HeaderMap) -> Self { + let mut ret = Vec::with_capacity(headers.len()); + let mut current_key: Option = None; + + for (key, value) in headers { + if let Some(k) = key { + current_key = Some(k); + } + + // If we don't have a key yet, skip to the next iteration. + let Some(k) = current_key.as_ref() else { + continue; + }; + let key_str = k.as_str(); + + if Ascii::is_valid_key(key_str) { + // We copy the header value here because the `HeaderValue` + // struct doesn't provide an API to fetch the underlying `Bytes`. + if let Ok(mut mv) = MetadataValue::::try_from(value.as_bytes()) { + mv.set_sensitive(value.is_sensitive()); + ret.push((k.clone(), mv.into_inner())); + } + } else if Binary::is_valid_key(key_str) + && let Ok(b) = Binary::decode(value.as_bytes(), private::Internal) + { + let mut mv = unsafe { MetadataValue::::from_shared_unchecked(b) }; + mv.set_sensitive(value.is_sensitive()); + ret.push((k.clone(), mv.into_inner())); + } + } + + Self { headers: ret } + } + + /// Convert a MetadataMap into a HTTP HeaderMap. + pub(crate) fn into_headers(self) -> HeaderMap { + let mut ret = HeaderMap::with_capacity(self.capacity()); + for (key, value) in self.headers { + let bytes = if key.as_str().ends_with("-bin") { + MetadataValue::::encode(value.into_bytes()) + } else { + MetadataValue::::encode(value.into_bytes()) + }; + // gRPC's validation is stricter than HTTP/2. + unsafe { + ret.append(key, HeaderValue::from_maybe_shared_unchecked(bytes)); + } + } + ret + } + + /// Create an empty `MetadataMap` with the specified capacity. + /// + /// The returned map will allocate internal storage in order to hold about + /// `capacity` elements without reallocating. However, this is a "best + /// effort" as there are usage patterns that could cause additional + /// allocations before `capacity` metadata entries are stored in the map. + /// + /// More capacity than requested may be allocated. + /// + /// # Examples + /// + /// ``` + /// # use grpc::metadata::*; + /// let map: MetadataMap = MetadataMap::with_capacity(10); + /// + /// assert!(map.is_empty()); + /// assert!(map.capacity() >= 10); + /// ``` + pub fn with_capacity(capacity: usize) -> MetadataMap { + MetadataMap { + headers: Vec::with_capacity(capacity), + } + } + + /// Returns the number of metadata entries (ascii and binary) stored in the + /// map. + /// + /// This number represents the total number of **values** stored in the map. + /// This number can be greater than or equal to the number of **keys** + /// stored given that a single key may have more than one associated value. + /// + /// # Examples + /// + /// ``` + /// # use grpc::metadata::*; + /// let mut map = MetadataMap::new(); + /// + /// assert_eq!(0, map.len()); + /// + /// map.insert("x-host-ip", "127.0.0.1".parse().unwrap()); + /// map.insert_bin("x-host-name-bin", MetadataValue::from_bytes(b"localhost")); + /// + /// assert_eq!(2, map.len()); + /// + /// map.append("x-host-ip", "text/html".parse().unwrap()); + /// + /// assert_eq!(3, map.len()); + /// ``` + pub fn len(&self) -> usize { + self.headers.len() + } + + /// Returns true if the map contains no elements. + /// + /// # Examples + /// + /// ``` + /// # use grpc::metadata::*; + /// let mut map = MetadataMap::new(); + /// + /// assert!(map.is_empty()); + /// + /// map.insert("x-host", "hello.world".parse().unwrap()); + /// + /// assert!(!map.is_empty()); + /// ``` + pub fn is_empty(&self) -> bool { + self.headers.is_empty() + } + + /// Clears the map, removing all key-value pairs. Keeps the allocated memory + /// for reuse. + /// + /// # Examples + /// + /// ``` + /// # use grpc::metadata::*; + /// let mut map = MetadataMap::new(); + /// map.insert("x-host", "hello.world".parse().unwrap()); + /// + /// map.clear(); + /// assert!(map.is_empty()); + /// assert!(map.capacity() > 0); + /// ``` + pub fn clear(&mut self) { + self.headers.clear(); + } + + /// Retains only the elements specified by the predicate. + /// + /// In other words, remove all key-value pairs `(k, v)` such that + /// `f(KeyAndValueRef)` returns `false`. + /// + /// # Examples + /// + /// ``` + /// # use grpc::metadata::*; + /// let mut map = MetadataMap::new(); + /// + /// map.insert("x-host", "hello".parse().unwrap()); + /// map.insert("x-number", "123".parse().unwrap()); + /// map.insert_bin("trace-proto-bin", MetadataValue::from_bytes(b"world")); + /// + /// map.retain(|entry| { + /// match entry { + /// KeyAndValueRef::Ascii(key, _) => key == "x-host", + /// _ => false, + /// } + /// }); + /// + /// assert_eq!(map.len(), 1); + /// assert!(map.contains_key("x-host")); + /// assert!(!map.contains_key("x-number")); + /// assert!(!map.contains_key("trace-proto-bin")); + /// ``` + pub fn retain(&mut self, mut f: F) + where + F: FnMut(KeyAndValueRef<'_>) -> bool, + { + self.headers.retain(|(name, value)| { + let key_and_value = if !name.as_str().ends_with("-bin") { + KeyAndValueRef::Ascii( + MetadataKey::unchecked_from_header_name_ref(name), + MetadataValue::unchecked_from_header_value_ref(value), + ) + } else { + KeyAndValueRef::Binary( + MetadataKey::unchecked_from_header_name_ref(name), + MetadataValue::unchecked_from_header_value_ref(value), + ) + }; + f(key_and_value) + }); + } + + /// Returns the number of custom metadata entries the map can hold without + /// reallocating. + /// + /// This number is an approximation as certain usage patterns could cause + /// additional allocations before the returned capacity is filled. + /// + /// # Examples + /// + /// ``` + /// # use grpc::metadata::*; + /// let mut map = MetadataMap::new(); + /// + /// assert_eq!(0, map.capacity()); + /// + /// map.insert("x-host", "hello.world".parse().unwrap()); + /// assert!(map.capacity() >= 1); + /// ``` + pub fn capacity(&self) -> usize { + self.headers.capacity() + } + + /// Reserves capacity for at least `additional` more custom metadata to be + /// inserted into the `MetadataMap`. + /// + /// The metadata map may reserve more space to avoid frequent reallocations. + /// The capacity is reserved relative to the existing length. After calling + /// `reserve`, capacity will be greater than or equal to + /// `self.len() + additional`. Does nothing if capacity is already + /// sufficient. + /// + /// Like with `with_capacity`, this will be a "best effort" to avoid + /// allocations until `additional` more custom metadata is inserted. Certain + /// usage patterns could cause additional allocations before the number is + /// reached. + /// + /// # Panics + /// + /// Panics if the new allocation size overflows `usize`. + /// + /// # Examples + /// + /// ``` + /// # use grpc::metadata::*; + /// let mut map = MetadataMap::new(); + /// map.reserve(10); + /// # map.insert("x-host", "bar".parse().unwrap()); + /// ``` + pub fn reserve(&mut self, additional: usize) { + self.headers.reserve(additional); + } + + /// Returns a reference to the value associated with the key. This method + /// is for ascii metadata entries (those whose names don't end with + /// "-bin"). For binary entries, use get_bin. + /// + /// If there are multiple values associated with the key, then the first one + /// is returned. Use `get_all` to get all values associated with a given + /// key. Returns `None` if there are no values associated with the key. + /// + /// # Examples + /// + /// ``` + /// # use grpc::metadata::*; + /// let mut map = MetadataMap::new(); + /// assert!(map.get("x-host").is_none()); + /// + /// map.insert("x-host", "hello".parse().unwrap()); + /// assert_eq!(map.get("x-host").unwrap(), &"hello"); + /// assert_eq!(map.get("x-host").unwrap(), &"hello"); + /// + /// map.append("x-host", "world".parse().unwrap()); + /// assert_eq!(map.get("x-host").unwrap(), &"hello"); + /// + /// // Attempting to read a key of the wrong type fails by not + /// // finding anything. + /// map.append_bin("host-bin", MetadataValue::from_bytes(b"world")); + /// assert!(map.get("host-bin").is_none()); + /// assert!(map.get("host-bin".to_string()).is_none()); + /// assert!(map.get(&("host-bin".to_string())).is_none()); + /// + /// // Attempting to read an invalid key string fails by not + /// // finding anything. + /// assert!(map.get("host{}bin").is_none()); + /// assert!(map.get("host{}bin".to_string()).is_none()); + /// assert!(map.get(&("host{}bin".to_string())).is_none()); + /// ``` + pub fn get(&self, key: K) -> Option<&MetadataValue> + where + K: AsMetadataKey, + { + key.get(self, private::Internal) + } + + /// Like get, but for Binary keys (for example "trace-proto-bin"). + /// + /// # Examples + /// + /// ``` + /// # use grpc::metadata::*; + /// let mut map = MetadataMap::new(); + /// assert!(map.get_bin("trace-proto-bin").is_none()); + /// + /// map.insert_bin("trace-proto-bin", MetadataValue::from_bytes(b"hello")); + /// assert_eq!(map.get_bin("trace-proto-bin").unwrap(), &"hello"); + /// assert_eq!(map.get_bin("trace-proto-bin").unwrap(), &"hello"); + /// + /// map.append_bin("trace-proto-bin", MetadataValue::from_bytes(b"world")); + /// assert_eq!(map.get_bin("trace-proto-bin").unwrap(), &"hello"); + /// + /// // Attempting to read a key of the wrong type fails by not + /// // finding anything. + /// map.append("host", "world".parse().unwrap()); + /// assert!(map.get_bin("host").is_none()); + /// assert!(map.get_bin("host".to_string()).is_none()); + /// assert!(map.get_bin(&("host".to_string())).is_none()); + /// + /// // Attempting to read an invalid key string fails by not + /// // finding anything. + /// assert!(map.get_bin("host{}-bin").is_none()); + /// assert!(map.get_bin("host{}-bin".to_string()).is_none()); + /// assert!(map.get_bin(&("host{}-bin".to_string())).is_none()); + /// ``` + pub fn get_bin(&self, key: K) -> Option<&MetadataValue> + where + K: AsMetadataKey, + { + key.get(self, private::Internal) + } + + /// Returns a view of all values associated with a key. This method is for + /// ascii metadata entries (those whose names don't end with "-bin"). For + /// binary entries, use get_all_bin. + /// + /// The returned view does not incur any allocations and allows iterating + /// the values associated with the key. See [`GetAll`] for more details. + /// Returns `None` if there are no values associated with the key. + /// + /// [`GetAll`]: crate::metadata::GetAll + /// + /// # Examples + /// + /// ``` + /// # use grpc::metadata::*; + /// let mut map = MetadataMap::new(); + /// + /// map.insert("x-host", "hello".parse().unwrap()); + /// map.append("x-host", "goodbye".parse().unwrap()); + /// + /// { + /// let view = map.get_all("x-host"); + /// + /// let mut iter = view.iter(); + /// assert_eq!(&"hello", iter.next().unwrap()); + /// assert_eq!(&"goodbye", iter.next().unwrap()); + /// assert!(iter.next().is_none()); + /// } + /// + /// // Attempting to read a key of the wrong type fails by not + /// // finding anything. + /// map.append_bin("host-bin", MetadataValue::from_bytes(b"world")); + /// assert!(map.get_all("host-bin").iter().next().is_none()); + /// assert!(map.get_all("host-bin".to_string()).iter().next().is_none()); + /// assert!(map.get_all(&("host-bin".to_string())).iter().next().is_none()); + /// + /// // Attempting to read an invalid key string fails by not + /// // finding anything. + /// assert!(map.get_all("host{}").iter().next().is_none()); + /// assert!(map.get_all("host{}".to_string()).iter().next().is_none()); + /// assert!(map.get_all(&("host{}".to_string())).iter().next().is_none()); + /// ``` + pub fn get_all(&self, key: K) -> GetAll<'_, Ascii> + where + K: AsMetadataKey, + { + key.get_all(self, private::Internal) + } + + /// Like get_all, but for Binary keys (for example "trace-proto-bin"). + /// + /// # Examples + /// + /// ``` + /// # use grpc::metadata::*; + /// let mut map = MetadataMap::new(); + /// + /// map.insert_bin("trace-proto-bin", MetadataValue::from_bytes(b"hello")); + /// map.append_bin("trace-proto-bin", MetadataValue::from_bytes(b"goodbye")); + /// + /// { + /// let view = map.get_all_bin("trace-proto-bin"); + /// + /// let mut iter = view.iter(); + /// assert_eq!(&"hello", iter.next().unwrap()); + /// assert_eq!(&"goodbye", iter.next().unwrap()); + /// assert!(iter.next().is_none()); + /// } + /// + /// // Attempting to read a key of the wrong type fails by not + /// // finding anything. + /// map.append("host", "world".parse().unwrap()); + /// assert!(map.get_all_bin("host").iter().next().is_none()); + /// assert!(map.get_all_bin("host".to_string()).iter().next().is_none()); + /// assert!(map.get_all_bin(&("host".to_string())).iter().next().is_none()); + /// + /// // Attempting to read an invalid key string fails by not + /// // finding anything. + /// assert!(map.get_all_bin("host{}-bin").iter().next().is_none()); + /// assert!(map.get_all_bin("host{}-bin".to_string()).iter().next().is_none()); + /// assert!(map.get_all_bin(&("host{}-bin".to_string())).iter().next().is_none()); + /// ``` + pub fn get_all_bin(&self, key: K) -> GetAll<'_, Binary> + where + K: AsMetadataKey, + { + key.get_all(self, private::Internal) + } + + /// Returns true if the map contains a value for the specified key. This + /// method works for both ascii and binary entries. + /// + /// # Examples + /// + /// ``` + /// # use grpc::metadata::*; + /// let mut map = MetadataMap::new(); + /// assert!(!map.contains_key("x-host")); + /// + /// map.append_bin("host-bin", MetadataValue::from_bytes(b"world")); + /// map.insert("x-host", "world".parse().unwrap()); + /// + /// // contains_key works for both Binary and Ascii keys: + /// assert!(map.contains_key("x-host")); + /// assert!(map.contains_key("host-bin")); + /// + /// // contains_key returns false for invalid keys: + /// assert!(!map.contains_key("x{}host")); + /// ``` + pub fn contains_key(&self, key: K) -> bool + where + K: AsEncodingAgnosticMetadataKey, + { + key.contains_key(self, private::Internal) + } + + /// An iterator visiting all key-value pairs (both ascii and binary). + /// + /// The iteration order is arbitrary, but consistent across platforms for + /// the same crate version. Each key will be yielded once per associated + /// value. So, if a key has 3 associated values, it will be yielded 3 times. + /// + /// # Examples + /// + /// ``` + /// # use grpc::metadata::*; + /// let mut map = MetadataMap::new(); + /// + /// map.insert("x-word", "hello".parse().unwrap()); + /// map.append("x-word", "goodbye".parse().unwrap()); + /// map.insert("x-number", "123".parse().unwrap()); + /// + /// for key_and_value in map.iter() { + /// match key_and_value { + /// KeyAndValueRef::Ascii(ref key, ref value) => + /// println!("Ascii: {:?}: {:?}", key, value), + /// KeyAndValueRef::Binary(ref key, ref value) => + /// println!("Binary: {:?}: {:?}", key, value), + /// } + /// } + /// ``` + pub fn iter(&self) -> Iter<'_> { + Iter { + inner: self.headers.iter(), + } + } + + /// Inserts an ascii key-value pair into the map. To insert a binary entry, + /// use `insert_bin`. + /// + /// This method panics when the given key is a string and it cannot be + /// converted to a `MetadataKey`. + /// + /// If the map did not previously have this key present, then `None` is + /// returned. + /// + /// If the map did have this key present, the new value is associated with + /// the key and all previous values are removed. **Note** that only a single + /// one of the previous values is returned. If there are multiple values + /// that have been previously associated with the key, then the first one is + /// returned. See `insert_mult` on `OccupiedEntry` for an API that returns + /// all values. + /// + /// The key is not updated, though; this matters for types that can be `==` + /// without being identical. + /// + /// # Examples + /// + /// ``` + /// # use grpc::metadata::*; + /// let mut map = MetadataMap::new(); + /// assert!(map.insert("x-host", "world".parse().unwrap()).is_none()); + /// assert!(!map.is_empty()); + /// + /// let mut prev = map.insert("x-host", "earth".parse().unwrap()).unwrap(); + /// assert_eq!("world", prev); + /// ``` + /// + /// ```should_panic + /// # use grpc::metadata::*; + /// let mut map = MetadataMap::new(); + /// // Trying to insert a key that is not valid panics. + /// map.insert("x{}host", "world".parse().unwrap()); + /// ``` + /// + /// ```should_panic + /// # use grpc::metadata::*; + /// let mut map = MetadataMap::new(); + /// // Trying to insert a key that is binary panics (use insert_bin). + /// map.insert("x-host-bin", "world".parse().unwrap()); + /// ``` + pub fn insert(&mut self, key: K, val: MetadataValue) -> Option> + where + K: IntoMetadataKey, + { + key.insert(self, val, private::Internal) + } + + /// Like insert, but for Binary keys (for example "trace-proto-bin"). + /// + /// This method panics when the given key is a string and it cannot be + /// converted to a `MetadataKey`. + /// + /// # Examples + /// + /// ``` + /// # use grpc::metadata::*; + /// let mut map = MetadataMap::new(); + /// assert!(map.insert_bin("trace-proto-bin", MetadataValue::from_bytes(b"world")).is_none()); + /// assert!(!map.is_empty()); + /// + /// let mut prev = map.insert_bin("trace-proto-bin", MetadataValue::from_bytes(b"earth")).unwrap(); + /// assert_eq!("world", prev); + /// ``` + /// + /// ```should_panic + /// # use grpc::metadata::*; + /// let mut map = MetadataMap::default(); + /// // Attempting to add a binary metadata entry with an invalid name + /// map.insert_bin("trace-proto", MetadataValue::from_bytes(b"hello")); // This line panics! + /// ``` + /// + /// ```should_panic + /// # use grpc::metadata::*; + /// let mut map = MetadataMap::new(); + /// // Trying to insert a key that is not valid panics. + /// map.insert_bin("x{}host-bin", MetadataValue::from_bytes(b"world")); // This line panics! + /// ``` + pub fn insert_bin( + &mut self, + key: K, + val: MetadataValue, + ) -> Option> + where + K: IntoMetadataKey, + { + key.insert(self, val, private::Internal) + } + + /// Inserts an ascii key-value pair into the map. To insert a binary entry, + /// use `append_bin`. + /// + /// This method panics when the given key is a string and it cannot be + /// converted to a `MetadataKey`. + /// + /// If the map did not previously have this key present, then `false` is + /// returned. + /// + /// If the map did have this key present, the new value is pushed to the end + /// of the list of values currently associated with the key. The key is not + /// updated, though; this matters for types that can be `==` without being + /// identical. + /// + /// # Examples + /// + /// ``` + /// # use grpc::metadata::*; + /// let mut map = MetadataMap::new(); + /// assert!(map.insert("x-host", "world".parse().unwrap()).is_none()); + /// assert!(!map.is_empty()); + /// + /// map.append("x-host", "earth".parse().unwrap()); + /// + /// let values = map.get_all("x-host"); + /// let mut i = values.iter(); + /// assert_eq!("world", *i.next().unwrap()); + /// assert_eq!("earth", *i.next().unwrap()); + /// ``` + /// + /// ```should_panic + /// # use grpc::metadata::*; + /// let mut map = MetadataMap::new(); + /// // Trying to append a key that is not valid panics. + /// map.append("x{}host", "world".parse().unwrap()); // This line panics! + /// ``` + /// + /// ```should_panic + /// # use grpc::metadata::*; + /// let mut map = MetadataMap::new(); + /// // Trying to append a key that is binary panics (use append_bin). + /// map.append("x-host-bin", "world".parse().unwrap()); // This line panics! + /// ``` + pub fn append(&mut self, key: K, value: MetadataValue) + where + K: IntoMetadataKey, + { + key.append(self, value, private::Internal); + } + + /// Like append, but for binary keys (for example "trace-proto-bin"). + /// + /// This method panics when the given key is a string and it cannot be + /// converted to a `MetadataKey`. + /// + /// # Examples + /// + /// ``` + /// # use grpc::metadata::*; + /// let mut map = MetadataMap::new(); + /// assert!(map.insert_bin("trace-proto-bin", MetadataValue::from_bytes(b"world")).is_none()); + /// assert!(!map.is_empty()); + /// + /// map.append_bin("trace-proto-bin", MetadataValue::from_bytes(b"earth")); + /// + /// let values = map.get_all_bin("trace-proto-bin"); + /// let mut i = values.iter(); + /// assert_eq!("world", *i.next().unwrap()); + /// assert_eq!("earth", *i.next().unwrap()); + /// ``` + /// + /// ```should_panic + /// # use grpc::metadata::*; + /// let mut map = MetadataMap::new(); + /// // Trying to append a key that is not valid panics. + /// map.append_bin("x{}host-bin", MetadataValue::from_bytes(b"world")); // This line panics! + /// ``` + /// + /// ```should_panic + /// # use grpc::metadata::*; + /// let mut map = MetadataMap::new(); + /// // Trying to append a key that is ascii panics (use append). + /// map.append_bin("x-host", MetadataValue::from_bytes(b"world")); // This line panics! + /// ``` + pub fn append_bin(&mut self, key: K, value: MetadataValue) + where + K: IntoMetadataKey, + { + key.append(self, value, private::Internal); + } + + /// Removes an ascii key from the map, returning the value associated with + /// the key. To remove a binary key, use `remove_bin`. + /// + /// Returns `None` if the map does not contain the key. If there are + /// multiple values associated with the key, then the first one is returned. + /// + /// # Examples + /// + /// ``` + /// # use grpc::metadata::*; + /// let mut map = MetadataMap::new(); + /// map.insert("x-host", "hello.world".parse().unwrap()); + /// + /// let prev = map.remove("x-host").unwrap(); + /// assert_eq!("hello.world", prev); + /// + /// assert!(map.remove("x-host").is_none()); + /// + /// // Attempting to remove a key of the wrong type fails by not + /// // finding anything. + /// map.append_bin("host-bin", MetadataValue::from_bytes(b"world")); + /// assert!(map.remove("host-bin").is_none()); + /// assert!(map.remove("host-bin".to_string()).is_none()); + /// assert!(map.remove(&("host-bin".to_string())).is_none()); + /// + /// // Attempting to remove an invalid key string fails by not + /// // finding anything. + /// assert!(map.remove("host{}").is_none()); + /// assert!(map.remove("host{}".to_string()).is_none()); + /// assert!(map.remove(&("host{}".to_string())).is_none()); + /// ``` + pub fn remove(&mut self, key: K) -> Option> + where + K: AsMetadataKey, + { + key.remove(self, private::Internal) + } + + /// Like remove, but for Binary keys (for example "trace-proto-bin"). + /// + /// # Examples + /// + /// ``` + /// # use grpc::metadata::*; + /// let mut map = MetadataMap::new(); + /// map.insert_bin("trace-proto-bin", MetadataValue::from_bytes(b"hello.world")); + /// + /// let prev = map.remove_bin("trace-proto-bin").unwrap(); + /// assert_eq!("hello.world", prev); + /// + /// assert!(map.remove_bin("trace-proto-bin").is_none()); + /// + /// // Attempting to remove a key of the wrong type fails by not + /// // finding anything. + /// map.append("host", "world".parse().unwrap()); + /// assert!(map.remove_bin("host").is_none()); + /// assert!(map.remove_bin("host".to_string()).is_none()); + /// assert!(map.remove_bin(&("host".to_string())).is_none()); + /// + /// // Attempting to remove an invalid key string fails by not + /// // finding anything. + /// assert!(map.remove_bin("host{}-bin").is_none()); + /// assert!(map.remove_bin("host{}-bin".to_string()).is_none()); + /// assert!(map.remove_bin(&("host{}-bin".to_string())).is_none()); + /// ``` + pub fn remove_bin(&mut self, key: K) -> Option> + where + K: AsMetadataKey, + { + key.remove(self, private::Internal) + } + + /// Removes all values for the given key. + /// + /// # Examples + /// + /// ``` + /// # use grpc::metadata::*; + /// let mut map = MetadataMap::new(); + /// map.insert("x-host", "example.com".parse().unwrap()); + /// map.append("x-host", "another.com".parse().unwrap()); + /// + /// map.remove_all("x-host"); + /// assert!(map.is_empty()); + /// ``` + pub fn remove_all(&mut self, key: K) + where + K: AsMetadataKey, + { + key.remove_all(self, private::Internal) + } + + /// Removes all entries matching the given binary key. + /// + /// This is the binary-key equivalent of [`remove_all`]. + /// + /// [`remove_all`]: Self::remove_all + pub fn remove_all_bin(&mut self, key: K) + where + K: AsMetadataKey, + { + key.remove_all(self, private::Internal) + } + + pub(crate) fn merge(&mut self, other: MetadataMap) { + self.headers.extend(other.headers); + } +} + +impl From for MetadataMap { + fn from(tonic_map: tonic::metadata::MetadataMap) -> Self { + Self::from_headers(tonic_map.into_headers()) + } +} + +impl From for tonic::metadata::MetadataMap { + fn from(map: MetadataMap) -> Self { + Self::from_headers(map.into_headers()) + } +} + +// ===== impl Iter ===== + +impl<'a> Iterator for Iter<'a> { + type Item = KeyAndValueRef<'a>; + + fn next(&mut self) -> Option { + self.inner.next().map(|(name, value)| { + if !name.as_str().ends_with("-bin") { + KeyAndValueRef::Ascii( + MetadataKey::unchecked_from_header_name_ref(name), + MetadataValue::unchecked_from_header_value_ref(value), + ) + } else { + KeyAndValueRef::Binary( + MetadataKey::unchecked_from_header_name_ref(name), + MetadataValue::unchecked_from_header_value_ref(value), + ) + } + }) + } + + fn size_hint(&self) -> (usize, Option) { + self.inner.size_hint() + } +} + +// ===== impl ValueIter ===== + +impl<'a, VE: ValueEncoding> Iterator for ValueIter<'a, VE> +where + VE: 'a, +{ + type Item = &'a MetadataValue; + + fn next(&mut self) -> Option { + let key = self.key.as_ref()?; + for (k, value) in self.inner.by_ref() { + if k == key.inner { + return Some(MetadataValue::unchecked_from_header_value_ref(value)); + } + } + None + } +} + +// ===== impl GetAll ===== + +impl<'a, VE: ValueEncoding> GetAll<'a, VE> { + /// Returns an iterator visiting all values associated with the entry. + /// + /// Values are iterated in insertion order. + /// + /// # Examples + /// + /// ``` + /// # use grpc::metadata::*; + /// let mut map = MetadataMap::new(); + /// map.insert("x-host", "hello.world".parse().unwrap()); + /// map.append("x-host", "hello.earth".parse().unwrap()); + /// + /// let values = map.get_all("x-host"); + /// let mut iter = values.iter(); + /// assert_eq!(&"hello.world", iter.next().unwrap()); + /// assert_eq!(&"hello.earth", iter.next().unwrap()); + /// assert!(iter.next().is_none()); + /// ``` + pub fn iter(&self) -> ValueIter<'a, VE> { + ValueIter { + inner: self.map.headers.iter(), + key: self.key.clone(), + } + } +} + +impl PartialEq for GetAll<'_, VE> { + fn eq(&self, other: &Self) -> bool { + self.iter().eq(other.iter()) + } +} + +impl<'a, VE: ValueEncoding> IntoIterator for GetAll<'a, VE> +where + VE: 'a, +{ + type Item = &'a MetadataValue; + type IntoIter = ValueIter<'a, VE>; + + fn into_iter(self) -> ValueIter<'a, VE> { + self.iter() + } +} + +impl<'a, 'b: 'a, VE: ValueEncoding> IntoIterator for &'b GetAll<'a, VE> { + type Item = &'a MetadataValue; + type IntoIter = ValueIter<'a, VE>; + + fn into_iter(self) -> ValueIter<'a, VE> { + self.iter() + } +} + +// ===== impl IntoMetadataKey / AsMetadataKey ===== + +/// A marker trait used to identify values that can be used as insert keys +/// to a `MetadataMap`. +pub trait IntoMetadataKey { + #[doc(hidden)] + fn insert( + self, + map: &mut MetadataMap, + val: MetadataValue, + _: private::Internal, + ) -> Option>; + + #[doc(hidden)] + fn append(self, map: &mut MetadataMap, val: MetadataValue, _: private::Internal); +} + +// ==== impls ==== + +impl IntoMetadataKey for MetadataKey { + #[doc(hidden)] + #[inline] + fn insert( + self, + map: &mut MetadataMap, + val: MetadataValue, + _: private::Internal, + ) -> Option> { + let key = self.inner; + let mut new_val = Some(val.into_inner()); + let mut old_val = None; + + let mut write_idx = 0; + + for read_idx in 0..map.headers.len() { + if map.headers[read_idx].0 == key { + if let Some(v) = new_val.take() { + let replaced = std::mem::replace(&mut map.headers[read_idx].1, v); + old_val = Some(MetadataValue::unchecked_from_header_value(replaced)); + write_idx += 1; + } + // Duplicates do nothing and are effectively dropped when we truncate. + } else { + // Keep non-matching elements. + if read_idx != write_idx { + map.headers.swap(read_idx, write_idx); + } + write_idx += 1; + } + } + + map.headers.truncate(write_idx); + + // If `new_val` was never taken, the key didn't exist. Push it. + if let Some(v) = new_val { + map.headers.push((key, v)); + } + + old_val + } + + #[doc(hidden)] + #[inline] + fn append(self, map: &mut MetadataMap, val: MetadataValue, _: private::Internal) { + map.headers.push((self.inner, val.into_inner())); + } +} + +impl IntoMetadataKey for &MetadataKey { + #[doc(hidden)] + #[inline] + fn insert( + self, + map: &mut MetadataMap, + val: MetadataValue, + _: private::Internal, + ) -> Option> { + let key = &self.inner; + let mut new_val = Some(val.into_inner()); + let mut old_val = None; + + let mut write_idx = 0; + + for read_idx in 0..map.headers.len() { + if map.headers[read_idx].0 == key { + if let Some(v) = new_val.take() { + let replaced = std::mem::replace(&mut map.headers[read_idx].1, v); + old_val = Some(MetadataValue::unchecked_from_header_value(replaced)); + write_idx += 1; + } + // Duplicates do nothing and are effectively dropped when we truncate. + } else { + // Keep non-matching elements. + if read_idx != write_idx { + map.headers.swap(read_idx, write_idx); + } + write_idx += 1; + } + } + + map.headers.truncate(write_idx); + + // If `new_val` was never taken, the key didn't exist. Push it. + if let Some(v) = new_val { + map.headers.push((key.clone(), v)); + } + + old_val + } + + #[doc(hidden)] + #[inline] + fn append(self, map: &mut MetadataMap, val: MetadataValue, _: private::Internal) { + map.headers.push((self.inner.clone(), val.into_inner())); + } +} + +impl IntoMetadataKey for &'static str { + #[doc(hidden)] + #[inline] + fn insert( + self, + map: &mut MetadataMap, + val: MetadataValue, + token: private::Internal, + ) -> Option> { + // Perform name validation + let key = MetadataKey::::from_static(self); + key.insert(map, val, token) + } + #[doc(hidden)] + #[inline] + fn append(self, map: &mut MetadataMap, val: MetadataValue, token: private::Internal) { + // Perform name validation + let key = MetadataKey::::from_static(self); + key.append(map, val, token); + } +} + +/// A marker trait used to identify values that can be used as search keys +/// to a `MetadataMap`. +pub trait AsMetadataKey { + #[doc(hidden)] + fn get(self, map: &MetadataMap, _: private::Internal) -> Option<&MetadataValue>; + + #[doc(hidden)] + fn get_all(self, map: &MetadataMap, _: private::Internal) -> GetAll<'_, VE>; + + #[doc(hidden)] + fn remove(self, map: &mut MetadataMap, _: private::Internal) -> Option>; + + #[doc(hidden)] + fn remove_all(self, map: &mut MetadataMap, _: private::Internal); +} + +// ==== impls ==== + +impl AsMetadataKey for MetadataKey { + #[doc(hidden)] + #[inline] + fn get(self, map: &MetadataMap, _: private::Internal) -> Option<&MetadataValue> { + map.headers + .iter() + .find(|(k, _)| k == self.inner) + .map(|(_, v)| MetadataValue::unchecked_from_header_value_ref(v)) + } + + #[doc(hidden)] + #[inline] + fn get_all(self, map: &MetadataMap, _: private::Internal) -> GetAll<'_, VE> { + GetAll { + map, + key: Some(self), + } + } + + #[doc(hidden)] + #[inline] + fn remove(self, map: &mut MetadataMap, token: private::Internal) -> Option> { + self.inner.as_str().remove(map, token) + } + + #[doc(hidden)] + #[inline] + fn remove_all(self, map: &mut MetadataMap, _: private::Internal) { + map.headers.retain(|h| h.0 != self.inner); + } +} + +impl AsMetadataKey for &MetadataKey { + #[doc(hidden)] + #[inline] + fn get(self, map: &MetadataMap, _: private::Internal) -> Option<&MetadataValue> { + map.headers + .iter() + .find(|(k, _)| k == self.inner) + .map(|(_, v)| MetadataValue::unchecked_from_header_value_ref(v)) + } + + #[doc(hidden)] + #[inline] + fn get_all(self, map: &MetadataMap, _: private::Internal) -> GetAll<'_, VE> { + GetAll { + map, + key: Some(self.clone()), + } + } + + #[doc(hidden)] + #[inline] + fn remove(self, map: &mut MetadataMap, token: private::Internal) -> Option> { + self.inner.as_str().remove(map, token) + } + + #[doc(hidden)] + #[inline] + fn remove_all(self, map: &mut MetadataMap, _: private::Internal) { + map.headers.retain(|h| h.0 != self.inner); + } +} + +impl AsMetadataKey for &str { + #[doc(hidden)] + #[inline] + fn get(self, map: &MetadataMap, _: private::Internal) -> Option<&MetadataValue> { + if !VE::is_valid_key(self) { + return None; + } + map.headers + .iter() + .find(|(k, _)| k == self) + .map(|(_, v)| MetadataValue::unchecked_from_header_value_ref(v)) + } + + #[doc(hidden)] + #[inline] + fn get_all(self, map: &MetadataMap, _: private::Internal) -> GetAll<'_, VE> { + let key = if VE::is_valid_key(self) { + Some(MetadataKey::::from_bytes(self.as_bytes()).unwrap()) + } else { + None + }; + GetAll { map, key } + } + + #[doc(hidden)] + #[inline] + fn remove(self, map: &mut MetadataMap, _: private::Internal) -> Option> { + if !VE::is_valid_key(self) { + return None; + } + + let mut extracted = map.headers.extract_if(.., |(k, _v)| *k == self); + let first_match = extracted + .next() + .map(|(_k, v)| MetadataValue::unchecked_from_header_value(v)); + + extracted.for_each(drop); + first_match + } + + #[doc(hidden)] + #[inline] + fn remove_all(self, map: &mut MetadataMap, _: private::Internal) { + if !VE::is_valid_key(self) { + return; + } + map.headers.retain(|h| h.0 != self); + } +} + +impl AsMetadataKey for String { + #[doc(hidden)] + #[inline] + fn get(self, map: &MetadataMap, token: private::Internal) -> Option<&MetadataValue> { + AsMetadataKey::::get(self.as_str(), map, token) + } + + #[doc(hidden)] + #[inline] + fn get_all(self, map: &MetadataMap, token: private::Internal) -> GetAll<'_, VE> { + AsMetadataKey::::get_all(self.as_str(), map, token) + } + + #[doc(hidden)] + #[inline] + fn remove(self, map: &mut MetadataMap, token: private::Internal) -> Option> { + AsMetadataKey::::remove(self.as_str(), map, token) + } + + #[doc(hidden)] + #[inline] + fn remove_all(self, map: &mut MetadataMap, token: private::Internal) { + AsMetadataKey::::remove_all(self.as_str(), map, token) + } +} + +impl AsMetadataKey for &String { + #[doc(hidden)] + #[inline] + fn get(self, map: &MetadataMap, token: private::Internal) -> Option<&MetadataValue> { + AsMetadataKey::::get(self.as_str(), map, token) + } + + #[doc(hidden)] + #[inline] + fn get_all(self, map: &MetadataMap, token: private::Internal) -> GetAll<'_, VE> { + AsMetadataKey::::get_all(self.as_str(), map, token) + } + + #[doc(hidden)] + #[inline] + fn remove(self, map: &mut MetadataMap, token: private::Internal) -> Option> { + AsMetadataKey::::remove(self.as_str(), map, token) + } + + #[doc(hidden)] + #[inline] + fn remove_all(self, map: &mut MetadataMap, token: private::Internal) { + AsMetadataKey::::remove_all(self.as_str(), map, token) + } +} + +/// A marker trait used to identify values that can be used as search keys +/// to a `MetadataMap`, for operations that don't expose the actual value. +pub trait AsEncodingAgnosticMetadataKey { + #[doc(hidden)] + fn contains_key(&self, map: &MetadataMap, _: private::Internal) -> bool; +} + +// ==== impls ==== + +impl AsEncodingAgnosticMetadataKey for MetadataKey { + #[doc(hidden)] + #[inline] + fn contains_key(&self, map: &MetadataMap, _: private::Internal) -> bool { + map.headers.iter().any(|(k, _)| k == self.inner) + } +} + +impl AsEncodingAgnosticMetadataKey for &MetadataKey { + #[doc(hidden)] + #[inline] + fn contains_key(&self, map: &MetadataMap, _: private::Internal) -> bool { + map.headers.iter().any(|(k, _)| k == self.inner) + } +} + +impl AsEncodingAgnosticMetadataKey for &str { + #[doc(hidden)] + #[inline] + fn contains_key(&self, map: &MetadataMap, _: private::Internal) -> bool { + map.headers.iter().any(|(k, _)| k == *self) + } +} + +impl AsEncodingAgnosticMetadataKey for String { + #[doc(hidden)] + #[inline] + fn contains_key(&self, map: &MetadataMap, _: private::Internal) -> bool { + map.headers.iter().any(|(k, _)| k == self.as_str()) + } +} + +impl AsEncodingAgnosticMetadataKey for &String { + #[doc(hidden)] + #[inline] + fn contains_key(&self, map: &MetadataMap, _: private::Internal) -> bool { + map.headers.iter().any(|(k, _)| k == self.as_str()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_from_headers_filters_invalid_headers() { + let mut http_map = http::HeaderMap::new(); + + // Valid ASCII + http_map.insert("x-host", "example.com".parse().unwrap()); + // Valid Binary (decoded from base64) + http_map.insert("trace-proto-bin", "SGVsbG8hIQ==".parse().unwrap()); + + // Invalid gRPC key name (contains '!', which is valid in HTTP but not in gRPC metadata) + http_map.insert(HeaderName::from_static("x-host!"), "val".parse().unwrap()); + + // Invalid ASCII value (contains characters >= 127) + // gRPC only allows visible ASCII [32-126]. + // Let's use a byte > 127 which is valid in HTTP HeaderValue but invalid + // in gRPC MetadataValue. + http_map.insert("x-invalid-ascii", HeaderValue::from_bytes(&[0xFA]).unwrap()); + + // Invalid Binary value (not valid base64) + http_map.insert("invalid-bin", "not-base64-!!!".parse().unwrap()); + + let map = MetadataMap::from_headers(http_map); + + assert_eq!(map.len(), 2); + assert_eq!(map.get("x-host").unwrap(), "example.com"); + assert_eq!(map.get_bin("trace-proto-bin").unwrap(), "Hello!!"); + + assert!(!map.contains_key("x-host!")); + assert!(!map.contains_key("x-invalid-ascii")); + assert!(!map.contains_key("invalid-bin")); + } + + #[test] + fn test_into_headers() { + let mut map = MetadataMap::new(); + + map.insert("x-host", "example.com".parse().unwrap()); + map.append("x-host", "google.com".parse().unwrap()); + map.insert_bin("trace-proto-bin", MetadataValue::from_bytes(b"Hello!!")); + + let headers = map.into_headers(); + + assert_eq!(headers.len(), 3); + + let hosts: Vec<_> = headers.get_all("x-host").iter().collect(); + assert_eq!(hosts.len(), 2); + assert_eq!(hosts[0], "example.com"); + assert_eq!(hosts[1], "google.com"); + + assert_eq!(headers.get("trace-proto-bin").unwrap(), "SGVsbG8hIQ"); + } + + #[test] + fn test_iter_categorizes_ascii_entries() { + let mut map = MetadataMap::new(); + + map.insert("x-word", "hello".parse().unwrap()); + map.append_bin("x-word-bin", MetadataValue::from_bytes(b"goodbye")); + map.insert_bin("x-number-bin", MetadataValue::from_bytes(b"123")); + + let mut found_x_word = false; + for key_and_value in map.iter() { + if let KeyAndValueRef::Ascii(key, _value) = key_and_value { + if key.as_str() == "x-word" { + found_x_word = true; + } else { + panic!("Unexpected key"); + } + } + } + assert!(found_x_word); + } + + #[test] + fn test_iter_categorizes_binary_entries() { + let mut map = MetadataMap::new(); + + map.insert("x-word", "hello".parse().unwrap()); + map.append_bin("x-word-bin", MetadataValue::from_bytes(b"goodbye")); + + let mut found_x_word_bin = false; + for key_and_value in map.iter() { + if let KeyAndValueRef::Binary(key, _value) = key_and_value { + if key.as_str() == "x-word-bin" { + found_x_word_bin = true; + } else { + panic!("Unexpected key"); + } + } + } + assert!(found_x_word_bin); + } + + #[test] + fn test_remove_all_preserves_other_keys_order() { + let mut map = MetadataMap::new(); + map.append("a", "1".parse().unwrap()); + map.append("b", "2".parse().unwrap()); + map.append("a", "3".parse().unwrap()); + map.append("b", "4".parse().unwrap()); + + map.remove_all("a"); + + let keys: Vec<_> = map + .iter() + .map(|kv| match kv { + KeyAndValueRef::Ascii(_, v) => v.to_str(), + _ => panic!(), + }) + .collect(); + assert_eq!(keys, vec!["2", "4"]); + } + + #[test] + fn test_remove_all_bin() { + let mut map = MetadataMap::new(); + map.insert_bin( + "trace-proto-bin", + MetadataValue::from_bytes(b"[binary data]"), + ); + map.append_bin( + "trace-proto-bin", + MetadataValue::from_bytes(b"[binary data 2]"), + ); + map.insert("x-host", "example.com".parse().unwrap()); + map.remove_all_bin("trace-proto-bin"); + + assert!(map.get_bin("trace-proto-bin").is_none()); + assert!(map.get("x-host").is_some()); + } + + #[test] + fn test_retain() { + let mut map = MetadataMap::new(); + map.insert("x-host", "hello".parse().unwrap()); + map.insert("x-number", "123".parse().unwrap()); + map.insert_bin("trace-proto-bin", MetadataValue::from_bytes(b"world")); + + map.retain(|entry| match entry { + KeyAndValueRef::Ascii(key, _) => key == "x-host", + _ => false, + }); + + assert_eq!(map.len(), 1); + assert!(map.contains_key("x-host")); + assert!(!map.contains_key("x-number")); + assert!(!map.contains_key("trace-proto-bin")); + } + + #[test] + fn test_tonic_conversions() { + let mut map = MetadataMap::new(); + map.insert("x-host", "example.com".parse().unwrap()); + map.insert_bin("trace-proto-bin", MetadataValue::from_bytes(b"Hello!!")); + + let tonic_map: tonic::metadata::MetadataMap = map.clone().into(); + assert_eq!(tonic_map.len(), 2); + assert_eq!(tonic_map.get("x-host").unwrap(), "example.com"); + assert_eq!(tonic_map.get_bin("trace-proto-bin").unwrap(), "Hello!!"); + + let back_map: MetadataMap = tonic_map.into(); + assert_eq!(back_map.len(), 2); + assert_eq!(back_map.get("x-host").unwrap(), "example.com"); + assert_eq!(back_map.get_bin("trace-proto-bin").unwrap(), "Hello!!"); + } +} diff --git a/grpc/src/metadata/mod.rs b/grpc/src/metadata/mod.rs new file mode 100644 index 000000000..138e8edd2 --- /dev/null +++ b/grpc/src/metadata/mod.rs @@ -0,0 +1,54 @@ +/* + * + * Copyright 2026 gRPC authors. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + * + */ + +//! Contains data structures and utilities for handling gRPC custom metadata. + +mod encoding; +mod key; +mod map; +mod value; + +pub use encoding::Ascii; +pub use encoding::Binary; +pub use key::AsciiMetadataKey; +pub use key::BinaryMetadataKey; +pub use key::MetadataKey; +pub use map::GetAll; +pub use map::Iter; +pub use map::Key; +pub use map::KeyAndValueRef; +pub use map::MetadataMap; +pub use map::ValueIter; +pub use value::AsciiMetadataValue; +pub use value::BinaryMetadataValue; +pub use value::MetadataValue; + +/// The metadata::errors module contains types for errors that can occur +/// while handling gRPC custom metadata. +pub mod errors { + pub use super::encoding::InvalidMetadataValue; + pub use super::encoding::InvalidMetadataValueBytes; + pub use super::key::InvalidMetadataKey; + pub use super::value::ToStrError; +} diff --git a/grpc/src/metadata/value.rs b/grpc/src/metadata/value.rs new file mode 100644 index 000000000..368d79817 --- /dev/null +++ b/grpc/src/metadata/value.rs @@ -0,0 +1,890 @@ +/* + * + * Copyright 2026 gRPC authors. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + * + */ + +use std::cmp; +use std::error::Error; +use std::fmt; +use std::fmt::Write; +use std::hash::Hash; +use std::hash::Hasher; +use std::marker::PhantomData; +use std::str::FromStr; + +use bytes::Bytes; +use bytes::BytesMut; + +use super::encoding::Ascii; +use super::encoding::Binary; +use super::encoding::InvalidMetadataValue; +use super::encoding::InvalidMetadataValueBytes; +use super::encoding::ValueEncoding; +use crate::private; + +/// Represents a custom metadata field value. +/// +/// `MetadataValue` is used as the [`MetadataMap`] value. +/// +/// [`MetadataMap`]: crate::metadata::MetadataMap +#[derive(Clone)] +#[repr(transparent)] +pub struct MetadataValue { + // Note: There are unsafe transmutes that assume that the memory layout + // of MetadataValue is identical to UnencodedHeaderValue. + inner: UnencodedHeaderValue, + _phantom: PhantomData, +} + +#[derive(Clone, PartialEq, Eq)] +pub struct UnencodedHeaderValue { + data: Bytes, + is_sensitive: bool, +} + +impl UnencodedHeaderValue { + // Assumes that the bytes have already been validated. + pub(crate) fn from_bytes(bytes: Bytes) -> Self { + UnencodedHeaderValue { + data: bytes, + is_sensitive: false, + } + } + + pub(crate) fn into_bytes(self) -> Bytes { + self.data + } + + pub(crate) fn as_bytes(&self) -> &Bytes { + &self.data + } +} + +const fn is_visible_ascii(b: u8) -> bool { + b >= 32 && b < 127 || b == b'\t' +} + +impl fmt::Debug for UnencodedHeaderValue { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.is_sensitive { + f.write_str("Sensitive") + } else { + f.write_str("\"")?; + let mut from = 0; + let bytes = self.as_bytes(); + for (i, &b) in bytes.iter().enumerate() { + if !is_visible_ascii(b) || b == b'"' { + if from != i { + f.write_str(unsafe { str::from_utf8_unchecked(&bytes[from..i]) })?; + } + if b == b'"' { + f.write_str("\\\"")?; + } else { + write!(f, "\\x{:x}", b)?; + } + from = i + 1; + } + } + + f.write_str(unsafe { str::from_utf8_unchecked(&bytes[from..]) })?; + f.write_str("\"") + } + } +} + +/// A possible error when converting a `MetadataValue` to a string representation. +/// +/// Metadata field values may contain opaque bytes, in which case it is not +/// possible to represent the value as a string. +#[derive(Debug)] +#[non_exhaustive] +pub struct ToStrError {} + +/// An ascii metadata value. +pub type AsciiMetadataValue = MetadataValue; +/// A binary metadata value. +pub type BinaryMetadataValue = MetadataValue; + +impl MetadataValue { + /// Convert a `Bytes` directly into a `MetadataValue` without validating. + /// + /// # Safety + /// + /// This function does NOT validate that illegal bytes are not contained + /// within the buffer. + #[inline] + pub unsafe fn from_shared_unchecked(src: Bytes) -> Self { + MetadataValue { + inner: UnencodedHeaderValue::from_bytes(src), + _phantom: PhantomData, + } + } + + /// Mark that the metadata value represents sensitive information. + /// + /// # Examples + /// + /// ``` + /// # use grpc::metadata::*; + /// let mut val = AsciiMetadataValue::from_static("my secret"); + /// + /// val.set_sensitive(true); + /// assert!(val.is_sensitive()); + /// + /// val.set_sensitive(false); + /// assert!(!val.is_sensitive()); + /// ``` + #[inline] + pub fn set_sensitive(&mut self, val: bool) { + self.inner.is_sensitive = val; + } + + /// Returns `true` if the value represents sensitive data. + /// + /// Sensitive data could represent passwords or other data that should not + /// be stored on disk or in memory. This setting can be used by components + /// like caches to avoid storing the value. HPACK encoders must set the + /// metadata field to never index when `is_sensitive` returns true. + /// + /// Note that sensitivity is not factored into equality or ordering. + /// + /// # Examples + /// + /// ``` + /// # use grpc::metadata::*; + /// let mut val = AsciiMetadataValue::from_static("my secret"); + /// + /// val.set_sensitive(true); + /// assert!(val.is_sensitive()); + /// + /// val.set_sensitive(false); + /// assert!(!val.is_sensitive()); + /// ``` + #[inline] + pub fn is_sensitive(&self) -> bool { + self.inner.is_sensitive + } + + /// Converts an `UnencodedHeaderValue` to a `MetadataValue`. This method + /// assumes that the caller has made sure that the value is of the correct + /// `Ascii` or `Binary` value encoding. + #[inline] + pub(crate) fn unchecked_from_header_value(value: UnencodedHeaderValue) -> Self { + MetadataValue { + inner: value, + _phantom: PhantomData, + } + } + + /// Converts an `UnencodedHeaderValue` reference to a `MetadataValue`. This + /// method assumes that the caller has made sure that the value is of the + /// correct `Ascii` or `Binary` value encoding. + #[inline] + pub(crate) fn unchecked_from_header_value_ref(header_value: &UnencodedHeaderValue) -> &Self { + unsafe { &*(header_value as *const UnencodedHeaderValue as *const Self) } + } + + /// Converts an `UnencodedHeaderValue` reference to a `MetadataValue`. This + /// method assumes that the caller has made sure that the value is of the + /// correct `Ascii` or `Binary` value encoding. + #[inline] + pub(crate) fn unchecked_from_mut_header_value_ref( + header_value: &mut UnencodedHeaderValue, + ) -> &mut Self { + unsafe { &mut *(header_value as *mut UnencodedHeaderValue as *mut Self) } + } + + pub(crate) fn into_inner(self) -> UnencodedHeaderValue { + self.inner + } +} + +impl MetadataValue { + pub(crate) fn encode(value: Bytes) -> Bytes { + VE::encode(value, private::Internal) + } + + /// Convert a static string to a `MetadataValue`. + /// + /// This function will not perform any copying, however the string is + /// checked to ensure that no invalid characters are present. + /// + /// For Ascii values, only visible ASCII characters (32-127) are permitted. + /// For Binary values, the string must be valid base64. + /// + /// # Panics + /// + /// This function panics if the argument contains invalid metadata value + /// characters. + /// + /// # Examples + /// + /// ``` + /// # use grpc::metadata::*; + /// let val = AsciiMetadataValue::from_static("hello"); + /// assert_eq!(val, "hello"); + /// ``` + /// + /// ``` + /// # use grpc::metadata::*; + /// let val = BinaryMetadataValue::from_static("Hello!!"); + /// assert_eq!(val, "Hello!!"); + /// ``` + #[inline] + pub fn from_static(src: &'static str) -> Self { + MetadataValue { + inner: VE::from_static(src, private::Internal), + _phantom: PhantomData, + } + } +} + +/// Attempt to convert a byte slice to a `MetadataValue`. +/// +/// For Ascii metadata values, If the argument contains invalid metadata +/// value bytes, an error is returned. Only byte values between 32 and 126 +/// (inclusive) are permitted. +/// +/// For Binary metadata values this method cannot fail. See also the Binary +/// only version of this method `from_bytes`. +/// +/// # Examples +/// +/// ``` +/// # use grpc::metadata::*; +/// let val = AsciiMetadataValue::try_from(b"hello").unwrap(); +/// assert_eq!(val, &b"hello"[..]); +/// ``` +/// +/// An invalid value +/// +/// ``` +/// # use grpc::metadata::*; +/// let val = AsciiMetadataValue::try_from(b"\n"); +/// assert!(val.is_err()); +/// ``` +impl TryFrom<&[u8]> for MetadataValue { + type Error = InvalidMetadataValueBytes; + + #[inline] + fn try_from(src: &[u8]) -> Result { + VE::from_bytes(src, private::Internal).map(|value| MetadataValue { + inner: value, + _phantom: PhantomData, + }) + } +} + +/// Attempt to convert a byte slice to a `MetadataValue`. +/// +/// For Ascii metadata values, If the argument contains invalid metadata +/// value bytes, an error is returned. Only byte values between 32 and 126 +/// (inclusive) are permitted. +/// +/// For Binary metadata values this method cannot fail. See also the Binary +/// only version of this method `from_bytes`. +/// +/// # Examples +/// +/// ``` +/// # use grpc::metadata::*; +/// let val = AsciiMetadataValue::try_from(b"hello").unwrap(); +/// assert_eq!(val, &b"hello"[..]); +/// ``` +/// +/// An invalid value +/// +/// ``` +/// # use grpc::metadata::*; +/// let val = AsciiMetadataValue::try_from(b"\n"); +/// assert!(val.is_err()); +/// ``` +impl TryFrom<&[u8; N]> for MetadataValue { + type Error = InvalidMetadataValueBytes; + + #[inline] + fn try_from(src: &[u8; N]) -> Result { + Self::try_from(src.as_ref()) + } +} + +/// Attempt to convert a `Bytes` buffer to a `MetadataValue`. +/// +/// For Ascii metadata values, If the argument contains invalid metadata +/// value bytes, an error is returned. Only byte values between 32 and 126 +/// (inclusive) are permitted. +/// +/// For Binary metadata values this method cannot fail. See also the Binary +/// only version of this method `from_bytes`. +impl TryFrom for MetadataValue { + type Error = InvalidMetadataValueBytes; + + #[inline] + fn try_from(src: Bytes) -> Result { + VE::from_shared(src, private::Internal).map(|value| MetadataValue { + inner: value, + _phantom: PhantomData, + }) + } +} + +/// Attempt to convert a Vec of bytes to a `MetadataValue`. +/// +/// For Ascii metadata values, If the argument contains invalid metadata +/// value bytes, an error is returned. Only byte values between 32 and 126 +/// (inclusive) are permitted. +/// +/// For Binary metadata values this method cannot fail. See also the Binary +/// only version of this method `from_bytes`. +impl TryFrom> for MetadataValue { + type Error = InvalidMetadataValueBytes; + + #[inline] + fn try_from(src: Vec) -> Result { + Self::try_from(src.as_slice()) + } +} + +/// Attempt to convert a string to a `MetadataValue`. +/// +/// If the argument contains invalid metadata value characters, an error is +/// returned. Only visible ASCII characters (32-126) are permitted. +impl<'a> TryFrom<&'a str> for MetadataValue { + type Error = InvalidMetadataValue; + + #[inline] + fn try_from(s: &'a str) -> Result { + s.parse() + } +} + +/// Attempt to convert a string to a `MetadataValue`. +/// +/// If the argument contains invalid metadata value characters, an error is +/// returned. Only visible ASCII characters (32-126) are permitted. +impl<'a> TryFrom<&'a String> for MetadataValue { + type Error = InvalidMetadataValue; + + #[inline] + fn try_from(s: &'a String) -> Result { + s.parse() + } +} + +/// Attempt to convert a string to a `MetadataValue`. +/// +/// If the argument contains invalid metadata value characters, an error is +/// returned. Only visible ASCII characters (32-126) are permitted. +impl TryFrom for MetadataValue { + type Error = InvalidMetadataValue; + + #[inline] + fn try_from(s: String) -> Result { + s.parse() + } +} + +impl MetadataValue { + /// Yields a `&str` slice. This is infallible since the `MetadataValue` + /// only contains visible ASCII characters. + /// + /// # Examples + /// + /// ``` + /// # use grpc::metadata::*; + /// let val = AsciiMetadataValue::from_static("hello"); + /// assert_eq!(val.to_str(), "hello"); + /// ``` + pub fn to_str(&self) -> &str { + unsafe { std::str::from_utf8_unchecked(self.inner.data.as_ref()) } + } + + /// Converts a `MetadataValue` to a byte slice. For Binary values, use + /// `to_bytes`. + /// + /// # Examples + /// + /// ``` + /// # use grpc::metadata::*; + /// let val = AsciiMetadataValue::from_static("hello"); + /// assert_eq!(val.as_bytes(), b"hello"); + /// ``` + #[inline] + pub fn as_bytes(&self) -> &[u8] { + self.inner.data.as_ref() + } +} + +impl MetadataValue { + /// Convert a byte slice to a `MetadataValue`. + /// + /// # Examples + /// + /// ``` + /// # use tonic::metadata::*; + /// let val = BinaryMetadataValue::from_bytes(b"hello\xfa"); + /// assert_eq!(val, &b"hello\xfa"[..]); + /// ``` + #[inline] + pub fn from_bytes(src: &[u8]) -> Self { + // Only the Ascii version of try_from can fail. + Self::try_from(src).unwrap() + } +} + +impl fmt::Debug for MetadataValue { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + VE::fmt(&self.inner, f, private::Internal) + } +} + +macro_rules! from_integers { + ($($name:ident: $t:ident => $max_len:expr),*) => {$( + impl From<$t> for MetadataValue { + fn from(num: $t) -> MetadataValue { + let mut buf = BytesMut::with_capacity($max_len); + let _ = buf.write_str(itoa::Buffer::new().format(num)); + let inner = UnencodedHeaderValue { + data: buf.freeze(), + is_sensitive: false, + }; + MetadataValue { + inner, + _phantom: PhantomData, + } + } + } + + #[test] + fn $name() { + let n: $t = 55; + let val = AsciiMetadataValue::from(n); + assert_eq!(val, &n.to_string()); + + let n = $t::MAX; + let val = AsciiMetadataValue::from(n); + assert_eq!(val, &n.to_string()); + } + )*}; +} + +from_integers! { + // integer type => maximum decimal length + + // u8 purposely left off... AsciiMetadataValue::from(b'3') could be confusing + from_u16: u16 => 5, + from_i16: i16 => 6, + from_u32: u32 => 10, + from_i32: i32 => 11, + from_u64: u64 => 20, + from_i64: i64 => 20 +} + +#[cfg(target_pointer_width = "16")] +from_integers! { + from_usize: usize => 5, + from_isize: isize => 6 +} + +#[cfg(target_pointer_width = "32")] +from_integers! { + from_usize: usize => 10, + from_isize: isize => 11 +} + +#[cfg(target_pointer_width = "64")] +from_integers! { + from_usize: usize => 20, + from_isize: isize => 20 +} + +impl FromStr for MetadataValue { + type Err = InvalidMetadataValue; + + #[inline] + fn from_str(s: &str) -> Result, Self::Err> { + AsciiMetadataValue::try_from(s.as_bytes()).map_err(|_| InvalidMetadataValue::new()) + } +} + +impl From> for Bytes { + #[inline] + fn from(value: MetadataValue) -> Bytes { + value.inner.data + } +} + +impl<'a, VE: ValueEncoding> From<&'a MetadataValue> for MetadataValue { + #[inline] + fn from(t: &'a MetadataValue) -> Self { + t.clone() + } +} + +// ===== ToStrError ===== + +impl fmt::Display for ToStrError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("failed to convert metadata to a str") + } +} + +impl Error for ToStrError {} + +impl Hash for MetadataValue { + fn hash(&self, state: &mut H) { + self.inner.data.hash(state) + } +} + +impl Hash for MetadataValue { + fn hash(&self, state: &mut H) { + self.inner.data.hash(state) + } +} + +// ===== PartialEq / PartialOrd ===== + +impl PartialEq for MetadataValue { + #[inline] + fn eq(&self, other: &MetadataValue) -> bool { + // Note: Different binary strings that after base64 decoding + // will count as the same value for Binary values. Also, + // different invalid base64 values count as equal for Binary + // values. + VE::values_equal(&self.inner, &other.inner, private::Internal) + } +} + +impl Eq for MetadataValue {} + +impl PartialOrd for MetadataValue { + #[inline] + fn partial_cmp(&self, other: &MetadataValue) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for MetadataValue { + #[inline] + fn cmp(&self, other: &Self) -> cmp::Ordering { + self.inner.data.cmp(&other.inner.data) + } +} + +impl PartialEq for MetadataValue { + #[inline] + fn eq(&self, other: &str) -> bool { + VE::equals(&self.inner, other.as_bytes(), private::Internal) + } +} + +impl PartialEq<[u8]> for MetadataValue { + #[inline] + fn eq(&self, other: &[u8]) -> bool { + VE::equals(&self.inner, other, private::Internal) + } +} + +impl PartialOrd for MetadataValue { + #[inline] + fn partial_cmp(&self, other: &str) -> Option { + self.inner.data.partial_cmp(other.as_bytes()) + } +} + +impl PartialOrd<[u8]> for MetadataValue { + #[inline] + fn partial_cmp(&self, other: &[u8]) -> Option { + self.inner.data.partial_cmp(other) + } +} + +impl PartialEq> for str { + #[inline] + fn eq(&self, other: &MetadataValue) -> bool { + *other == *self + } +} + +impl PartialEq> for [u8] { + #[inline] + fn eq(&self, other: &MetadataValue) -> bool { + *other == *self + } +} + +impl PartialOrd> for str { + #[inline] + fn partial_cmp(&self, other: &MetadataValue) -> Option { + self.as_bytes().partial_cmp(other.inner.data.as_ref()) + } +} + +impl PartialOrd> for [u8] { + #[inline] + fn partial_cmp(&self, other: &MetadataValue) -> Option { + self.partial_cmp(other.inner.data.as_ref()) + } +} + +impl PartialEq for MetadataValue { + #[inline] + fn eq(&self, other: &String) -> bool { + *self == other[..] + } +} + +impl PartialOrd for MetadataValue { + #[inline] + fn partial_cmp(&self, other: &String) -> Option { + self.inner.data.partial_cmp(other.as_bytes()) + } +} + +impl PartialEq> for String { + #[inline] + fn eq(&self, other: &MetadataValue) -> bool { + *other == *self + } +} + +impl PartialOrd> for String { + #[inline] + fn partial_cmp(&self, other: &MetadataValue) -> Option { + self.as_bytes().partial_cmp(other.inner.data.as_ref()) + } +} + +impl PartialEq> for &MetadataValue { + #[inline] + fn eq(&self, other: &MetadataValue) -> bool { + **self == *other + } +} + +impl PartialOrd> for &MetadataValue { + #[inline] + fn partial_cmp(&self, other: &MetadataValue) -> Option { + (**self).partial_cmp(other) + } +} + +impl<'a, VE: ValueEncoding, T: ?Sized> PartialEq<&'a T> for MetadataValue +where + MetadataValue: PartialEq, +{ + #[inline] + fn eq(&self, other: &&'a T) -> bool { + *self == **other + } +} + +impl<'a, VE: ValueEncoding, T: ?Sized> PartialOrd<&'a T> for MetadataValue +where + MetadataValue: PartialOrd, +{ + #[inline] + fn partial_cmp(&self, other: &&'a T) -> Option { + self.partial_cmp(*other) + } +} + +impl PartialEq> for &str { + #[inline] + fn eq(&self, other: &MetadataValue) -> bool { + *other == *self + } +} + +impl PartialOrd> for &str { + #[inline] + fn partial_cmp(&self, other: &MetadataValue) -> Option { + self.as_bytes().partial_cmp(other.inner.data.as_ref()) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_debug() { + let cases = &[ + ("hello", "\"hello\""), + ("hello \"world\"", "\"hello \\\"world\\\"\""), + ]; + + for &(value, expected) in cases { + let val = AsciiMetadataValue::try_from(value.as_bytes()).unwrap(); + let actual = format!("{val:?}"); + assert_eq!(expected, actual); + } + + let mut sensitive = AsciiMetadataValue::from_static("password"); + sensitive.set_sensitive(true); + assert_eq!("Sensitive", format!("{sensitive:?}")); + } + + #[test] + fn test_valid_metadata_values() { + assert!(MetadataValue::::try_from("".as_bytes()).is_err()); + assert!(MetadataValue::::try_from(" ".as_bytes()).is_err()); // empty after trimming. + assert!(MetadataValue::::try_from("".as_bytes()).is_ok()); + assert!(MetadataValue::::try_from("a".as_bytes()).is_ok()); + assert!(MetadataValue::::try_from("abc".as_bytes()).is_ok()); + + // Non-printable ASCII characters + assert!(MetadataValue::::try_from("\0".as_bytes()).is_err()); + assert!(MetadataValue::::try_from("\n".as_bytes()).is_err()); + assert!(MetadataValue::::try_from("\x7f".as_bytes()).is_err()); + assert!(MetadataValue::::try_from("\0".as_bytes()).is_ok()); + assert!(MetadataValue::::try_from("\n".as_bytes()).is_ok()); + + // Unicode characters + assert!(MetadataValue::::try_from("🦀".as_bytes()).is_err()); + assert!(MetadataValue::::try_from("ü".as_bytes()).is_err()); + assert!(MetadataValue::::try_from("🦀".as_bytes()).is_ok()); + assert!(MetadataValue::::try_from("ü".as_bytes()).is_ok()); + } + + #[test] + fn test_value_eq_value() { + type Bmv = BinaryMetadataValue; + type Amv = AsciiMetadataValue; + + assert_eq!(Amv::from_static("abc"), Amv::from_static("abc")); + assert_ne!(Amv::from_static("abc"), Amv::from_static("ABC")); + + assert_eq!(Bmv::from_bytes(b"abc"), Bmv::from_bytes(b"abc")); + assert_ne!(Bmv::from_bytes(b"abc"), Bmv::from_bytes(b"ABC")); + + // Invalid values are all just invalid from this point of view. + unsafe { + assert_ne!( + Bmv::from_shared_unchecked(Bytes::from_static(b"..{}")), + Bmv::from_shared_unchecked(Bytes::from_static(b"{}..")) + ); + } + } + + #[test] + fn test_value_eq_str() { + type Bmv = BinaryMetadataValue; + type Amv = AsciiMetadataValue; + + assert_eq!(Amv::from_static("abc"), "abc"); + assert_ne!(Amv::from_static("abc"), "ABC"); + assert_eq!("abc", Amv::from_static("abc")); + assert_ne!("ABC", Amv::from_static("abc")); + + assert_eq!(Bmv::from_bytes(b"abc"), "abc"); + assert_ne!(Bmv::from_bytes(b"abc"), "ABC"); + assert_eq!("abc", Bmv::from_bytes(b"abc")); + assert_ne!("ABC", Bmv::from_bytes(b"abc")); + } + + #[test] + fn test_value_eq_bytes() { + type Bmv = BinaryMetadataValue; + type Amv = AsciiMetadataValue; + + assert_eq!(Amv::from_static("abc"), "abc".as_bytes()); + assert_ne!(Amv::from_static("abc"), "ABC".as_bytes()); + assert_eq!(*"abc".as_bytes(), Amv::from_static("abc")); + assert_ne!(*"ABC".as_bytes(), Amv::from_static("abc")); + + assert_eq!(*"abc".as_bytes(), Bmv::from_bytes(b"abc")); + assert_ne!(*"ABC".as_bytes(), Bmv::from_bytes(b"abc")); + } + + #[test] + fn test_ascii_value_hash() { + use std::collections::hash_map::DefaultHasher; + type Amv = AsciiMetadataValue; + + fn hash(value: Amv) -> u64 { + let mut hasher = DefaultHasher::new(); + value.hash(&mut hasher); + hasher.finish() + } + + let value1 = Amv::from_static("abc"); + let value2 = Amv::from_static("abc"); + assert_eq!(value1, value2); + assert_eq!(hash(value1), hash(value2)); + + let value1 = Amv::from_static("abc"); + let value2 = Amv::from_static("xyz"); + + assert_ne!(value1, value2); + assert_ne!(hash(value1), hash(value2)); + } + + #[test] + fn test_valid_binary_value_hash() { + use std::collections::hash_map::DefaultHasher; + type Bmv = BinaryMetadataValue; + + fn hash(value: Bmv) -> u64 { + let mut hasher = DefaultHasher::new(); + value.hash(&mut hasher); + hasher.finish() + } + + let value1 = Bmv::from_bytes(b"abc"); + let value2 = Bmv::from_bytes(b"abc"); + assert_eq!(value1, value2); + assert_eq!(hash(value1), hash(value2)); + + let value1 = Bmv::from_bytes(b"abc"); + let value2 = Bmv::from_bytes(b"xyz"); + assert_ne!(value1, value2); + assert_ne!(hash(value1), hash(value2)); + } + + #[test] + fn test_invalid_binary_value_hash() { + use std::collections::hash_map::DefaultHasher; + type Bmv = BinaryMetadataValue; + + fn hash(value: Bmv) -> u64 { + let mut hasher = DefaultHasher::new(); + value.hash(&mut hasher); + hasher.finish() + } + + unsafe { + let value1 = Bmv::from_shared_unchecked(Bytes::from_static(b"..{}")); + let value2 = Bmv::from_shared_unchecked(Bytes::from_static(b"{}..")); + assert_ne!(value1, value2); + assert_ne!(hash(value1), hash(value2)); + } + + unsafe { + let valid = Bmv::from_bytes(b"abc"); + let invalid = Bmv::from_shared_unchecked(Bytes::from_static(b"{}..")); + assert_ne!(valid, invalid); + assert_ne!(hash(valid), hash(invalid)); + } + } +}