1use std::str::FromStr;
31use std::{
32 error::Error,
33 fmt::Display,
34 net::{Ipv4Addr, Ipv6Addr},
35};
36
37#[derive(Debug, PartialEq)]
45pub struct UrlParser {
46 pub scheme: Scheme,
47 pub target: String,
48 pub target_type: TargetType,
49 pub port: u16,
50 pub subdirectory: String,
51 pub full_url: String,
52}
53
54impl Display for UrlParser {
55 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56 write!(
57 f,
58 "Scheme: {}\nTarget: {}\nTarget type: {}\nPort: {}\nSub directory: {}\nFull url: {}",
59 self.scheme, self.target, self.target_type, self.port, self.subdirectory, self.full_url
60 )
61 }
62}
63
64#[derive(Debug, PartialEq)]
66pub enum Scheme {
67 Http,
68 Https,
69}
70
71impl Display for Scheme {
72 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
73 match self {
74 Self::Http => write!(f, "http"),
75 Self::Https => write!(f, "https"),
76 }
77 }
78}
79
80#[derive(Debug, PartialEq)]
87pub enum TargetType {
88 Dns,
89 IPv4,
90 IPv6,
91}
92
93impl Display for TargetType {
94 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95 match self {
96 Self::Dns => write!(f, "dns"),
97 Self::IPv4 => write!(f, "ipv4"),
98 Self::IPv6 => write!(f, "ipv6"),
99 }
100 }
101}
102
103impl TargetType {
104 pub fn is_dns(target: &str) -> Result<TargetType, UrlParserErrors> {
112 if target.len() > 253 {
113 return Err(UrlParserErrors::InvalidTargetType);
114 }
115
116 let valid: bool = target.split('.').all(|label| {
117 if label.is_empty() || label.len() > 63 {
118 return false;
119 }
120
121 if label.starts_with('-') || label.ends_with('-') {
122 return false;
123 }
124
125 if !label.chars().all(|c| c.is_ascii_alphanumeric() || c == '-') {
126 return false;
127 }
128
129 true
130 });
131
132 if valid {
133 Ok(TargetType::Dns)
134 } else {
135 Err(UrlParserErrors::InvalidTargetType)
136 }
137 }
138
139 pub fn is_ipv4(target: &str) -> Result<TargetType, UrlParserErrors> {
141 match Ipv4Addr::from_str(target) {
142 Ok(_) => Ok(TargetType::IPv4),
143 Err(_) => Err(UrlParserErrors::InvalidTargetType),
144 }
145 }
146
147 pub fn is_ipv6(target: &str) -> Result<TargetType, UrlParserErrors> {
149 let clean_ip = target.trim_matches(['[', ']'].as_ref());
150 match Ipv6Addr::from_str(clean_ip) {
151 Ok(_) => Ok(TargetType::IPv6),
152 Err(_) => Err(UrlParserErrors::InvalidTargetType),
153 }
154 }
155}
156
157#[derive(Debug)]
159pub enum UrlParserErrors {
160 UrlEmpty,
161 InvalidSize,
162 InvalidScheme,
163 InvalidTargetType,
164 InvalidSchemeSyntax,
165 InvalidPort,
166}
167
168impl Display for UrlParserErrors {
169 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
170 match self {
171 Self::UrlEmpty => {
172 write!(f, "The url is empty")
173 }
174 Self::InvalidSize => {
175 write!(f, "Invalid size!")
176 }
177 Self::InvalidScheme => {
178 write!(f, "Invalid scheme => http or https")
179 }
180 Self::InvalidTargetType => {
181 write!(f, "Invalid target type => Must be a DNS or IPV4 OR IPV6")
182 }
183 Self::InvalidSchemeSyntax => {
184 write!(f, "Invalid scheme sintax => http:// or https://")
185 }
186 Self::InvalidPort => {
187 write!(f, "Invalid port => (1 -> 65,535)")
188 }
189 }
190 }
191}
192
193impl Error for UrlParserErrors {}
194
195#[macro_export]
206macro_rules! subslice {
207 ($s:expr, $slice:expr, $err:expr) => {
208 $s.get($slice).ok_or($err)?
209 };
210}
211
212impl std::str::FromStr for UrlParser {
213 type Err = UrlParserErrors;
214 fn from_str(s: &str) -> Result<Self, Self::Err> {
215 UrlParser::new(s)
216 }
217}
218
219impl TryFrom<&str> for UrlParser {
220 type Error = UrlParserErrors;
221 fn try_from(value: &str) -> Result<Self, Self::Error> {
222 UrlParser::new(value)
223 }
224}
225
226impl UrlParser {
227 pub fn new(input_url: &str) -> Result<UrlParser, UrlParserErrors> {
248 let url = input_url;
249
250 if url.is_empty() {
251 return Err(UrlParserErrors::UrlEmpty);
252 }
253
254 if subslice!(&url, ..7, UrlParserErrors::InvalidSize) != "http://"
255 && subslice!(&url, ..8, UrlParserErrors::InvalidSize) != "https://"
256 {
257 return Err(UrlParserErrors::InvalidSchemeSyntax);
258 }
259
260 let scheme = if url.starts_with("http://") {
261 Scheme::Http
262 } else if url.starts_with("https://") {
263 Scheme::Https
264 } else {
265 return Err(UrlParserErrors::InvalidScheme);
266 };
267
268 let target: String = {
269 match scheme {
270 Scheme::Http => {
271 if url[7..].starts_with('[') {
272 url[7..]
273 .chars()
274 .take_while(|c| *c != ']')
275 .chain(std::iter::once(']'))
276 .collect()
277 } else {
278 url.chars()
279 .skip(7)
280 .take_while(|c| *c != ':' && *c != '/')
281 .collect()
282 }
283 }
284 Scheme::Https => {
285 if url[8..].starts_with('[') {
286 url[8..]
287 .chars()
288 .take_while(|c| *c != ']')
289 .chain(std::iter::once(']'))
290 .collect()
291 } else {
292 url.chars()
293 .skip(8)
294 .take_while(|c| *c != ':' && *c != '/')
295 .collect()
296 }
297 }
298 }
299 };
300
301 let target_type: TargetType = TargetType::is_ipv4(&target)
302 .or(TargetType::is_ipv6(&target))
303 .or(TargetType::is_dns(&target))?;
304
305 let quant_to_skip = match scheme {
306 Scheme::Http => "http://".len() + target.len(),
307 Scheme::Https => "https://".len() + target.len(),
308 };
309
310 let port: u16 = {
311 if quant_to_skip >= url.len() {
312 0
313 } else if url
314 .chars()
315 .nth(quant_to_skip)
316 .ok_or(UrlParserErrors::InvalidSize)?
317 == ':'
318 {
319 let string_port_temp: String = url
320 .chars()
321 .skip(quant_to_skip + 1)
322 .take_while(|v| v.is_ascii_digit())
323 .collect();
324
325 match string_port_temp.parse::<u16>() {
326 Ok(port) => port,
327 Err(_) => return Err(UrlParserErrors::InvalidPort),
328 }
329 } else {
330 0
331 }
332 };
333
334 let subdirectory = {
335 let chars_to_skip = {
336 match scheme {
337 Scheme::Http => "http://".len(),
338 Scheme::Https => "https://".len(),
339 }
340 } + target.len()
341 + {
342 match port {
343 0 => 0,
344 n => format!(":{}", n).len(),
345 }
346 };
347 let subdirectory: String = url.chars().skip(chars_to_skip).collect();
348 subdirectory
349 };
350
351 let full_url = format!(
352 "{}://{}{}{}",
353 scheme,
354 target,
355 match port {
356 0 => String::new(),
357 n => format!(":{}", n),
358 },
359 subdirectory
360 );
361
362 Ok(UrlParser {
363 scheme,
364 target,
365 target_type,
366 port,
367 subdirectory,
368 full_url,
369 })
370 }
371}
372
373#[cfg(test)]
374mod tests {
375 use super::*;
376
377 #[test]
378 fn test_url_urlparser_valid_http_dns() {
379 let url = UrlParser::new("http://example.com").unwrap();
380 assert_eq!(format!("{}", url.scheme), "http");
381 assert_eq!(url.target, "example.com");
382 assert_eq!(format!("{}", url.target_type), "dns");
383 assert_eq!(url.port, 0);
384 assert_eq!(url.subdirectory, "");
385 assert_eq!(url.full_url, "http://example.com");
386 assert_ne!(
387 url.to_string(),
388 "Scheme: http Target: example.com Target type: dns Port: 0 Sub directory: Full url: http://example.com"
389 );
390 }
391
392 #[test]
393 fn test_url_urlparser_valid_https_dns_with_path() {
394 let url = UrlParser::new("https://example.com/test/path").unwrap();
395 assert_eq!(format!("{}", url.scheme), "https");
396 assert_eq!(url.target, "example.com");
397 assert_eq!(format!("{}", url.target_type), "dns");
398 assert_eq!(url.port, 0);
399 assert_eq!(url.subdirectory, "/test/path");
400 assert_eq!(url.full_url, "https://example.com/test/path");
401 assert_ne!(
402 url.to_string(),
403 "Scheme: https Target: example.com Target type: dns Port: 0 Sub directory: /test/path Full url: https://example.com/test/path"
404 );
405 }
406
407 #[test]
408 fn test_url_urlparser_valid_https_dns_with_port_and_path() {
409 let url = UrlParser::new("https://example.com:420/test/path").unwrap();
410 assert_eq!(format!("{}", url.scheme), "https");
411 assert_eq!(url.target, "example.com");
412 assert_eq!(format!("{}", url.target_type), "dns");
413 assert_eq!(url.port, 420);
414 assert_eq!(url.subdirectory, "/test/path");
415 assert_eq!(url.full_url, "https://example.com:420/test/path");
416 assert_ne!(
417 url.to_string(),
418 "Scheme: https Target: example.com Target type: dns Port: 420 Sub directory: /test/path Full url: https://example.com:420/test/path"
419 );
420 }
421
422 #[test]
423 fn test_url_urlparser_valid_http_with_port() {
424 let url = UrlParser::new("http://localhost:8080").unwrap();
425 assert_eq!(format!("{}", url.scheme), "http");
426 assert_eq!(url.target, "localhost");
427 assert_eq!(format!("{}", url.target_type), "dns");
428 assert_eq!(url.port, 8080);
429 assert_eq!(url.subdirectory, "");
430 assert_eq!(url.full_url, "http://localhost:8080");
431 assert_ne!(
432 url.to_string(),
433 "Scheme: http Target: localhost Target type: dns Port: 8080 Sub directory: Full url: http://localhost:8080"
434 );
435 }
436
437 #[test]
438 fn test_url_urlparser_valid_ipv4() {
439 let url = UrlParser::new("http://127.0.0.1").unwrap();
440 assert_eq!(format!("{}", url.scheme), "http");
441 assert_eq!(url.target, "127.0.0.1");
442 assert_eq!(format!("{}", url.target_type), "ipv4");
443 assert_eq!(url.port, 0);
444 assert_eq!(url.subdirectory, "");
445 assert_eq!(url.full_url, "http://127.0.0.1");
446 assert_ne!(
447 url.to_string(),
448 "Scheme: http Target: 127.0.0.1 Target type: ipv4 Port: 0 Sub directory: Full url: http://127.0.0.1"
449 );
450 }
451
452 #[test]
453 fn test_url_urlparser_valid_ipv6() {
454 let url = UrlParser::new("https://[::1]").unwrap();
455 assert_eq!(format!("{}", url.scheme), "https");
456 assert_eq!(url.target, "[::1]");
457 assert_eq!(format!("{}", url.target_type), "ipv6");
458 assert_eq!(url.port, 0);
459 assert_eq!(url.subdirectory, "");
460 assert_eq!(url.full_url, "https://[::1]");
461 assert_ne!(
462 url.to_string(),
463 "Scheme: https Target: [::1] Target type: ipv6 Port: 0 Sub directory: Full url: https://[::1]"
464 );
465 }
466
467 #[test]
468 fn test_url_urlparser_direct_parse() {
469 let url = "https://example.com:33".parse::<UrlParser>().unwrap();
470 assert_eq!(format!("{}", url.scheme), "https");
471 assert_eq!(url.target, "example.com");
472 assert_eq!(format!("{}", url.target_type), "dns");
473 assert_eq!(url.port, 33);
474 assert_eq!(url.subdirectory, "");
475 assert_eq!(url.full_url, "https://example.com:33");
476 assert_ne!(
477 url.to_string(),
478 "Scheme: https Target: example.com Target type: dns Port: 33 Sub directory: Full url: https://example.com:33"
479 );
480 }
481
482 #[test]
483 fn test_url_urlparser_direct_parse_try_from() {
484 let url = UrlParser::try_from("http://example.com").unwrap();
485 assert_eq!(format!("{}", url.scheme), "http");
486 assert_eq!(url.target, "example.com");
487 assert_eq!(format!("{}", url.target_type), "dns");
488 assert_eq!(url.port, 0);
489 assert_eq!(url.subdirectory, "");
490 assert_eq!(url.full_url, "http://example.com");
491 assert_ne!(
492 url.to_string(),
493 "Scheme: http Target: example.com Target type: dns Port: 0 Sub directory: Full url: https://example.com"
494 );
495 }
496
497 #[test]
498 fn test_url_urlparser_invalid_empty_url() {
499 let res = UrlParser::new("");
500 assert!(matches!(res, Err(UrlParserErrors::UrlEmpty)));
501 }
502
503 #[test]
504 fn test_url_urlparser_invalid_scheme() {
505 let res = UrlParser::new("ftp://example.com");
506 assert!(matches!(res, Err(UrlParserErrors::InvalidSchemeSyntax)));
507 }
508
509 #[test]
510 fn test_url_urlparser_invalid_dns() {
511 let res = UrlParser::new("http://exa$mple.com");
512 assert!(matches!(res, Err(UrlParserErrors::InvalidTargetType)));
513 }
514
515 #[test]
516 fn test_url_urlparser_invalid_port_not_number() {
517 let res = UrlParser::new("http://example.com:abcd");
518 assert!(matches!(res, Err(UrlParserErrors::InvalidPort)));
519 }
520
521 #[test]
522 fn test_url_urlparser_invalid_port_out_of_range() {
523 let res = UrlParser::new("http://example.com:70000");
524 assert!(matches!(res, Err(UrlParserErrors::InvalidPort)));
525 }
526}