use std::any::Any;
use std::net::{Ipv4Addr, SocketAddrV6};
use bacnet_transport::bip6::{
parse_vmac_text, Bip6Transport, DEFAULT_BACNET6_PORT, VMAC_TEXT_PREFIX,
};
use bacnet_transport::port::TransportPort;
use bacnet_types::error::Error;
pub enum Target {
Mac(Vec<u8>),
Bip6(SocketAddrV6),
Instance(u32),
Routed(u16, u32),
}
pub fn ip_to_bip_mac(ip: Ipv4Addr, port: u16) -> Vec<u8> {
let mut mac = Vec::with_capacity(6);
mac.extend_from_slice(&ip.octets());
mac.extend_from_slice(&port.to_be_bytes());
mac
}
pub fn parse_target(s: &str) -> Result<Target, String> {
if s.starts_with(VMAC_TEXT_PREFIX) {
return parse_vmac_text(s)
.map(|vmac| Target::Mac(vmac.octets().to_vec()))
.map_err(|error| match error {
Error::Encoding(message) | Error::OutOfRange(message) => message,
other => other.to_string(),
});
}
if s.starts_with('[') {
return parse_bip6_address(s).map(Target::Bip6);
}
if let Some((left, right)) = s.split_once(':') {
if let (Ok(dnet), Ok(instance)) = (left.parse::<u16>(), right.parse::<u32>()) {
return Ok(Target::Routed(dnet, instance));
}
}
if let Some((ip_str, port_str)) = s.split_once(':') {
if let Ok(ip) = ip_str.parse::<Ipv4Addr>() {
let port = port_str
.parse::<u16>()
.map_err(|_| format!("invalid port in target '{s}'"))?;
return Ok(Target::Mac(ip_to_bip_mac(ip, port)));
}
}
if let Ok(ip) = s.parse::<Ipv4Addr>() {
return Ok(Target::Mac(ip_to_bip_mac(ip, 0xBAC0)));
}
if let Ok(instance) = s.parse::<u32>() {
return Ok(Target::Instance(instance));
}
Err(format!(
"invalid target '{s}': expected IP[:port], bip6:INSTANCE, bip6:0xVMAC, \
[IPv6][:port], device instance, or dnet:instance"
))
}
fn parse_bip6_address(s: &str) -> Result<SocketAddrV6, String> {
let with_port;
let text = if s.ends_with(']') {
with_port = format!("{s}:{DEFAULT_BACNET6_PORT}");
&with_port
} else {
s
};
text.parse()
.map_err(|_| format!("invalid B/IPv6 address '{s}': expected [IPv6] or [IPv6]:port"))
}
pub fn as_bip6<T: TransportPort + 'static>(transport: &T) -> Option<&Bip6Transport> {
(transport as &dyn Any).downcast_ref()
}
pub fn parse_target_on<T: TransportPort + 'static>(
transport: &T,
s: &str,
) -> Result<Target, String> {
if s.starts_with(VMAC_TEXT_PREFIX) && as_bip6(transport).is_none() {
return Err(format!(
"'{s}' is a B/IPv6 VMAC: bip6: addresses need the --ipv6 transport"
));
}
parse_target(s)
}
pub async fn bip6_vmac<T: TransportPort + 'static>(
transport: &T,
peer: SocketAddrV6,
) -> Result<Vec<u8>, String> {
let bip6 = as_bip6(transport)
.ok_or_else(|| format!("{peer} is a B/IPv6 address: use it with --ipv6"))?;
let vmac = bip6
.resolve_virtual_address(peer)
.await
.map_err(|e| format!("could not find the VMAC at {peer}: {e}"))?;
Ok(vmac.octets().to_vec())
}
pub const TARGET_NOT_AN_ADDRESS: &str =
"--target requires an address, not a device instance or routed address";
pub async fn address_mac<T: TransportPort + 'static>(
transport: &T,
text: &str,
refusal: &str,
) -> Result<Vec<u8>, String> {
match parse_target_on(transport, text)? {
Target::Mac(mac) => Ok(mac),
Target::Bip6(peer) => bip6_vmac(transport, peer).await,
Target::Instance(_) | Target::Routed(..) => Err(refusal.to_string()),
}
}
#[cfg(test)]
#[path = "resolve_tests.rs"]
mod tests;