1use crate::nfc_uid::NfcUid;
11use bitmask_enum::bitmask;
12use byteorder::ReadBytesExt;
13use md5::Digest;
14use std::io::{self, Cursor, Read};
15use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
16use uuid::Uuid;
17
18#[derive(Debug, Clone)]
20pub struct Message {
21 message_type: u8,
23 payload: Vec<u8>,
25}
26
27impl Message {
28 pub fn new(message_type: u8, payload: Vec<u8>) -> Self {
30 Self {
31 message_type,
32 payload,
33 }
34 }
35
36 pub fn into_bytes(self) -> Vec<u8> {
42 let mut bytes = Vec::with_capacity(1 + 4 + self.payload.len());
43 bytes.push(self.message_type);
44 bytes.extend_from_slice(&(self.payload.len() as u32).to_le_bytes());
45 bytes.extend(self.payload);
46
47 bytes
48 }
49}
50
51#[derive(Clone, Copy, Debug, PartialEq, Eq)]
53pub struct DataHash(Digest);
54
55impl From<Digest> for DataHash {
56 fn from(value: Digest) -> Self {
57 Self(value)
58 }
59}
60
61impl DataHash {
62 pub fn as_bytes(&self) -> &[u8] {
64 self.0.as_ref()
65 }
66
67 fn into_tagged_bytes(self) -> [u8; 17] {
70 let mut bytes = [0u8; 17];
71 bytes[0] = 16;
72 bytes[1..].copy_from_slice(self.0.as_slice());
73
74 bytes
75 }
76
77 fn from_cursor_opt(cursor: &mut Cursor<Vec<u8>>) -> Result<Option<Self>, io::Error> {
79 let length = cursor.read_u8()?;
80
81 if length == 0 {
82 return Ok(None);
83 }
84
85 if length != 16 {
86 return Err(io::Error::new(
87 io::ErrorKind::InvalidInput,
88 format!("invalid data hash length: {length}"),
89 ));
90 }
91
92 let mut bytes = [0u8; 16];
93 cursor.read_exact(&mut bytes)?;
94
95 Ok(Some(Self(Digest(bytes))))
96 }
97}
98
99#[bitmask(u64)]
101#[bitmask_config(vec_debug)]
102pub enum ServerFeatures {
103 PreloadCheck = 0x1,
105}
106
107#[derive(Debug)]
109pub enum ClientMessage {
110 ClientHandshake { min_version: u8, max_version: u8 },
112
113 Authentication {
115 client_id: String,
116 client_secret: String,
117 ip_addr: IpAddr,
118 },
119
120 Ping,
122
123 Quit,
125
126 Bloop { nfc_uid: NfcUid },
128
129 RetrieveAudio { achievement_id: Uuid },
131
132 PreloadCheck {
134 audio_manifest_hash: Option<DataHash>,
135 },
136
137 Unknown(Message),
139}
140
141impl TryFrom<Message> for ClientMessage {
142 type Error = io::Error;
143
144 fn try_from(message: Message) -> Result<Self, Self::Error> {
148 let mut cursor = Cursor::new(message.payload);
149
150 match message.message_type {
151 0x01 => {
152 let min_version = cursor.read_u8()?;
153 let max_version = cursor.read_u8()?;
154
155 Ok(Self::ClientHandshake {
156 min_version,
157 max_version,
158 })
159 }
160 0x03 => {
161 let client_id = read_string(&mut cursor)?;
162 let client_secret = read_string(&mut cursor)?;
163 let ip_addr = read_ip_addr(&mut cursor)?;
164
165 Ok(Self::Authentication {
166 client_id,
167 client_secret,
168 ip_addr,
169 })
170 }
171 0x05 => Ok(Self::Ping),
172 0x07 => Ok(Self::Quit),
173 0x08 => {
174 let length = cursor.read_u8()? as usize;
175 let mut buffer = vec![0u8; length];
176 cursor.read_exact(&mut buffer)?;
177
178 let nfc_uid = NfcUid::try_from(buffer.as_slice()).map_err(|_| {
179 io::Error::new(io::ErrorKind::InvalidData, "Invalid NFC UID length or data")
180 })?;
181
182 Ok(Self::Bloop { nfc_uid })
183 }
184 0x0a => {
185 let mut uuid = [0u8; 16];
186 cursor.read_exact(&mut uuid)?;
187
188 Ok(Self::RetrieveAudio {
189 achievement_id: Uuid::from_bytes(uuid),
190 })
191 }
192 0x0c => {
193 let hash = DataHash::from_cursor_opt(&mut cursor)?;
194
195 Ok(Self::PreloadCheck {
196 audio_manifest_hash: hash,
197 })
198 }
199 code => Ok(Self::Unknown(Message::new(code, cursor.into_inner()))),
200 }
201 }
202}
203
204fn read_string(cursor: &mut Cursor<Vec<u8>>) -> Result<String, io::Error> {
208 let length = cursor.read_u8()? as usize;
209 let mut buffer = vec![0; length];
210 cursor.read_exact(&mut buffer)?;
211
212 String::from_utf8(buffer)
213 .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "invalid UTF-8 string"))
214}
215
216fn read_ip_addr(cursor: &mut Cursor<Vec<u8>>) -> Result<IpAddr, io::Error> {
221 let kind = cursor.read_u8()?;
222
223 match kind {
224 4 => {
225 let mut bytes = [0u8; 4];
226 cursor.read_exact(&mut bytes)?;
227 Ok(IpAddr::V4(Ipv4Addr::from(bytes)))
228 }
229 6 => {
230 let mut bytes = [0u8; 16];
231 cursor.read_exact(&mut bytes)?;
232 Ok(IpAddr::V6(Ipv6Addr::from(bytes)))
233 }
234 _ => Err(io::Error::new(
235 io::ErrorKind::InvalidData,
236 format!("invalid IP address type: {kind}"),
237 )),
238 }
239}
240
241#[derive(Debug)]
244pub struct AchievementRecord {
245 pub id: Uuid,
247 pub audio_file_hash: Option<DataHash>,
249}
250
251impl AchievementRecord {
252 fn into_bytes(self) -> Vec<u8> {
257 let mut bytes = Vec::with_capacity(16 + 17);
258 bytes.extend_from_slice(&self.id.into_bytes());
259
260 match self.audio_file_hash {
261 Some(hash) => bytes.extend_from_slice(&hash.into_tagged_bytes()),
262 None => bytes.push(0),
263 }
264
265 bytes
266 }
267}
268
269#[derive(Debug)]
271pub enum ErrorResponse {
272 UnexpectedMessage,
273 MalformedMessage,
274 UnsupportedVersionRange,
275 InvalidCredentials,
276 UnknownNfcUid,
277 NfcUidThrottled,
278 AudioUnavailable,
279 Custom(u8),
281}
282
283impl From<ErrorResponse> for u8 {
284 fn from(error: ErrorResponse) -> u8 {
285 error.into_error_code()
286 }
287}
288
289impl ErrorResponse {
290 fn into_error_code(self) -> u8 {
292 match self {
293 Self::UnexpectedMessage => 0,
294 Self::MalformedMessage => 1,
295 Self::UnsupportedVersionRange => 2,
296 Self::InvalidCredentials => 3,
297 Self::UnknownNfcUid => 4,
298 Self::NfcUidThrottled => 5,
299 Self::AudioUnavailable => 6,
300 Self::Custom(code) => code,
301 }
302 }
303}
304
305#[derive(Debug)]
307pub enum ServerMessage {
308 Error(ErrorResponse),
310
311 ServerHandshake {
313 accepted_version: u8,
314 features: ServerFeatures,
315 },
316
317 AuthenticationAccepted,
319
320 Pong,
322
323 BloopAccepted {
325 achievements: Vec<AchievementRecord>,
326 },
327
328 AudioData { data: Vec<u8> },
330
331 PreloadMatch,
333
334 PreloadMismatch {
336 audio_manifest_hash: DataHash,
337 achievements: Vec<AchievementRecord>,
338 },
339
340 Custom(Message),
342}
343
344impl From<ServerMessage> for Message {
345 fn from(server_message: ServerMessage) -> Message {
348 match server_message {
349 ServerMessage::Error(error) => Message::new(0x00, vec![error.into_error_code()]),
350 ServerMessage::ServerHandshake {
351 accepted_version,
352 features,
353 } => {
354 let mut payload = Vec::with_capacity(9);
355 payload.push(accepted_version);
356 payload.extend_from_slice(&features.bits().to_le_bytes());
357 Message::new(0x02, payload)
358 }
359 ServerMessage::AuthenticationAccepted => Message::new(0x04, vec![]),
360 ServerMessage::Pong => Message::new(0x06, vec![]),
361 ServerMessage::BloopAccepted { achievements } => {
362 let mut payload = Vec::with_capacity(1 + achievements.len() * (16 + 17));
363 payload.push(achievements.len() as u8);
364
365 for achievement in achievements {
366 payload.extend(achievement.into_bytes())
367 }
368
369 Message::new(0x09, payload)
370 }
371 ServerMessage::AudioData { data } => {
372 let mut payload = Vec::with_capacity(4 + data.len());
373 payload.extend_from_slice(&(data.len() as u32).to_le_bytes());
374 payload.extend(data);
375
376 Message::new(0x0b, payload)
377 }
378 ServerMessage::PreloadMatch => Message::new(0x0d, vec![]),
379 ServerMessage::PreloadMismatch {
380 audio_manifest_hash,
381 achievements,
382 } => {
383 let mut payload = Vec::with_capacity(17 + 1 + achievements.len() * (16 + 17));
384 payload.extend_from_slice(&audio_manifest_hash.into_tagged_bytes());
385 payload.extend_from_slice(&(achievements.len() as u32).to_le_bytes());
386
387 for achievement in achievements {
388 payload.extend(achievement.into_bytes())
389 }
390
391 Message::new(0x0e, payload)
392 }
393 ServerMessage::Custom(message) => message,
394 }
395 }
396}
397
398#[cfg(test)]
399mod tests {
400 use super::*;
401 use std::net::{IpAddr, Ipv4Addr};
402 use uuid::Uuid;
403
404 fn make_message(msg_type: u8, payload: &[u8]) -> Message {
405 Message::new(msg_type, payload.to_vec())
406 }
407
408 #[test]
409 fn client_handshake_parses_correctly_from_message() {
410 let payload = [1u8, 5];
411 let msg = make_message(0x01, &payload);
412 let client_msg = ClientMessage::try_from(msg).unwrap();
413
414 match client_msg {
415 ClientMessage::ClientHandshake {
416 min_version,
417 max_version,
418 } => {
419 assert_eq!(min_version, 1);
420 assert_eq!(max_version, 5);
421 }
422 _ => panic!("Expected ClientHandshake variant"),
423 }
424 }
425
426 #[test]
427 fn authentication_parses_correctly_from_message() {
428 let mut payload = vec![];
429 payload.push(3);
430 payload.extend(b"foo");
431 payload.push(3);
432 payload.extend(b"bar");
433 payload.push(4);
434 payload.extend(&[127, 0, 0, 1]);
435
436 let msg = make_message(0x03, &payload);
437 let client_msg = ClientMessage::try_from(msg).unwrap();
438
439 match client_msg {
440 ClientMessage::Authentication {
441 client_id,
442 client_secret,
443 ip_addr,
444 } => {
445 assert_eq!(client_id, "foo");
446 assert_eq!(client_secret, "bar");
447 assert_eq!(ip_addr, IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)));
448 }
449 _ => panic!("Expected Authentication variant"),
450 }
451 }
452
453 #[test]
454 fn ping_message_parses_as_ping_variant() {
455 let msg = make_message(0x05, &[]);
456 let client_msg = ClientMessage::try_from(msg).unwrap();
457 assert!(matches!(client_msg, ClientMessage::Ping));
458 }
459
460 #[test]
461 fn quit_message_parses_as_quit_variant() {
462 let msg = make_message(0x07, &[]);
463 let client_msg = ClientMessage::try_from(msg).unwrap();
464 assert!(matches!(client_msg, ClientMessage::Quit));
465 }
466
467 #[test]
468 fn bloop_message_parses_single_nfc_uid_correctly() {
469 let payload = [4u8, 1, 2, 3, 4];
470 let msg = make_message(0x08, &payload);
471 let client_msg = ClientMessage::try_from(msg).unwrap();
472
473 match client_msg {
474 ClientMessage::Bloop { nfc_uid } => {
475 let expected = NfcUid::try_from(&[1, 2, 3, 4][..]).unwrap();
476 assert_eq!(nfc_uid, expected);
477 }
478 _ => panic!("Expected Bloop variant"),
479 }
480 }
481
482 #[test]
483 fn retrieve_audio_message_parses_uuid_correctly() {
484 let uuid = Uuid::new_v4();
485 let payload = uuid.as_bytes();
486 let msg = make_message(0x0a, payload);
487 let client_msg = ClientMessage::try_from(msg).unwrap();
488
489 match client_msg {
490 ClientMessage::RetrieveAudio { achievement_id } => {
491 assert_eq!(achievement_id, uuid);
492 }
493 _ => panic!("Expected RetrieveAudio variant"),
494 }
495 }
496
497 #[test]
498 fn preload_check_message_parses_with_some_hash() {
499 let mut payload = vec![16];
500 payload.extend_from_slice(&[0u8; 16]);
501 let msg = make_message(0x0c, &payload);
502 let client_msg = ClientMessage::try_from(msg).unwrap();
503
504 match client_msg {
505 ClientMessage::PreloadCheck {
506 audio_manifest_hash,
507 } => {
508 assert!(audio_manifest_hash.is_some());
509 let hash = audio_manifest_hash.unwrap();
510 assert_eq!(hash.0.as_slice(), &[0u8; 16]);
511 }
512 _ => panic!("Expected PreloadCheck variant"),
513 }
514 }
515
516 #[test]
517 fn preload_check_message_parses_with_none_hash() {
518 let payload = [0];
519 let msg = make_message(0x0c, &payload);
520 let client_msg = ClientMessage::try_from(msg).unwrap();
521
522 match client_msg {
523 ClientMessage::PreloadCheck {
524 audio_manifest_hash,
525 } => {
526 assert!(audio_manifest_hash.is_none());
527 }
528 _ => panic!("Expected PreloadCheck variant"),
529 }
530 }
531
532 #[test]
533 fn unknown_message_parses_correctly_with_payload_preserved() {
534 let payload = [1, 2, 3];
535 let msg = make_message(0xFF, &payload);
536 let client_msg = ClientMessage::try_from(msg).unwrap();
537
538 match client_msg {
539 ClientMessage::Unknown(m) => {
540 assert_eq!(m.message_type, 0xFF);
541 assert_eq!(m.payload, payload);
542 }
543 _ => panic!("Expected Unknown variant"),
544 }
545 }
546
547 #[test]
548 fn client_handshake_fails_if_payload_too_short() {
549 let msg = make_message(0x01, &[1]);
550 assert!(ClientMessage::try_from(msg).is_err());
551 }
552
553 #[test]
554 fn authentication_fails_on_invalid_utf8_client_id() {
555 let mut payload = vec![2];
556 payload.extend(&[0xff, 0xff]);
557 payload.push(3);
558 payload.extend(b"bar");
559 payload.push(4);
560 payload.extend(&[127, 0, 0, 1]);
561
562 let msg = make_message(0x03, &payload);
563 assert!(ClientMessage::try_from(msg).is_err());
564 }
565
566 #[test]
567 fn authentication_fails_on_invalid_utf8_client_secret() {
568 let mut payload = vec![3];
569 payload.extend(b"foo");
570 payload.push(2);
571 payload.extend(&[0xff, 0xff]);
572 payload.push(4);
573 payload.extend(&[127, 0, 0, 1]);
574
575 let msg = make_message(0x03, &payload);
576 assert!(ClientMessage::try_from(msg).is_err());
577 }
578
579 #[test]
580 fn authentication_fails_on_invalid_ip_kind() {
581 let mut payload = vec![3];
582 payload.extend(b"foo");
583 payload.push(3);
584 payload.extend(b"bar");
585 payload.push(0xff);
586 payload.extend(&[1, 2, 3, 4]);
587
588 let msg = make_message(0x03, &payload);
589 assert!(ClientMessage::try_from(msg).is_err());
590 }
591
592 #[test]
593 fn bloop_fails_if_nfc_uid_length_mismatch() {
594 let payload = [5u8, 1, 2, 3, 4];
595 let msg = make_message(0x08, &payload);
596 assert!(ClientMessage::try_from(msg).is_err());
597 }
598
599 #[test]
600 fn retrieve_audio_fails_if_uuid_too_short() {
601 let payload = [0u8; 15];
602 let msg = make_message(0x0a, &payload);
603 assert!(ClientMessage::try_from(msg).is_err());
604 }
605
606 #[test]
607 fn preload_check_fails_on_invalid_length() {
608 let payload = [1, 0];
609 let msg = make_message(0x0c, &payload);
610 assert!(ClientMessage::try_from(msg).is_err());
611 }
612
613 #[test]
614 fn server_message_error_serializes_correctly() {
615 let server_msg = ServerMessage::Error(ErrorResponse::InvalidCredentials);
616 let message: Message = server_msg.into();
617 assert_eq!(message.message_type, 0x00);
618 assert_eq!(message.payload, vec![3]);
619 }
620
621 #[test]
622 fn server_handshake_serializes_correctly() {
623 let features = ServerFeatures::none();
624 let server_msg = ServerMessage::ServerHandshake {
625 accepted_version: 7,
626 features,
627 };
628 let message: Message = server_msg.into();
629 assert_eq!(message.message_type, 0x02);
630 assert_eq!(message.payload.len(), 9);
631 assert_eq!(message.payload[0], 7);
632 assert_eq!(&message.payload[1..], &features.bits().to_le_bytes());
633 }
634
635 #[test]
636 fn authentication_accepted_serializes_to_empty_payload() {
637 let server_msg = ServerMessage::AuthenticationAccepted;
638 let message: Message = server_msg.into();
639 assert_eq!(message.message_type, 0x04);
640 assert!(message.payload.is_empty());
641 }
642
643 #[test]
644 fn pong_serializes_to_empty_payload() {
645 let server_msg = ServerMessage::Pong;
646 let message: Message = server_msg.into();
647 assert_eq!(message.message_type, 0x06);
648 assert!(message.payload.is_empty());
649 }
650
651 #[test]
652 fn bloop_accepted_serializes_with_achievements() {
653 let uuid = Uuid::new_v4();
654 let record = AchievementRecord {
655 id: uuid,
656 audio_file_hash: None,
657 };
658
659 let server_msg = ServerMessage::BloopAccepted {
660 achievements: vec![record],
661 };
662
663 let message: Message = server_msg.into();
664 assert_eq!(message.message_type, 0x09);
665 assert_eq!(message.payload[0], 1);
666
667 assert_eq!(&message.payload[1..17], uuid.as_bytes());
668 assert_eq!(message.payload.len(), 1 + 16 + 1);
669 }
670
671 #[test]
672 fn audio_data_serializes_correctly() {
673 let data = vec![1, 2, 3, 4, 5];
674 let server_msg = ServerMessage::AudioData { data: data.clone() };
675 let message: Message = server_msg.into();
676
677 assert_eq!(message.message_type, 0x0b);
678 assert_eq!(&message.payload[4..], &data[..]);
679
680 let length = u32::from_le_bytes(message.payload[0..4].try_into().unwrap());
681 assert_eq!(length as usize, data.len());
682 }
683
684 #[test]
685 fn preload_match_serializes_to_empty_payload() {
686 let server_msg = ServerMessage::PreloadMatch;
687 let message: Message = server_msg.into();
688
689 assert_eq!(message.message_type, 0x0d);
690 assert!(message.payload.is_empty());
691 }
692
693 #[test]
694 fn preload_mismatch_serializes_with_hash_and_achievements() {
695 let hash = DataHash(Digest([1u8; 16]));
696 let uuid = Uuid::new_v4();
697 let record = AchievementRecord {
698 id: uuid,
699 audio_file_hash: None,
700 };
701
702 let server_msg = ServerMessage::PreloadMismatch {
703 audio_manifest_hash: hash,
704 achievements: vec![record],
705 };
706
707 let message: Message = server_msg.into();
708 assert_eq!(message.message_type, 0x0e);
709
710 assert_eq!(message.payload[0], 16);
712 assert_eq!(&message.payload[1..17], &hash.0.as_slice()[..]);
713
714 let count = u32::from_le_bytes(message.payload[17..21].try_into().unwrap());
716 assert_eq!(count, 1);
717
718 assert_eq!(&message.payload[21..37], uuid.as_bytes());
720 }
721
722 #[test]
723 fn custom_server_message_passes_through_as_is() {
724 let original = Message::new(0xAB, vec![9, 8, 7]);
725 let server_msg = ServerMessage::Custom(original.clone());
726 let message: Message = server_msg.into();
727
728 assert_eq!(message.message_type, 0xAB);
729 assert_eq!(message.payload, vec![9, 8, 7]);
730 }
731}