use crate::error::{Error, Result};
use crate::objects;
use broadcast_common::{Parse, Serialize};
pub mod tag {
use crate::tag::ApduTag;
pub const FILE_SYSTEM_OFFER: ApduTag = ApduTag::from_bytes(0x9F, 0x94, 0x00);
pub const FILE_SYSTEM_ACK: ApduTag = ApduTag::from_bytes(0x9F, 0x94, 0x01);
pub const FILE_REQUEST: ApduTag = ApduTag::from_bytes(0x9F, 0x94, 0x02);
pub const FILE_ACKNOWLEDGE: ApduTag = ApduTag::from_bytes(0x9F, 0x94, 0x03);
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub enum AckCode {
Ok,
UnknownDomainIdentifier,
Reserved(u8),
}
impl AckCode {
#[must_use]
pub fn from_u8(v: u8) -> Self {
match v {
0x01 => Self::Ok,
0x02 => Self::UnknownDomainIdentifier,
other => Self::Reserved(other),
}
}
#[must_use]
pub const fn to_u8(self) -> u8 {
match self {
Self::Ok => 0x01,
Self::UnknownDomainIdentifier => 0x02,
Self::Reserved(v) => v,
}
}
#[must_use]
pub fn name(&self) -> &'static str {
match self {
Self::Ok => "ok",
Self::UnknownDomainIdentifier => "unknown_domain_identifier",
Self::Reserved(_) => "reserved",
}
}
}
broadcast_common::impl_spec_display!(AckCode, Reserved);
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct FileSystemOffer<'a> {
#[cfg_attr(feature = "serde", serde(borrow, with = "crate::objects::bytes_serde"))]
pub domain_identifier: &'a [u8],
}
const OFFER_PREFIX: usize = 1;
impl<'a> Parse<'a> for FileSystemOffer<'a> {
type Error = Error;
fn parse(bytes: &'a [u8]) -> Result<Self> {
let body = objects::parse_apdu_header(bytes, tag::FILE_SYSTEM_OFFER, "FileSystemOffer")?;
if body.len() < OFFER_PREFIX {
return Err(Error::BufferTooShort {
need: OFFER_PREFIX,
have: body.len(),
what: "FileSystemOffer",
});
}
let len = body[0] as usize;
let end = OFFER_PREFIX + len;
if body.len() < end {
return Err(Error::LengthMismatch {
what: "FileSystemOffer DomainIdentifier",
declared: len,
actual: body.len().saturating_sub(OFFER_PREFIX),
});
}
Ok(Self {
domain_identifier: &body[OFFER_PREFIX..end],
})
}
}
impl Serialize for FileSystemOffer<'_> {
type Error = Error;
fn serialized_len(&self) -> usize {
objects::apdu_len(OFFER_PREFIX + self.domain_identifier.len())
}
fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
objects::fit_bits(
self.domain_identifier.len() as u64,
8,
"FileSystemOffer DomainIdentifierLength",
)?;
let body_len = OFFER_PREFIX + self.domain_identifier.len();
let pos = objects::write_apdu_header(tag::FILE_SYSTEM_OFFER, body_len, buf)?;
buf[pos] = self.domain_identifier.len() as u8;
buf[pos + OFFER_PREFIX..pos + body_len].copy_from_slice(self.domain_identifier);
Ok(pos + body_len)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct FileSystemAck {
pub ack_code: AckCode,
}
const ACK_BODY: usize = 1;
impl<'a> Parse<'a> for FileSystemAck {
type Error = Error;
fn parse(bytes: &'a [u8]) -> Result<Self> {
let body = objects::parse_apdu_header(bytes, tag::FILE_SYSTEM_ACK, "FileSystemAck")?;
if body.len() < ACK_BODY {
return Err(Error::BufferTooShort {
need: ACK_BODY,
have: body.len(),
what: "FileSystemAck",
});
}
crate::objects::reject_trailing_body(body, ACK_BODY, "FileSystemAck")?;
Ok(Self {
ack_code: AckCode::from_u8(body[0]),
})
}
}
impl Serialize for FileSystemAck {
type Error = Error;
fn serialized_len(&self) -> usize {
objects::apdu_len(ACK_BODY)
}
fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
let pos = objects::write_apdu_header(tag::FILE_SYSTEM_ACK, ACK_BODY, buf)?;
buf[pos] = self.ack_code.to_u8();
Ok(pos + ACK_BODY)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct FileRequest<'a> {
#[cfg_attr(feature = "serde", serde(borrow, with = "crate::objects::bytes_serde"))]
pub body: &'a [u8],
}
impl<'a> Parse<'a> for FileRequest<'a> {
type Error = Error;
fn parse(bytes: &'a [u8]) -> Result<Self> {
let body = objects::parse_apdu_header(bytes, tag::FILE_REQUEST, "FileRequest")?;
Ok(Self { body })
}
}
impl Serialize for FileRequest<'_> {
type Error = Error;
fn serialized_len(&self) -> usize {
objects::apdu_len(self.body.len())
}
fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
let pos = objects::write_apdu_header(tag::FILE_REQUEST, self.body.len(), buf)?;
buf[pos..pos + self.body.len()].copy_from_slice(self.body);
Ok(pos + self.body.len())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct FileAcknowledge<'a> {
#[cfg_attr(feature = "serde", serde(borrow, with = "crate::objects::bytes_serde"))]
pub body: &'a [u8],
}
impl<'a> Parse<'a> for FileAcknowledge<'a> {
type Error = Error;
fn parse(bytes: &'a [u8]) -> Result<Self> {
let body = objects::parse_apdu_header(bytes, tag::FILE_ACKNOWLEDGE, "FileAcknowledge")?;
Ok(Self { body })
}
}
impl Serialize for FileAcknowledge<'_> {
type Error = Error;
fn serialized_len(&self) -> usize {
objects::apdu_len(self.body.len())
}
fn serialize_into(&self, buf: &mut [u8]) -> Result<usize> {
let pos = objects::write_apdu_header(tag::FILE_ACKNOWLEDGE, self.body.len(), buf)?;
buf[pos..pos + self.body.len()].copy_from_slice(self.body);
Ok(pos + self.body.len())
}
}
crate::dispatch::declare_resource_apdus! {
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[non_exhaustive]
pub enum FileRetrievalApdu<'a> ("file_retrieval") {
FileSystemOffer(#[cfg_attr(feature = "serde", serde(borrow))] FileSystemOffer<'a>) = tag::FILE_SYSTEM_OFFER,
FileSystemAck(FileSystemAck) = tag::FILE_SYSTEM_ACK,
FileRequest(#[cfg_attr(feature = "serde", serde(borrow))] FileRequest<'a>) = tag::FILE_REQUEST,
FileAcknowledge(#[cfg_attr(feature = "serde", serde(borrow))] FileAcknowledge<'a>) = tag::FILE_ACKNOWLEDGE,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn offer_round_trips_and_bites() {
let o = FileSystemOffer {
domain_identifier: &[0x61, 0x62],
};
let bytes = o.to_bytes();
assert_eq!(bytes, [0x9F, 0x94, 0x00, 0x03, 0x02, 0x61, 0x62]);
assert_eq!(FileSystemOffer::parse(&bytes).unwrap(), o);
let other = FileSystemOffer {
domain_identifier: &[0x61, 0x63],
};
assert_ne!(bytes, other.to_bytes());
}
#[test]
fn offer_empty_domain() {
let o = FileSystemOffer {
domain_identifier: &[],
};
let bytes = o.to_bytes();
assert_eq!(bytes, [0x9F, 0x94, 0x00, 0x01, 0x00]);
assert_eq!(FileSystemOffer::parse(&bytes).unwrap(), o);
}
#[test]
fn ack_round_trips() {
let a = FileSystemAck {
ack_code: AckCode::UnknownDomainIdentifier,
};
let bytes = a.to_bytes();
assert_eq!(bytes, [0x9F, 0x94, 0x01, 0x01, 0x02]);
assert_eq!(FileSystemAck::parse(&bytes).unwrap(), a);
let ok = FileSystemAck {
ack_code: AckCode::Ok,
};
assert_eq!(ok.to_bytes()[4], 0x01);
}
#[test]
fn file_request_opaque_body_round_trips() {
let r = FileRequest {
body: &[0x01, 0x02, 0x03],
};
let bytes = r.to_bytes();
assert_eq!(bytes, [0x9F, 0x94, 0x02, 0x03, 0x01, 0x02, 0x03]);
assert_eq!(FileRequest::parse(&bytes).unwrap(), r);
}
#[test]
fn file_acknowledge_opaque_body_round_trips() {
let a = FileAcknowledge {
body: &[0xAA, 0xBB],
};
let bytes = a.to_bytes();
assert_eq!(bytes, [0x9F, 0x94, 0x03, 0x02, 0xAA, 0xBB]);
assert_eq!(FileAcknowledge::parse(&bytes).unwrap(), a);
}
#[test]
fn dispatch_routes_each_tag() {
let cases: alloc::vec::Vec<alloc::vec::Vec<u8>> = alloc::vec![
FileSystemOffer {
domain_identifier: &[0x61]
}
.to_bytes(),
FileSystemAck {
ack_code: AckCode::Ok
}
.to_bytes(),
FileRequest { body: &[0x00] }.to_bytes(),
FileAcknowledge { body: &[0x00] }.to_bytes(),
];
for c in &cases {
assert_eq!(&FileRetrievalApdu::parse(c).unwrap().to_bytes(), c);
}
assert!(matches!(
FileRetrievalApdu::parse(&[0x9F, 0x94, 0x7E, 0x00]),
Err(Error::UnexpectedApduTag { .. })
));
}
#[test]
fn oversized_domain_identifier_is_rejected_not_wrapped() {
let big = alloc::vec![0x61u8; 256];
let offer = FileSystemOffer {
domain_identifier: &big,
};
let mut buf = alloc::vec![0u8; 300];
assert!(matches!(
offer.serialize_into(&mut buf),
Err(Error::InvalidObject { .. })
));
}
#[test]
fn max_domain_identifier_still_serializes_and_round_trips() {
let data = alloc::vec![0x61u8; 255];
let offer = FileSystemOffer {
domain_identifier: &data,
};
let bytes = offer.to_bytes();
assert_eq!(FileSystemOffer::parse(&bytes).unwrap(), offer);
}
}