use regex::Regex;
use std::cmp::{Ordering, max};
#[derive(Debug, Clone, PartialEq)]
enum ParamPattern {
Simple { value: u64 },
Braced {
start: u64,
end: u64,
step: u64,
width: usize,
},
Bracket {
start: u64,
end: u64,
step: u64,
width: usize,
},
}
fn find_param_patterns(uri: &str) -> Vec<(usize, ParamPattern)> {
let mut patterns = Vec::new();
let simple_re = Regex::new(r"\$(\d+)").unwrap();
for cap in simple_re.captures_iter(uri) {
let full_match = cap.get(0).unwrap();
let start = full_match.start();
if start > 0 {
let prev_char = uri.as_bytes()[start - 1];
if prev_char == b'{' || prev_char == b'$' {
continue;
}
}
let digits = cap.get(1).unwrap().as_str();
let value: u64 = digits.parse().unwrap_or(1);
patterns.push((start, ParamPattern::Simple { value }));
}
let braced_re = Regex::new(r"\$\{([^}]+)\}").unwrap();
for cap in braced_re.captures_iter(uri) {
let full_match = cap.get(0).unwrap();
let inner = cap.get(1).unwrap().as_str();
if let Some(pattern) = parse_braced_pattern(inner) {
patterns.push((full_match.start(), pattern));
}
}
let bracket_re = Regex::new(r"\[(\d+)-(\d+)(?::(\d+))?\]").unwrap();
for cap in bracket_re.captures_iter(uri) {
let full_match = cap.get(0).unwrap();
let before_bracket = &uri[..full_match.start()];
if before_bracket.ends_with(':') && before_bracket.contains("::") {
continue; }
let start_str = cap.get(1).unwrap().as_str();
let end_str = cap.get(2).unwrap().as_str();
let step_str = cap.get(3).map(|m| m.as_str());
if let (Ok(start), Ok(end)) = (start_str.parse::<u64>(), end_str.parse::<u64>()) {
let step = step_str.and_then(|s| s.parse::<u64>().ok()).unwrap_or(1);
if step == 0 {
continue; }
let width = max(start_str.len(), end_str.len());
patterns.push((
full_match.start(),
ParamPattern::Bracket {
start,
end,
step,
width,
},
));
}
}
patterns.sort_by_key(|(pos, _)| *pos);
patterns
}
fn parse_braced_pattern(inner: &str) -> Option<ParamPattern> {
if let Some(cap) = Regex::new(r"^(\d+)-(\d+):(\d+)$").unwrap().captures(inner) {
let start: u64 = cap[1].parse().ok()?;
let end: u64 = cap[2].parse().ok()?;
let step: u64 = cap[3].parse().ok()?;
if step == 0 {
return None;
}
let width = cap[1].len();
return Some(ParamPattern::Braced {
start,
end,
step,
width,
});
}
if let Some(cap) = Regex::new(r"^(\d+)-(\d+)$").unwrap().captures(inner) {
let start: u64 = cap[1].parse().ok()?;
let end: u64 = cap[2].parse().ok()?;
let width = cap[1].len();
return Some(ParamPattern::Braced {
start,
end,
step: 1,
width,
});
}
if let Some(cap) = Regex::new(r"^(\d+)$").unwrap().captures(inner) {
let count: u64 = cap[1].parse().ok()?;
let width = cap[1].len();
return Some(ParamPattern::Braced {
start: 1,
end: count,
step: 1,
width,
});
}
None
}
fn format_with_width(n: u64, width: usize) -> String {
format!("{:0width$}", n, width = width)
}
fn expand_pattern(pattern: &ParamPattern) -> Vec<String> {
match pattern {
ParamPattern::Simple { value } => {
let count = 10u64.pow(*value as u32);
(1..=count).map(|n| n.to_string()).collect()
}
ParamPattern::Braced {
start,
end,
step,
width,
} => generate_range(*start, *end, *step, *width),
ParamPattern::Bracket {
start,
end,
step,
width,
} => generate_range(*start, *end, *step, *width),
}
}
fn generate_range(start: u64, end: u64, step: u64, width: usize) -> Vec<String> {
if step == 0 {
return Vec::new();
}
let mut values = Vec::new();
match start.cmp(&end) {
Ordering::Less => {
let mut current = start;
while current <= end {
values.push(format_with_width(current, width));
current += step;
}
}
Ordering::Greater => {
let mut current = start;
while current >= end {
values.push(format_with_width(current, width));
if current < step {
break; }
current -= step;
}
}
Ordering::Equal => {
values.push(format_with_width(start, width));
}
}
values
}
pub fn expand_parameterized_uri(uri: &str) -> Vec<String> {
let patterns = find_param_patterns(uri);
if patterns.is_empty() {
return vec![uri.to_string()];
}
let mut all_expansions: Vec<Vec<String>> = Vec::new();
for (_, pattern) in &patterns {
match pattern {
ParamPattern::Simple { value } => {
let count = 10u64.pow(*value as u32);
let values: Vec<String> = (1..=count).map(|n| n.to_string()).collect();
all_expansions.push(values);
}
_ => {
let values = expand_pattern(pattern);
if values.is_empty() {
return vec![uri.to_string()];
}
all_expansions.push(values);
}
}
}
if all_expansions.is_empty() {
return vec![uri.to_string()];
}
cartesian_product_replace(uri, &patterns, &all_expansions)
}
fn cartesian_product_replace(
uri: &str,
_patterns: &[(usize, ParamPattern)],
expansions: &[Vec<String>],
) -> Vec<String> {
if expansions.is_empty() {
return vec![uri.to_string()];
}
let mut results = vec![uri.to_string()];
for expansion_set in expansions {
let mut new_results = Vec::new();
for result in &results {
for value in expansion_set {
let replaced = replace_next_pattern(result, value);
new_results.push(replaced);
}
}
results = new_results;
}
results
}
fn replace_next_pattern(uri: &str, replacement: &str) -> String {
if let Some(pos) = find_simple_pattern_pos(uri) {
let before = &uri[..pos];
let after_start = pos + 1; let mut end = after_start;
while end < uri.len() && uri.as_bytes()[end].is_ascii_digit() {
end += 1;
}
let after = &uri[end..];
return format!("{}{}{}", before, replacement, after);
}
if let Some(pos) = uri.find("${")
&& let Some(end) = uri[pos..].find('}')
{
let end_pos = pos + end + 1;
let before = &uri[..pos];
let after = &uri[end_pos..];
return format!("{}{}{}", before, replacement, after);
}
if let Some(pos) = find_bracket_pattern_pos(uri)
&& let Some(end) = uri[pos..].find(']')
{
let end_pos = pos + end + 1;
let before = &uri[..pos];
let after = &uri[end_pos..];
return format!("{}{}{}", before, replacement, after);
}
uri.to_string()
}
fn find_simple_pattern_pos(uri: &str) -> Option<usize> {
let re = Regex::new(r"\$(\d+)").unwrap();
for m in re.find_iter(uri) {
let start = m.start();
if start > 0 {
let prev_char = uri.as_bytes()[start - 1];
if prev_char == b'{' || prev_char == b'$' {
continue;
}
}
return Some(start);
}
None
}
fn find_bracket_pattern_pos(uri: &str) -> Option<usize> {
let re = Regex::new(r"\[(\d+-\d+(?::\d+)?)\]").unwrap();
for m in re.find_iter(uri) {
let before = &uri[..m.start()];
if !before.ends_with(':') || !before.contains("::") {
return Some(m.start());
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_simple_dollar_num_basic() {
let uri = "http://example.com/file$3.txt";
let expanded = expand_parameterized_uri(uri);
assert!(!expanded.is_empty());
assert!(expanded.len() > 1, "Should produce multiple URIs");
assert!(
expanded[0].contains("file1"),
"First URI should contain file1"
);
}
#[test]
fn test_simple_dollar_num_with_3() {
let uri = "http://example.com/file$3.txt";
let expanded = expand_parameterized_uri(uri);
assert_eq!(expanded.len(), 1000); assert!(expanded[0].ends_with("file1.txt"));
assert!(expanded[1].ends_with("file2.txt"));
assert!(expanded.last().unwrap().ends_with("file1000.txt"));
}
#[test]
fn test_simple_dollar_num_single_digit() {
let uri = "http://example.com/file$1.txt";
let expanded = expand_parameterized_uri(uri);
assert_eq!(expanded.len(), 10); assert!(expanded[0].contains("file1"));
assert!(expanded[9].contains("file10"));
}
#[test]
fn test_braced_zero_padded_single_number() {
let uri = "http://example.com/file${03}.txt";
let expanded = expand_parameterized_uri(uri);
assert_eq!(expanded.len(), 3);
assert_eq!(expanded[0], "http://example.com/file01.txt");
assert_eq!(expanded[1], "http://example.com/file02.txt");
assert_eq!(expanded[2], "http://example.com/file03.txt");
}
#[test]
fn test_braced_zero_padded_width_detection() {
let uri = "http://example.com/data${0005}.bin";
let expanded = expand_parameterized_uri(uri);
assert_eq!(expanded.len(), 5);
assert!(expanded[0].ends_with("data0001.bin"));
assert!(expanded[4].ends_with("data0005.bin"));
for uri in &expanded {
assert!(
uri.contains("data000") || uri.contains("data005"),
"Should be zero-padded to width 4"
);
}
}
#[test]
fn test_braced_range_forward() {
let uri = "http://example.com/chapter${01-05}.html";
let expanded = expand_parameterized_uri(uri);
assert_eq!(expanded.len(), 5);
assert_eq!(expanded[0], "http://example.com/chapter01.html");
assert_eq!(expanded[1], "http://example.com/chapter02.html");
assert_eq!(expanded[2], "http://example.com/chapter03.html");
assert_eq!(expanded[3], "http://example.com/chapter04.html");
assert_eq!(expanded[4], "http://example.com/chapter05.html");
}
#[test]
fn test_braced_range_large_numbers() {
let uri = "http://example.com/archive${100-105}.zip";
let expanded = expand_parameterized_uri(uri);
assert_eq!(expanded.len(), 6);
assert_eq!(expanded[0], "http://example.com/archive100.zip");
assert_eq!(expanded[5], "http://example.com/archive105.zip");
}
#[test]
fn test_braced_range_with_step() {
let uri = "http://example.com/part${01-10:2}.dat";
let expanded = expand_parameterized_uri(uri);
assert_eq!(expanded.len(), 5);
assert_eq!(expanded[0], "http://example.com/part01.dat");
assert_eq!(expanded[1], "http://example.com/part03.dat");
assert_eq!(expanded[2], "http://example.com/part05.dat");
assert_eq!(expanded[3], "http://example.com/part07.dat");
assert_eq!(expanded[4], "http://example.com/part09.dat");
}
#[test]
fn test_braced_step_of_3() {
let uri = "http://example.com/img${001-009:3}.jpg";
let expanded = expand_parameterized_uri(uri);
assert_eq!(expanded.len(), 3);
assert_eq!(expanded[0], "http://example.com/img001.jpg");
assert_eq!(expanded[1], "http://example.com/img004.jpg");
assert_eq!(expanded[2], "http://example.com/img007.jpg");
}
#[test]
fn test_bracket_range_basic() {
let uri = "http://example.com/file[01-05].zip";
let expanded = expand_parameterized_uri(uri);
assert_eq!(expanded.len(), 5);
assert_eq!(expanded[0], "http://example.com/file01.zip");
assert_eq!(expanded[1], "http://example.com/file02.zip");
assert_eq!(expanded[2], "http://example.com/file03.zip");
assert_eq!(expanded[3], "http://example.com/file04.zip");
assert_eq!(expanded[4], "http://example.com/file05.zip");
}
#[test]
fn test_bracket_range_different_widths() {
let uri = "http://example.com/data[1-10].bin";
let expanded = expand_parameterized_uri(uri);
assert_eq!(expanded.len(), 10);
assert_eq!(expanded[0], "http://example.com/data01.bin");
assert_eq!(expanded[9], "http://example.com/data10.bin");
}
#[test]
fn test_bracket_range_with_step() {
let uri = "http://example.com/file[01-10:2].zip";
let expanded = expand_parameterized_uri(uri);
assert_eq!(expanded.len(), 5);
assert_eq!(expanded[0], "http://example.com/file01.zip");
assert_eq!(expanded[1], "http://example.com/file03.zip");
assert_eq!(expanded[2], "http://example.com/file05.zip");
assert_eq!(expanded[3], "http://example.com/file07.zip");
assert_eq!(expanded[4], "http://example.com/file09.zip");
}
#[test]
fn test_bracket_step_of_5() {
let uri = "http://example.com/vol[005-100:5].pdf";
let expanded = expand_parameterized_uri(uri);
assert_eq!(expanded.len(), 20);
assert_eq!(expanded[0], "http://example.com/vol005.pdf");
assert_eq!(expanded[1], "http://example.com/vol010.pdf");
assert_eq!(expanded.last().unwrap(), &"http://example.com/vol100.pdf");
}
#[test]
fn test_reverse_bracket_range() {
let uri = "http://example.com/file[10-01].zip";
let expanded = expand_parameterized_uri(uri);
assert_eq!(expanded.len(), 10);
assert_eq!(expanded[0], "http://example.com/file10.zip");
assert_eq!(expanded[1], "http://example.com/file09.zip");
assert_eq!(expanded[8], "http://example.com/file02.zip");
assert_eq!(expanded[9], "http://example.com/file01.zip");
}
#[test]
fn test_reverse_braced_range() {
let uri = "http://example.com/ch${10-05}.html";
let expanded = expand_parameterized_uri(uri);
assert_eq!(expanded.len(), 6);
assert_eq!(expanded[0], "http://example.com/ch10.html");
assert_eq!(expanded[5], "http://example.com/ch05.html");
}
#[test]
fn test_multiple_patterns_cartesian_product() {
let _uri = "http://example.com/${chapter}-${page}.html";
let uri_with_ranges = "http://example.com/${01-03}-${01-03}.html";
let expanded = expand_parameterized_uri(uri_with_ranges);
assert_eq!(expanded.len(), 9); assert_eq!(expanded[0], "http://example.com/01-01.html");
assert_eq!(expanded[1], "http://example.com/01-02.html");
assert_eq!(expanded[2], "http://example.com/01-03.html");
assert_eq!(expanded[3], "http://example.com/02-01.html");
assert_eq!(expanded[8], "http://example.com/03-03.html");
}
#[test]
fn test_three_patterns_cartesian() {
let uri = "http://example.com/[1-2]-[a-d]-${01-02}.txt";
let expanded = expand_parameterized_uri(uri);
assert_eq!(expanded.len(), 4); assert!(expanded[0].contains("[a-d]")); }
#[test]
fn test_mixed_brace_and_bracket() {
let uri = "http://example.com/${01-02}-[01-03].dat";
let expanded = expand_parameterized_uri(uri);
assert_eq!(expanded.len(), 6); assert_eq!(expanded[0], "http://example.com/01-01.dat");
assert_eq!(expanded[1], "http://example.com/01-02.dat");
assert_eq!(expanded[2], "http://example.com/01-03.dat");
assert_eq!(expanded[3], "http://example.com/02-01.dat");
assert_eq!(expanded[5], "http://example.com/02-03.dat");
}
#[test]
fn test_no_pattern_passthrough() {
let uris = vec![
"http://example.com/normal_file.txt",
"https://cdn.example.com/static/image.png",
"ftp://files.example.com/document.pdf",
];
for uri in uris {
let expanded = expand_parameterized_uri(uri);
assert_eq!(expanded.len(), 1);
assert_eq!(expanded[0], uri);
}
}
#[test]
fn test_uri_with_query_params_no_pattern() {
let uri = "http://example.com/path?query=value&other=123";
let expanded = expand_parameterized_uri(uri);
assert_eq!(expanded.len(), 1);
assert_eq!(expanded[0], uri);
}
#[test]
fn test_single_value_range() {
let uri = "http://example.com/file[5-5].txt";
let expanded = expand_parameterized_uri(uri);
assert_eq!(expanded.len(), 1);
assert_eq!(expanded[0], "http://example.com/file5.txt");
}
#[test]
fn test_single_value_braced() {
let uri = "http://example.com/file${07-07}.txt";
let expanded = expand_parameterized_uri(uri);
assert_eq!(expanded.len(), 1);
assert_eq!(expanded[0], "http://example.com/file07.txt");
}
#[test]
fn test_large_numbers() {
let uri = "http://example.com/big[099999-100005].bin";
let expanded = expand_parameterized_uri(uri);
assert_eq!(expanded.len(), 7);
assert_eq!(expanded[0], "http://example.com/big099999.bin");
assert_eq!(expanded[6], "http://example.com/big100005.bin");
}
#[test]
fn test_width_overflow_handling() {
let uri = "http://example.com/f[1-100].txt";
let expanded = expand_parameterized_uri(uri);
assert_eq!(expanded.len(), 100);
assert_eq!(expanded[0], "http://example.com/f001.txt");
assert_eq!(expanded[99], "http://example.com/f100.txt");
}
#[test]
fn test_empty_uri() {
let expanded = expand_parameterized_uri("");
assert_eq!(expanded.len(), 1);
assert_eq!(expanded[0], "");
}
#[test]
fn test_invalid_bracket_content() {
let uri = "http://example.com/[abc-def].txt";
let expanded = expand_parameterized_uri(uri);
assert_eq!(expanded.len(), 1);
assert_eq!(expanded[0], uri);
}
#[test]
fn test_invalid_braced_content() {
let uri = "http://example.com/${not-a-number}.txt";
let expanded = expand_parameterized_uri(uri);
assert_eq!(expanded.len(), 1);
assert_eq!(expanded[0], uri);
}
#[test]
fn test_zero_step_invalid() {
let uri = "http://example.com/file[01-10:0].zip";
let expanded = expand_parameterized_uri(uri);
assert_eq!(expanded.len(), 1);
assert_eq!(expanded[0], uri);
}
#[test]
fn test_unclosed_braces() {
let uri = "http://example.com/${unclosed.txt";
let expanded = expand_parameterized_uri(uri);
assert_eq!(expanded.len(), 1);
assert_eq!(expanded[0], uri);
}
#[test]
fn test_malformed_range() {
let uri = "http://example.com/${10-}.txt";
let expanded = expand_parameterized_uri(uri);
assert_eq!(expanded.len(), 1);
assert_eq!(expanded[0], uri);
}
#[test]
fn test_special_chars_in_uri_preserved() {
let uri = "http://example.com/path%20with%20spaces/${01-02}.html?query=test&special=%2F";
let expanded = expand_parameterized_uri(uri);
assert_eq!(expanded.len(), 2);
assert_eq!(
expanded[0],
"http://example.com/path%20with%20spaces/01.html?query=test&special=%2F"
);
assert_eq!(
expanded[1],
"http://example.com/path%20with%20spaces/02.html?query=test&special=%2F"
);
}
#[test]
fn test_uri_with_auth_and_port() {
let uri = "http://user:pass@example.com:8080/files/${01-03}.dat";
let expanded = expand_parameterized_uri(uri);
assert_eq!(expanded.len(), 3);
assert_eq!(
expanded[0],
"http://user:pass@example.com:8080/files/01.dat"
);
assert_eq!(
expanded[2],
"http://user:pass@example.com:8080/files/03.dat"
);
}
#[test]
fn test_ipv6_address_not_confused() {
let uri = "http://[2001:db8::1]:8080/file[01-02].txt";
let expanded = expand_parameterized_uri(uri);
assert_eq!(expanded.len(), 2);
assert!(expanded[0].ends_with("file01.txt"));
assert!(expanded[1].ends_with("file02.txt"));
}
#[test]
fn test_fragment_preserved() {
let uri = "http://example.com/doc${01-02}.pdf#section=1";
let expanded = expand_parameterized_uri(uri);
assert_eq!(expanded.len(), 2);
assert_eq!(expanded[0], "http://example.com/doc01.pdf#section=1");
assert_eq!(expanded[1], "http://example.com/doc02.pdf#section=1");
}
#[test]
fn test_format_with_width_basic() {
assert_eq!(format_with_width(1, 3), "001");
assert_eq!(format_with_width(42, 5), "00042");
assert_eq!(format_with_width(999, 2), "999"); assert_eq!(format_with_width(0, 4), "0000");
}
#[test]
fn test_generate_range_forward() {
let result = generate_range(1, 5, 1, 2);
assert_eq!(result, vec!["01", "02", "03", "04", "05"]);
}
#[test]
fn test_generate_range_reverse() {
let result = generate_range(5, 1, 1, 2);
assert_eq!(result, vec!["05", "04", "03", "02", "01"]);
}
#[test]
fn test_generate_range_with_step() {
let result = generate_range(1, 10, 3, 1);
assert_eq!(result, vec!["1", "4", "7", "10"]);
}
#[test]
fn test_generate_range_single_value() {
let result = generate_range(5, 5, 1, 3);
assert_eq!(result, vec!["005"]);
}
#[test]
fn test_find_param_patterns_simple() {
let patterns = find_param_patterns("http://ex.com/$2/file.txt");
assert_eq!(patterns.len(), 1);
match &patterns[0].1 {
ParamPattern::Simple { value } => assert_eq!(*value, 2),
_ => panic!("Expected Simple pattern"),
}
}
#[test]
fn test_find_param_patterns_braced() {
let patterns = find_param_patterns("http://ex.com/${01-05}.txt");
assert_eq!(patterns.len(), 1);
match &patterns[0].1 {
ParamPattern::Braced { start, end, .. } => {
assert_eq!(*start, 1);
assert_eq!(*end, 5);
}
_ => panic!("Expected Braced pattern"),
}
}
#[test]
fn test_find_param_patterns_bracket() {
let patterns = find_param_patterns("http://ex.com/file[01-10].zip");
assert_eq!(patterns.len(), 1);
match &patterns[0].1 {
ParamPattern::Bracket { start, end, .. } => {
assert_eq!(*start, 1);
assert_eq!(*end, 10);
}
_ => panic!("Expected Bracket pattern"),
}
}
#[test]
fn test_complex_real_world_example() {
let uri = "https://cdn.example.com/gallery/2024/photo${001-050}_hd.jpg";
let expanded = expand_parameterized_uri(uri);
assert_eq!(expanded.len(), 50);
assert_eq!(
expanded[0],
"https://cdn.example.com/gallery/2024/photo001_hd.jpg"
);
assert_eq!(
expanded[49],
"https://cdn.example.com/gallery/2024/photo050_hd.jpg"
);
}
#[test]
fn test_step_larger_than_range() {
let uri = "http://example.com/f[01-05:10].txt";
let expanded = expand_parameterized_uri(uri);
assert_eq!(expanded.len(), 1); assert_eq!(expanded[0], "http://example.com/f01.txt");
}
#[test]
fn test_reverse_range_with_step() {
let uri = "http://example.com/f[10-01:2].txt";
let expanded = expand_parameterized_uri(uri);
assert_eq!(expanded.len(), 5);
assert_eq!(expanded[0], "http://example.com/f10.txt");
assert_eq!(expanded[1], "http://example.com/f08.txt");
assert_eq!(expanded[2], "http://example.com/f06.txt");
assert_eq!(expanded[3], "http://example.com/f04.txt");
assert_eq!(expanded[4], "http://example.com/f02.txt");
}
}