1#![doc = include_str!("../README.md")]
2#![forbid(unsafe_code)]
3
4use std::collections::HashMap;
5use std::fmt;
6use std::net::SocketAddr;
7use std::time::{Duration, Instant};
8
9pub const DEFAULT_PORT: u16 = 5583;
10pub const MULTICAST_ADDR: [u8; 4] = [239, 255, 10, 24];
11pub const HEADER_LEN: usize = 52;
12pub const MAX_DATAGRAM_LEN: usize = 65_507;
13pub const DEFAULT_MAX_ASSEMBLIES: usize = 64;
14pub const DEFAULT_MAX_FRAME_PAYLOAD_BYTES: usize = 1_048_576;
15pub const DEFAULT_MAX_BUFFERED_BYTES: usize = 8_388_608;
16pub const DEFAULT_MAX_PATHS: usize = 4_096;
17pub const DEFAULT_MAX_POINTS: usize = 65_535;
18pub const DEFAULT_ASSEMBLY_TIMEOUT: Duration = Duration::from_millis(300);
19
20const MAGIC: &[u8; 8] = b"PONK-UDP";
21const PROTOCOL_VERSION: u8 = 0;
22pub const SENDER_NAME_LEN: usize = 32;
27const METADATA_KEY_LEN: usize = 8;
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum DataFormat {
31 XyRgbU16,
32 XyF32RgbU8,
33}
34
35impl DataFormat {
36 fn from_wire(value: u8) -> Result<Self, PonkError> {
37 match value {
38 0 => Ok(Self::XyRgbU16),
39 1 => Ok(Self::XyF32RgbU8),
40 other => Err(PonkError::UnsupportedDataFormat(other)),
41 }
42 }
43
44 fn wire(self) -> u8 {
45 match self {
46 Self::XyRgbU16 => 0,
47 Self::XyF32RgbU8 => 1,
48 }
49 }
50
51 fn bytes_per_point(self) -> usize {
52 match self {
53 Self::XyRgbU16 => 10,
54 Self::XyF32RgbU8 => 11,
55 }
56 }
57}
58
59#[derive(Debug, Clone, PartialEq)]
60pub struct PonkPoint {
61 pub x: f32,
62 pub y: f32,
63 pub rgb: [u8; 3],
64}
65
66#[derive(Debug, Clone, PartialEq)]
67pub struct PonkMetadata {
68 pub key: String,
69 pub value: f32,
70}
71
72#[derive(Debug, Clone, PartialEq)]
73pub struct PonkPath {
74 pub metadata: Vec<PonkMetadata>,
75 pub points: Vec<PonkPoint>,
76}
77
78#[derive(Debug, Clone, PartialEq)]
79pub struct PonkFrame {
80 pub sender_id: u32,
81 pub sender_name: String,
82 pub frame_number: u8,
83 pub paths: Vec<PonkPath>,
84}
85
86#[derive(Debug, Clone, Copy, PartialEq, Eq)]
88pub struct PonkLimits {
89 pub max_assemblies: usize,
90 pub max_frame_payload_bytes: usize,
91 pub max_buffered_bytes: usize,
92 pub max_paths: usize,
93 pub max_points: usize,
94 pub assembly_timeout: Duration,
95}
96
97impl Default for PonkLimits {
98 fn default() -> Self {
99 Self {
100 max_assemblies: DEFAULT_MAX_ASSEMBLIES,
101 max_frame_payload_bytes: DEFAULT_MAX_FRAME_PAYLOAD_BYTES,
102 max_buffered_bytes: DEFAULT_MAX_BUFFERED_BYTES,
103 max_paths: DEFAULT_MAX_PATHS,
104 max_points: DEFAULT_MAX_POINTS,
105 assembly_timeout: DEFAULT_ASSEMBLY_TIMEOUT,
106 }
107 }
108}
109
110#[derive(Debug, Clone, PartialEq, Eq)]
111pub enum PonkError {
112 UnsupportedProtocolVersion(u8),
113 UnsupportedDataFormat(u8),
114 DatagramTooSmall,
115 DatagramTooLarge { max: usize, actual: usize },
116 MaxDatagramTooSmall { min: usize, actual: usize },
117 TooManyChunks(usize),
118 TooManyMetadata(usize),
119 TooManyPoints(usize),
120 TooManyPaths { max: usize },
121 TooManyTotalPoints { max: usize },
122 FramePayloadTooLarge { max: usize },
123 InvalidCoordinate,
124 InvalidChunkHeader,
125 MalformedPayload,
126}
127
128impl fmt::Display for PonkError {
129 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
130 match self {
131 Self::UnsupportedProtocolVersion(version) => {
132 write!(f, "unsupported PONK protocol version {version}")
133 }
134 Self::UnsupportedDataFormat(format) => {
135 write!(f, "Unsupported PONK data format {format}")
136 }
137 Self::DatagramTooSmall => write!(f, "PONK datagram is too small"),
138 Self::DatagramTooLarge { max, actual } => {
139 write!(f, "PONK datagram is {actual} bytes, max is {max}")
140 }
141 Self::MaxDatagramTooSmall { min, actual } => write!(
142 f,
143 "PONK max datagram size {actual} is too small, minimum is {min}"
144 ),
145 Self::TooManyChunks(count) => write!(f, "PONK frame needs too many chunks: {count}"),
146 Self::TooManyMetadata(count) => {
147 write!(f, "PONK path has too many metadata entries: {count}")
148 }
149 Self::TooManyPoints(count) => write!(f, "PONK path has too many points: {count}"),
150 Self::TooManyPaths { max } => write!(f, "PONK frame has more than {max} paths"),
151 Self::TooManyTotalPoints { max } => {
152 write!(f, "PONK frame has more than {max} total points")
153 }
154 Self::FramePayloadTooLarge { max } => {
155 write!(f, "PONK frame payload exceeds {max} bytes")
156 }
157 Self::InvalidCoordinate => write!(f, "PONK frame contains an invalid coordinate"),
158 Self::InvalidChunkHeader => write!(f, "invalid PONK chunk header"),
159 Self::MalformedPayload => write!(f, "malformed PONK payload"),
160 }
161 }
162}
163
164impl std::error::Error for PonkError {}
165
166#[derive(Debug, Clone)]
167struct Header {
168 sender_id: u32,
169 sender_name: String,
170 frame_number: u8,
171 chunk_count: u8,
172 chunk_number: u8,
173 data_crc: u32,
174}
175
176struct Assembly {
177 header: Header,
178 chunks: Vec<Option<Vec<u8>>>,
179 received_bytes: usize,
180 order: u64,
181 updated_at: Instant,
182}
183
184impl Assembly {
185 fn new(header: Header) -> Self {
186 Self {
187 chunks: vec![None; header.chunk_count as usize],
188 header,
189 received_bytes: 0,
190 order: 0,
191 updated_at: Instant::now(),
192 }
193 }
194
195 fn accepts(&self, header: &Header) -> bool {
196 self.header.sender_id == header.sender_id
197 && self.header.frame_number == header.frame_number
198 && self.header.chunk_count == header.chunk_count
199 && self.header.data_crc == header.data_crc
200 }
201
202 fn insert(&mut self, header: &Header, payload: &[u8]) -> Result<(), PonkError> {
203 let Some(slot) = self.chunks.get_mut(header.chunk_number as usize) else {
204 return Err(PonkError::InvalidChunkHeader);
205 };
206 if let Some(existing) = slot.as_ref() {
207 self.received_bytes = self.received_bytes.saturating_sub(existing.len());
208 }
209 self.received_bytes += payload.len();
210 *slot = Some(payload.to_vec());
211 self.updated_at = Instant::now();
212 Ok(())
213 }
214
215 fn complete(&self) -> bool {
216 self.chunks.iter().all(Option::is_some)
217 }
218
219 fn chunk_len(&self, chunk_number: u8) -> usize {
220 self.chunks
221 .get(chunk_number as usize)
222 .and_then(Option::as_ref)
223 .map_or(0, Vec::len)
224 }
225
226 fn into_frame(self, limits: &PonkLimits) -> Result<Option<PonkFrame>, PonkError> {
227 let mut payload = Vec::with_capacity(self.received_bytes);
228 for chunk in self.chunks {
229 let Some(chunk) = chunk else {
230 return Ok(None);
231 };
232 payload.extend(chunk);
233 }
234 decode_payload_checked(&self.header, &payload, limits)
235 }
236}
237
238pub struct PonkAssembler {
239 assemblies: HashMap<(SocketAddr, u32), Assembly>,
240 limits: PonkLimits,
241 next_order: u64,
242}
243
244impl Default for PonkAssembler {
245 fn default() -> Self {
246 Self::new()
247 }
248}
249
250impl PonkAssembler {
251 pub fn new() -> Self {
253 Self::with_limits(PonkLimits::default())
254 }
255
256 pub fn with_max_assemblies(max_assemblies: usize) -> Self {
261 Self::with_limits(PonkLimits {
262 max_assemblies,
263 ..PonkLimits::default()
264 })
265 }
266
267 pub fn with_limits(limits: PonkLimits) -> Self {
269 Self {
270 assemblies: HashMap::new(),
271 limits,
272 next_order: 0,
273 }
274 }
275
276 pub fn assembly_count(&self) -> usize {
277 self.assemblies.len()
278 }
279
280 pub fn buffered_bytes(&self) -> usize {
281 self.assemblies
282 .values()
283 .map(|assembly| assembly.received_bytes)
284 .sum()
285 }
286
287 pub fn prune_stale(&mut self) {
289 let now = Instant::now();
290 self.assemblies.retain(|_, assembly| {
291 now.duration_since(assembly.updated_at) <= self.limits.assembly_timeout
292 });
293 }
294
295 fn evict_oldest(&mut self) -> bool {
296 let Some(oldest_key) = self
297 .assemblies
298 .iter()
299 .min_by_key(|(_, assembly)| assembly.order)
300 .map(|(key, _)| *key)
301 else {
302 return false;
303 };
304 self.assemblies.remove(&oldest_key);
305 true
306 }
307
308 pub fn push_datagram(
309 &mut self,
310 datagram: &[u8],
311 peer_addr: SocketAddr,
312 ) -> Result<Option<PonkFrame>, PonkError> {
313 self.prune_stale();
314 let Some((header, payload)) = split_datagram(datagram)? else {
315 return Ok(None);
316 };
317
318 if header.chunk_count == 1 {
319 return decode_payload_checked(&header, payload, &self.limits);
320 }
321 if self.limits.max_assemblies == 0 {
322 return Ok(None);
323 }
324 if payload.len() > self.limits.max_frame_payload_bytes {
325 return Err(PonkError::FramePayloadTooLarge {
326 max: self.limits.max_frame_payload_bytes,
327 });
328 }
329
330 let key = (peer_addr, header.sender_id);
331 if !self
332 .assemblies
333 .get(&key)
334 .is_some_and(|assembly| assembly.accepts(&header))
335 {
336 while self.assemblies.len() >= self.limits.max_assemblies {
337 if !self.evict_oldest() {
338 return Ok(None);
339 }
340 }
341 self.assemblies.insert(key, Assembly::new(header.clone()));
342 if let Some(assembly) = self.assemblies.get_mut(&key) {
343 assembly.order = self.next_order;
344 self.next_order = self.next_order.wrapping_add(1);
345 }
346 }
347
348 let Some(assembly) = self.assemblies.get(&key) else {
349 return Ok(None);
350 };
351 let replaced_bytes = assembly.chunk_len(header.chunk_number);
352 let frame_bytes = assembly
353 .received_bytes
354 .saturating_sub(replaced_bytes)
355 .saturating_add(payload.len());
356 if frame_bytes > self.limits.max_frame_payload_bytes {
357 self.assemblies.remove(&key);
358 return Err(PonkError::FramePayloadTooLarge {
359 max: self.limits.max_frame_payload_bytes,
360 });
361 }
362 let additional_bytes = payload.len().saturating_sub(replaced_bytes);
363 while self.buffered_bytes().saturating_add(additional_bytes)
364 > self.limits.max_buffered_bytes
365 {
366 if !self.evict_oldest() {
367 return Err(PonkError::FramePayloadTooLarge {
368 max: self.limits.max_buffered_bytes,
369 });
370 }
371 }
372 let Some(assembly) = self.assemblies.get_mut(&key) else {
373 return Ok(None);
374 };
375 assembly.insert(&header, payload)?;
376 if !assembly.complete() {
377 return Ok(None);
378 }
379
380 self.assemblies
381 .remove(&key)
382 .map(|assembly| assembly.into_frame(&self.limits))
383 .unwrap_or(Ok(None))
384 }
385}
386
387pub fn checksum(data: &[u8]) -> u32 {
388 data.iter()
389 .fold(0u32, |sum, byte| sum.wrapping_add(*byte as u32))
390}
391
392pub fn decode_datagram(datagram: &[u8]) -> Result<Option<PonkFrame>, PonkError> {
393 decode_datagram_with_limits(datagram, &PonkLimits::default())
394}
395
396pub fn decode_datagram_with_limits(
397 datagram: &[u8],
398 limits: &PonkLimits,
399) -> Result<Option<PonkFrame>, PonkError> {
400 let Some((header, payload)) = split_datagram(datagram)? else {
401 return Ok(None);
402 };
403 if header.chunk_count != 1 || header.chunk_number != 0 {
404 return Ok(None);
405 }
406 decode_payload_checked(&header, payload, limits)
407}
408
409pub fn encode_datagrams(
410 frame: &PonkFrame,
411 data_format: DataFormat,
412 max_datagram_len: usize,
413) -> Result<Vec<Vec<u8>>, PonkError> {
414 if max_datagram_len > MAX_DATAGRAM_LEN {
415 return Err(PonkError::DatagramTooLarge {
416 max: MAX_DATAGRAM_LEN,
417 actual: max_datagram_len,
418 });
419 }
420 if max_datagram_len <= HEADER_LEN {
421 return Err(PonkError::MaxDatagramTooSmall {
422 min: HEADER_LEN + 1,
423 actual: max_datagram_len,
424 });
425 }
426
427 let payload = encode_payload(&frame.paths, data_format)?;
428 let max_payload_len = max_datagram_len - HEADER_LEN;
429 let chunk_count = payload.len().max(1).div_ceil(max_payload_len);
430 if chunk_count > u8::MAX as usize {
431 return Err(PonkError::TooManyChunks(chunk_count));
432 }
433
434 let crc = checksum(&payload);
435 let mut datagrams = Vec::with_capacity(chunk_count);
436 if payload.is_empty() {
437 datagrams.push(encode_datagram_header(frame, 1, 0, crc));
438 return Ok(datagrams);
439 }
440
441 for (chunk_number, chunk) in payload.chunks(max_payload_len).enumerate() {
442 let mut datagram =
443 encode_datagram_header(frame, chunk_count as u8, chunk_number as u8, crc);
444 datagram.extend_from_slice(chunk);
445 datagrams.push(datagram);
446 }
447
448 Ok(datagrams)
449}
450
451fn encode_datagram_header(
452 frame: &PonkFrame,
453 chunk_count: u8,
454 chunk_number: u8,
455 data_crc: u32,
456) -> Vec<u8> {
457 let mut datagram = Vec::with_capacity(HEADER_LEN);
458 datagram.extend_from_slice(MAGIC);
459 datagram.push(PROTOCOL_VERSION);
460 datagram.extend_from_slice(&frame.sender_id.to_le_bytes());
461 datagram.extend_from_slice(&fixed_bytes::<SENDER_NAME_LEN>(&frame.sender_name));
462 datagram.push(frame.frame_number);
463 datagram.push(chunk_count);
464 datagram.push(chunk_number);
465 datagram.extend_from_slice(&data_crc.to_le_bytes());
466 datagram
467}
468
469fn encode_payload(paths: &[PonkPath], data_format: DataFormat) -> Result<Vec<u8>, PonkError> {
470 if paths.is_empty() {
471 return Ok(vec![data_format.wire(), 0, 0, 0]);
472 }
473 if paths.len() > DEFAULT_MAX_PATHS {
474 return Err(PonkError::TooManyPaths {
475 max: DEFAULT_MAX_PATHS,
476 });
477 }
478
479 let mut payload_len = 0usize;
480 let mut total_points = 0usize;
481 for path in paths {
482 if path.metadata.len() > u8::MAX as usize {
483 return Err(PonkError::TooManyMetadata(path.metadata.len()));
484 }
485 if path.points.len() > u16::MAX as usize {
486 return Err(PonkError::TooManyPoints(path.points.len()));
487 }
488 total_points =
489 total_points
490 .checked_add(path.points.len())
491 .ok_or(PonkError::TooManyTotalPoints {
492 max: DEFAULT_MAX_POINTS,
493 })?;
494 if total_points > DEFAULT_MAX_POINTS {
495 return Err(PonkError::TooManyTotalPoints {
496 max: DEFAULT_MAX_POINTS,
497 });
498 }
499 if data_format == DataFormat::XyF32RgbU8
500 && path
501 .points
502 .iter()
503 .any(|point| !point.x.is_finite() || !point.y.is_finite())
504 {
505 return Err(PonkError::InvalidCoordinate);
506 }
507 let path_len = 4usize
508 .checked_add(path.metadata.len().checked_mul(12).ok_or(
509 PonkError::FramePayloadTooLarge {
510 max: DEFAULT_MAX_FRAME_PAYLOAD_BYTES,
511 },
512 )?)
513 .and_then(|len| {
514 path.points
515 .len()
516 .checked_mul(data_format.bytes_per_point())
517 .and_then(|point_bytes| len.checked_add(point_bytes))
518 })
519 .ok_or(PonkError::FramePayloadTooLarge {
520 max: DEFAULT_MAX_FRAME_PAYLOAD_BYTES,
521 })?;
522 payload_len = payload_len
523 .checked_add(path_len)
524 .ok_or(PonkError::FramePayloadTooLarge {
525 max: DEFAULT_MAX_FRAME_PAYLOAD_BYTES,
526 })?;
527 if payload_len > DEFAULT_MAX_FRAME_PAYLOAD_BYTES {
528 return Err(PonkError::FramePayloadTooLarge {
529 max: DEFAULT_MAX_FRAME_PAYLOAD_BYTES,
530 });
531 }
532 }
533
534 let mut payload = Vec::with_capacity(payload_len);
535 for path in paths {
536 payload.push(data_format.wire());
537 payload.push(path.metadata.len() as u8);
538 for metadata in &path.metadata {
539 payload.extend_from_slice(&fixed_bytes::<METADATA_KEY_LEN>(&metadata.key));
540 payload.extend_from_slice(&metadata.value.to_le_bytes());
541 }
542 payload.extend_from_slice(&(path.points.len() as u16).to_le_bytes());
543
544 for point in &path.points {
545 match data_format {
546 DataFormat::XyRgbU16 => {
547 payload.extend_from_slice(&coord_to_u16(point.x).to_le_bytes());
548 payload.extend_from_slice(&coord_to_u16(point.y).to_le_bytes());
549 payload.extend_from_slice(&((point.rgb[0] as u16) * 257).to_le_bytes());
550 payload.extend_from_slice(&((point.rgb[1] as u16) * 257).to_le_bytes());
551 payload.extend_from_slice(&((point.rgb[2] as u16) * 257).to_le_bytes());
552 }
553 DataFormat::XyF32RgbU8 => {
554 payload.extend_from_slice(&point.x.clamp(-1.0, 1.0).to_le_bytes());
555 payload.extend_from_slice(&point.y.clamp(-1.0, 1.0).to_le_bytes());
556 payload.extend_from_slice(&point.rgb);
557 }
558 }
559 }
560 }
561 Ok(payload)
562}
563
564fn split_datagram(datagram: &[u8]) -> Result<Option<(Header, &[u8])>, PonkError> {
565 if datagram.get(0..MAGIC.len()) != Some(MAGIC.as_slice()) {
566 return Ok(None);
567 }
568 if datagram.len() < HEADER_LEN {
569 return Err(PonkError::DatagramTooSmall);
570 }
571 if datagram.len() > MAX_DATAGRAM_LEN {
572 return Err(PonkError::DatagramTooLarge {
573 max: MAX_DATAGRAM_LEN,
574 actual: datagram.len(),
575 });
576 }
577
578 let mut offset = MAGIC.len();
579 let Some(protocol_version) = datagram.get(offset).copied() else {
580 return Ok(None);
581 };
582 offset += 1;
583 if protocol_version != PROTOCOL_VERSION {
584 return Err(PonkError::UnsupportedProtocolVersion(protocol_version));
585 }
586
587 let Some(sender_id) = read_u32_le(datagram, &mut offset) else {
588 return Ok(None);
589 };
590 let Some(sender_name_bytes) = datagram.get(offset..offset + SENDER_NAME_LEN) else {
591 return Ok(None);
592 };
593 let sender_name = parse_fixed_string(sender_name_bytes);
594 offset += SENDER_NAME_LEN;
595 let Some(frame_number) = datagram.get(offset).copied() else {
596 return Ok(None);
597 };
598 offset += 1;
599 let Some(chunk_count) = datagram.get(offset).copied() else {
600 return Ok(None);
601 };
602 offset += 1;
603 let Some(chunk_number) = datagram.get(offset).copied() else {
604 return Ok(None);
605 };
606 offset += 1;
607 let Some(data_crc) = read_u32_le(datagram, &mut offset) else {
608 return Ok(None);
609 };
610
611 if chunk_count == 0 || chunk_number >= chunk_count {
612 return Ok(None);
613 }
614
615 Ok(Some((
616 Header {
617 sender_id,
618 sender_name,
619 frame_number,
620 chunk_count,
621 chunk_number,
622 data_crc,
623 },
624 &datagram[HEADER_LEN..],
625 )))
626}
627
628fn decode_payload_checked(
629 header: &Header,
630 payload: &[u8],
631 limits: &PonkLimits,
632) -> Result<Option<PonkFrame>, PonkError> {
633 if payload.len() > limits.max_frame_payload_bytes {
634 return Err(PonkError::FramePayloadTooLarge {
635 max: limits.max_frame_payload_bytes,
636 });
637 }
638 if checksum(payload) != header.data_crc {
639 return Ok(None);
640 }
641 let paths = decode_payload(payload, limits)?;
642 Ok(Some(PonkFrame {
643 sender_id: header.sender_id,
644 sender_name: header.sender_name.clone(),
645 frame_number: header.frame_number,
646 paths,
647 }))
648}
649
650fn decode_payload(data: &[u8], limits: &PonkLimits) -> Result<Vec<PonkPath>, PonkError> {
651 let mut offset = 0;
652 let mut paths = Vec::new();
653 let mut total_points = 0usize;
654
655 while offset < data.len() {
656 if paths.len() >= limits.max_paths {
657 return Err(PonkError::TooManyPaths {
658 max: limits.max_paths,
659 });
660 }
661 let Some(data_format) = data.get(offset).copied() else {
662 return Err(PonkError::MalformedPayload);
663 };
664 let data_format = DataFormat::from_wire(data_format)?;
665 offset += 1;
666 let Some(metadata_count) = data.get(offset).copied() else {
667 return Err(PonkError::MalformedPayload);
668 };
669 offset += 1;
670
671 let mut metadata = Vec::with_capacity(metadata_count as usize);
672 for _ in 0..metadata_count {
673 let Some(key_bytes) = data.get(offset..offset + METADATA_KEY_LEN) else {
674 return Err(PonkError::MalformedPayload);
675 };
676 let key = parse_fixed_string(key_bytes);
677 offset += METADATA_KEY_LEN;
678 let Some(value) = read_f32_le(data, &mut offset) else {
679 return Err(PonkError::MalformedPayload);
680 };
681 metadata.push(PonkMetadata { key, value });
682 }
683
684 let Some(point_count) = read_u16_le(data, &mut offset) else {
685 return Err(PonkError::MalformedPayload);
686 };
687 let point_count = point_count as usize;
688 total_points =
689 total_points
690 .checked_add(point_count)
691 .ok_or(PonkError::TooManyTotalPoints {
692 max: limits.max_points,
693 })?;
694 if total_points > limits.max_points {
695 return Err(PonkError::TooManyTotalPoints {
696 max: limits.max_points,
697 });
698 }
699 let Some(byte_count) = point_count.checked_mul(data_format.bytes_per_point()) else {
700 return Err(PonkError::MalformedPayload);
701 };
702 let Some(end_offset) = offset.checked_add(byte_count) else {
703 return Err(PonkError::MalformedPayload);
704 };
705 if data.len() < end_offset {
706 return Err(PonkError::MalformedPayload);
707 }
708
709 let mut points = Vec::with_capacity(point_count);
710 for _ in 0..point_count {
711 points.push(match data_format {
712 DataFormat::XyRgbU16 => {
713 let Some(x) = read_u16_le(data, &mut offset) else {
714 return Err(PonkError::MalformedPayload);
715 };
716 let Some(y) = read_u16_le(data, &mut offset) else {
717 return Err(PonkError::MalformedPayload);
718 };
719 let Some(r) = read_u16_le(data, &mut offset) else {
720 return Err(PonkError::MalformedPayload);
721 };
722 let Some(g) = read_u16_le(data, &mut offset) else {
723 return Err(PonkError::MalformedPayload);
724 };
725 let Some(b) = read_u16_le(data, &mut offset) else {
726 return Err(PonkError::MalformedPayload);
727 };
728 PonkPoint {
729 x: u16_to_coord(x),
730 y: u16_to_coord(y),
731 rgb: [(r >> 8) as u8, (g >> 8) as u8, (b >> 8) as u8],
732 }
733 }
734 DataFormat::XyF32RgbU8 => {
735 let Some(x) = read_f32_le(data, &mut offset) else {
736 return Err(PonkError::MalformedPayload);
737 };
738 let Some(y) = read_f32_le(data, &mut offset) else {
739 return Err(PonkError::MalformedPayload);
740 };
741 let Some(rgb) = data.get(offset..offset + 3) else {
742 return Err(PonkError::MalformedPayload);
743 };
744 offset += 3;
745 if !x.is_finite() || !y.is_finite() {
746 return Err(PonkError::InvalidCoordinate);
747 }
748 PonkPoint {
749 x: x.clamp(-1.0, 1.0),
750 y: y.clamp(-1.0, 1.0),
751 rgb: [rgb[0], rgb[1], rgb[2]],
752 }
753 }
754 });
755 }
756
757 paths.push(PonkPath { metadata, points });
758 }
759
760 Ok(paths)
761}
762
763fn fixed_bytes<const N: usize>(value: &str) -> [u8; N] {
764 let mut bytes = [0u8; N];
765 let mut len = value.len().min(N);
766 while !value.is_char_boundary(len) {
767 len -= 1;
768 }
769 bytes[..len].copy_from_slice(&value.as_bytes()[..len]);
770 bytes
771}
772
773fn parse_fixed_string(bytes: &[u8]) -> String {
774 let len = bytes.iter().position(|b| *b == 0).unwrap_or(bytes.len());
775 String::from_utf8_lossy(&bytes[..len]).to_string()
776}
777
778fn read_u16_le(data: &[u8], offset: &mut usize) -> Option<u16> {
779 let bytes = data.get(*offset..*offset + 2)?;
780 *offset += 2;
781 Some(u16::from_le_bytes([bytes[0], bytes[1]]))
782}
783
784fn read_u32_le(data: &[u8], offset: &mut usize) -> Option<u32> {
785 let bytes = data.get(*offset..*offset + 4)?;
786 *offset += 4;
787 Some(u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
788}
789
790fn read_f32_le(data: &[u8], offset: &mut usize) -> Option<f32> {
791 read_u32_le(data, offset).map(f32::from_bits)
792}
793
794fn coord_to_u16(value: f32) -> u16 {
795 (((value.clamp(-1.0, 1.0) + 1.0) * 0.5) * u16::MAX as f32).round() as u16
796}
797
798fn u16_to_coord(value: u16) -> f32 {
799 -1.0 + 2.0 * (value as f32 / u16::MAX as f32)
800}
801
802#[cfg(test)]
803mod tests {
804 use std::net::{Ipv4Addr, SocketAddr};
805
806 use super::*;
807
808 const CANONICAL_F32_DATAGRAM: &[u8] = &[
811 0x50, 0x4f, 0x4e, 0x4b, 0x2d, 0x55, 0x44, 0x50, 0x00, 0x04, 0x03, 0x02, 0x01, 0x53, 0x00,
812 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
813 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
814 0x05, 0x01, 0x00, 0x3a, 0x05, 0x00, 0x00, 0x01, 0x01, 0x50, 0x41, 0x54, 0x48, 0x4e, 0x55,
815 0x4d, 0x42, 0x00, 0x00, 0x80, 0x3f, 0x01, 0x00, 0x00, 0x00, 0x00, 0x3f, 0x00, 0x00, 0x80,
816 0xbe, 0x12, 0x34, 0x56,
817 ];
818 const CANONICAL_U16_DATAGRAM: &[u8] = &[
819 0x50, 0x4f, 0x4e, 0x4b, 0x2d, 0x55, 0x44, 0x50, 0x00, 0x04, 0x03, 0x02, 0x01, 0x53, 0x00,
820 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
821 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
822 0x05, 0x01, 0x00, 0x7d, 0x05, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0xff, 0xff,
823 0x80, 0x80, 0x40, 0x40, 0xff, 0xff,
824 ];
825
826 fn sample_frame() -> PonkFrame {
827 PonkFrame {
828 sender_id: 42,
829 sender_name: "test sender".to_string(),
830 frame_number: 7,
831 paths: vec![PonkPath {
832 metadata: vec![PonkMetadata {
833 key: "PATHNUMB".to_string(),
834 value: 3.0,
835 }],
836 points: vec![
837 PonkPoint {
838 x: -0.5,
839 y: 0.25,
840 rgb: [255, 0, 64],
841 },
842 PonkPoint {
843 x: 0.5,
844 y: -0.25,
845 rgb: [0, 255, 128],
846 },
847 ],
848 }],
849 }
850 }
851
852 #[test]
853 fn canonical_f32_vector_decodes_and_encodes_exactly() {
854 let decoded = decode_datagram(CANONICAL_F32_DATAGRAM).unwrap().unwrap();
855 assert_eq!(decoded.sender_id, 0x0102_0304);
856 assert_eq!(decoded.sender_name, "S");
857 assert_eq!(decoded.frame_number, 5);
858 assert_eq!(decoded.paths[0].metadata[0].key, "PATHNUMB");
859 assert_eq!(decoded.paths[0].metadata[0].value, 1.0);
860 assert_eq!(decoded.paths[0].points[0].x, 0.5);
861 assert_eq!(decoded.paths[0].points[0].y, -0.25);
862 assert_eq!(decoded.paths[0].points[0].rgb, [0x12, 0x34, 0x56]);
863
864 assert_eq!(
865 encode_datagrams(&decoded, DataFormat::XyF32RgbU8, 1_472).unwrap(),
866 [CANONICAL_F32_DATAGRAM]
867 );
868 }
869
870 #[test]
871 fn canonical_u16_vector_decodes_and_encodes_exactly() {
872 let decoded = decode_datagram(CANONICAL_U16_DATAGRAM).unwrap().unwrap();
873 assert_eq!(decoded.sender_id, 0x0102_0304);
874 assert_eq!(decoded.sender_name, "S");
875 assert_eq!(decoded.frame_number, 5);
876 assert_eq!(decoded.paths[0].points[0].x, -1.0);
877 assert_eq!(decoded.paths[0].points[0].y, 1.0);
878 assert_eq!(decoded.paths[0].points[0].rgb, [0x80, 0x40, 0xff]);
879
880 assert_eq!(
881 encode_datagrams(&decoded, DataFormat::XyRgbU16, 1_472).unwrap(),
882 [CANONICAL_U16_DATAGRAM]
883 );
884 }
885
886 #[test]
887 fn empty_frame_encodes_as_explicit_zero_point_path() {
888 let frame = PonkFrame {
889 paths: Vec::new(),
890 ..sample_frame()
891 };
892
893 let datagram = encode_datagrams(&frame, DataFormat::XyF32RgbU8, 1_472)
894 .unwrap()
895 .remove(0);
896
897 assert!(datagram.len() > HEADER_LEN);
898 let decoded = decode_datagram(&datagram).unwrap().unwrap();
899 assert_eq!(decoded.paths.len(), 1);
900 assert!(decoded.paths[0].points.is_empty());
901 }
902
903 #[test]
904 fn encoded_single_datagram_decodes_to_frame() {
905 let datagrams = encode_datagrams(&sample_frame(), DataFormat::XyF32RgbU8, 1200).unwrap();
906 assert_eq!(datagrams.len(), 1);
907
908 let decoded = decode_datagram(&datagrams[0]).unwrap().unwrap();
909
910 assert_eq!(decoded.sender_id, 42);
911 assert_eq!(decoded.sender_name, "test sender");
912 assert_eq!(decoded.frame_number, 7);
913 assert_eq!(decoded.paths.len(), 1);
914 assert_eq!(decoded.paths[0].metadata[0].key, "PATHNUMB");
915 assert_eq!(decoded.paths[0].metadata[0].value, 3.0);
916 assert_eq!(decoded.paths[0].points, sample_frame().paths[0].points);
917 }
918
919 #[test]
920 fn encoded_chunked_datagrams_reassemble_to_frame() {
921 let mut frame = sample_frame();
922 frame.paths[0].points = (0..50)
923 .map(|i| PonkPoint {
924 x: -1.0 + i as f32 * 0.04,
925 y: 0.25,
926 rgb: [i as u8, 255, 128],
927 })
928 .collect();
929
930 let datagrams = encode_datagrams(&frame, DataFormat::XyF32RgbU8, HEADER_LEN + 80).unwrap();
931 assert!(datagrams.len() > 1);
932
933 let peer = SocketAddr::from((Ipv4Addr::LOCALHOST, 5583));
934 let mut assembler = PonkAssembler::new();
935 let mut decoded = None;
936 for datagram in datagrams.iter().rev() {
937 decoded = assembler.push_datagram(datagram, peer).unwrap().or(decoded);
938 }
939
940 let decoded = decoded.expect("all chunks should reassemble");
941 assert_eq!(decoded.sender_id, frame.sender_id);
942 assert_eq!(decoded.sender_name, frame.sender_name);
943 assert_eq!(decoded.paths[0].points, frame.paths[0].points);
944 }
945
946 #[test]
947 fn malformed_datagram_classes_have_stable_public_results() {
948 let truncated_header = b"PONK-UDP\0";
949 assert_eq!(
950 decode_datagram(truncated_header),
951 Err(PonkError::DatagramTooSmall)
952 );
953
954 let mut unsupported_version = CANONICAL_F32_DATAGRAM.to_vec();
955 unsupported_version[MAGIC.len()] = 1;
956 assert_eq!(
957 decode_datagram(&unsupported_version),
958 Err(PonkError::UnsupportedProtocolVersion(1))
959 );
960
961 let mut invalid_chunk_numbering = CANONICAL_F32_DATAGRAM.to_vec();
962 let chunk_count_offset = HEADER_LEN - 6;
963 invalid_chunk_numbering[chunk_count_offset] = 0;
964 assert_eq!(decode_datagram(&invalid_chunk_numbering), Ok(None));
965
966 let mut unsupported_format = CANONICAL_F32_DATAGRAM.to_vec();
967 unsupported_format[HEADER_LEN] = 99;
968 let updated_checksum = checksum(&unsupported_format[HEADER_LEN..]);
969 unsupported_format[HEADER_LEN - 4..HEADER_LEN]
970 .copy_from_slice(&updated_checksum.to_le_bytes());
971 assert_eq!(
972 decode_datagram(&unsupported_format),
973 Err(PonkError::UnsupportedDataFormat(99))
974 );
975
976 let mut truncated_payload =
977 CANONICAL_F32_DATAGRAM[..CANONICAL_F32_DATAGRAM.len() - 1].to_vec();
978 let updated_checksum = checksum(&truncated_payload[HEADER_LEN..]);
979 truncated_payload[HEADER_LEN - 4..HEADER_LEN]
980 .copy_from_slice(&updated_checksum.to_le_bytes());
981 assert_eq!(
982 decode_datagram(&truncated_payload),
983 Err(PonkError::MalformedPayload)
984 );
985 }
986
987 #[test]
988 fn bad_checksum_is_ignored() {
989 let mut datagram = encode_datagrams(&sample_frame(), DataFormat::XyF32RgbU8, 1200)
990 .unwrap()
991 .remove(0);
992 datagram[HEADER_LEN - 4] = datagram[HEADER_LEN - 4].wrapping_add(1);
993
994 assert!(decode_datagram(&datagram).unwrap().is_none());
995 }
996
997 #[test]
998 fn protocol_accepts_255_chunks_and_rejects_256() {
999 let point = PonkPoint {
1000 x: 0.0,
1001 y: 0.0,
1002 rgb: [0, 0, 0],
1003 };
1004 let metadata = PonkMetadata {
1005 key: "PATHNUMB".to_string(),
1006 value: 0.0,
1007 };
1008 let frame_255 = PonkFrame {
1009 paths: vec![PonkPath {
1010 metadata: vec![metadata.clone(); 9],
1011 points: vec![point.clone(); 13],
1012 }],
1013 ..sample_frame()
1014 };
1015 assert_eq!(
1016 encode_datagrams(&frame_255, DataFormat::XyF32RgbU8, HEADER_LEN + 1)
1017 .unwrap()
1018 .len(),
1019 255
1020 );
1021
1022 let frame_256 = PonkFrame {
1023 paths: vec![PonkPath {
1024 metadata: vec![metadata; 10],
1025 points: vec![point; 12],
1026 }],
1027 ..sample_frame()
1028 };
1029 assert_eq!(
1030 encode_datagrams(&frame_256, DataFormat::XyF32RgbU8, HEADER_LEN + 1),
1031 Err(PonkError::TooManyChunks(256))
1032 );
1033 }
1034
1035 #[test]
1036 fn zero_assembly_limit_never_retains_multipart_frames() {
1037 let datagrams =
1038 encode_datagrams(&sample_frame(), DataFormat::XyF32RgbU8, HEADER_LEN + 8).unwrap();
1039 assert!(datagrams.len() > 1);
1040 let peer = SocketAddr::from((Ipv4Addr::LOCALHOST, 5583));
1041 let mut assembler = PonkAssembler::with_max_assemblies(0);
1042
1043 assert!(
1044 assembler
1045 .push_datagram(&datagrams[0], peer)
1046 .unwrap()
1047 .is_none()
1048 );
1049 assert_eq!(assembler.assembly_count(), 0);
1050 assert_eq!(assembler.buffered_bytes(), 0);
1051 }
1052
1053 #[test]
1054 fn assembly_payload_limit_rejects_frame_before_retaining_chunk() {
1055 let datagrams =
1056 encode_datagrams(&sample_frame(), DataFormat::XyF32RgbU8, HEADER_LEN + 16).unwrap();
1057 let limits = PonkLimits {
1058 max_frame_payload_bytes: 8,
1059 ..PonkLimits::default()
1060 };
1061 let peer = SocketAddr::from((Ipv4Addr::LOCALHOST, 5583));
1062 let mut assembler = PonkAssembler::with_limits(limits);
1063
1064 assert_eq!(
1065 assembler.push_datagram(&datagrams[0], peer),
1066 Err(PonkError::FramePayloadTooLarge { max: 8 })
1067 );
1068 assert_eq!(assembler.assembly_count(), 0);
1069 assert_eq!(assembler.buffered_bytes(), 0);
1070 }
1071
1072 #[test]
1073 fn aggregate_buffer_limit_evicts_oldest_incomplete_frame() {
1074 let datagrams =
1075 encode_datagrams(&sample_frame(), DataFormat::XyF32RgbU8, HEADER_LEN + 8).unwrap();
1076 let limits = PonkLimits {
1077 max_buffered_bytes: 12,
1078 ..PonkLimits::default()
1079 };
1080 let first_peer = SocketAddr::from((Ipv4Addr::LOCALHOST, 5583));
1081 let second_peer = SocketAddr::from((Ipv4Addr::LOCALHOST, 5584));
1082 let mut assembler = PonkAssembler::with_limits(limits);
1083
1084 assembler.push_datagram(&datagrams[0], first_peer).unwrap();
1085 assembler.push_datagram(&datagrams[0], second_peer).unwrap();
1086
1087 assert_eq!(assembler.assembly_count(), 1);
1088 assert!(assembler.buffered_bytes() <= limits.max_buffered_bytes);
1089 }
1090
1091 #[test]
1092 fn stale_incomplete_assemblies_are_pruned() {
1093 let datagrams =
1094 encode_datagrams(&sample_frame(), DataFormat::XyF32RgbU8, HEADER_LEN + 8).unwrap();
1095 let limits = PonkLimits {
1096 assembly_timeout: std::time::Duration::ZERO,
1097 ..PonkLimits::default()
1098 };
1099 let peer = SocketAddr::from((Ipv4Addr::LOCALHOST, 5583));
1100 let mut assembler = PonkAssembler::with_limits(limits);
1101
1102 assembler.push_datagram(&datagrams[0], peer).unwrap();
1103 assert_eq!(assembler.assembly_count(), 1);
1104 assembler.prune_stale();
1105 assert_eq!(assembler.assembly_count(), 0);
1106 assert_eq!(assembler.buffered_bytes(), 0);
1107 }
1108
1109 #[test]
1110 fn oversized_decode_input_is_rejected() {
1111 let mut datagram = encode_datagrams(&sample_frame(), DataFormat::XyF32RgbU8, 1200)
1112 .unwrap()
1113 .remove(0);
1114 datagram.resize(MAX_DATAGRAM_LEN + 1, 0);
1115
1116 assert_eq!(
1117 decode_datagram(&datagram),
1118 Err(PonkError::DatagramTooLarge {
1119 max: MAX_DATAGRAM_LEN,
1120 actual: MAX_DATAGRAM_LEN + 1,
1121 })
1122 );
1123 }
1124
1125 #[test]
1126 fn checksum_wraps_instead_of_panicking() {
1127 let data = vec![u8::MAX; (u32::MAX as usize / u8::MAX as usize) + 2];
1128 let expected = data
1129 .iter()
1130 .fold(0u32, |sum, byte| sum.wrapping_add(*byte as u32));
1131
1132 assert_eq!(checksum(&data), expected);
1133 }
1134
1135 #[test]
1136 fn decode_limits_reject_path_amplification() {
1137 let frame = PonkFrame {
1138 paths: vec![
1139 PonkPath {
1140 metadata: Vec::new(),
1141 points: Vec::new(),
1142 },
1143 PonkPath {
1144 metadata: Vec::new(),
1145 points: Vec::new(),
1146 },
1147 ],
1148 ..sample_frame()
1149 };
1150 let datagram = encode_datagrams(&frame, DataFormat::XyF32RgbU8, 1200)
1151 .unwrap()
1152 .remove(0);
1153 let limits = PonkLimits {
1154 max_paths: 1,
1155 ..PonkLimits::default()
1156 };
1157
1158 assert_eq!(
1159 decode_datagram_with_limits(&datagram, &limits),
1160 Err(PonkError::TooManyPaths { max: 1 })
1161 );
1162 }
1163
1164 #[test]
1165 fn float_coordinates_are_finite_and_bounded_at_wire_boundaries() {
1166 let mut frame = sample_frame();
1167 frame.paths[0].points[0].x = f32::NAN;
1168 assert_eq!(
1169 encode_datagrams(&frame, DataFormat::XyF32RgbU8, 1200),
1170 Err(PonkError::InvalidCoordinate)
1171 );
1172
1173 let mut invalid_datagram = CANONICAL_F32_DATAGRAM.to_vec();
1174 let x_offset = HEADER_LEN + 16;
1175 invalid_datagram[x_offset..x_offset + 4].copy_from_slice(&f32::NAN.to_le_bytes());
1176 let crc = checksum(&invalid_datagram[HEADER_LEN..]);
1177 invalid_datagram[HEADER_LEN - 4..HEADER_LEN].copy_from_slice(&crc.to_le_bytes());
1178 assert_eq!(
1179 decode_datagram(&invalid_datagram),
1180 Err(PonkError::InvalidCoordinate)
1181 );
1182
1183 frame.paths[0].points[0].x = 2.0;
1184 let datagram = encode_datagrams(&frame, DataFormat::XyF32RgbU8, 1200)
1185 .unwrap()
1186 .remove(0);
1187 let decoded = decode_datagram(&datagram).unwrap().unwrap();
1188 assert_eq!(decoded.paths[0].points[0].x, 1.0);
1189 }
1190
1191 #[test]
1192 fn encoder_rejects_aggregate_path_amplification() {
1193 let frame = PonkFrame {
1194 paths: (0..=DEFAULT_MAX_PATHS)
1195 .map(|_| PonkPath {
1196 metadata: Vec::new(),
1197 points: Vec::new(),
1198 })
1199 .collect(),
1200 ..sample_frame()
1201 };
1202
1203 assert_eq!(
1204 encode_datagrams(&frame, DataFormat::XyF32RgbU8, 1_472),
1205 Err(PonkError::TooManyPaths {
1206 max: DEFAULT_MAX_PATHS
1207 })
1208 );
1209 }
1210
1211 #[test]
1212 fn fixed_utf8_fields_do_not_split_code_points() {
1213 let mut frame = sample_frame();
1214 frame.sender_name = format!("{}é", "a".repeat(SENDER_NAME_LEN - 1));
1215 frame.paths[0].metadata[0].key = "abcdefgé".to_string();
1216
1217 let datagram = encode_datagrams(&frame, DataFormat::XyF32RgbU8, 1200)
1218 .unwrap()
1219 .remove(0);
1220 let decoded = decode_datagram(&datagram).unwrap().unwrap();
1221
1222 assert_eq!(decoded.sender_name, "a".repeat(SENDER_NAME_LEN - 1));
1223 assert_eq!(decoded.paths[0].metadata[0].key, "abcdefg");
1224 assert!(!decoded.sender_name.contains('\u{fffd}'));
1225 assert!(!decoded.paths[0].metadata[0].key.contains('\u{fffd}'));
1226 }
1227}