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;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum Target {
Mac(Vec<u8>),
Bip6(SocketAddrV6),
Instance(u32),
Routed {
dnet: u16,
instance: u32,
},
RoutedMac {
dnet: u16,
mac: Vec<u8>,
},
}
pub(crate) 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(crate) const TARGET_FORMS: &str = "IP[:port], a 6-octet MAC or SC VMAC \
(02:00:00:00:00:01 or 020000000001), bip6:INSTANCE, bip6:0xVMAC, [IPv6][:port], \
mstp:STATION, device instance, dnet:instance or dnet:0xMAC";
pub(crate) const MSTP_TEXT_PREFIX: &str = "mstp:";
fn parse_mstp_station(text: &str) -> Result<u8, String> {
let value = match text.strip_prefix("0x").or(text.strip_prefix("0X")) {
Some(hex) if hex.bytes().all(|b| b.is_ascii_hexdigit()) => u8::from_str_radix(hex, 16).ok(),
None if text.bytes().all(|b| b.is_ascii_digit()) => text.parse::<u8>().ok(),
_ => None,
};
match value {
Some(station) if station != bacnet_transport::mstp::codec::BROADCAST_MAC => Ok(station),
_ => Err(format!(
"invalid MS/TP station '{MSTP_TEXT_PREFIX}{text}': expected 0 to 254, in decimal or 0xHH"
)),
}
}
pub(crate) fn parse_target(s: &str) -> Result<Target, String> {
if let Some(station) = s.strip_prefix(MSTP_TEXT_PREFIX) {
return parse_mstp_station(station).map(|mac| Target::Mac(vec![mac]));
}
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(mac) = parse_six_octets(s) {
return Ok(Target::Mac(mac));
}
if let Some((left, right)) = s.split_once(':') {
if let Ok(dnet) = left.parse::<u16>() {
let routed =
right.parse::<u32>().is_ok() || right.starts_with("0x") || right.starts_with("0X");
if routed && matches!(dnet, 0 | u16::MAX) {
return Err(format!(
"invalid network {dnet} in routed target '{s}': expected 1 to 65534"
));
}
if let Ok(instance) = right.parse::<u32>() {
return Ok(Target::Routed { dnet, instance });
}
if let Some(hex) = right.strip_prefix("0x").or(right.strip_prefix("0X")) {
let mac = parse_hex(hex).ok_or_else(|| {
format!("invalid MAC in routed target '{s}': expected 0x and hex octets")
})?;
return Ok(Target::RoutedMac { dnet, mac });
}
}
if let Ok(ip) = left.parse::<Ipv4Addr>() {
let port = right
.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 {TARGET_FORMS}"))
}
fn parse_six_octets(s: &str) -> Option<Vec<u8>> {
if s.len() == 12 {
return parse_hex(s);
}
let separator = if s.contains(':') { ':' } else { '-' };
let parts: Vec<&str> = s.split(separator).collect();
if parts.len() != 6 || parts.iter().any(|part| part.len() != 2) {
return None;
}
parts
.iter()
.map(|part| u8::from_str_radix(part, 16).ok())
.collect()
}
fn parse_hex(s: &str) -> Option<Vec<u8>> {
if s.is_empty() || !s.len().is_multiple_of(2) || !s.bytes().all(|b| b.is_ascii_hexdigit()) {
return None;
}
(0..s.len())
.step_by(2)
.map(|at| u8::from_str_radix(&s[at..at + 2], 16).ok())
.collect()
}
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(crate) fn as_bip6<T: TransportPort + 'static>(transport: &T) -> Option<&Bip6Transport> {
(transport as &dyn Any).downcast_ref()
}
fn is_sc<T: TransportPort + 'static>(transport: &T) -> bool {
#[cfg(feature = "sc-tls")]
{
(transport as &dyn Any)
.is::<bacnet_transport::sc::ScTransport<bacnet_transport::sc_tls::TlsWebSocket>>()
}
#[cfg(not(feature = "sc-tls"))]
{
let _ = transport;
false
}
}
pub(crate) fn is_mstp<T: TransportPort + 'static>(transport: &T) -> bool {
transport.local_mac().len() == 1
}
pub(crate) fn parse_target_on<T: TransportPort + 'static>(
transport: &T,
s: &str,
) -> Result<Target, String> {
let bip6 = as_bip6(transport).is_some();
let mstp = is_mstp(transport);
if s.starts_with(VMAC_TEXT_PREFIX) && !bip6 {
return Err(format!(
"'{s}' is a B/IPv6 VMAC: bip6: addresses need the --ipv6 transport"
));
}
if s.starts_with(MSTP_TEXT_PREFIX) && !mstp {
return Err(format!(
"'{s}' is an MS/TP station: mstp: addresses need the --mstp transport"
));
}
let target = parse_target(s)?;
if let Target::Mac(mac) = &target {
if bip6 && mac.len() == 6 {
return Err(format!(
"'{s}' is a 6-octet MAC; a B/IPv6 node is addressed by its 3-octet VMAC (bip6:...)"
));
}
if mstp && mac.len() != 1 {
return Err(format!(
"'{s}' is a {}-octet MAC; an MS/TP station is addressed by mstp:N",
mac.len()
));
}
}
Ok(target)
}
pub(crate) 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(crate) const TARGET_NOT_AN_ADDRESS: &str =
"--target requires an address, not a device instance or routed address";
pub(crate) 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 { .. } | Target::RoutedMac { .. } => {
Err(refusal.to_string())
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum AddressStyle {
Bip,
Bip6,
Hex,
Mstp,
}
impl AddressStyle {
pub(crate) fn of<T: TransportPort + 'static>(transport: &T) -> Self {
if as_bip6(transport).is_some() {
Self::Bip6
} else if is_mstp(transport) {
Self::Mstp
} else if is_sc(transport) {
Self::Hex
} else {
Self::Bip
}
}
pub(crate) fn format(self, mac: &[u8]) -> String {
match (self, mac) {
(Self::Mstp, [station]) => format!("{MSTP_TEXT_PREFIX}{station}"),
(Self::Bip, [_, _, _, _, _, _]) | (Self::Bip6, [_, _, _]) => {
crate::output::format_mac(mac)
}
_ => hex(mac),
}
}
#[cfg(feature = "tui")]
pub(crate) fn locate(
self,
mac: &[u8],
routed: Option<(u16, &[u8])>,
) -> (String, Option<String>) {
match routed.and_then(|(network, station)| routed_station(network, station)) {
Some(station) => (station, Some(self.format(mac))),
None => (self.format(mac), None),
}
}
}
pub(crate) fn routed_station(network: u16, mac: &[u8]) -> Option<String> {
if mac.is_empty() {
return None;
}
let mut out = format!("{network}:0x");
for b in mac {
out.push_str(&format!("{b:02X}"));
}
Some(out)
}
pub(crate) fn hex(bytes: &[u8]) -> String {
let mut out = String::with_capacity(bytes.len() * 3);
for (i, b) in bytes.iter().enumerate() {
if i > 0 {
out.push(':');
}
out.push_str(&format!("{b:02x}"));
}
out
}
#[cfg(test)]
#[path = "target_tests.rs"]
mod tests;