| 1 | use serde_json::Value; |
| 2 | use std::collections::HashMap; |
| 3 | |
| 4 | |
| 5 | pub fn extract_har_files(har: &Value) -> Vec<(String, Value)> { |
| 6 | let entries = match har.pointer("/log/entries") { |
| 7 | Some(Value::Array(arr)) => arr, |
| 8 | _ => return vec![], |
| 9 | }; |
| 10 | |
| 11 | let mut name_counts: HashMap<String, usize> = HashMap::new(); |
| 12 | let mut results = Vec::new(); |
| 13 | |
| 14 | for entry in entries { |
| 15 | |
| 16 | let status = entry |
| 17 | .pointer("/response/status") |
| 18 | .and_then(|v| v.as_u64()) |
| 19 | .unwrap_or(0); |
| 20 | if status < 200 || status >= 300 { |
| 21 | continue; |
| 22 | } |
| 23 | |
| 24 | |
| 25 | let mime = entry |
| 26 | .pointer("/response/content/mimeType") |
| 27 | .and_then(|v| v.as_str()) |
| 28 | .unwrap_or(""); |
| 29 | if !mime.contains("json") { |
| 30 | continue; |
| 31 | } |
| 32 | |
| 33 | |
| 34 | let encoding = entry |
| 35 | .pointer("/response/content/encoding") |
| 36 | .and_then(|v| v.as_str()) |
| 37 | .unwrap_or(""); |
| 38 | if encoding == "base64" { |
| 39 | continue; |
| 40 | } |
| 41 | |
| 42 | |
| 43 | let text = match entry.pointer("/response/content/text").and_then(|v| v.as_str()) { |
| 44 | Some(t) => t, |
| 45 | None => continue, |
| 46 | }; |
| 47 | let parsed: Value = match serde_json::from_str(text) { |
| 48 | Ok(v) => v, |
| 49 | Err(_) => continue, |
| 50 | }; |
| 51 | |
| 52 | |
| 53 | let url = entry |
| 54 | .pointer("/request/url") |
| 55 | .and_then(|v| v.as_str()) |
| 56 | .unwrap_or(""); |
| 57 | let method = entry |
| 58 | .pointer("/request/method") |
| 59 | .and_then(|v| v.as_str()) |
| 60 | .unwrap_or("GET"); |
| 61 | |
| 62 | let path_seg = extract_path_segment(url); |
| 63 | let values = collect_distinguishing_values(entry, method, url); |
| 64 | |
| 65 | let base_name = if values.is_empty() { |
| 66 | path_seg.clone() |
| 67 | } else { |
| 68 | format!("{}_{}", path_seg, values.join("_")) |
| 69 | }; |
| 70 | |
| 71 | let count = name_counts.entry(base_name.clone()).or_insert(0); |
| 72 | *count += 1; |
| 73 | let filename = if *count == 1 { |
| 74 | format!("{}.json", base_name) |
| 75 | } else { |
| 76 | format!("{}_{}.json", base_name, count) |
| 77 | }; |
| 78 | |
| 79 | results.push((filename, parsed)); |
| 80 | } |
| 81 | |
| 82 | results |
| 83 | } |
| 84 | |
| 85 | |
| 86 | |
| 87 | pub fn extract_path_segment(url: &str) -> String { |
| 88 | |
| 89 | let path_part = url.split('?').next().unwrap_or(url); |
| 90 | let path_part = path_part.split('#').next().unwrap_or(path_part); |
| 91 | |
| 92 | |
| 93 | let path = if let Some(idx) = path_part.find("://") { |
| 94 | let after_scheme = &path_part[idx + 3..]; |
| 95 | after_scheme.find('/').map_or("", |i| &after_scheme[i..]) |
| 96 | } else { |
| 97 | path_part |
| 98 | }; |
| 99 | |
| 100 | let segments: Vec<&str> = path |
| 101 | .split('/') |
| 102 | .filter(|s| !s.is_empty()) |
| 103 | .filter(|s| { |
| 104 | let lower = s.to_lowercase(); |
| 105 | lower != "api" && lower != "rest" && !is_version_segment(s) |
| 106 | }) |
| 107 | .collect(); |
| 108 | |
| 109 | |
| 110 | for seg in segments.iter().rev() { |
| 111 | if !seg.chars().all(|c| c.is_ascii_digit()) { |
| 112 | return seg.to_string(); |
| 113 | } |
| 114 | } |
| 115 | |
| 116 | |
| 117 | segments |
| 118 | .last() |
| 119 | .map(|s| s.to_string()) |
| 120 | .unwrap_or_else(|| "response".to_string()) |
| 121 | } |
| 122 | |
| 123 | |
| 124 | fn collect_distinguishing_values(entry: &Value, method: &str, url: &str) -> Vec<String> { |
| 125 | let mut values = Vec::new(); |
| 126 | |
| 127 | |
| 128 | if let Some(query) = url.split('?').nth(1) { |
| 129 | for pair in query.split('&') { |
| 130 | let val = pair.split('=').nth(1).unwrap_or(""); |
| 131 | if is_useful_value(val) { |
| 132 | values.push(val.to_string()); |
| 133 | } |
| 134 | } |
| 135 | } |
| 136 | |
| 137 | |
| 138 | if method.eq_ignore_ascii_case("POST") { |
| 139 | if let Some(text) = entry |
| 140 | .pointer("/request/postData/text") |
| 141 | .and_then(|v| v.as_str()) |
| 142 | { |
| 143 | if let Ok(Value::Object(map)) = serde_json::from_str::<Value>(text) { |
| 144 | for (_key, val) in &map { |
| 145 | if let Some(s) = val.as_str() { |
| 146 | if is_useful_value(s) { |
| 147 | values.push(s.to_string()); |
| 148 | } |
| 149 | } |
| 150 | } |
| 151 | } |
| 152 | } |
| 153 | } |
| 154 | |
| 155 | values |
| 156 | } |
| 157 | |
| 158 | |
| 159 | fn is_version_segment(s: &str) -> bool { |
| 160 | let lower = s.to_lowercase(); |
| 161 | if let Some(rest) = lower.strip_prefix('v') { |
| 162 | !rest.is_empty() && rest.chars().all(|c| c.is_ascii_digit()) |
| 163 | } else { |
| 164 | false |
| 165 | } |
| 166 | } |
| 167 | |
| 168 | |
| 169 | fn is_useful_value(s: &str) -> bool { |
| 170 | if s.is_empty() || s.len() > 15 { |
| 171 | return false; |
| 172 | } |
| 173 | if s.chars().all(|c| c.is_ascii_digit()) { |
| 174 | return false; |
| 175 | } |
| 176 | if is_hex_like(s) { |
| 177 | return false; |
| 178 | } |
| 179 | true |
| 180 | } |
| 181 | |
| 182 | |
| 183 | pub fn is_hex_like(s: &str) -> bool { |
| 184 | |
| 185 | let stripped = s.replace('-', ""); |
| 186 | if stripped.len() == 32 && stripped.chars().all(|c| c.is_ascii_hexdigit()) { |
| 187 | return true; |
| 188 | } |
| 189 | |
| 190 | if s.len() > 15 { |
| 191 | let hex_count = s.chars().filter(|c| c.is_ascii_hexdigit()).count(); |
| 192 | if hex_count as f64 / s.len() as f64 > 0.5 { |
| 193 | return true; |
| 194 | } |
| 195 | } |
| 196 | false |
| 197 | } |