use alloc::string::String;
use alloc::vec::Vec;
pub struct KvPairs {
entries: Vec<KvEntry>,
warnings: Vec<KvWarning>,
}
struct KvEntry {
key: String,
value: String,
consumed_by: Option<&'static str>,
}
#[derive(Clone, Debug)]
pub struct KvWarning {
pub key: String,
pub kind: KvWarningKind,
pub message: String,
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum KvWarningKind {
UnrecognizedKey,
InvalidValue,
DeprecatedKey,
DuplicateKey,
}
impl KvPairs {
pub fn from_querystring(qs: &str) -> Self {
let mut entries = Vec::new();
let mut seen = alloc::collections::BTreeSet::new();
let mut warnings = Vec::new();
for part in qs.split('&') {
if part.is_empty() {
continue;
}
let (key, value) = match part.split_once('=') {
Some((k, v)) => (k, v),
None => (part, ""),
};
let key_lower = key.to_lowercase();
if !seen.insert(key_lower.clone()) {
warnings.push(KvWarning {
key: key_lower.clone(),
kind: KvWarningKind::DuplicateKey,
message: alloc::format!("duplicate key '{key_lower}', using last value"),
});
entries.retain(|e: &KvEntry| e.key != key_lower);
}
entries.push(KvEntry {
key: key_lower,
value: percent_decode(value),
consumed_by: None,
});
}
Self { entries, warnings }
}
pub fn from_pairs(pairs: impl Iterator<Item = (String, String)>) -> Self {
let entries = pairs
.map(|(key, value)| KvEntry {
key,
value,
consumed_by: None,
})
.collect();
Self {
entries,
warnings: Vec::new(),
}
}
pub fn take(&mut self, key: &str, consumer: &'static str) -> Option<&str> {
for entry in &mut self.entries {
if entry.key == key && entry.consumed_by.is_none() {
entry.consumed_by = Some(consumer);
return Some(&entry.value);
}
}
None
}
pub fn take_owned(&mut self, key: &str, consumer: &'static str) -> Option<String> {
for entry in &mut self.entries {
if entry.key == key && entry.consumed_by.is_none() {
entry.consumed_by = Some(consumer);
return Some(entry.value.clone());
}
}
None
}
pub fn take_f32(&mut self, key: &str, consumer: &'static str) -> Option<f32> {
let val_str = self.take_owned(key, consumer)?;
match val_str.parse::<f32>() {
Ok(v) => Some(v),
Err(_) => {
self.warn(
key,
KvWarningKind::InvalidValue,
alloc::format!("cannot parse '{val_str}' as number for key '{key}'"),
);
None
}
}
}
pub fn take_i32(&mut self, key: &str, consumer: &'static str) -> Option<i32> {
let val_str = self.take_owned(key, consumer)?;
match val_str.parse::<i32>() {
Ok(v) => Some(v),
Err(_) => {
self.warn(
key,
KvWarningKind::InvalidValue,
alloc::format!("cannot parse '{val_str}' as integer for key '{key}'"),
);
None
}
}
}
pub fn take_u32(&mut self, key: &str, consumer: &'static str) -> Option<u32> {
let val_str = self.take_owned(key, consumer)?;
match val_str.parse::<u32>() {
Ok(v) => Some(v),
Err(_) => {
self.warn(
key,
KvWarningKind::InvalidValue,
alloc::format!("cannot parse '{val_str}' as unsigned integer for key '{key}'"),
);
None
}
}
}
pub fn take_bool(&mut self, key: &str, consumer: &'static str) -> Option<bool> {
let val_str = self.take_owned(key, consumer)?;
match val_str.to_lowercase().as_str() {
"true" | "1" | "yes" => Some(true),
"false" | "0" | "no" => Some(false),
_ => {
self.warn(
key,
KvWarningKind::InvalidValue,
alloc::format!("cannot parse '{val_str}' as boolean for key '{key}'"),
);
None
}
}
}
pub fn peek(&self, key: &str) -> Option<&str> {
self.entries
.iter()
.find(|e| e.key == key && e.consumed_by.is_none())
.map(|e| e.value.as_str())
}
pub fn unconsumed(&self) -> impl Iterator<Item = (&str, &str)> {
self.entries
.iter()
.filter(|e| e.consumed_by.is_none())
.map(|e| (e.key.as_str(), e.value.as_str()))
}
pub fn warnings(&self) -> &[KvWarning] {
&self.warnings
}
pub fn warn(
&mut self,
key: impl Into<String>,
kind: KvWarningKind,
message: impl Into<String>,
) {
self.warnings.push(KvWarning {
key: key.into(),
kind,
message: message.into(),
});
}
pub fn snapshot(&self) -> Vec<KvEntrySnapshot> {
self.entries
.iter()
.map(|e| KvEntrySnapshot {
key: e.key.clone(),
value: e.value.clone(),
consumed_by: e.consumed_by,
})
.collect()
}
}
#[derive(Clone, Debug)]
pub struct KvEntrySnapshot {
pub key: String,
pub value: String,
pub consumed_by: Option<&'static str>,
}
fn percent_decode(s: &str) -> String {
let mut bytes = Vec::with_capacity(s.len());
let mut iter = s.bytes();
while let Some(b) = iter.next() {
if b == b'+' {
bytes.push(b' ');
} else if b == b'%' {
let hi = iter.next().and_then(from_hex);
let lo = iter.next().and_then(from_hex);
if let (Some(h), Some(l)) = (hi, lo) {
bytes.push(h << 4 | l);
} else {
bytes.push(b'%');
if let Some(h_val) = hi {
bytes.push(unhex(h_val));
}
}
} else {
bytes.push(b);
}
}
String::from_utf8(bytes).unwrap_or_else(|e| String::from_utf8_lossy(e.as_bytes()).into_owned())
}
fn unhex(nibble: u8) -> u8 {
if nibble < 10 {
b'0' + nibble
} else {
b'a' + nibble - 10
}
}
fn from_hex(b: u8) -> Option<u8> {
match b {
b'0'..=b'9' => Some(b - b'0'),
b'a'..=b'f' => Some(b - b'a' + 10),
b'A'..=b'F' => Some(b - b'A' + 10),
_ => None,
}
}
#[cfg(test)]
mod tests {
use alloc::vec;
use super::*;
#[test]
fn parse_querystring() {
let mut kv = KvPairs::from_querystring("w=800&h=600&quality=85");
assert_eq!(kv.take_u32("w", "test"), Some(800));
assert_eq!(kv.take_u32("h", "test"), Some(600));
assert_eq!(kv.take_i32("quality", "test"), Some(85));
assert_eq!(kv.unconsumed().count(), 0);
}
#[test]
fn unconsumed_keys() {
let mut kv = KvPairs::from_querystring("w=800&unknown=foo&h=600");
kv.take_u32("w", "test");
kv.take_u32("h", "test");
let unconsumed: Vec<_> = kv.unconsumed().collect();
assert_eq!(unconsumed, vec![("unknown", "foo")]);
}
#[test]
fn duplicate_keys_last_wins() {
let mut kv = KvPairs::from_querystring("w=100&w=200");
assert_eq!(kv.take_u32("w", "test"), Some(200));
assert!(
kv.warnings()
.iter()
.any(|w| w.kind == KvWarningKind::DuplicateKey)
);
}
#[test]
fn bool_parsing() {
let mut kv = KvPairs::from_querystring("a=true&b=0&c=yes&d=NO");
assert_eq!(kv.take_bool("a", "t"), Some(true));
assert_eq!(kv.take_bool("b", "t"), Some(false));
assert_eq!(kv.take_bool("c", "t"), Some(true));
assert_eq!(kv.take_bool("d", "t"), Some(false));
}
#[test]
fn percent_decoding() {
let mut kv = KvPairs::from_querystring("name=hello+world&path=%2Ffoo%2Fbar");
assert_eq!(kv.take("name", "t"), Some("hello world"));
assert_eq!(kv.take("path", "t"), Some("/foo/bar"));
}
#[test]
fn percent_decoding_multibyte_utf8() {
let mut kv = KvPairs::from_querystring("name=caf%C3%A9");
assert_eq!(kv.take("name", "t"), Some("café"));
}
#[test]
fn percent_decoding_invalid_hex_preserves_percent() {
let mut kv = KvPairs::from_querystring("x=%ZZ");
let val = kv.take("x", "t").unwrap();
assert!(val.contains('%'), "invalid hex should preserve %: {val}");
}
#[test]
fn consumed_not_returned_again() {
let mut kv = KvPairs::from_querystring("w=800");
assert_eq!(kv.take_u32("w", "first"), Some(800));
assert_eq!(kv.take_u32("w", "second"), None);
}
#[test]
fn case_insensitive_keys() {
let mut kv = KvPairs::from_querystring("Quality=85&WIDTH=800");
assert_eq!(kv.take_i32("quality", "t"), Some(85));
assert_eq!(kv.take_u32("width", "t"), Some(800));
}
}