555287fbe2da54538fc82f014941f11804f9e7fe / frogue / src / activitypub / signatures / rfc9421 / sf.rs
Raw
//! Minimal RFC 8941 Structured Fields parser.
//!
//! Parses the subset needed for RFC 9421 HTTP Message Signatures:
//! - Dictionaries with inner list or byte sequence values
//! - Parameters (string, token, integer)
//! - Preserves raw inner list text for @signature-params
use thiserror::Error;
#[derive(Debug, Error)]
pub enum SfError {
#[error("unexpected end of input")]
UnexpectedEof,
#[error("invalid character: {0:?}")]
InvalidChar(char),
#[error("invalid base64")]
InvalidBase64,
}
/// A structured field parameter value.
#[derive(Debug, Clone, PartialEq)]
pub enum SfParam {
String(String),
Token(String),
Integer(i64),
}
/// A structured field inner list item (bare value + parameters).
#[derive(Debug, Clone)]
pub struct SfItem {
pub value: SfValue,
}
/// A structured field value.
#[derive(Debug, Clone)]
pub enum SfValue {
InnerList(Vec<SfItem>),
ByteSeq(Vec<u8>),
String(String),
#[allow(dead_code)]
Token(String),
#[allow(dead_code)]
Integer(i64),
}
/// A dictionary member: value + parameters on the member itself.
#[derive(Debug, Clone)]
pub struct SfMember {
pub value: SfValue,
pub params: Vec<(String, SfParam)>,
/// Raw text of the value (for inner lists, needed for @signature-params).
pub raw_text: String,
}
/// Parse a structured field dictionary.
pub fn parse_dictionary(input: &str) -> Result<Vec<(String, SfMember)>, SfError> {
let mut result = Vec::new();
let mut chars = input.chars().peekable();
while chars.peek().is_some() {
skip_spaces(&mut chars);
let key = parse_key(&mut chars)?;
skip_spaces(&mut chars);
let mut member_params = Vec::new();
let value = if chars.peek() == Some(&'=') {
chars.next();
skip_spaces(&mut chars);
let (val, mut raw) = parse_value(&mut chars)?;
parse_member_params(&mut chars, &mut member_params)?;
// Append member-level parameters to raw_text
raw.push_str(&serialize_params(&member_params));
SfMember {
value: val,
params: member_params,
raw_text: raw,
}
} else {
// Boolean true (member present without value)
parse_member_params(&mut chars, &mut member_params)?;
let raw = serialize_params(&member_params);
SfMember {
value: SfValue::Token("true".to_string()),
params: member_params,
raw_text: raw,
}
};
result.push((key, value));
skip_spaces(&mut chars);
if chars.peek() == Some(&',') {
chars.next();
}
}
Ok(result)
}
fn skip_spaces(chars: &mut std::iter::Peekable<std::str::Chars>) {
while let Some(&c) = chars.peek() {
if c == ' ' || c == '\t' {
chars.next();
} else {
break;
}
}
}
fn parse_key(chars: &mut std::iter::Peekable<std::str::Chars>) -> Result<String, SfError> {
let mut key = String::new();
while let Some(&c) = chars.peek() {
if c.is_ascii_lowercase()
|| c.is_ascii_digit()
|| c == '_'
|| c == '-'
|| c == '.'
|| c == '*'
{
key.push(c);
chars.next();
} else {
break;
}
}
if key.is_empty() {
return Err(SfError::UnexpectedEof);
}
Ok(key)
}
fn parse_value(
chars: &mut std::iter::Peekable<std::str::Chars>,
) -> Result<(SfValue, String), SfError> {
let mut raw = String::new();
match chars.peek() {
Some(&'"') => {
let s = parse_string(chars, &mut raw)?;
Ok((SfValue::String(s), raw))
}
Some(&':') => {
let bytes = parse_byte_seq(chars, &mut raw)?;
Ok((SfValue::ByteSeq(bytes), raw))
}
Some(&'(') => {
let items = parse_inner_list(chars, &mut raw)?;
Ok((SfValue::InnerList(items), raw))
}
Some(&c) if c.is_ascii_digit() || c == '-' => {
let i = parse_integer(chars, &mut raw)?;
Ok((SfValue::Integer(i), raw))
}
Some(&c) if c.is_ascii_alphabetic() || c == '*' => {
let t = parse_token(chars, &mut raw)?;
Ok((SfValue::Token(t), raw))
}
Some(&c) => Err(SfError::InvalidChar(c)),
None => Err(SfError::UnexpectedEof),
}
}
fn parse_string(
chars: &mut std::iter::Peekable<std::str::Chars>,
raw: &mut String,
) -> Result<String, SfError> {
chars.next(); // opening quote
raw.push('"');
let mut s = String::new();
let mut escape = false;
for c in chars.by_ref() {
raw.push(c);
if escape {
s.push(c);
escape = false;
} else if c == '\\' {
escape = true;
} else if c == '"' {
return Ok(s);
} else {
s.push(c);
}
}
Err(SfError::UnexpectedEof)
}
fn parse_byte_seq(
chars: &mut std::iter::Peekable<std::str::Chars>,
raw: &mut String,
) -> Result<Vec<u8>, SfError> {
chars.next(); // opening colon
raw.push(':');
let mut b64 = String::new();
while let Some(&c) = chars.peek() {
if c == ':' {
chars.next();
raw.push(':');
return base64::Engine::decode(&base64::engine::general_purpose::STANDARD, &b64)
.map_err(|_| SfError::InvalidBase64);
}
b64.push(c);
raw.push(c);
chars.next();
}
Err(SfError::UnexpectedEof)
}
fn parse_inner_list(
chars: &mut std::iter::Peekable<std::str::Chars>,
raw: &mut String,
) -> Result<Vec<SfItem>, SfError> {
chars.next(); // opening paren
raw.push('(');
let mut items = Vec::new();
let mut first = true;
loop {
skip_spaces(chars);
if chars.peek() == Some(&')') {
chars.next();
raw.push(')');
return Ok(items);
}
// Add space before items (except the first)
if !first {
raw.push(' ');
}
first = false;
let (val, item_raw) = parse_value(chars)?;
raw.push_str(&item_raw);
items.push(SfItem { value: val });
skip_spaces(chars);
}
}
fn parse_integer(
chars: &mut std::iter::Peekable<std::str::Chars>,
raw: &mut String,
) -> Result<i64, SfError> {
let mut s = String::new();
if let Some(&'-') = chars.peek() {
s.push('-');
raw.push('-');
chars.next();
}
while let Some(&c) = chars.peek() {
if c.is_ascii_digit() {
s.push(c);
raw.push(c);
chars.next();
} else {
break;
}
}
s.parse().map_err(|_| SfError::UnexpectedEof)
}
fn parse_token(
chars: &mut std::iter::Peekable<std::str::Chars>,
raw: &mut String,
) -> Result<String, SfError> {
let mut s = String::new();
while let Some(&c) = chars.peek() {
if c.is_ascii_alphanumeric()
|| c == '_'
|| c == '-'
|| c == '.'
|| c == ':'
|| c == '/'
|| c == '*'
{
s.push(c);
raw.push(c);
chars.next();
} else {
break;
}
}
if s.is_empty() {
return Err(SfError::UnexpectedEof);
}
Ok(s)
}
fn parse_member_params(
chars: &mut std::iter::Peekable<std::str::Chars>,
params: &mut Vec<(String, SfParam)>,
) -> Result<(), SfError> {
while chars.peek() == Some(&';') {
chars.next();
skip_spaces(chars);
let key = parse_key(chars)?;
skip_spaces(chars);
let value = if chars.peek() == Some(&'=') {
chars.next();
skip_spaces(chars);
parse_param_value(chars)?
} else {
SfParam::Token("true".to_string())
};
params.push((key, value));
}
Ok(())
}
fn parse_param_value(chars: &mut std::iter::Peekable<std::str::Chars>) -> Result<SfParam, SfError> {
let mut raw = String::new();
match chars.peek() {
Some(&'"') => {
let s = parse_string(chars, &mut raw)?;
Ok(SfParam::String(s))
}
Some(&c) if c.is_ascii_digit() || c == '-' => {
let mut s = String::new();
if c == '-' {
s.push(c);
chars.next();
}
while let Some(&c) = chars.peek() {
if c.is_ascii_digit() {
s.push(c);
chars.next();
} else {
break;
}
}
let i: i64 = s.parse().map_err(|_| SfError::UnexpectedEof)?;
Ok(SfParam::Integer(i))
}
Some(&c) if c.is_ascii_alphabetic() || c == '*' => {
let t = parse_token(chars, &mut raw)?;
Ok(SfParam::Token(t))
}
Some(&c) => Err(SfError::InvalidChar(c)),
None => Err(SfError::UnexpectedEof),
}
}
/// Serialize parameters back to their string form (for @signature-params).
fn serialize_params(params: &[(String, SfParam)]) -> String {
let mut result = String::new();
for (key, value) in params {
result.push(';');
result.push_str(key);
match value {
SfParam::String(s) => {
result.push('=');
result.push('"');
// Escape backslashes and quotes
for c in s.chars() {
match c {
'\\' => result.push_str("\\\\"),
'"' => result.push_str("\\\""),
_ => result.push(c),
}
}
result.push('"');
}
SfParam::Integer(i) => {
result.push('=');
result.push_str(&i.to_string());
}
SfParam::Token(t) => {
result.push('=');
result.push_str(t);
}
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_simple_dictionary() {
let input = r#"sig1=("@method" "@path");created=1234;keyid="test-key""#;
let dict = parse_dictionary(input).unwrap();
assert_eq!(dict.len(), 1);
let (key, member) = &dict[0];
assert_eq!(key, "sig1");
assert_eq!(member.params.len(), 2);
assert_eq!(member.params[0].0, "created");
assert_eq!(member.params[0].1, SfParam::Integer(1234));
assert_eq!(member.params[1].0, "keyid");
assert_eq!(member.params[1].1, SfParam::String("test-key".to_string()));
}
#[test]
fn parse_inner_list() {
let input = r#"sig1=("@method" "host" "date");created=1234"#;
let dict = parse_dictionary(input).unwrap();
let (_, member) = &dict[0];
if let SfValue::InnerList(items) = &member.value {
assert_eq!(items.len(), 3);
} else {
panic!("expected inner list");
}
}
#[test]
fn parse_byte_sequence() {
let input = r#"sig1=:dGVzdA==:"#;
let dict = parse_dictionary(input).unwrap();
let (_, member) = &dict[0];
if let SfValue::ByteSeq(bytes) = &member.value {
assert_eq!(bytes, b"test");
} else {
panic!("expected byte sequence");
}
}
}