1use byte_unit::{Byte, UnitType};
2use std::{collections::HashSet, future::Future};
3
4pub fn run_future<F, T>(input: F) -> T
5where
6 F: Future<Output = T>,
7{
8 let runtime = tokio::runtime::Runtime::new().unwrap();
9 runtime.block_on(input)
10}
11
12pub fn humanize_bytes(bytes: u64) -> String {
13 let b = Byte::from_u64(bytes).get_appropriate_unit(UnitType::Binary);
14 format!("{b:.2}")
15}
16
17pub fn byte_string_to_number(byte_string: &str) -> Option<u64> {
20 Byte::parse_str(byte_string, true).map(|b| b.into()).ok()
21}
22
23pub fn replace_invalid_chars_in_filename(input: &str) -> String {
24 let replacement: char = ' ';
25 let invalid_chars: Vec<char> = vec![
26 '/', '\\', '?', '%', '*', ':', '|', '"', '<', '>', ';', '=', '\n',
27 ];
28
29 input
30 .chars()
31 .map(|c| {
32 if invalid_chars.contains(&c) {
33 replacement
34 } else {
35 c
36 }
37 })
38 .collect::<String>()
39 .trim()
40 .to_string()
41}
42
43pub fn extract_filename_from_url(url: &str) -> Option<String> {
44 let url = reqwest::Url::parse(url);
45 if url.is_err() {
46 return None;
47 }
48
49 let url = url.unwrap();
50 let path_segments: Vec<&str> = url.path_segments()?.collect();
51 match path_segments[path_segments.len() - 1] {
52 "" => None,
53 segment => Some(segment.to_string()),
54 }
55}
56
57pub fn str_vectors_intersect<T1, T2>(first: &[T1], second: &[T2]) -> bool
58where
59 T1: AsRef<str>,
60 T2: AsRef<str>,
61{
62 if first.is_empty() || second.is_empty() {
63 return false;
64 }
65
66 let mut first_set = HashSet::new();
67
68 for first_item in first {
69 first_set.insert(first_item.as_ref().to_lowercase());
70 }
71
72 for second_item in second {
73 if first_set.contains(&second_item.as_ref().to_lowercase()) {
74 return true;
75 }
76 }
77
78 false
79}
80
81pub fn parse_usize_range(value: &str, max_value: usize) -> Option<Vec<usize>> {
91 let dash_idx = value.find('-');
92
93 if dash_idx.is_none() {
94 return value.parse::<usize>().map(|v| vec![v]).ok();
95 }
96
97 let dash_idx = dash_idx.unwrap();
98
99 let left = &value[0..dash_idx];
100 let right = &value[dash_idx + 1..];
101
102 let range_left = if !left.is_empty() {
103 match left.parse::<usize>() {
104 Ok(v) => v,
105 Err(_) => return None,
106 }
107 } else {
108 1
109 };
110
111 let range_right = if !right.is_empty() {
112 match right.parse::<usize>() {
113 Ok(v) => v,
114 Err(_) => return None,
115 }
116 } else {
117 max_value
118 };
119
120 Some((range_left..range_right + 1).collect())
123}
124
125pub fn union_usize_ranges(values: &[&str], max_value: usize) -> Result<Vec<usize>, anyhow::Error> {
126 let mut invalid_values = vec![];
127 let mut parsed = HashSet::new();
128
129 for &v in values {
130 match parse_usize_range(v, max_value) {
131 Some(usize_values) => parsed.extend(usize_values),
132 None => invalid_values.push(v),
133 }
134 }
135
136 if !invalid_values.is_empty() {
137 let msg = invalid_values
138 .into_iter()
139 .map(|v| format!("'{}'", v))
140 .collect::<Vec<_>>()
141 .join(", ");
142
143 return Err(anyhow::anyhow!("{}", msg));
144 }
145
146 let mut output = Vec::from_iter(parsed);
147 output.sort();
148 Ok(output)
149}
150
151#[test]
152fn test_remove_invalid_chars() {
153 let test_data = vec![
154 ("Humble Bundle: Nice book", "Humble Bundle Nice book"),
155 ("::Make::", "Make"),
156 ("Test\nFile", "Test File"),
157 ];
158
159 for (input, expected) in test_data {
160 let got = replace_invalid_chars_in_filename(input);
161 assert_eq!(expected, got);
162 }
163}
164
165#[test]
166fn test_extract_filename_from_url() {
167 let test_data = vec![(
168 "with filename",
169 "https://dl.humble.com/grokkingalgorithms.mobi?gamekey=xxxxxx&ttl=1655031034&t=yyyyyyyyyy",
170 Some("grokkingalgorithms.mobi".to_string()),
171 ), (
172 "no filename",
173 "https://www.google.com/",
174 None
175 )];
176
177 for (name, url, expected) in test_data {
178 assert_eq!(
179 extract_filename_from_url(url),
180 expected,
181 "test case '{}'",
182 name
183 );
184 }
185}
186
187#[test]
188fn test_humanize_bytes() {
193 let test_data = vec![(1, "1 B"), (3 * 1024, "3.00 KiB")];
194
195 for (input, want) in test_data {
196 assert_eq!(humanize_bytes(input), want.to_string());
197 }
198}
199#[test]
200fn test_vectors_intersect() {
201 let test_data = vec![
202 (vec!["FOO", "bar"], vec!["foo"], true),
203 (vec!["foo", "bar"], vec!["baz"], false),
204 (vec!["foo"], vec![], false),
205 (vec![], vec!["baz"], false),
206 ];
207
208 for (first, second, result) in test_data {
209 let msg = format!(
210 "intersect of {:?} and {:?}, expected: {}",
211 first, second, result
212 );
213 assert_eq!(str_vectors_intersect(&first, &second), result, "{}", msg);
214 }
215}
216
217#[test]
218fn test_parse_usize_range() {
219 const MAX_VAL: usize = 50;
220
221 let test_data = vec![
222 ("empty string", "", None),
223 ("invalid string", "abcd", None),
224 ("single value", "42", Some(vec![42])),
225 (
226 "range with start and end",
227 "5-10",
228 Some(vec![5, 6, 7, 8, 9, 10]),
229 ),
230 ("range with no start", "-5", Some(vec![1, 2, 3, 4, 5])),
231 (
232 "range with no end",
233 "45-",
234 Some(vec![45, 46, 47, 48, 49, 50]),
235 ), ("invalid start", "abc-", None),
237 ("invalid end", "-abc", None),
238 ("invalid start and end", "abc-def", None),
239 ];
240
241 for (name, input, expected) in test_data {
242 let msg = format!(
243 "'{}' failed: input = {}, expected = {:?}",
244 name, input, &expected
245 );
246 assert_eq!(parse_usize_range(input, MAX_VAL), expected, "{}", msg);
247 }
248}
249
250#[test]
251fn test_union_valid_usize_ranges() {
252 const MAX_VAL: usize = 10;
253 let test_data = vec![
254 ("simple values", vec!["5", "10"], vec![5, 10]),
255 ("simple value and range", vec!["8", "7-"], vec![7, 8, 9, 10]),
256 ("two ranges", vec!["-3", "7-"], vec![1, 2, 3, 7, 8, 9, 10]),
257 ];
258
259 for (name, input, expected) in test_data {
260 let output = union_usize_ranges(&input, MAX_VAL);
261
262 let msg = format!(
263 "'{}' failed: input = {:?}, expected = {:?}",
264 name, &input, &expected
265 );
266
267 assert!(output.is_ok(), "{}", msg);
268 assert_eq!(output.unwrap(), expected, "{}", msg);
269 }
270}
271
272#[test]
273fn test_union_invalid_usize_ranges() {
274 const MAX_VAL: usize = 10;
275 let test_data = vec![
276 ("invalid simple values", vec!["a", "b"]),
277 ("invalid ranges", vec!["a-", "-b"]),
278 ];
279
280 for (name, input) in test_data {
281 let expected_err_msg = input
283 .iter()
284 .map(|v| format!("'{}'", v))
285 .collect::<Vec<_>>()
286 .join(", ");
287
288 let output = union_usize_ranges(&input, MAX_VAL);
289
290 let assert_msg = format!(
291 "'{}' failed: input = {:?}, expected = {:?}",
292 name, &input, &expected_err_msg
293 );
294
295 assert!(output.is_err(), "{}", assert_msg);
296 let output_err_msg: String = output.unwrap_err().downcast().unwrap();
297 assert_eq!(output_err_msg, expected_err_msg, "{}", assert_msg);
298 }
299}
300
301#[cfg(target_os = "windows")]
302#[test]
303fn test_windows_filename_validation() {
304 use std::fs::File;
305
306 let invalid_chars = vec!['/', '\\', '?', '*', '|', '"', '<', '>', '\n'];
309
310 for &c in &invalid_chars {
311 let filename = format!("test_{}_file.txt", c);
312 let result = File::create(&filename);
314 assert!(
315 result.is_err(),
316 "Expected error creating file with invalid character '{}'",
317 c
318 );
319
320 let cleaned = replace_invalid_chars_in_filename(&filename);
322 assert_ne!(
323 cleaned, filename,
324 "Filename not cleaned for character '{}'",
325 c
326 );
327
328 File::create(&cleaned).expect(&format!(
330 "Failed to create cleaned file for character '{}'",
331 c
332 ));
333 std::fs::remove_file(&cleaned).expect("Failed to remove test file");
335 }
336}