use crate::bytes::{BufMut, Bytes, BytesMut};
use super::status::{Code, GrpcError, Status};
use super::streaming::{
Metadata, MetadataValue, normalize_metadata_key, sanitize_metadata_ascii_value,
};
const TRAILER_FLAG: u8 = 0x80;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ContentType {
GrpcWeb,
GrpcWebText,
}
impl ContentType {
fn matches_media_type(value: &str, prefix: &str) -> bool {
value.starts_with(prefix)
&& matches!(value.as_bytes().get(prefix.len()), None | Some(b'+' | b';'))
}
#[must_use]
pub fn from_header_value(value: &str) -> Option<Self> {
let lower = value.trim().to_ascii_lowercase();
if Self::matches_media_type(&lower, "application/grpc-web-text") {
Some(Self::GrpcWebText)
} else if Self::matches_media_type(&lower, "application/grpc-web") {
Some(Self::GrpcWeb)
} else {
None
}
}
#[must_use]
pub const fn as_header_value(self) -> &'static str {
match self {
Self::GrpcWeb => "application/grpc-web+proto",
Self::GrpcWebText => "application/grpc-web-text+proto",
}
}
#[must_use]
pub const fn is_text_mode(self) -> bool {
matches!(self, Self::GrpcWebText)
}
}
#[derive(Debug, Clone)]
pub enum WebFrame {
Data {
compressed: bool,
data: Bytes,
},
Trailers(TrailerFrame),
}
#[derive(Debug, Clone)]
pub struct TrailerFrame {
pub status: Status,
pub metadata: Metadata,
}
pub fn encode_trailers(status: &Status, metadata: &Metadata, dst: &mut BytesMut) {
let mut block = String::new();
block.push_str("grpc-status: ");
block.push_str(&status.code().as_i32().to_string());
block.push_str("\r\n");
if !status.message().is_empty() {
block.push_str("grpc-message: ");
let sanitized_msg = status
.message()
.replace('%', "%25")
.replace('\r', "%0D")
.replace('\n', "%0A");
block.push_str(&sanitized_msg);
block.push_str("\r\n");
}
for (key, value) in metadata.iter() {
let Some(key_lower) =
normalize_metadata_key(key, matches!(value, MetadataValue::Binary(_)))
else {
continue;
};
if key_lower == "grpc-status" || key_lower == "grpc-message" {
continue;
}
block.push_str(&key_lower);
block.push_str(": ");
match value {
MetadataValue::Ascii(s) => block.push_str(sanitize_metadata_ascii_value(s).as_ref()),
MetadataValue::Binary(b) => {
use base64::Engine;
block.push_str(&base64::engine::general_purpose::STANDARD.encode(b.as_ref()));
}
}
block.push_str("\r\n");
}
let block_bytes = block.as_bytes();
dst.reserve(5 + block_bytes.len());
dst.put_u8(TRAILER_FLAG);
let block_len = u32::try_from(block_bytes.len())
.expect("gRPC trailer block exceeds 4 GiB — metadata must be bounded before encoding");
dst.put_u32(block_len);
dst.extend_from_slice(block_bytes);
}
pub fn decode_trailers(body: &[u8]) -> Result<TrailerFrame, GrpcError> {
let text = std::str::from_utf8(body)
.map_err(|e| GrpcError::protocol(format!("invalid UTF-8 in trailer block: {e}")))?;
let mut status_code: Option<i32> = None;
let mut status_message = String::new();
let mut metadata = Metadata::new();
for line in text.split("\r\n") {
if line.is_empty() {
continue;
}
let Some((key, value)) = line.split_once(':') else {
continue;
};
let key = key.trim().to_ascii_lowercase();
let value = value.trim();
match key.as_str() {
"grpc-status" => {
status_code = value.parse::<i32>().ok();
}
"grpc-message" => {
status_message = value
.replace("%0D", "\r")
.replace("%0d", "\r")
.replace("%0A", "\n")
.replace("%0a", "\n")
.replace("%25", "%");
}
_ => {
if key.ends_with("-bin") {
use base64::Engine;
if let Ok(decoded) = base64::engine::general_purpose::STANDARD.decode(value) {
metadata.insert_bin(&key, Bytes::from(decoded));
}
} else {
metadata.insert(&key, value);
}
}
}
}
let code = Code::from_i32(status_code.unwrap_or(13)); let status = if status_message.is_empty() {
Status::new(code, code.as_str())
} else {
Status::new(code, status_message)
};
Ok(TrailerFrame { status, metadata })
}
const DEFAULT_MAX_FRAME_SIZE: usize = 4 * 1024 * 1024;
#[derive(Debug)]
pub struct WebFrameCodec {
max_frame_size: usize,
}
impl WebFrameCodec {
#[must_use]
pub fn new() -> Self {
Self {
max_frame_size: DEFAULT_MAX_FRAME_SIZE,
}
}
#[must_use]
pub fn with_max_size(max_frame_size: usize) -> Self {
Self { max_frame_size }
}
pub fn decode(&self, src: &mut BytesMut) -> Result<Option<WebFrame>, GrpcError> {
if src.len() < 5 {
return Ok(None);
}
let flag = src[0];
let length = u32::from_be_bytes([src[1], src[2], src[3], src[4]]) as usize;
if length > self.max_frame_size {
return Err(GrpcError::MessageTooLarge);
}
if src.len() < 5 + length {
return Ok(None);
}
let _ = src.split_to(5);
let payload = src.split_to(length).freeze();
let is_trailer = flag & TRAILER_FLAG != 0;
if is_trailer {
let trailer = decode_trailers(&payload)?;
Ok(Some(WebFrame::Trailers(trailer)))
} else {
let compressed = flag & 0x01 != 0;
Ok(Some(WebFrame::Data {
compressed,
data: payload,
}))
}
}
pub fn encode_data(
&self,
data: &[u8],
compressed: bool,
dst: &mut BytesMut,
) -> Result<(), GrpcError> {
if data.len() > self.max_frame_size {
return Err(GrpcError::MessageTooLarge);
}
let len = u32::try_from(data.len()).map_err(|_| GrpcError::MessageTooLarge)?;
dst.reserve(5 + data.len());
dst.put_u8(u8::from(compressed));
dst.put_u32(len);
dst.extend_from_slice(data);
Ok(())
}
pub fn encode_trailers(
&self,
status: &Status,
metadata: &Metadata,
dst: &mut BytesMut,
) -> Result<(), GrpcError> {
encode_trailers(status, metadata, dst);
Ok(())
}
}
impl Default for WebFrameCodec {
fn default() -> Self {
Self::new()
}
}
#[must_use]
pub fn base64_encode(binary: &[u8]) -> String {
use base64::Engine;
base64::engine::general_purpose::STANDARD.encode(binary)
}
pub fn base64_decode(text: &str) -> Result<Vec<u8>, GrpcError> {
use base64::Engine;
base64::engine::general_purpose::STANDARD
.decode(text)
.map_err(|e| GrpcError::protocol(format!("invalid base64 in grpc-web-text: {e}")))
}
#[must_use]
pub fn is_grpc_web_request(content_type: &str) -> bool {
ContentType::from_header_value(content_type).is_some()
}
#[must_use]
pub fn is_text_mode(content_type: &str) -> bool {
ContentType::from_header_value(content_type).is_some_and(ContentType::is_text_mode)
}
#[cfg(test)]
mod tests {
use super::*;
use base64::Engine as _;
use insta::assert_snapshot;
use std::fmt::Write as _;
fn init_test(name: &str) {
crate::test_utils::init_test_logging();
crate::test_phase!(name);
}
fn scrub_grpc_web_frame_length(length: usize) -> String {
format!("<{length} bytes>")
}
fn render_grpc_web_frames_for_snapshot_test(bytes: &[u8]) -> String {
let codec = WebFrameCodec::new();
let mut buf = BytesMut::from(bytes);
let mut rendered = String::new();
let mut index = 0usize;
while !buf.is_empty() {
assert!(
buf.len() >= 5,
"snapshot input must contain a full gRPC-Web header"
);
let flag = buf[0];
let length = u32::from_be_bytes([buf[1], buf[2], buf[3], buf[4]]) as usize;
let payload = buf[5..5 + length].to_vec();
let frame = codec
.decode(&mut buf)
.expect("snapshot frame should decode")
.expect("snapshot frame should be complete");
let _ = writeln!(&mut rendered, "frame[{index}]");
let _ = writeln!(&mut rendered, " flag=0x{flag:02x}");
let _ = writeln!(
&mut rendered,
" length={}",
scrub_grpc_web_frame_length(length)
);
match frame {
WebFrame::Data { compressed, data } => {
let _ = writeln!(&mut rendered, " kind=data");
let _ = writeln!(&mut rendered, " compressed={compressed}");
let _ = writeln!(
&mut rendered,
" payload_utf8={:?}",
String::from_utf8_lossy(data.as_ref())
);
}
WebFrame::Trailers(trailers) => {
let _ = writeln!(&mut rendered, " kind=trailers");
let _ = writeln!(
&mut rendered,
" trailer_block={:?}",
String::from_utf8_lossy(&payload)
);
let _ = writeln!(
&mut rendered,
" status_code={}",
trailers.status.code().as_i32()
);
let _ = writeln!(
&mut rendered,
" status_message={:?}",
trailers.status.message()
);
for (metadata_index, (key, value)) in trailers.metadata.iter().enumerate() {
match value {
MetadataValue::Ascii(text) => {
let _ = writeln!(
&mut rendered,
" metadata[{metadata_index}] {key}={text:?}"
);
}
MetadataValue::Binary(binary) => {
let _ = writeln!(
&mut rendered,
" metadata[{metadata_index}] {key}={:?}",
base64::engine::general_purpose::STANDARD
.encode(binary.as_ref())
);
}
}
}
}
}
index += 1;
}
rendered
}
#[test]
fn test_content_type_parse_binary() {
init_test("test_content_type_parse_binary");
let ct = ContentType::from_header_value("application/grpc-web+proto");
crate::assert_with_log!(
ct == Some(ContentType::GrpcWeb),
"binary content type",
Some(ContentType::GrpcWeb),
ct
);
crate::test_complete!("test_content_type_parse_binary");
}
#[test]
fn test_content_type_parse_text() {
init_test("test_content_type_parse_text");
let ct = ContentType::from_header_value("application/grpc-web-text+proto");
crate::assert_with_log!(
ct == Some(ContentType::GrpcWebText),
"text content type",
Some(ContentType::GrpcWebText),
ct
);
crate::test_complete!("test_content_type_parse_text");
}
#[test]
fn test_content_type_parse_plain() {
init_test("test_content_type_parse_plain");
let ct = ContentType::from_header_value("application/grpc-web");
crate::assert_with_log!(
ct == Some(ContentType::GrpcWeb),
"plain grpc-web",
Some(ContentType::GrpcWeb),
ct
);
crate::test_complete!("test_content_type_parse_plain");
}
#[test]
fn test_content_type_parse_invalid() {
init_test("test_content_type_parse_invalid");
let ct = ContentType::from_header_value("application/json");
crate::assert_with_log!(ct.is_none(), "invalid content type", true, ct.is_none());
crate::test_complete!("test_content_type_parse_invalid");
}
#[test]
fn test_content_type_parse_standard_grpc() {
init_test("test_content_type_parse_standard_grpc");
let ct = ContentType::from_header_value("application/grpc");
crate::assert_with_log!(
ct.is_none(),
"standard grpc is not grpc-web",
true,
ct.is_none()
);
crate::test_complete!("test_content_type_parse_standard_grpc");
}
#[test]
fn test_content_type_case_insensitive() {
init_test("test_content_type_case_insensitive");
let ct = ContentType::from_header_value("Application/gRPC-Web-Text+proto");
crate::assert_with_log!(
ct == Some(ContentType::GrpcWebText),
"case insensitive parse",
Some(ContentType::GrpcWebText),
ct
);
crate::test_complete!("test_content_type_case_insensitive");
}
#[test]
fn test_content_type_parse_with_parameters() {
init_test("test_content_type_parse_with_parameters");
let ct = ContentType::from_header_value("application/grpc-web; charset=utf-8");
crate::assert_with_log!(
ct == Some(ContentType::GrpcWeb),
"parameterized grpc-web content type",
Some(ContentType::GrpcWeb),
ct
);
crate::test_complete!("test_content_type_parse_with_parameters");
}
#[test]
fn test_content_type_rejects_similar_prefixes() {
init_test("test_content_type_rejects_similar_prefixes");
let bogus_binary = ContentType::from_header_value("application/grpc-websocket");
crate::assert_with_log!(
bogus_binary.is_none(),
"grpc-websocket is not grpc-web",
true,
bogus_binary.is_none()
);
let bogus_text = ContentType::from_header_value("application/grpc-web-textplain");
crate::assert_with_log!(
bogus_text.is_none(),
"grpc-web-textplain is not grpc-web-text",
true,
bogus_text.is_none()
);
crate::test_complete!("test_content_type_rejects_similar_prefixes");
}
#[test]
fn test_trailer_encode_decode_roundtrip() {
init_test("test_trailer_encode_decode_roundtrip");
let status = Status::ok();
let metadata = Metadata::new();
let mut buf = BytesMut::new();
encode_trailers(&status, &metadata, &mut buf);
crate::assert_with_log!(
buf[0] == TRAILER_FLAG,
"trailer flag set",
TRAILER_FLAG,
buf[0]
);
let frame_codec = WebFrameCodec::new();
let frame = frame_codec.decode(&mut buf).unwrap().unwrap();
let WebFrame::Trailers(trailers) = frame else {
panic!("expected trailer frame")
};
crate::assert_with_log!(
trailers.status.code() == Code::Ok,
"status code OK",
Code::Ok,
trailers.status.code()
);
crate::test_complete!("test_trailer_encode_decode_roundtrip");
}
#[test]
fn test_trailer_with_message() {
init_test("test_trailer_with_message");
let status = Status::not_found("entity missing");
let metadata = Metadata::new();
let mut buf = BytesMut::new();
encode_trailers(&status, &metadata, &mut buf);
let frame_codec = WebFrameCodec::new();
let frame = frame_codec.decode(&mut buf).unwrap().unwrap();
let WebFrame::Trailers(trailers) = frame else {
panic!("expected trailer frame")
};
crate::assert_with_log!(
trailers.status.code() == Code::NotFound,
"status code NotFound",
Code::NotFound,
trailers.status.code()
);
let msg = trailers.status.message();
crate::assert_with_log!(
msg == "entity missing",
"status message",
"entity missing",
msg
);
crate::test_complete!("test_trailer_with_message");
}
#[test]
fn test_trailer_message_percent_encoding_roundtrip() {
init_test("test_trailer_message_percent_encoding_roundtrip");
let original_msg = "error on line\r\n42: 100% failure";
let status = Status::new(Code::Internal, original_msg);
let metadata = Metadata::new();
let mut buf = BytesMut::new();
encode_trailers(&status, &metadata, &mut buf);
let frame_codec = WebFrameCodec::new();
let frame = frame_codec.decode(&mut buf).unwrap().unwrap();
let WebFrame::Trailers(trailers) = frame else {
panic!("expected trailer frame")
};
let decoded_msg = trailers.status.message();
crate::assert_with_log!(
decoded_msg == original_msg,
"percent-encoded grpc-message must round-trip",
original_msg,
decoded_msg
);
crate::test_complete!("test_trailer_message_percent_encoding_roundtrip");
}
#[test]
fn test_trailer_with_custom_metadata() {
init_test("test_trailer_with_custom_metadata");
let status = Status::ok();
let mut metadata = Metadata::new();
metadata.insert("x-request-id", "abc-123");
let mut buf = BytesMut::new();
encode_trailers(&status, &metadata, &mut buf);
let frame_codec = WebFrameCodec::new();
let frame = frame_codec.decode(&mut buf).unwrap().unwrap();
let WebFrame::Trailers(trailers) = frame else {
panic!("expected trailer frame")
};
let request_id = trailers.metadata.get("x-request-id");
let has_id = request_id.is_some();
crate::assert_with_log!(has_id, "custom metadata present", true, has_id);
crate::test_complete!("test_trailer_with_custom_metadata");
}
#[test]
fn test_trailer_metadata_key_injection_is_rejected() {
init_test("test_trailer_metadata_key_injection_is_rejected");
let status = Status::ok();
let mut metadata = Metadata::new();
let inserted = metadata.insert("x-safe\r\nx-evil", "boom");
crate::assert_with_log!(
!inserted,
"malicious trailer metadata key rejected at insertion",
false,
inserted
);
let mut buf = BytesMut::new();
encode_trailers(&status, &metadata, &mut buf);
let wire = String::from_utf8(buf[5..].to_vec()).expect("trailer block utf8");
let injected = wire.contains("x-evil");
crate::assert_with_log!(
!injected,
"rejected trailer key never reaches the wire format",
false,
injected
);
crate::test_complete!("test_trailer_metadata_key_injection_is_rejected");
}
#[test]
fn test_trailer_pseudo_header_metadata_is_rejected() {
init_test("test_trailer_pseudo_header_metadata_is_rejected");
let status = Status::ok();
let mut metadata = Metadata::new();
let inserted = metadata.insert(":path", "/evil");
crate::assert_with_log!(
!inserted,
"pseudo-header metadata key rejected at insertion",
false,
inserted
);
let mut buf = BytesMut::new();
encode_trailers(&status, &metadata, &mut buf);
let wire = String::from_utf8(buf[5..].to_vec()).expect("trailer block utf8");
let injected = wire.contains(":path");
crate::assert_with_log!(
!injected,
"rejected pseudo-header never reaches the wire format",
false,
injected
);
crate::test_complete!("test_trailer_pseudo_header_metadata_is_rejected");
}
#[test]
fn test_data_frame_roundtrip() {
init_test("test_data_frame_roundtrip");
let codec = WebFrameCodec::new();
let mut buf = BytesMut::new();
codec
.encode_data(b"hello grpc-web", false, &mut buf)
.unwrap();
let frame = codec.decode(&mut buf).unwrap().unwrap();
let WebFrame::Data { compressed, data } = frame else {
panic!("expected data frame")
};
crate::assert_with_log!(!compressed, "not compressed", false, compressed);
crate::assert_with_log!(
data.as_ref() == b"hello grpc-web",
"data matches",
"hello grpc-web",
std::str::from_utf8(data.as_ref()).unwrap_or("<binary>")
);
crate::test_complete!("test_data_frame_roundtrip");
}
#[test]
fn test_data_frame_compressed_flag() {
init_test("test_data_frame_compressed_flag");
let codec = WebFrameCodec::new();
let mut buf = BytesMut::new();
codec.encode_data(b"compressed", true, &mut buf).unwrap();
crate::assert_with_log!(buf[0] == 1, "compressed flag byte", 1u8, buf[0]);
let frame = codec.decode(&mut buf).unwrap().unwrap();
let WebFrame::Data { compressed, .. } = frame else {
panic!("expected data frame")
};
crate::assert_with_log!(compressed, "compressed set", true, compressed);
crate::test_complete!("test_data_frame_compressed_flag");
}
#[test]
fn test_frame_too_large() {
init_test("test_frame_too_large");
let codec = WebFrameCodec::with_max_size(10);
let mut buf = BytesMut::new();
let result = codec.encode_data(&[0u8; 100], false, &mut buf);
let ok = matches!(result, Err(GrpcError::MessageTooLarge));
crate::assert_with_log!(ok, "encode rejects oversized frame", true, ok);
crate::test_complete!("test_frame_too_large");
}
#[test]
fn test_decode_partial_header() {
init_test("test_decode_partial_header");
let codec = WebFrameCodec::new();
let mut buf = BytesMut::from(&[0u8, 0, 0][..]);
let result = codec.decode(&mut buf).unwrap();
crate::assert_with_log!(
result.is_none(),
"partial header returns None",
true,
result.is_none()
);
crate::test_complete!("test_decode_partial_header");
}
#[test]
fn test_decode_partial_body() {
init_test("test_decode_partial_body");
let codec = WebFrameCodec::new();
let mut buf = BytesMut::new();
buf.put_u8(0);
buf.put_u32(10);
buf.extend_from_slice(&[1, 2, 3]);
let result = codec.decode(&mut buf).unwrap();
crate::assert_with_log!(
result.is_none(),
"partial body returns None",
true,
result.is_none()
);
crate::test_complete!("test_decode_partial_body");
}
#[test]
fn test_mixed_data_and_trailers() {
init_test("test_mixed_data_and_trailers");
let codec = WebFrameCodec::new();
let mut buf = BytesMut::new();
codec.encode_data(b"msg1", false, &mut buf).unwrap();
codec.encode_data(b"msg2", false, &mut buf).unwrap();
codec
.encode_trailers(&Status::ok(), &Metadata::new(), &mut buf)
.unwrap();
let f1 = codec.decode(&mut buf).unwrap().unwrap();
let is_data1 = matches!(&f1, WebFrame::Data { data, .. } if data.as_ref() == b"msg1");
crate::assert_with_log!(is_data1, "first data frame", true, is_data1);
let f2 = codec.decode(&mut buf).unwrap().unwrap();
let is_data2 = matches!(&f2, WebFrame::Data { data, .. } if data.as_ref() == b"msg2");
crate::assert_with_log!(is_data2, "second data frame", true, is_data2);
let f3 = codec.decode(&mut buf).unwrap().unwrap();
let is_trailer = matches!(f3, WebFrame::Trailers(_));
crate::assert_with_log!(is_trailer, "trailer frame", true, is_trailer);
let empty = buf.is_empty();
crate::assert_with_log!(empty, "buffer consumed", true, empty);
crate::test_complete!("test_mixed_data_and_trailers");
}
#[test]
fn grpc_web_frame_layouts_snapshot() {
init_test("grpc_web_frame_layouts_snapshot");
let codec = WebFrameCodec::new();
let mut happy_path = BytesMut::new();
codec
.encode_data(b"hello grpc-web", false, &mut happy_path)
.expect("happy-path data frame encodes");
let mut happy_metadata = Metadata::new();
let inserted_trace = happy_metadata.insert("x-trace-id", "trace-123");
crate::assert_with_log!(
inserted_trace,
"happy-path trace metadata inserted",
true,
inserted_trace
);
let inserted_bin =
happy_metadata.insert_bin("trace-bin", Bytes::from_static(&[0x01, 0x02]));
crate::assert_with_log!(
inserted_bin,
"happy-path binary metadata inserted",
true,
inserted_bin
);
codec
.encode_trailers(&Status::ok(), &happy_metadata, &mut happy_path)
.expect("happy-path trailers encode");
let mut error_trailers_only = BytesMut::new();
let mut error_metadata = Metadata::new();
let inserted_hint = error_metadata.insert("retry-after", "3");
crate::assert_with_log!(
inserted_hint,
"error-path retry metadata inserted",
true,
inserted_hint
);
codec
.encode_trailers(
&Status::invalid_argument("bad\nfield"),
&error_metadata,
&mut error_trailers_only,
)
.expect("error trailers encode");
let mut trailers_only = BytesMut::new();
let mut trailers_only_metadata = Metadata::new();
let inserted_cache = trailers_only_metadata.insert("x-cache", "MISS");
crate::assert_with_log!(
inserted_cache,
"trailers-only metadata inserted",
true,
inserted_cache
);
codec
.encode_trailers(&Status::ok(), &trailers_only_metadata, &mut trailers_only)
.expect("trailers-only encode");
let mut snapshot = String::new();
let _ = writeln!(&mut snapshot, "[happy_path]");
snapshot.push_str(&render_grpc_web_frames_for_snapshot_test(
happy_path.as_ref(),
));
let _ = writeln!(&mut snapshot, "[error_trailers_only]");
snapshot.push_str(&render_grpc_web_frames_for_snapshot_test(
error_trailers_only.as_ref(),
));
let _ = writeln!(&mut snapshot, "[trailers_only]");
snapshot.push_str(&render_grpc_web_frames_for_snapshot_test(
trailers_only.as_ref(),
));
assert_snapshot!("grpc_web_frame_layouts", snapshot);
crate::test_complete!("grpc_web_frame_layouts_snapshot");
}
#[test]
fn test_base64_roundtrip() {
init_test("test_base64_roundtrip");
let original = b"hello gRPC-web text mode";
let encoded = base64_encode(original);
let decoded = base64_decode(&encoded).unwrap();
crate::assert_with_log!(
decoded == original,
"base64 roundtrip",
original.as_slice(),
decoded.as_slice()
);
crate::test_complete!("test_base64_roundtrip");
}
#[test]
fn test_base64_rfc4648_single_octet_vector() {
init_test("test_base64_rfc4648_single_octet_vector");
let encoded = base64_encode(b"f");
crate::assert_with_log!(
encoded == "Zg==",
"rfc4648 encode vector",
"Zg==",
encoded.as_str()
);
let decoded = base64_decode("Zg==").unwrap();
crate::assert_with_log!(
decoded == b"f",
"rfc4648 decode vector",
b"f".as_slice(),
decoded.as_slice()
);
crate::test_complete!("test_base64_rfc4648_single_octet_vector");
}
#[test]
fn test_base64_rfc3548_two_octet_vector() {
init_test("test_base64_rfc3548_two_octet_vector");
let encoded = base64_encode(b"fo");
crate::assert_with_log!(
encoded == "Zm8=",
"rfc3548 encode vector",
"Zm8=",
encoded.as_str()
);
let decoded = base64_decode("Zm8=").unwrap();
crate::assert_with_log!(
decoded == b"fo",
"rfc3548 decode vector",
b"fo".as_slice(),
decoded.as_slice()
);
crate::test_complete!("test_base64_rfc3548_two_octet_vector");
}
#[test]
fn test_base64_invalid_input() {
init_test("test_base64_invalid_input");
let result = base64_decode("not valid base64!!!");
let ok = matches!(result, Err(GrpcError::Protocol(_)));
crate::assert_with_log!(ok, "invalid base64 rejected", true, ok);
crate::test_complete!("test_base64_invalid_input");
}
#[test]
fn test_text_mode_full_stream() {
init_test("test_text_mode_full_stream");
let codec = WebFrameCodec::new();
let mut binary_buf = BytesMut::new();
codec
.encode_data(b"message-payload", false, &mut binary_buf)
.unwrap();
codec
.encode_trailers(&Status::ok(), &Metadata::new(), &mut binary_buf)
.unwrap();
let text = base64_encode(&binary_buf);
let binary = base64_decode(&text).unwrap();
let mut decode_buf = BytesMut::from(binary.as_slice());
let f1 = codec.decode(&mut decode_buf).unwrap().unwrap();
let is_data =
matches!(&f1, WebFrame::Data { data, .. } if data.as_ref() == b"message-payload");
crate::assert_with_log!(is_data, "data frame decoded from text mode", true, is_data);
let f2 = codec.decode(&mut decode_buf).unwrap().unwrap();
let is_trailer = matches!(f2, WebFrame::Trailers(_));
crate::assert_with_log!(
is_trailer,
"trailer frame decoded from text mode",
true,
is_trailer
);
crate::test_complete!("test_text_mode_full_stream");
}
#[test]
fn test_is_grpc_web_request() {
init_test("test_is_grpc_web_request");
crate::assert_with_log!(
is_grpc_web_request("application/grpc-web"),
"binary",
true,
true
);
crate::assert_with_log!(
is_grpc_web_request("application/grpc-web-text+proto"),
"text",
true,
true
);
crate::assert_with_log!(
!is_grpc_web_request("application/grpc"),
"not grpc-web",
true,
true
);
crate::test_complete!("test_is_grpc_web_request");
}
#[test]
fn test_is_text_mode() {
init_test("test_is_text_mode");
crate::assert_with_log!(
is_text_mode("application/grpc-web-text"),
"text mode",
true,
true
);
crate::assert_with_log!(
!is_text_mode("application/grpc-web"),
"binary mode",
true,
true
);
crate::test_complete!("test_is_text_mode");
}
#[test]
fn test_decode_oversized_trailer_rejected() {
init_test("test_decode_oversized_trailer_rejected");
let codec = WebFrameCodec::with_max_size(10);
let mut buf = BytesMut::new();
buf.put_u8(TRAILER_FLAG);
buf.put_u32(100);
buf.extend_from_slice(&[b'x'; 100]);
let result = codec.decode(&mut buf);
let ok = matches!(result, Err(GrpcError::MessageTooLarge));
crate::assert_with_log!(ok, "oversized trailer rejected", true, ok);
crate::test_complete!("test_decode_oversized_trailer_rejected");
}
}