1use std::fmt;
5
6use crate::images;
7use crate::model::Machine;
8
9#[derive(Debug, Clone, PartialEq, Eq)]
12pub enum Host {
13 Machine {
15 name: String,
17 network: String,
19 },
20 Literal(String),
22}
23
24impl fmt::Display for Host {
25 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
26 match self {
27 Host::Machine { name, .. } => f.write_str(name),
28 Host::Literal(h) => f.write_str(h),
29 }
30 }
31}
32
33#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct Url {
36 pub https: bool,
38 pub host: Host,
40 pub port: Option<u16>,
42 pub path: String,
44}
45
46impl Url {
47 #[must_use]
49 pub fn parse(s: &str) -> Option<Url> {
50 let (https, rest) = match s.strip_prefix("https://") {
51 Some(r) => (true, r),
52 None => (false, s.strip_prefix("http://")?),
53 };
54 let (hostport, path) = match rest.split_once('/') {
55 Some((hostport, path)) => (hostport, format!("/{path}")),
56 None => (rest, "/".to_owned()),
57 };
58 let (host, port) = host_and_port(hostport)?;
59 if host.is_empty() || host.chars().any(|c| c.is_whitespace() || c == '\'' || c == '"' || c == '@') {
60 return None;
61 }
62 Some(Url {
63 https,
64 host: Host::Literal(host.to_owned()),
65 port,
66 path,
67 })
68 }
69
70 #[must_use]
73 pub fn render(&self, host: &str) -> String {
74 let scheme = if self.https { "https" } else { "http" };
75 let bracketed;
76 let host = if host.contains(':') {
77 bracketed = format!("[{host}]");
78 bracketed.as_str()
79 } else {
80 host
81 };
82 match self.port {
83 Some(p) => format!("{scheme}://{host}:{p}{}", self.path),
84 None => format!("{scheme}://{host}{}", self.path),
85 }
86 }
87}
88
89fn host_and_port(hostport: &str) -> Option<(&str, Option<u16>)> {
91 let port = |p: &str| p.parse::<u16>().ok().filter(|p| *p > 0);
92 if let Some(rest) = hostport.strip_prefix('[') {
93 let (host, after) = rest.split_once(']')?;
94 return match after.strip_prefix(':') {
95 Some(p) => Some((host, Some(port(p)?))),
96 None if after.is_empty() => Some((host, None)),
97 None => None,
98 };
99 }
100 match hostport.rsplit_once(':') {
101 Some((h, p)) => Some((h, Some(port(p)?))),
102 None => Some((hostport, None)),
103 }
104}
105
106#[must_use]
108pub fn can_run_checks(m: &Machine) -> bool {
109 !m.vm.as_ref().is_some_and(|v| images::is_windows(&v.os)) && m.docker.as_ref().is_none_or(|d| d.appliance.is_none())
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116
117 #[test]
118 fn an_ipv6_literal_goes_in_brackets() {
119 let u = Url::parse("http://[fd00:20::10]:8080/x").unwrap();
120 assert_eq!((u.port, u.host.to_string()), (Some(8080), "fd00:20::10".to_owned()));
121 assert_eq!(u.render("fd00:20::10"), "http://[fd00:20::10]:8080/x");
122 assert!(Url::parse("http://[fd00::1]/").is_some_and(|bare| bare.port.is_none()));
123 assert!(Url::parse("http://[fd00::1/").is_none());
124 assert!(Url::parse("http://[fd00::1]x/").is_none());
125 }
126
127 #[test]
128 fn a_url_reads_into_its_scheme_host_port_and_path() {
129 assert_eq!(
130 Url::parse("https://web:8443/health?x=1"),
131 Some(Url {
132 https: true,
133 host: Host::Literal("web".into()),
134 port: Some(8443),
135 path: "/health?x=1".into(),
136 })
137 );
138 assert_eq!(Url::parse("http://web").map(|u| (u.https, u.port, u.path)), Some((false, None, "/".to_owned())));
139 }
140
141 #[test]
142 fn a_host_is_one_plain_name_and_a_port_is_1_or_more() {
143 for bad in [
144 "http:///x",
145 "http://a b/",
146 "http://a'b/",
147 "http://a\"b/",
148 "http://user@web/",
149 "http://web:0/",
150 "ftp://web/",
151 ] {
152 assert_eq!(Url::parse(bad), None, "{bad}");
153 }
154 assert_eq!(Url::parse("http://web:1/").and_then(|u| u.port), Some(1));
155 }
156
157 #[test]
158 fn windows_vms_and_appliances_run_no_checks() -> Result<(), String> {
159 let spec = crate::testing::model(&crate::testing::lab(
160 "machines:\n web: { docker: { image: nginx } }\n box: { vm: { os: debian@12 } }\n dc: { vm: { os: windows-server@2022 } }\n fw: { docker: { appliance: cisco-iol } }\n",
161 ))?;
162 let can: Vec<(&str, bool)> = spec.machines.iter().map(|(n, m)| (n.as_str(), can_run_checks(m))).collect();
163 assert_eq!(can, [("web", true), ("box", true), ("dc", false), ("fw", false)]);
164 Ok(())
165 }
166}