arcbox_helper/validate/
mod.rs1mod bridge_iface;
12mod cli_name;
13mod cli_target;
14mod dns_port;
15mod domain;
16mod socket_target;
17mod subnet;
18
19pub use bridge_iface::BridgeIface;
20pub use cli_name::CliName;
21pub use cli_target::CliTarget;
22pub use dns_port::DnsPort;
23pub use domain::Domain;
24pub use socket_target::SocketTarget;
25pub use subnet::Subnet;
26
27pub fn validate_subnet(s: &str) -> Result<(), String> {
28 s.parse::<Subnet>().map(|_| ())
29}
30
31pub fn validate_iface(s: &str) -> Result<(), String> {
32 s.parse::<BridgeIface>().map(|_| ())
33}
34
35pub fn validate_domain(s: &str) -> Result<(), String> {
36 s.parse::<Domain>().map(|_| ())
37}
38
39pub fn validate_port(port: u16) -> Result<(), String> {
40 DnsPort::try_from(port).map(|_| ())
41}
42
43pub fn validate_socket_target(s: &str) -> Result<(), String> {
44 s.parse::<SocketTarget>().map(|_| ())
45}
46
47pub fn validate_cli_name(name: &str) -> Result<(), String> {
48 name.parse::<CliName>().map(|_| ())
49}
50
51pub fn validate_cli_target(target: &str) -> Result<(), String> {
52 target.parse::<CliTarget>().map(|_| ())
53}
54
55#[cfg(test)]
56mod tests {
57 use super::*;
58
59 #[test]
60 fn valid_subnets() {
61 assert!("10.0.0.0/8".parse::<Subnet>().is_ok());
62 assert!("172.16.0.0/12".parse::<Subnet>().is_ok());
63 assert!("192.168.1.0/24".parse::<Subnet>().is_ok());
64 assert!("10.10.10.0/24".parse::<Subnet>().is_ok());
65 }
66
67 #[test]
68 fn invalid_subnets() {
69 assert!("8.8.8.0/24".parse::<Subnet>().is_err());
70 assert!("1.1.1.0/24".parse::<Subnet>().is_err());
71 assert!("not-a-cidr".parse::<Subnet>().is_err());
72 assert!("10.0.0.0".parse::<Subnet>().is_err());
73 assert!("10.0.0.0/33".parse::<Subnet>().is_err());
74 }
75
76 #[test]
77 fn subnet_rejects_nonzero_host_bits() {
78 let r = "10.0.0.5/8".parse::<Subnet>();
79 assert!(r.is_err());
80 assert!(r.unwrap_err().contains("host bits"));
81
82 let r = "192.168.1.1/24".parse::<Subnet>();
83 assert!(r.is_err());
84 assert!(r.unwrap_err().contains("192.168.1.0/24"));
85 }
86
87 #[test]
88 fn subnet_display_roundtrip() {
89 let s = "10.0.0.0/8".parse::<Subnet>().unwrap();
90 assert_eq!(s.to_string(), "10.0.0.0/8");
91 }
92
93 #[test]
94 fn valid_ifaces() {
95 assert!("bridge0".parse::<BridgeIface>().is_ok());
96 assert!("bridge100".parse::<BridgeIface>().is_ok());
97 assert!("bridge999".parse::<BridgeIface>().is_ok());
98 }
99
100 #[test]
101 fn invalid_ifaces() {
102 assert!("eth0".parse::<BridgeIface>().is_err());
103 assert!("bridge".parse::<BridgeIface>().is_err());
104 assert!("bridge100a".parse::<BridgeIface>().is_err());
105 assert!("BRIDGE100".parse::<BridgeIface>().is_err());
106 }
107
108 #[test]
109 fn iface_rejects_overflow() {
110 assert!("bridge99999999999999999".parse::<BridgeIface>().is_err());
111 }
112
113 #[test]
114 fn valid_domains() {
115 assert!("arcbox.local".parse::<Domain>().is_ok());
116 assert!("test.arcbox.local".parse::<Domain>().is_ok());
117 assert!("my-domain.local".parse::<Domain>().is_ok());
118 }
119
120 #[test]
121 fn invalid_domains() {
122 assert!("".parse::<Domain>().is_err());
123 assert!("UPPER.case".parse::<Domain>().is_err());
124 assert!("has space.local".parse::<Domain>().is_err());
125 let long = "a".repeat(254);
126 assert!(long.parse::<Domain>().is_err());
127 assert!(".".parse::<Domain>().is_err());
128 assert!("..".parse::<Domain>().is_err());
129 assert!(".leading".parse::<Domain>().is_err());
130 assert!("trailing.".parse::<Domain>().is_err());
131 assert!("empty..label".parse::<Domain>().is_err());
132 }
133
134 #[test]
135 fn domain_rejects_label_starting_or_ending_with_hyphen() {
136 assert!("-bad.local".parse::<Domain>().is_err());
137 assert!("bad-.local".parse::<Domain>().is_err());
138 assert!("ok.-bad".parse::<Domain>().is_err());
139 assert!("ok.bad-".parse::<Domain>().is_err());
140 }
141
142 #[test]
143 fn domain_rejects_label_exceeding_63_chars() {
144 let long_label = "a".repeat(64);
145 assert!(format!("{long_label}.local").parse::<Domain>().is_err());
146
147 let ok_label = "a".repeat(63);
148 assert!(format!("{ok_label}.local").parse::<Domain>().is_ok());
149 }
150
151 #[test]
152 fn valid_ports() {
153 assert!(DnsPort::try_from(1024_u16).is_ok());
154 assert!(DnsPort::try_from(5553_u16).is_ok());
155 assert!(DnsPort::try_from(65535_u16).is_ok());
156 }
157
158 #[test]
159 fn invalid_ports() {
160 assert!(DnsPort::try_from(0_u16).is_err());
161 assert!(DnsPort::try_from(80_u16).is_err());
162 assert!(DnsPort::try_from(1023_u16).is_err());
163 }
164
165 #[test]
166 fn valid_socket_targets() {
167 assert!(
168 "/Users/alice/.arcbox/run/docker.sock"
169 .parse::<SocketTarget>()
170 .is_ok()
171 );
172 assert!(
173 "/Users/bob/.arcbox/run/arcbox.sock"
174 .parse::<SocketTarget>()
175 .is_ok()
176 );
177 }
178
179 #[test]
180 fn invalid_socket_targets() {
181 assert!("/tmp/docker.sock".parse::<SocketTarget>().is_err());
182 assert!(
183 "/Users//.arcbox/run/docker.sock"
184 .parse::<SocketTarget>()
185 .is_err()
186 );
187 assert!(
188 "/Users/alice/.config/docker.sock"
189 .parse::<SocketTarget>()
190 .is_err()
191 );
192 assert!(
193 "/Users/alice/.arcbox/run/docker.txt"
194 .parse::<SocketTarget>()
195 .is_err()
196 );
197 }
198
199 #[test]
200 fn socket_target_rejects_uppercase_extension() {
201 assert!(
202 "/Users/alice/.arcbox/run/docker.SOCK"
203 .parse::<SocketTarget>()
204 .is_err()
205 );
206 assert!(
207 "/Users/alice/.arcbox/run/docker.Sock"
208 .parse::<SocketTarget>()
209 .is_err()
210 );
211 }
212
213 #[test]
214 fn socket_target_rejects_path_traversal() {
215 let r = "/Users/alice/.arcbox/../../../../var/run/other.sock".parse::<SocketTarget>();
216 assert!(r.is_err());
217 assert!(r.unwrap_err().contains(".."));
218 }
219
220 #[test]
221 fn valid_cli_names() {
222 assert!("docker".parse::<CliName>().is_ok());
223 assert!("docker-buildx".parse::<CliName>().is_ok());
224 assert!("docker-compose".parse::<CliName>().is_ok());
225 assert!("docker-credential-osxkeychain".parse::<CliName>().is_ok());
226 }
227
228 #[test]
229 fn invalid_cli_names() {
230 assert!("".parse::<CliName>().is_err());
231 assert!("curl".parse::<CliName>().is_err());
232 assert!("rm".parse::<CliName>().is_err());
233 assert!("../docker".parse::<CliName>().is_err());
234 }
235
236 #[test]
237 fn valid_cli_targets() {
238 assert!(
239 "/Applications/ArcBox.app/Contents/MacOS/xbin/docker"
240 .parse::<CliTarget>()
241 .is_ok()
242 );
243 assert!(
244 "/Users/test/Apps/ArcBox.app/Contents/MacOS/xbin/docker-compose"
245 .parse::<CliTarget>()
246 .is_ok()
247 );
248 }
249
250 #[test]
251 fn invalid_cli_targets() {
252 assert!("Contents/MacOS/xbin/docker".parse::<CliTarget>().is_err());
253 assert!("/usr/local/bin/docker".parse::<CliTarget>().is_err());
254 assert!(
255 "/Applications/ArcBox.app/Contents/MacOS/xbin/../../evil"
256 .parse::<CliTarget>()
257 .is_err()
258 );
259 assert!(
260 "/tmp/evil.app/Contents/MacOS/xbin/docker"
261 .parse::<CliTarget>()
262 .is_err()
263 );
264 }
265
266 #[test]
267 fn cli_target_allows_nested_app_structure() {
268 assert!(
269 "/Users/evil/not-really.app/Contents/MacOS/xbin/nested.app/Contents/MacOS/xbin/docker"
270 .parse::<CliTarget>()
271 .is_ok()
272 );
273 }
274}