use crate::error::Error;
use crate::payload_util::require_at_least;
use alloc::vec::Vec;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u8)]
#[allow(missing_docs)]
pub enum TextCommand {
PermanentNoWrap = 0x01,
PermanentWrap = 0x02,
TemporaryNoWrap = 0x03,
TemporaryWrap = 0x04,
}
impl TextCommand {
pub const fn from_byte(b: u8) -> Result<Self, Error> {
Ok(match b {
0x01 => Self::PermanentNoWrap,
0x02 => Self::PermanentWrap,
0x03 => Self::TemporaryNoWrap,
0x04 => Self::TemporaryWrap,
_ => {
return Err(Error::MalformedPayload {
code: 0x6B,
reason: "unknown TEXT command code",
});
}
})
}
pub const fn as_byte(self) -> u8 {
self as u8
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Text {
pub reader: u8,
pub command: TextCommand,
pub temp_time_s: u8,
pub row: u8,
pub column: u8,
pub text: Vec<u8>,
}
impl Text {
fn validate_ascii(s: &[u8]) -> Result<(), Error> {
for &b in s {
if !(0x20..=0x7E).contains(&b) {
return Err(Error::MalformedPayload {
code: 0x6B,
reason: "TEXT must be printable ASCII",
});
}
}
Ok(())
}
pub fn encode(&self) -> Result<Vec<u8>, Error> {
Self::validate_ascii(&self.text)?;
if self.text.len() > u8::MAX as usize {
return Err(Error::MalformedPayload {
code: 0x6B,
reason: "TEXT length exceeds 255 bytes",
});
}
let mut out = Vec::with_capacity(6 + self.text.len());
out.push(self.reader);
out.push(self.command.as_byte());
out.push(self.temp_time_s);
out.push(self.row);
out.push(self.column);
out.push(self.text.len() as u8);
out.extend_from_slice(&self.text);
Ok(out)
}
pub fn decode(data: &[u8]) -> Result<Self, Error> {
require_at_least(data, 6, 0x6B)?;
let length = data[5] as usize;
if data.len() != 6 + length {
return Err(Error::MalformedPayload {
code: 0x6B,
reason: "TEXT length field disagrees with payload",
});
}
let text = data[6..6 + length].to_vec();
Self::validate_ascii(&text)?;
Ok(Self {
reader: data[0],
command: TextCommand::from_byte(data[1])?,
temp_time_s: data[2],
row: data[3],
column: data[4],
text,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn roundtrip() {
let body = Text {
reader: 0x00,
command: TextCommand::PermanentNoWrap,
temp_time_s: 0,
row: 1,
column: 1,
text: alloc::vec![b'O', b'K'],
};
let bytes = body.encode().unwrap();
assert_eq!(bytes, [0x00, 0x01, 0x00, 0x01, 0x01, 0x02, b'O', b'K']);
assert_eq!(Text::decode(&bytes).unwrap(), body);
}
#[test]
fn rejects_non_ascii_text_on_encode() {
let body = Text {
reader: 0,
command: TextCommand::PermanentNoWrap,
temp_time_s: 0,
row: 1,
column: 1,
text: alloc::vec![0x1F], };
assert!(matches!(
body.encode(),
Err(Error::MalformedPayload { code: 0x6B, .. })
));
}
#[test]
fn decode_rejects_unknown_command_code() {
assert!(matches!(
Text::decode(&[0x00, 0x99, 0x00, 0x01, 0x01, 0x00]),
Err(Error::MalformedPayload { code: 0x6B, .. })
));
}
#[test]
fn decode_rejects_length_mismatch() {
assert!(matches!(
Text::decode(&[0x00, 0x01, 0x00, 0x01, 0x01, 0x05, b'A']),
Err(Error::MalformedPayload { code: 0x6B, .. })
));
}
}