use alloc::vec::Vec;
use crate::message::ApduType;
pub const APCI_MASK: u16 = 0x03FF;
pub const APCI_SHORT_TYPE_MASK: u16 = 0x03C0;
pub const APCI_SHORT_DATA_MASK: u8 = 0x3F;
const APCI_FAMILY_SHIFT: u16 = 6;
const APCI_SHORT_FAMILY_MAX: u16 = 11;
const APCI_LONG_ESCAPE_FAMILY: u16 = 7;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Apdu {
pub apdu_type: ApduType,
pub data: Vec<u8>,
}
impl Apdu {
pub fn parse(payload: &[u8], npdu_length: u8) -> Option<Self> {
if payload.len() < 2 {
return None;
}
let apci_raw = u16::from_be_bytes([payload[0], payload[1]]) & 0x03FF;
let (apdu_type, data) = decode_apci(apci_raw, payload, npdu_length)?;
Some(Self { apdu_type, data })
}
pub fn to_bytes(&self, tpci_bits: u8) -> Vec<u8> {
let apci = self.apdu_type as u16;
let byte0 = (tpci_bits & 0xFC) | ((apci >> 8) as u8 & 0x03);
#[expect(clippy::cast_possible_truncation)]
let apci_low = apci as u8;
if uses_short_form(apci, &self.data) {
let value = self.data.first().copied().unwrap_or(0);
let byte1 = (apci_low & 0xC0) | (value & APCI_SHORT_DATA_MASK);
alloc::vec![byte0, byte1]
} else {
let mut buf = alloc::vec![byte0, apci_low];
buf.extend_from_slice(&self.data);
buf
}
}
}
const fn is_short_apci(apci: u16) -> bool {
let family = apci >> APCI_FAMILY_SHIFT;
family < APCI_SHORT_FAMILY_MAX && family != APCI_LONG_ESCAPE_FAMILY
}
const fn uses_short_form(apci: u16, data: &[u8]) -> bool {
if !is_short_apci(apci) || data.len() > 1 {
return false;
}
match data.first() {
Some(&value) => value <= APCI_SHORT_DATA_MASK,
None => true,
}
}
const fn normalize_apci(raw: u16) -> u16 {
let apci = raw & APCI_MASK;
if is_short_apci(apci) {
apci & APCI_SHORT_TYPE_MASK
} else {
apci
}
}
fn decode_apci(apci_raw: u16, payload: &[u8], npdu_length: u8) -> Option<(ApduType, Vec<u8>)> {
let apdu_type = match_apdu_type(normalize_apci(apci_raw))?;
let data = if is_short_apci(apci_raw) && npdu_length <= 1 {
alloc::vec![payload[1] & APCI_SHORT_DATA_MASK]
} else if payload.len() > 2 {
payload[2..].to_vec()
} else {
Vec::new()
};
Some((apdu_type, data))
}
pub const fn apdu_type_from_raw(raw: u16) -> Option<ApduType> {
match_apdu_type(normalize_apci(raw))
}
const fn match_apdu_type(bits: u16) -> Option<ApduType> {
Some(match bits {
0x000 => ApduType::GroupValueRead,
0x040 => ApduType::GroupValueResponse,
0x080 => ApduType::GroupValueWrite,
0x0C0 => ApduType::IndividualAddressWrite,
0x100 => ApduType::IndividualAddressRead,
0x140 => ApduType::IndividualAddressResponse,
0x180 => ApduType::AdcRead,
0x1C0 => ApduType::AdcResponse,
0x1C8 => ApduType::SystemNetworkParameterRead,
0x1C9 => ApduType::SystemNetworkParameterResponse,
0x1CA => ApduType::SystemNetworkParameterWrite,
0x1CC => ApduType::PropertyValueExtRead,
0x1CD => ApduType::PropertyValueExtResponse,
0x1CE => ApduType::PropertyValueExtWriteCon,
0x1CF => ApduType::PropertyValueExtWriteConResponse,
0x1D0 => ApduType::PropertyValueExtWriteUnCon,
0x1D2 => ApduType::PropertyExtDescriptionRead,
0x1D3 => ApduType::PropertyExtDescriptionResponse,
0x1D4 => ApduType::FunctionPropertyExtCommand,
0x1D5 => ApduType::FunctionPropertyExtState,
0x1D6 => ApduType::FunctionPropertyExtStateResponse,
0x1FB => ApduType::MemoryExtWrite,
0x1FC => ApduType::MemoryExtWriteResponse,
0x1FD => ApduType::MemoryExtRead,
0x1FE => ApduType::MemoryExtReadResponse,
0x200 => ApduType::MemoryRead,
0x240 => ApduType::MemoryResponse,
0x280 => ApduType::MemoryWrite,
0x2C0 => ApduType::UserMemoryRead,
0x2C1 => ApduType::UserMemoryResponse,
0x2C2 => ApduType::UserMemoryWrite,
0x2C5 => ApduType::UserManufacturerInfoRead,
0x2C6 => ApduType::UserManufacturerInfoResponse,
0x2C7 => ApduType::FunctionPropertyCommand,
0x2C8 => ApduType::FunctionPropertyState,
0x2C9 => ApduType::FunctionPropertyStateResponse,
0x300 => ApduType::DeviceDescriptorRead,
0x340 => ApduType::DeviceDescriptorResponse,
0x380 => ApduType::Restart,
0x381 => ApduType::RestartMasterReset,
0x3C0 => ApduType::RoutingTableOpen,
0x3C1 => ApduType::RoutingTableRead,
0x3C2 => ApduType::RoutingTableReadResponse,
0x3C3 => ApduType::RoutingTableWrite,
0x3C9 => ApduType::MemoryRouterReadResponse,
0x3CA => ApduType::MemoryRouterWrite,
0x3D1 => ApduType::AuthorizeRequest,
0x3D2 => ApduType::AuthorizeResponse,
0x3D3 => ApduType::KeyWrite,
0x3D4 => ApduType::KeyResponse,
0x3D5 => ApduType::PropertyValueRead,
0x3D6 => ApduType::PropertyValueResponse,
0x3D7 => ApduType::PropertyValueWrite,
0x3D8 => ApduType::PropertyDescriptionRead,
0x3D9 => ApduType::PropertyDescriptionResponse,
0x3DC => ApduType::IndividualAddressSerialNumberRead,
0x3DD => ApduType::IndividualAddressSerialNumberResponse,
0x3DE => ApduType::IndividualAddressSerialNumberWrite,
0x3E0 => ApduType::DomainAddressWrite,
0x3E1 => ApduType::DomainAddressRead,
0x3E2 => ApduType::DomainAddressResponse,
0x3E3 => ApduType::DomainAddressSelectiveRead,
0x3EC => ApduType::DomainAddressSerialNumberRead,
0x3ED => ApduType::DomainAddressSerialNumberResponse,
0x3EE => ApduType::DomainAddressSerialNumberWrite,
0x3F1 => ApduType::SecureService,
_ => return None,
})
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
#[test]
fn parse_group_value_write_short() {
let payload = &[0x00, 0x81]; let apdu = Apdu::parse(payload, 1).unwrap();
assert_eq!(apdu.apdu_type, ApduType::GroupValueWrite);
assert_eq!(apdu.data, &[0x01]);
}
#[test]
fn parse_group_value_read() {
let payload = &[0x00, 0x00]; let apdu = Apdu::parse(payload, 0).unwrap();
assert_eq!(apdu.apdu_type, ApduType::GroupValueRead);
}
#[test]
fn parse_group_value_response_short() {
let payload = &[0x00, 0x41]; let apdu = Apdu::parse(payload, 1).unwrap();
assert_eq!(apdu.apdu_type, ApduType::GroupValueResponse);
assert_eq!(apdu.data, &[0x01]);
}
#[test]
fn parse_group_value_write_long() {
let payload = &[0x00, 0x80, 0x0C, 0x1A];
let apdu = Apdu::parse(payload, 3).unwrap();
assert_eq!(apdu.apdu_type, ApduType::GroupValueWrite);
assert_eq!(apdu.data, &[0x0C, 0x1A]);
}
#[test]
fn roundtrip_short_apdu() {
let apdu = Apdu {
apdu_type: ApduType::GroupValueWrite,
data: alloc::vec![0x01],
};
let bytes = apdu.to_bytes(0x00);
assert_eq!(bytes, &[0x00, 0x81]);
let parsed = Apdu::parse(&bytes, 1).unwrap();
assert_eq!(parsed.apdu_type, ApduType::GroupValueWrite);
assert_eq!(parsed.data, &[0x01]);
}
#[test]
fn roundtrip_long_apdu() {
let apdu = Apdu {
apdu_type: ApduType::GroupValueWrite,
data: alloc::vec![0x0C, 0x1A],
};
let bytes = apdu.to_bytes(0x00);
assert_eq!(bytes, &[0x00, 0x80, 0x0C, 0x1A]);
}
#[test]
fn parse_property_value_read() {
let payload = &[0x03, 0xD5, 0x01, 0x02, 0x03];
let apdu = Apdu::parse(payload, 4).unwrap();
assert_eq!(apdu.apdu_type, ApduType::PropertyValueRead);
assert_eq!(apdu.data, &[0x01, 0x02, 0x03]);
}
#[test]
fn parse_device_descriptor_read() {
let payload = &[0x03, 0x00];
let apdu = Apdu::parse(payload, 1).unwrap();
assert_eq!(apdu.apdu_type, ApduType::DeviceDescriptorRead);
}
#[test]
fn parse_too_short() {
assert!(Apdu::parse(&[0x00], 0).is_none());
assert!(Apdu::parse(&[], 0).is_none());
}
#[test]
fn apdu_type_from_raw_masks_short_inline_data() {
assert_eq!(ApduType::from_raw(0x081), Some(ApduType::GroupValueWrite));
assert_eq!(
ApduType::from_raw(0x041),
Some(ApduType::GroupValueResponse)
);
assert_eq!(ApduType::from_raw(0xC081), Some(ApduType::GroupValueWrite));
assert_eq!(ApduType::from_raw(0x3D5), Some(ApduType::PropertyValueRead));
}
#[test]
fn roundtrip_short_apdu_empty_data() {
let apdu = Apdu {
apdu_type: ApduType::GroupValueWrite,
data: Vec::new(),
};
let bytes = apdu.to_bytes(0x00);
assert_eq!(bytes, &[0x00, 0x80]);
let parsed = Apdu::parse(&bytes, 1).unwrap();
assert_eq!(parsed.apdu_type, ApduType::GroupValueWrite);
assert_eq!(parsed.data, &[0x00]);
}
#[test]
fn single_byte_over_6bit_uses_long_form() {
let apdu = Apdu {
apdu_type: ApduType::GroupValueWrite,
data: alloc::vec![0xC8],
};
let bytes = apdu.to_bytes(0x00);
assert_eq!(bytes, &[0x00, 0x80, 0xC8]);
let parsed = Apdu::parse(&bytes, 2).unwrap();
assert_eq!(parsed.data, &[0xC8]);
}
#[test]
fn to_apci_bytes_matches_discriminant() {
assert_eq!(ApduType::GroupValueWrite.to_apci_bytes(), [0x00, 0x80]);
assert_eq!(ApduType::PropertyValueRead.to_apci_bytes(), [0x03, 0xD5]);
}
}