Skip to content
use std::collections::HashMap;

/// Parses `application/x-www-form-urlencoded` while preserving repeated keys,
/// as used by checkbox groups in the ticket and lane forms.
pub fn parse_form(body: &[u8]) -> Result<HashMap<String, Vec<String>>, ()> {
    let mut fields: HashMap<String, Vec<String>> = HashMap::new();
    for pair in body
        .split(|byte| *byte == b'&')
        .filter(|pair| !pair.is_empty())
    {
        let (key, value) = match pair.iter().position(|byte| *byte == b'=') {
            Some(index) => (&pair[..index], &pair[index + 1..]),
            None => (pair, &[] as &[u8]),
        };
        fields
            .entry(decode_component(key)?)
            .or_default()
            .push(decode_component(value)?);
    }
    Ok(fields)
}

fn decode_component(input: &[u8]) -> Result<String, ()> {
    let mut output = Vec::with_capacity(input.len());
    let mut index = 0;
    while index < input.len() {
        match input[index] {
            b'+' => {
                output.push(b' ');
                index += 1;
            }
            b'%' if index + 2 < input.len() => {
                let high = hex(input[index + 1]).ok_or(())?;
                let low = hex(input[index + 2]).ok_or(())?;
                output.push(high * 16 + low);
                index += 3;
            }
            b'%' => return Err(()),
            byte => {
                output.push(byte);
                index += 1;
            }
        }
    }
    String::from_utf8(output).map_err(|_| ())
}

fn hex(byte: u8) -> Option<u8> {
    match byte {
        b'0'..=b'9' => Some(byte - b'0'),
        b'a'..=b'f' => Some(byte - b'a' + 10),
        b'A'..=b'F' => Some(byte - b'A' + 10),
        _ => None,
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn repeated_form_fields_and_encoded_values_are_preserved() {
        let fields = parse_form(b"tag_ids=one&tag_ids=two&title=Ship+it%21").unwrap();
        assert_eq!(fields["tag_ids"], ["one".to_string(), "two".to_string()]);
        assert_eq!(fields["title"], ["Ship it!".to_string()]);
    }

    #[test]
    fn malformed_percent_encoding_is_rejected() {
        assert!(parse_form(b"name=%Q0").is_err());
    }
}