1use std::any::Any;
8use std::net::{Ipv4Addr, SocketAddrV6};
9
10use bacnet_transport::bip6::{
11 Bip6Transport, DEFAULT_BACNET6_PORT, VMAC_TEXT_PREFIX, parse_vmac_text,
12};
13use bacnet_transport::port::TransportPort;
14use bacnet_types::error::Error;
15
16#[derive(Debug, Clone, PartialEq, Eq)]
18pub enum Target {
19 Mac(Vec<u8>),
21 Bip6(SocketAddrV6),
24 Instance(u32),
26 Routed {
28 dnet: u16,
30 instance: u32,
32 },
33 RoutedMac {
35 dnet: u16,
37 mac: Vec<u8>,
39 },
40}
41
42pub fn ip_to_bip_mac(ip: Ipv4Addr, port: u16) -> Vec<u8> {
47 let mut mac = Vec::with_capacity(6);
48 mac.extend_from_slice(&ip.octets());
49 mac.extend_from_slice(&port.to_be_bytes());
50 mac
51}
52
53pub const TARGET_FORMS: &str = "IP[:port], a 6-octet MAC or SC VMAC \
55 (02:00:00:00:00:01 or 020000000001), bip6:INSTANCE, bip6:0xVMAC, [IPv6][:port], \
56 mstp:STATION, device instance, dnet:instance or dnet:0xMAC";
57
58pub const MSTP_TEXT_PREFIX: &str = "mstp:";
60
61fn parse_mstp_station(text: &str) -> Result<u8, String> {
64 let value = match text.strip_prefix("0x").or(text.strip_prefix("0X")) {
66 Some(hex) if hex.bytes().all(|b| b.is_ascii_hexdigit()) => u8::from_str_radix(hex, 16).ok(),
67 None if text.bytes().all(|b| b.is_ascii_digit()) => text.parse::<u8>().ok(),
68 _ => None,
69 };
70 match value {
71 Some(station) if station != bacnet_transport::mstp::codec::BROADCAST_MAC => Ok(station),
72 _ => Err(format!(
73 "invalid MS/TP station '{MSTP_TEXT_PREFIX}{text}': expected 0 to 254, in decimal or 0xHH"
74 )),
75 }
76}
77
78pub fn parse_target(s: &str) -> Result<Target, String> {
96 if let Some(station) = s.strip_prefix(MSTP_TEXT_PREFIX) {
97 return parse_mstp_station(station).map(|mac| Target::Mac(vec![mac]));
98 }
99 if s.starts_with(VMAC_TEXT_PREFIX) {
100 return parse_vmac_text(s)
101 .map(|vmac| Target::Mac(vmac.octets().to_vec()))
102 .map_err(|error| match error {
103 Error::Encoding(message) | Error::OutOfRange(message) => message,
104 other => other.to_string(),
105 });
106 }
107 if s.starts_with('[') {
108 return parse_bip6_address(s).map(Target::Bip6);
109 }
110 if let Some(mac) = parse_six_octets(s) {
111 return Ok(Target::Mac(mac));
112 }
113
114 if let Some((left, right)) = s.split_once(':') {
115 if let Ok(dnet) = left.parse::<u16>() {
116 let routed =
117 right.parse::<u32>().is_ok() || right.starts_with("0x") || right.starts_with("0X");
118 if routed && matches!(dnet, 0 | u16::MAX) {
120 return Err(format!(
121 "invalid network {dnet} in routed target '{s}': expected 1 to 65534"
122 ));
123 }
124 if let Ok(instance) = right.parse::<u32>() {
126 return Ok(Target::Routed { dnet, instance });
127 }
128 if let Some(hex) = right.strip_prefix("0x").or(right.strip_prefix("0X")) {
129 let mac = parse_hex(hex).ok_or_else(|| {
130 format!("invalid MAC in routed target '{s}': expected 0x and hex octets")
131 })?;
132 return Ok(Target::RoutedMac { dnet, mac });
133 }
134 }
135 if let Ok(ip) = left.parse::<Ipv4Addr>() {
137 let port = right
138 .parse::<u16>()
139 .map_err(|_| format!("invalid port in target '{s}'"))?;
140 return Ok(Target::Mac(ip_to_bip_mac(ip, port)));
141 }
142 }
143
144 if let Ok(ip) = s.parse::<Ipv4Addr>() {
146 return Ok(Target::Mac(ip_to_bip_mac(ip, 0xBAC0)));
147 }
148
149 if let Ok(instance) = s.parse::<u32>() {
151 return Ok(Target::Instance(instance));
152 }
153
154 Err(format!("invalid target '{s}': expected {TARGET_FORMS}"))
155}
156
157fn parse_six_octets(s: &str) -> Option<Vec<u8>> {
161 if s.len() == 12 {
162 return parse_hex(s);
163 }
164 let separator = if s.contains(':') { ':' } else { '-' };
165 let parts: Vec<&str> = s.split(separator).collect();
166 if parts.len() != 6 || parts.iter().any(|part| part.len() != 2) {
167 return None;
168 }
169 parts
170 .iter()
171 .map(|part| u8::from_str_radix(part, 16).ok())
172 .collect()
173}
174
175fn parse_hex(s: &str) -> Option<Vec<u8>> {
177 if s.is_empty() || !s.len().is_multiple_of(2) || !s.bytes().all(|b| b.is_ascii_hexdigit()) {
178 return None;
179 }
180 (0..s.len())
181 .step_by(2)
182 .map(|at| u8::from_str_radix(&s[at..at + 2], 16).ok())
183 .collect()
184}
185
186fn parse_bip6_address(s: &str) -> Result<SocketAddrV6, String> {
189 let with_port;
190 let text = if s.ends_with(']') {
191 with_port = format!("{s}:{DEFAULT_BACNET6_PORT}");
192 &with_port
193 } else {
194 s
195 };
196 text.parse()
197 .map_err(|_| format!("invalid B/IPv6 address '{s}': expected [IPv6] or [IPv6]:port"))
198}
199
200pub fn as_bip6<T: TransportPort + 'static>(transport: &T) -> Option<&Bip6Transport> {
203 (transport as &dyn Any).downcast_ref()
204}
205
206fn is_sc<T: TransportPort + 'static>(transport: &T) -> bool {
208 #[cfg(feature = "sc-tls")]
209 {
210 (transport as &dyn Any)
211 .is::<bacnet_transport::sc::ScTransport<bacnet_transport::sc_tls::TlsWebSocket>>()
212 }
213 #[cfg(not(feature = "sc-tls"))]
214 {
215 let _ = transport;
216 false
217 }
218}
219
220pub fn is_mstp<T: TransportPort + 'static>(transport: &T) -> bool {
223 transport.local_mac().len() == 1
224}
225
226pub fn parse_target_on<T: TransportPort + 'static>(
231 transport: &T,
232 s: &str,
233) -> Result<Target, String> {
234 let bip6 = as_bip6(transport).is_some();
235 let mstp = is_mstp(transport);
236 if s.starts_with(VMAC_TEXT_PREFIX) && !bip6 {
237 return Err(format!(
238 "'{s}' is a B/IPv6 VMAC: bip6: addresses need the --ipv6 transport"
239 ));
240 }
241 if s.starts_with(MSTP_TEXT_PREFIX) && !mstp {
242 return Err(format!(
243 "'{s}' is an MS/TP station: mstp: addresses need the --mstp transport"
244 ));
245 }
246 let target = parse_target(s)?;
247 if let Target::Mac(mac) = &target {
248 if bip6 && mac.len() == 6 {
249 return Err(format!(
250 "'{s}' is a 6-octet MAC; a B/IPv6 node is addressed by its 3-octet VMAC (bip6:...)"
251 ));
252 }
253 if mstp && mac.len() != 1 {
254 return Err(format!(
255 "'{s}' is a {}-octet MAC; an MS/TP station is addressed by mstp:N",
256 mac.len()
257 ));
258 }
259 }
260 Ok(target)
261}
262
263pub async fn bip6_vmac<T: TransportPort + 'static>(
271 transport: &T,
272 peer: SocketAddrV6,
273) -> Result<Vec<u8>, String> {
274 let bip6 = as_bip6(transport)
275 .ok_or_else(|| format!("{peer} is a B/IPv6 address: use it with --ipv6"))?;
276 let vmac = bip6
277 .resolve_virtual_address(peer)
278 .await
279 .map_err(|e| format!("could not find the VMAC at {peer}: {e}"))?;
280 Ok(vmac.octets().to_vec())
281}
282
283pub const TARGET_NOT_AN_ADDRESS: &str =
285 "--target requires an address, not a device instance or routed address";
286
287pub async fn address_mac<T: TransportPort + 'static>(
291 transport: &T,
292 text: &str,
293 refusal: &str,
294) -> Result<Vec<u8>, String> {
295 match parse_target_on(transport, text)? {
296 Target::Mac(mac) => Ok(mac),
297 Target::Bip6(peer) => bip6_vmac(transport, peer).await,
298 Target::Instance(_) | Target::Routed { .. } | Target::RoutedMac { .. } => {
299 Err(refusal.to_string())
300 }
301 }
302}
303
304#[derive(Debug, Clone, Copy, PartialEq, Eq)]
306pub enum AddressStyle {
307 Bip,
309 Bip6,
311 Hex,
313 Mstp,
315}
316
317impl AddressStyle {
318 pub fn of<T: TransportPort + 'static>(transport: &T) -> Self {
320 if as_bip6(transport).is_some() {
321 Self::Bip6
322 } else if is_mstp(transport) {
323 Self::Mstp
324 } else if is_sc(transport) {
325 Self::Hex
326 } else {
327 Self::Bip
328 }
329 }
330
331 pub fn format(self, mac: &[u8]) -> String {
333 match (self, mac) {
334 (Self::Mstp, [station]) => format!("{MSTP_TEXT_PREFIX}{station}"),
335 (Self::Bip, [_, _, _, _, _, _]) | (Self::Bip6, [_, _, _]) => format_mac(mac),
336 _ => hex(mac),
337 }
338 }
339
340 pub fn locate(self, mac: &[u8], routed: Option<(u16, &[u8])>) -> (String, Option<String>) {
347 match routed.and_then(|(network, station)| routed_station(network, station)) {
348 Some(station) => (station, Some(self.format(mac))),
349 None => (self.format(mac), None),
350 }
351 }
352}
353
354pub fn routed_station(network: u16, mac: &[u8]) -> Option<String> {
361 if mac.is_empty() {
362 return None;
363 }
364 let mut out = format!("{network}:0x");
365 for b in mac {
366 out.push_str(&format!("{b:02X}"));
367 }
368 Some(out)
369}
370
371pub fn format_mac(mac: &[u8]) -> String {
377 if mac.len() == 6 {
378 let ip = Ipv4Addr::new(mac[0], mac[1], mac[2], mac[3]);
379 let port = u16::from_be_bytes([mac[4], mac[5]]);
380 format!("{ip}:{port}")
381 } else if let Ok(vmac) = <[u8; 3]>::try_from(mac) {
382 bacnet_transport::bip6::format_vmac_text(bacnet_transport::bip6::Bip6Vmac::new(vmac))
383 } else {
384 mac.iter()
385 .map(|b| format!("{b:02x}"))
386 .collect::<Vec<_>>()
387 .join(":")
388 }
389}
390
391pub fn hex(bytes: &[u8]) -> String {
393 let mut out = String::with_capacity(bytes.len() * 3);
394 for (i, b) in bytes.iter().enumerate() {
395 if i > 0 {
396 out.push(':');
397 }
398 out.push_str(&format!("{b:02x}"));
399 }
400 out
401}
402
403#[cfg(test)]
404#[path = "target_tests.rs"]
405mod tests;