1use crate::{
7 is_ax25_like_path_component, is_ax25_like_source, is_latitude, is_longitude,
8 is_printable_ascii, is_symbol_table_identifier, is_timestamp, MAX_PACKET_LEN,
9};
10
11#[derive(Clone, Copy, Debug, Eq, PartialEq)]
13pub enum EncodeError {
14 EmptyPath,
16 InvalidAddress,
18 LowercaseAddress,
20 InvalidField,
22 EmptyPayload,
24 OversizedPacket,
26}
27
28impl EncodeError {
29 #[must_use]
31 pub const fn code(self) -> &'static str {
32 match self {
33 Self::EmptyPath => "encode.empty_path",
34 Self::InvalidAddress => "encode.invalid_address",
35 Self::LowercaseAddress => "encode.lowercase_address",
36 Self::InvalidField => "encode.invalid_field",
37 Self::EmptyPayload => "encode.empty_payload",
38 Self::OversizedPacket => "encode.oversized_packet",
39 }
40 }
41}
42
43impl std::fmt::Display for EncodeError {
44 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45 formatter.write_str(self.code())
46 }
47}
48
49impl std::error::Error for EncodeError {}
50
51#[derive(Clone, Copy, Debug, Eq, PartialEq)]
53pub struct UncompressedPositionEncoding<'a> {
54 pub messaging: bool,
56 pub latitude: &'a [u8],
58 pub symbol_table: u8,
60 pub longitude: &'a [u8],
62 pub symbol_code: u8,
64 pub comment: &'a [u8],
66}
67
68#[derive(Clone, Copy, Debug, Eq, PartialEq)]
70pub struct ObjectEncoding<'a> {
71 pub name: &'a [u8],
73 pub live: bool,
75 pub timestamp: &'a [u8],
77 pub body: &'a [u8],
79}
80
81#[derive(Clone, Copy, Debug, Eq, PartialEq)]
83pub struct ItemEncoding<'a> {
84 pub name: &'a [u8],
86 pub live: bool,
88 pub body: &'a [u8],
90}
91
92#[derive(Clone, Copy, Debug, Eq, PartialEq)]
94pub enum TelemetryMetadataEncodingKind {
95 Parameters,
97 Units,
99 Equations,
101 BitSense,
103}
104
105impl TelemetryMetadataEncodingKind {
106 fn addressee(self) -> &'static [u8; 9] {
107 match self {
108 Self::Parameters => b"PARM. ",
109 Self::Units => b"UNIT. ",
110 Self::Equations => b"EQNS. ",
111 Self::BitSense => b"BITS. ",
112 }
113 }
114}
115
116pub fn encode_packet(
118 source: &[u8],
119 path: &[&[u8]],
120 payload: &[u8],
121) -> Result<Vec<u8>, EncodeError> {
122 if payload.is_empty() {
123 return Err(EncodeError::EmptyPayload);
124 }
125 ensure_packet_shape(source, path, payload.len())?;
126
127 let mut encoded = Vec::with_capacity(packet_len(source, path, payload.len())?);
128 encoded.extend_from_slice(source);
129 encoded.push(b'>');
130 encoded.extend_from_slice(path[0]);
131 for component in &path[1..] {
132 encoded.push(b',');
133 encoded.extend_from_slice(component);
134 }
135 encoded.push(b':');
136 encoded.extend_from_slice(payload);
137
138 Ok(encoded)
139}
140
141pub fn encode_status(source: &[u8], path: &[&[u8]], text: &[u8]) -> Result<Vec<u8>, EncodeError> {
143 ensure_packet_shape(source, path, 1usize.saturating_add(text.len()))?;
144 let mut payload = Vec::with_capacity(text.len().saturating_add(1));
145 payload.push(b'>');
146 payload.extend_from_slice(text);
147 encode_packet(source, path, &payload)
148}
149
150pub fn encode_uncompressed_position(
152 source: &[u8],
153 path: &[&[u8]],
154 position: UncompressedPositionEncoding<'_>,
155) -> Result<Vec<u8>, EncodeError> {
156 if !is_latitude(position.latitude)
157 || !is_symbol_table_identifier(position.symbol_table)
158 || !is_longitude(position.longitude)
159 || !is_printable_ascii(position.symbol_code)
160 {
161 return Err(EncodeError::InvalidField);
162 }
163
164 ensure_packet_shape(source, path, 20usize.saturating_add(position.comment.len()))?;
165 let mut payload = Vec::with_capacity(20usize.saturating_add(position.comment.len()));
166 payload.push(if position.messaging { b'=' } else { b'!' });
167 payload.extend_from_slice(position.latitude);
168 payload.push(position.symbol_table);
169 payload.extend_from_slice(position.longitude);
170 payload.push(position.symbol_code);
171 payload.extend_from_slice(position.comment);
172 encode_packet(source, path, &payload)
173}
174
175pub fn encode_message(
178 source: &[u8],
179 path: &[&[u8]],
180 addressee: &[u8],
181 text: &[u8],
182 id: Option<&[u8]>,
183) -> Result<Vec<u8>, EncodeError> {
184 if !is_fixed_printable(addressee, 9)
185 || id.is_some_and(|message_id| !is_valid_message_id(message_id))
186 {
187 return Err(EncodeError::InvalidField);
188 }
189
190 let id_len = id.map_or(0, |message_id| 1usize.saturating_add(message_id.len()));
191 let payload_len = 11usize.saturating_add(text.len()).saturating_add(id_len);
192 ensure_packet_shape(source, path, payload_len)?;
193
194 let mut payload = Vec::with_capacity(payload_len);
195 payload.push(b':');
196 payload.extend_from_slice(addressee);
197 payload.push(b':');
198 payload.extend_from_slice(text);
199 if let Some(id) = id {
200 payload.push(b'{');
201 payload.extend_from_slice(id);
202 }
203
204 encode_packet(source, path, &payload)
205}
206
207pub fn encode_ack(
209 source: &[u8],
210 path: &[&[u8]],
211 addressee: &[u8],
212 message_id: &[u8],
213) -> Result<Vec<u8>, EncodeError> {
214 encode_message_response(source, path, addressee, b"ack", message_id)
215}
216
217pub fn encode_reject(
219 source: &[u8],
220 path: &[&[u8]],
221 addressee: &[u8],
222 message_id: &[u8],
223) -> Result<Vec<u8>, EncodeError> {
224 encode_message_response(source, path, addressee, b"rej", message_id)
225}
226
227pub fn encode_bulletin(
229 source: &[u8],
230 path: &[&[u8]],
231 bulletin_id: u8,
232 text: &[u8],
233) -> Result<Vec<u8>, EncodeError> {
234 if !bulletin_id.is_ascii_digit() {
235 return Err(EncodeError::InvalidField);
236 }
237
238 let mut addressee = *b"BLN ";
239 addressee[3] = bulletin_id;
240 encode_message(source, path, &addressee, text, None)
241}
242
243pub fn encode_announcement(
246 source: &[u8],
247 path: &[&[u8]],
248 announcement_id: u8,
249 text: &[u8],
250) -> Result<Vec<u8>, EncodeError> {
251 if !announcement_id.is_ascii_uppercase() {
252 return Err(EncodeError::InvalidField);
253 }
254
255 let mut addressee = *b"BLN ";
256 addressee[3] = announcement_id;
257 encode_message(source, path, &addressee, text, None)
258}
259
260pub fn encode_telemetry(
262 source: &[u8],
263 path: &[&[u8]],
264 sequence: u16,
265 analog: [u16; 5],
266 digital: Option<[bool; 8]>,
267) -> Result<Vec<u8>, EncodeError> {
268 let payload_len = if digital.is_some() { 34 } else { 25 };
269 ensure_packet_shape(source, path, payload_len)?;
270
271 let mut payload = Vec::with_capacity(payload_len);
272 payload.extend_from_slice(b"T#");
273 push_three_digits(&mut payload, sequence)?;
274 for value in analog {
275 payload.push(b',');
276 push_three_digits(&mut payload, value)?;
277 }
278 if let Some(digital) = digital {
279 payload.push(b',');
280 for bit in digital {
281 payload.push(if bit { b'1' } else { b'0' });
282 }
283 }
284
285 encode_packet(source, path, &payload)
286}
287
288pub fn encode_telemetry_metadata(
290 source: &[u8],
291 path: &[&[u8]],
292 kind: TelemetryMetadataEncodingKind,
293 body: &[u8],
294) -> Result<Vec<u8>, EncodeError> {
295 if body.is_empty() {
296 return Err(EncodeError::InvalidField);
297 }
298
299 encode_message(source, path, kind.addressee(), body, None)
300}
301
302pub fn encode_object(
304 source: &[u8],
305 path: &[&[u8]],
306 object: ObjectEncoding<'_>,
307) -> Result<Vec<u8>, EncodeError> {
308 if !is_fixed_printable(object.name, 9)
309 || !is_timestamp(object.timestamp)
310 || object.body.is_empty()
311 {
312 return Err(EncodeError::InvalidField);
313 }
314
315 ensure_packet_shape(source, path, 18usize.saturating_add(object.body.len()))?;
316 let mut payload = Vec::with_capacity(18usize.saturating_add(object.body.len()));
317 payload.push(b';');
318 payload.extend_from_slice(object.name);
319 payload.push(if object.live { b'*' } else { b'_' });
320 payload.extend_from_slice(object.timestamp);
321 payload.extend_from_slice(object.body);
322 encode_packet(source, path, &payload)
323}
324
325pub fn encode_item(
327 source: &[u8],
328 path: &[&[u8]],
329 item: ItemEncoding<'_>,
330) -> Result<Vec<u8>, EncodeError> {
331 if item.name.is_empty()
332 || item.name.len() > 9
333 || !item
334 .name
335 .iter()
336 .all(|byte| is_printable_ascii(*byte) && !matches!(*byte, b'!' | b'_'))
337 || item.body.is_empty()
338 {
339 return Err(EncodeError::InvalidField);
340 }
341
342 ensure_packet_shape(
343 source,
344 path,
345 2usize.saturating_add(item.name.len().saturating_add(item.body.len())),
346 )?;
347 let mut payload =
348 Vec::with_capacity(2usize.saturating_add(item.name.len().saturating_add(item.body.len())));
349 payload.push(b')');
350 payload.extend_from_slice(item.name);
351 payload.push(if item.live { b'!' } else { b'_' });
352 payload.extend_from_slice(item.body);
353 encode_packet(source, path, &payload)
354}
355
356fn validate_addresses(source: &[u8], path: &[&[u8]]) -> Result<(), EncodeError> {
357 if path.is_empty() {
358 return Err(EncodeError::EmptyPath);
359 }
360
361 if source.iter().any(u8::is_ascii_lowercase)
362 || path
363 .iter()
364 .any(|component| component.iter().any(u8::is_ascii_lowercase))
365 {
366 return Err(EncodeError::LowercaseAddress);
367 }
368
369 if !is_ax25_like_source(source)
370 || !path
371 .iter()
372 .all(|component| is_ax25_like_path_component(component))
373 {
374 return Err(EncodeError::InvalidAddress);
375 }
376
377 Ok(())
378}
379
380fn ensure_packet_shape(
381 source: &[u8],
382 path: &[&[u8]],
383 payload_len: usize,
384) -> Result<(), EncodeError> {
385 validate_addresses(source, path)?;
386 if packet_len(source, path, payload_len)? > MAX_PACKET_LEN {
387 return Err(EncodeError::OversizedPacket);
388 }
389 Ok(())
390}
391
392fn packet_len(source: &[u8], path: &[&[u8]], payload_len: usize) -> Result<usize, EncodeError> {
393 let path_len = path.iter().try_fold(0usize, |accumulator, component| {
394 accumulator.checked_add(component.len())
395 });
396 let Some(path_len) = path_len else {
397 return Err(EncodeError::OversizedPacket);
398 };
399
400 source
401 .len()
402 .checked_add(1)
403 .and_then(|len| len.checked_add(path_len))
404 .and_then(|len| len.checked_add(path.len().saturating_sub(1)))
405 .and_then(|len| len.checked_add(1))
406 .and_then(|len| len.checked_add(payload_len))
407 .ok_or(EncodeError::OversizedPacket)
408}
409
410fn is_fixed_printable(value: &[u8], len: usize) -> bool {
411 value.len() == len && value.iter().all(|byte| is_printable_ascii(*byte))
412}
413
414fn is_valid_message_id(value: &[u8]) -> bool {
415 (1..=5).contains(&value.len())
416 && value
417 .iter()
418 .all(|byte| is_printable_ascii(*byte) && *byte != b'{')
419}
420
421fn encode_message_response(
422 source: &[u8],
423 path: &[&[u8]],
424 addressee: &[u8],
425 prefix: &[u8; 3],
426 message_id: &[u8],
427) -> Result<Vec<u8>, EncodeError> {
428 if !is_valid_message_id(message_id) {
429 return Err(EncodeError::InvalidField);
430 }
431
432 let mut text = Vec::with_capacity(3usize.saturating_add(message_id.len()));
433 text.extend_from_slice(prefix);
434 text.extend_from_slice(message_id);
435 encode_message(source, path, addressee, &text, None)
436}
437
438fn push_three_digits(output: &mut Vec<u8>, value: u16) -> Result<(), EncodeError> {
439 if value > 999 {
440 return Err(EncodeError::InvalidField);
441 }
442
443 output.push(b'0' + u8::try_from(value / 100).map_err(|_| EncodeError::InvalidField)?);
444 output.push(b'0' + u8::try_from((value / 10) % 10).map_err(|_| EncodeError::InvalidField)?);
445 output.push(b'0' + u8::try_from(value % 10).map_err(|_| EncodeError::InvalidField)?);
446 Ok(())
447}