pub const MAX_PARAM_COUNT: usize = 16;
pub const MAX_KEY_SIZE: usize = 32;
pub const MAX_VALUE_SIZE: usize = 128;
#[derive(Debug, Clone, Copy)]
pub struct StackParam<const KEY_SIZE: usize, const VALUE_SIZE: usize> {
key: StackString<KEY_SIZE>,
value: StackString<VALUE_SIZE>,
}
impl<const KEY_SIZE: usize, const VALUE_SIZE: usize> StackParam<KEY_SIZE, VALUE_SIZE> {
pub fn new() -> Self {
StackParam {
key: StackString::new(),
value: StackString::new(),
}
}
pub fn key(&self) -> &str {
self.key.as_str()
}
pub fn value(&self) -> &str {
self.value.as_str()
}
}
#[derive(Debug)]
pub struct StackQueryParams<
const PARAM_COUNT: usize,
const KEY_SIZE: usize,
const VALUE_SIZE: usize,
> {
params: [StackParam<KEY_SIZE, VALUE_SIZE>; PARAM_COUNT],
count: usize,
}
impl<const PARAM_COUNT: usize, const KEY_SIZE: usize, const VALUE_SIZE: usize>
StackQueryParams<PARAM_COUNT, KEY_SIZE, VALUE_SIZE>
{
pub fn custom_new(url: &str) -> Self {
let mut stack_query_params = StackQueryParams {
params: [StackParam::<KEY_SIZE, VALUE_SIZE>::new(); PARAM_COUNT],
count: 0,
};
stack_query_params.parse_from_url(url);
stack_query_params
}
}
impl StackQueryParams<MAX_PARAM_COUNT, MAX_KEY_SIZE, MAX_VALUE_SIZE> {
pub fn new(url: &str) -> Self {
let mut stack_query_params = StackQueryParams {
params: [StackParam::<MAX_KEY_SIZE, MAX_VALUE_SIZE>::new(); MAX_PARAM_COUNT],
count: 0,
};
stack_query_params.parse_from_url(url);
stack_query_params
}
}
impl<const PARAM_COUNT: usize, const KEY_SIZE: usize, const VALUE_SIZE: usize>
StackQueryParams<PARAM_COUNT, KEY_SIZE, VALUE_SIZE>
{
fn parse_from_url(&mut self, url: &str) {
let query = match url.find('?') {
Some(pos) => &url[pos + 1..],
None => return, };
let mut start = 0;
let bytes = query.as_bytes();
while start < bytes.len() && self.count < PARAM_COUNT {
let mut end = start;
while end < bytes.len() && bytes[end] != b'&' {
end += 1;
}
let pair = &query[start..end];
let param = &mut self.params[self.count];
if let Some(eq_pos) = pair.find('=') {
let key_str = &pair[0..eq_pos];
let value_str = &pair[eq_pos + 1..];
if !key_str.is_empty() {
let key_decoded = percent_decode::<KEY_SIZE>(key_str);
let value_decoded = percent_decode::<VALUE_SIZE>(value_str);
param.key = key_decoded;
param.value = value_decoded;
self.count += 1;
}
} else if !pair.is_empty() {
let key_decoded = percent_decode::<KEY_SIZE>(pair);
param.key = key_decoded;
self.count += 1;
}
start = end + 1;
}
}
pub fn get(&self, key: &str) -> Option<&str> {
for i in 0..self.count {
if self.params[i].key() == key {
return Some(self.params[i].value());
}
}
None
}
pub fn len(&self) -> usize {
self.count
}
pub fn is_empty(&self) -> bool {
self.count == 0
}
pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> + '_ {
(0..self.count).map(move |i| (self.params[i].key(), self.params[i].value()))
}
}
#[derive(Debug, Clone, Copy)]
pub struct StackString<const SIZE: usize> {
buf: [u8; SIZE],
len: usize,
}
impl<const SIZE: usize> StackString<SIZE> {
pub fn new() -> Self {
StackString {
buf: [0; SIZE],
len: 0,
}
}
pub fn push(&mut self, c: char) {
let mut buf = [0u8; 4]; let char_bytes = c.encode_utf8(&mut buf).as_bytes();
if self.len + char_bytes.len() <= SIZE {
self.buf[self.len..self.len + char_bytes.len()].copy_from_slice(char_bytes);
self.len += char_bytes.len();
}
}
pub fn len(&self) -> usize {
self.len
}
pub fn is_empty(&self) -> bool {
self.len == 0
}
pub fn as_str(&self) -> &str {
std::str::from_utf8(&self.buf[0..self.len]).unwrap_or("")
}
}
impl<const SIZE: usize> AsRef<str> for StackString<SIZE> {
fn as_ref(&self) -> &str {
self.as_str()
}
}
fn percent_decode<const OUTPUT_SIZE: usize>(input: &str) -> StackString<OUTPUT_SIZE> {
let mut result = StackString::<OUTPUT_SIZE>::new();
let mut i = 0;
let bytes = input.as_bytes();
while i < bytes.len() && result.len() < OUTPUT_SIZE {
if bytes[i] == b'%' && i + 2 < bytes.len() {
if let (Some(hi), Some(lo)) = (hex_value(bytes[i + 1]), hex_value(bytes[i + 2])) {
result.push((hi << 4 | lo) as char);
i += 3;
} else {
result.push('%');
i += 1;
}
} else if bytes[i] == b'+' {
result.push(' ');
i += 1;
} else {
result.push(bytes[i] as char);
i += 1;
}
}
result
}
fn hex_value(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 test_basic_parsing() {
let url = "https://example.com/path?name=John&age=25&city=New%20York";
let params = StackQueryParams::new(url);
assert_eq!(params.len(), 3);
assert_eq!(params.get("name"), Some("John"));
assert_eq!(params.get("age"), Some("25"));
assert_eq!(params.get("city"), Some("New York")); }
#[test]
fn test_no_query_params() {
let url = "https://example.com/path";
let params = StackQueryParams::new(url);
assert_eq!(params.len(), 0);
assert!(params.is_empty());
}
#[test]
fn test_empty_value() {
let url = "https://example.com/path?param=";
let params = StackQueryParams::new(url);
assert_eq!(params.len(), 1);
assert_eq!(params.get("param"), Some(""));
}
#[test]
fn test_percent_decode() {
assert_eq!(percent_decode::<32>("hello+world").as_str(), "hello world");
assert_eq!(
percent_decode::<32>("hello%20world").as_str(),
"hello world"
);
assert_eq!(percent_decode::<32>("50%25").as_str(), "50%");
assert_eq!(percent_decode::<32>("a%2Fb%2Fc").as_str(), "a/b/c");
assert_eq!(percent_decode::<32>("a+b+c").as_str(), "a b c");
}
#[test]
fn test_max_params_limit() {
let mut url = String::from("https://example.com/path?");
for i in 0..MAX_PARAM_COUNT + 5 {
if i > 0 {
url.push('&');
}
url.push_str(&format!("param{}=value{}", i, i));
}
let params = StackQueryParams::new(&url);
assert_eq!(params.len(), MAX_PARAM_COUNT);
}
#[test]
fn test_key_value_length_limits() {
let long_key = "a".repeat(MAX_KEY_SIZE + 10);
let long_value = "b".repeat(MAX_VALUE_SIZE + 10);
let url = format!("https://example.com/path?{}={}", long_key, long_value);
let params = StackQueryParams::new(&url);
assert_eq!(params.len(), 1);
let expected_key = "a".repeat(MAX_KEY_SIZE);
let expected_value = "b".repeat(MAX_VALUE_SIZE);
let (actual_key, actual_value) = params.iter().next().unwrap();
assert_eq!(actual_key, expected_key);
assert_eq!(actual_value, expected_value);
}
#[test]
fn test_custom_new() {
let url = "https://example.com/path?name=John&age=25&city=New%20York";
let params = StackQueryParams::<8, 16, 64>::custom_new(url);
assert_eq!(params.len(), 3);
assert_eq!(params.get("name"), Some("John"));
assert_eq!(params.get("age"), Some("25"));
assert_eq!(params.get("city"), Some("New York"));
}
}