#[derive(Debug, Clone, Default)]
pub struct RequestView {
pub method: String,
pub path: String,
pub url_path: String,
pub protocol: String,
pub scheme: String,
pub host: String,
pub source_addr: String,
pub headers: Vec<(String, String)>,
pub body: bytes::Bytes,
}
pub fn encode_header_map_pairs(pairs: &[(String, String)]) -> Vec<u8> {
let mut out = Vec::new();
out.extend_from_slice(&(pairs.len() as u32).to_le_bytes());
for (k, v) in pairs {
out.extend_from_slice(&(k.len() as u32).to_le_bytes());
out.extend_from_slice(&(v.len() as u32).to_le_bytes());
}
for (k, v) in pairs {
out.extend_from_slice(k.as_bytes());
out.push(0);
out.extend_from_slice(v.as_bytes());
out.push(0);
}
out
}
pub fn decode_header_map_pairs(buf: &[u8]) -> Option<Vec<(Vec<u8>, Vec<u8>)>> {
let read_u32 = |b: &[u8], off: usize| -> Option<usize> {
let slice = b.get(off..off + 4)?;
Some(u32::from_le_bytes(slice.try_into().ok()?) as usize)
};
let num = read_u32(buf, 0)?;
let mut sizes = Vec::with_capacity(num);
let mut off = 4;
for _ in 0..num {
let kl = read_u32(buf, off)?;
let vl = read_u32(buf, off + 4)?;
off += 8;
sizes.push((kl, vl));
}
let mut out = Vec::with_capacity(num);
for (kl, vl) in sizes {
let key = buf.get(off..off + kl)?.to_vec();
off += kl;
if buf.get(off) != Some(&0) {
return None; }
off += 1;
let val = buf.get(off..off + vl)?.to_vec();
off += vl;
if buf.get(off) != Some(&0) {
return None; }
off += 1;
out.push((key, val));
}
Some(out)
}
pub fn resolve_property(path: &[u8], req: &RequestView) -> Option<Vec<u8>> {
let segs: Vec<&[u8]> = path.split(|&b| b == 0).filter(|s| !s.is_empty()).collect();
let s: Vec<&str> = segs.iter().filter_map(|b| std::str::from_utf8(b).ok()).collect();
let val: &str = match s.as_slice() {
["request", "path"] => &req.path,
["request", "url_path"] => &req.url_path,
["request", "method"] => &req.method,
["request", "protocol"] => &req.protocol,
["request", "scheme"] => &req.scheme,
["request", "host"] => &req.host,
["source", "address"] => &req.source_addr,
["request", "headers", name] => {
return header_value(&req.headers, name).map(|v| v.as_bytes().to_vec());
}
_ => return None,
};
Some(val.as_bytes().to_vec())
}
pub fn header_value<'a>(headers: &'a [(String, String)], name: &str) -> Option<&'a String> {
let lname = name.to_ascii_lowercase();
headers.iter().find(|(k, _)| *k == lname).map(|(_, v)| v)
}
#[cfg(test)]
mod tests {
use super::*;
fn view() -> RequestView {
RequestView {
method: "POST".into(),
path: "/login?next=/x".into(),
url_path: "/login".into(),
protocol: "HTTP/1.1".into(),
scheme: "http".into(),
host: "example.test".into(),
source_addr: "203.0.113.7:4444".into(),
headers: vec![
("content-type".into(), "application/json".into()),
("x-block".into(), "1".into()),
],
body: bytes::Bytes::from_static(b"payload"),
}
}
#[test]
fn header_map_round_trip() {
let pairs = vec![
("content-type".to_string(), "application/json".to_string()),
("x-block".to_string(), "1".to_string()),
];
let enc = encode_header_map_pairs(&pairs);
let dec = decode_header_map_pairs(&enc).unwrap();
assert_eq!(dec.len(), 2);
assert_eq!(dec[0].0, b"content-type");
assert_eq!(dec[0].1, b"application/json");
assert_eq!(dec[1].0, b"x-block");
assert_eq!(dec[1].1, b"1");
}
#[test]
fn empty_header_map() {
let enc = encode_header_map_pairs(&[]);
assert_eq!(enc, 0u32.to_le_bytes());
assert_eq!(decode_header_map_pairs(&enc).unwrap().len(), 0);
}
#[test]
fn decode_rejects_truncated_buffer() {
let mut enc = encode_header_map_pairs(&[("a".into(), "b".into())]);
enc.truncate(enc.len() - 3); assert!(decode_header_map_pairs(&enc).is_none());
}
#[test]
fn decode_rejects_missing_nul() {
let mut enc = Vec::new();
enc.extend_from_slice(&1u32.to_le_bytes());
enc.extend_from_slice(&1u32.to_le_bytes()); enc.extend_from_slice(&1u32.to_le_bytes()); enc.extend_from_slice(b"kXv\x00"); assert!(decode_header_map_pairs(&enc).is_none());
}
#[test]
fn property_resolution() {
let r = view();
assert_eq!(resolve_property(b"request\0path", &r).unwrap(), b"/login?next=/x");
assert_eq!(resolve_property(b"request\0url_path", &r).unwrap(), b"/login");
assert_eq!(resolve_property(b"request\0method", &r).unwrap(), b"POST");
assert_eq!(resolve_property(b"source\0address", &r).unwrap(), b"203.0.113.7:4444");
assert_eq!(resolve_property(b"request\0headers\0x-block", &r).unwrap(), b"1");
assert!(resolve_property(b"request\0nonsense", &r).is_none());
assert!(resolve_property(b"connection\0id", &r).is_none());
}
}