1use crate::error::Error;
14use crate::payload_util::require_at_least;
15use alloc::vec::Vec;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
19#[repr(u8)]
20#[allow(missing_docs)]
21pub enum TextCommand {
22 PermanentNoWrap = 0x01,
23 PermanentWrap = 0x02,
24 TemporaryNoWrap = 0x03,
25 TemporaryWrap = 0x04,
26}
27
28impl TextCommand {
29 pub const fn from_byte(b: u8) -> Result<Self, Error> {
31 Ok(match b {
32 0x01 => Self::PermanentNoWrap,
33 0x02 => Self::PermanentWrap,
34 0x03 => Self::TemporaryNoWrap,
35 0x04 => Self::TemporaryWrap,
36 _ => {
37 return Err(Error::MalformedPayload {
38 code: 0x6B,
39 reason: "unknown TEXT command code",
40 });
41 }
42 })
43 }
44
45 pub const fn as_byte(self) -> u8 {
47 self as u8
48 }
49}
50
51#[derive(Debug, Clone, PartialEq, Eq)]
53pub struct Text {
54 pub reader: u8,
56 pub command: TextCommand,
58 pub temp_time_s: u8,
60 pub row: u8,
62 pub column: u8,
64 pub text: Vec<u8>,
66}
67
68impl Text {
69 fn validate_ascii(s: &[u8]) -> Result<(), Error> {
71 for &b in s {
72 if !(0x20..=0x7E).contains(&b) {
73 return Err(Error::MalformedPayload {
74 code: 0x6B,
75 reason: "TEXT must be printable ASCII",
76 });
77 }
78 }
79 Ok(())
80 }
81
82 pub fn encode(&self) -> Result<Vec<u8>, Error> {
84 Self::validate_ascii(&self.text)?;
85 if self.text.len() > u8::MAX as usize {
86 return Err(Error::MalformedPayload {
87 code: 0x6B,
88 reason: "TEXT length exceeds 255 bytes",
89 });
90 }
91 let mut out = Vec::with_capacity(6 + self.text.len());
92 out.push(self.reader);
93 out.push(self.command.as_byte());
94 out.push(self.temp_time_s);
95 out.push(self.row);
96 out.push(self.column);
97 out.push(self.text.len() as u8);
98 out.extend_from_slice(&self.text);
99 Ok(out)
100 }
101
102 pub fn decode(data: &[u8]) -> Result<Self, Error> {
104 require_at_least(data, 6, 0x6B)?;
105 let length = data[5] as usize;
106 if data.len() != 6 + length {
107 return Err(Error::MalformedPayload {
108 code: 0x6B,
109 reason: "TEXT length field disagrees with payload",
110 });
111 }
112 let text = data[6..6 + length].to_vec();
113 Self::validate_ascii(&text)?;
114 Ok(Self {
115 reader: data[0],
116 command: TextCommand::from_byte(data[1])?,
117 temp_time_s: data[2],
118 row: data[3],
119 column: data[4],
120 text,
121 })
122 }
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128
129 #[test]
130 fn roundtrip() {
131 let body = Text {
132 reader: 0x00,
133 command: TextCommand::PermanentNoWrap,
134 temp_time_s: 0,
135 row: 1,
136 column: 1,
137 text: alloc::vec![b'O', b'K'],
138 };
139 let bytes = body.encode().unwrap();
140 assert_eq!(bytes, [0x00, 0x01, 0x00, 0x01, 0x01, 0x02, b'O', b'K']);
141 assert_eq!(Text::decode(&bytes).unwrap(), body);
142 }
143
144 #[test]
145 fn rejects_non_ascii_text_on_encode() {
146 let body = Text {
147 reader: 0,
148 command: TextCommand::PermanentNoWrap,
149 temp_time_s: 0,
150 row: 1,
151 column: 1,
152 text: alloc::vec![0x1F], };
154 assert!(matches!(
155 body.encode(),
156 Err(Error::MalformedPayload { code: 0x6B, .. })
157 ));
158 }
159
160 #[test]
161 fn decode_rejects_unknown_command_code() {
162 assert!(matches!(
163 Text::decode(&[0x00, 0x99, 0x00, 0x01, 0x01, 0x00]),
164 Err(Error::MalformedPayload { code: 0x6B, .. })
165 ));
166 }
167
168 #[test]
169 fn decode_rejects_length_mismatch() {
170 assert!(matches!(
172 Text::decode(&[0x00, 0x01, 0x00, 0x01, 0x01, 0x05, b'A']),
173 Err(Error::MalformedPayload { code: 0x6B, .. })
174 ));
175 }
176}