Skip to main content

spotapi/
utils.rs

1use anyhow::{anyhow, Result};
2use rand::{distributions::Alphanumeric, Rng};
3use regex::Regex;
4
5pub fn random_string(len: usize) -> String {
6    rand::thread_rng()
7        .sample_iter(&Alphanumeric)
8        .take(len)
9        .map(char::from)
10        .collect()
11}
12
13pub fn random_hex_string(len: usize) -> String {
14    let mut bytes = vec![0u8; len.div_ceil(2)];
15    rand::thread_rng().fill(&mut bytes[..]);
16    let hex_string = hex::encode(bytes);
17    hex_string[..len].to_string()
18}
19
20pub fn extract_js_links(html: &str) -> Vec<String> {
21    let re = Regex::new(r#"src="([^"]+\.js)""#).unwrap();
22    re.captures_iter(html)
23        .map(|cap| cap[1].to_string())
24        .collect()
25}
26
27pub fn extract_mappings(
28    js_code: &str,
29) -> Result<(
30    std::collections::HashMap<i32, String>,
31    std::collections::HashMap<i32, String>,
32)> {
33    let re_obj = Regex::new(r#"\{(\d+:"[^"]+"(?:,\d+:"[^"]+")*)\}"#).unwrap();
34
35    let matches: Vec<_> = re_obj.find_iter(js_code).map(|m| m.as_str()).collect();
36
37    if matches.len() < 5 {
38        return Err(anyhow!(
39            "Could not find both mappings in the JS code (matches found: {})",
40            matches.len()
41        ));
42    }
43
44    let map1 = parse_js_dict(matches[3])?;
45    let map2 = parse_js_dict(matches[4])?;
46
47    Ok((map1, map2))
48}
49
50fn parse_js_dict(s: &str) -> Result<std::collections::HashMap<i32, String>> {
51    let content = s.trim_start_matches('{').trim_end_matches('}');
52    let mut map = std::collections::HashMap::new();
53
54    let mut current = content;
55    while !current.is_empty() {
56        if let Some(colon_idx) = current.find(':') {
57            let key_str = &current[..colon_idx];
58            let key: i32 = key_str
59                .parse()
60                .map_err(|_| anyhow!("Failed to parse key: {}", key_str))?;
61
62            let remainder = &current[colon_idx + 1..];
63            if !remainder.starts_with('"') {
64                return Err(anyhow!("Value does not start with quote"));
65            }
66
67            if let Some(end_quote_idx) = remainder[1..].find('"') {
68                let end_quote_real_idx = end_quote_idx + 1;
69                let value = &remainder[1..end_quote_real_idx];
70                map.insert(key, value.to_string());
71
72                if remainder.len() > end_quote_real_idx + 1 {
73                    if remainder.as_bytes()[end_quote_real_idx + 1] == b',' {
74                        current = &remainder[end_quote_real_idx + 2..];
75                    } else {
76                        break;
77                    }
78                } else {
79                    break;
80                }
81            } else {
82                return Err(anyhow!("Value does not end with quote"));
83            }
84        } else {
85            break;
86        }
87    }
88
89    Ok(map)
90}
91
92/// Combines name and hash mappings to generate chunk filenames.
93/// Returns filenames in format: name.hash.js
94pub fn combine_chunks(
95    name_map: &std::collections::HashMap<i32, String>,
96    hash_map: &std::collections::HashMap<i32, String>,
97) -> Vec<String> {
98    let mut combined = Vec::new();
99    for (key, name) in name_map {
100        if let Some(hash) = hash_map.get(key) {
101            // Format: name.hash.js (e.g., "xpui-routes-search.8bfb4d6d.js")
102            combined.push(format!("{}.{}.js", name, hash));
103        }
104    }
105    combined
106}