aioduct 0.2.5

Async-native HTTP client built directly on hyper 1.x — no hyper-util, no legacy
Documentation
use base64::Engine as _;

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum StructuredFieldType {
    Dictionary,
    List,
    Item,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) enum StructuredFieldValue {
    Dictionary(Dictionary),
    List(List),
    Item(Item),
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct Dictionary {
    pub(super) entries: Vec<(String, ItemOrInnerList)>,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct List {
    pub(super) members: Vec<ItemOrInnerList>,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) enum ItemOrInnerList {
    Item(Item),
    InnerList(InnerList),
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct InnerList {
    pub(super) items: Vec<Item>,
    pub(super) parameters: Parameters,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct Item {
    pub(super) bare_item: BareItem,
    pub(super) parameters: Parameters,
}

#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub(crate) struct Parameters {
    pub(super) entries: Vec<(String, BareItem)>,
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) enum BareItem {
    Integer(String),
    Decimal(String),
    String(String),
    Token(String),
    ByteSequence(Vec<u8>),
    Boolean(bool),
    Date(String),
    DisplayString(String),
}

impl StructuredFieldValue {
    pub(crate) fn serialize(&self) -> String {
        match self {
            Self::Dictionary(value) => value.serialize(),
            Self::List(value) => value.serialize(),
            Self::Item(value) => value.serialize(),
        }
    }
}

impl Dictionary {
    pub(crate) fn member(&self, key: &str) -> Option<&ItemOrInnerList> {
        self.entries
            .iter()
            .find(|(member_key, _)| member_key == key)
            .map(|(_, member)| member)
    }

    pub(crate) fn serialize(&self) -> String {
        let mut out = String::new();
        for (index, (key, member)) in self.entries.iter().enumerate() {
            if index > 0 {
                out.push_str(", ");
            }
            out.push_str(&serialize_dictionary_member(key, &member.serialize()));
        }
        out
    }
}

impl List {
    pub(crate) fn serialize(&self) -> String {
        self.members
            .iter()
            .map(ItemOrInnerList::serialize)
            .collect::<Vec<_>>()
            .join(", ")
    }
}

impl ItemOrInnerList {
    pub(crate) fn serialize(&self) -> String {
        match self {
            Self::Item(item) => item.serialize(),
            Self::InnerList(inner_list) => inner_list.serialize(),
        }
    }
}

impl InnerList {
    pub(crate) fn serialize(&self) -> String {
        let items = self
            .items
            .iter()
            .map(Item::serialize)
            .collect::<Vec<_>>()
            .join(" ");
        format!("({}){}", items, self.parameters.serialize())
    }
}

impl Item {
    pub(crate) fn serialize(&self) -> String {
        format!(
            "{}{}",
            self.bare_item.serialize(),
            self.parameters.serialize()
        )
    }
}

impl Parameters {
    pub(crate) fn serialize(&self) -> String {
        let mut out = String::new();
        for (key, value) in &self.entries {
            out.push(';');
            out.push_str(key);
            if !value.is_boolean_true() {
                out.push('=');
                out.push_str(&value.serialize());
            }
        }
        out
    }
}

impl BareItem {
    pub(crate) fn serialize(&self) -> String {
        match self {
            Self::Integer(value)
            | Self::Decimal(value)
            | Self::Token(value)
            | Self::Date(value) => value.clone(),
            Self::String(value) => serialize_string(value),
            Self::ByteSequence(value) => {
                format!(
                    ":{}:",
                    base64::engine::general_purpose::STANDARD.encode(value)
                )
            }
            Self::Boolean(true) => "?1".to_owned(),
            Self::Boolean(false) => "?0".to_owned(),
            Self::DisplayString(value) => serialize_display_string(value),
        }
    }

    pub(crate) fn is_boolean_true(&self) -> bool {
        matches!(self, Self::Boolean(true))
    }
}

pub(super) fn serialize_integer(negative: bool, digits: &[u8]) -> String {
    let digits = strip_leading_zeroes(digits);
    let mut out = String::new();
    if negative && digits != b"0" {
        out.push('-');
    }
    out.push_str(&as_ascii_string(digits));
    out
}

pub(super) fn serialize_decimal(
    negative: bool,
    integer_digits: &[u8],
    fraction_digits: &[u8],
) -> String {
    let integer_digits = strip_leading_zeroes(integer_digits);
    let fraction_digits = strip_trailing_zeroes(fraction_digits);
    let has_non_zero_digit = integer_digits != b"0" || fraction_digits.iter().any(|b| *b != b'0');

    let mut out = String::new();
    if negative && has_non_zero_digit {
        out.push('-');
    }
    out.push_str(&as_ascii_string(integer_digits));
    out.push('.');
    out.push_str(&as_ascii_string(fraction_digits));
    out
}

fn serialize_string(value: &str) -> String {
    let mut out = String::from("\"");
    for byte in value.bytes() {
        match byte {
            b'"' | b'\\' => {
                out.push('\\');
                out.push(byte as char);
            }
            _ => out.push(byte as char),
        }
    }
    out.push('"');
    out
}

fn serialize_display_string(value: &str) -> String {
    const HEX: &[u8; 16] = b"0123456789abcdef";

    let mut out = String::from("%\"");
    for byte in value.bytes() {
        if matches!(byte, b'%' | b'"' | 0x00..=0x1f | 0x7f..=0xff) {
            out.push('%');
            out.push(HEX[(byte >> 4) as usize] as char);
            out.push(HEX[(byte & 0x0f) as usize] as char);
        } else {
            out.push(byte as char);
        }
    }
    out.push('"');
    out
}

pub(super) fn serialize_dictionary_member(key: &str, member: &str) -> String {
    let mut out = String::from(key);
    match member.strip_prefix("?1") {
        Some(parameters) if parameters.is_empty() || parameters.starts_with(';') => {
            out.push_str(parameters);
        }
        _ => {
            out.push('=');
            out.push_str(member);
        }
    }
    out
}

fn strip_leading_zeroes(mut digits: &[u8]) -> &[u8] {
    while digits.len() > 1 && digits.first() == Some(&b'0') {
        digits = &digits[1..];
    }
    if digits.is_empty() { b"0" } else { digits }
}

fn strip_trailing_zeroes(mut digits: &[u8]) -> &[u8] {
    while digits.len() > 1 && digits.last() == Some(&b'0') {
        digits = &digits[..digits.len() - 1];
    }
    if digits.is_empty() { b"0" } else { digits }
}

fn as_ascii_string(bytes: &[u8]) -> String {
    bytes.iter().map(|byte| *byte as char).collect()
}