mod client;
mod error;
mod record;
mod server;
pub use client::{Authentication, Bloop, ClientHandshake, Ping, PreloadCheck, Quit, RetrieveAudio};
pub use error::ErrorResponse;
pub use record::AchievementRecord;
pub use server::{
AudioData, AuthenticationAccepted, BloopAccepted, Pong, PreloadMatch, PreloadMismatch,
ServerHandshake,
};
use crate::set::NoExtension;
use bloop_protocol_derive::MessageSet;
#[derive(Clone, Debug, MessageSet, PartialEq)]
pub enum ClientMessage<Ext = NoExtension> {
Handshake(ClientHandshake),
Authentication(Authentication),
Ping(Ping),
Quit(Quit),
Bloop(Bloop),
RetrieveAudio(RetrieveAudio),
PreloadCheck(PreloadCheck),
Custom(Ext),
}
#[derive(Clone, Debug, MessageSet, PartialEq)]
pub enum ServerMessage<Ext = NoExtension> {
Error(ErrorResponse),
Handshake(ServerHandshake),
AuthenticationAccepted(AuthenticationAccepted),
Pong(Pong),
BloopAccepted(BloopAccepted),
AudioData(AudioData),
PreloadMatch(PreloadMatch),
PreloadMismatch(PreloadMismatch),
Custom(Ext),
}
#[cfg(test)]
mod tests {
use std::net::{IpAddr, Ipv4Addr};
use uuid::Uuid;
use super::*;
use crate::capabilities::Capabilities;
use crate::codec::DecodeError;
use crate::data_hash::DataHash;
use crate::frame::RawMessage;
use crate::nfc_uid::NfcUid;
use crate::set::{MessageSet, MessageSetError};
fn decode_client(message_type: u8, payload: &[u8]) -> Result<ClientMessage, MessageSetError> {
ClientMessage::<NoExtension>::decode(&RawMessage::new(message_type, payload.to_vec()))
}
#[test]
fn client_handshake_decodes() {
let decoded = decode_client(0x01, &[1, 5]).unwrap();
assert_eq!(
decoded,
ClientMessage::Handshake(ClientHandshake {
min_version: 1,
max_version: 5,
})
);
}
#[test]
fn authentication_decodes() {
let mut payload = vec![3];
payload.extend(b"foo");
payload.push(3);
payload.extend(b"bar");
payload.push(4);
payload.extend(&[127, 0, 0, 1]);
let decoded = decode_client(0x03, &payload).unwrap();
assert_eq!(
decoded,
ClientMessage::Authentication(Authentication {
client_id: "foo".to_string(),
client_secret: "bar".to_string(),
ip_address: IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)),
})
);
}
#[test]
fn empty_client_messages_decode() {
assert_eq!(decode_client(0x05, &[]).unwrap(), ClientMessage::Ping(Ping));
assert_eq!(decode_client(0x07, &[]).unwrap(), ClientMessage::Quit(Quit));
}
#[test]
fn bloop_decodes_single_nfc_uid() {
let decoded = decode_client(0x08, &[4, 1, 2, 3, 4]).unwrap();
assert_eq!(
decoded,
ClientMessage::Bloop(Bloop {
nfc_uid: NfcUid::try_from(&[1u8, 2, 3, 4][..]).unwrap(),
})
);
}
#[test]
fn retrieve_audio_decodes_uuid() {
let uuid = Uuid::from_bytes([9; 16]);
let decoded = decode_client(0x0a, uuid.as_bytes()).unwrap();
assert_eq!(
decoded,
ClientMessage::RetrieveAudio(RetrieveAudio {
achievement_id: uuid,
})
);
}
#[test]
fn preload_check_decodes_with_some_hash() {
let mut payload = vec![16];
payload.extend_from_slice(&[0u8; 16]);
let decoded = decode_client(0x0c, &payload).unwrap();
assert_eq!(
decoded,
ClientMessage::PreloadCheck(PreloadCheck {
audio_manifest_hash: Some(DataHash::try_from(vec![0u8; 16]).unwrap()),
})
);
}
#[test]
fn preload_check_decodes_with_none_hash() {
let decoded = decode_client(0x0c, &[0]).unwrap();
assert_eq!(
decoded,
ClientMessage::PreloadCheck(PreloadCheck {
audio_manifest_hash: None,
})
);
}
#[test]
fn preload_check_accepts_arbitrary_hash_lengths() {
let decoded = decode_client(0x0c, &[1, 0]).unwrap();
assert_eq!(
decoded,
ClientMessage::PreloadCheck(PreloadCheck {
audio_manifest_hash: Some(DataHash::try_from(vec![0u8]).unwrap()),
})
);
}
#[test]
fn unknown_opcode_is_reported_without_extension() {
let error = decode_client(0xff, &[1, 2, 3]).unwrap_err();
assert!(matches!(error, MessageSetError::UnknownOpcode(0xff)));
}
#[test]
fn unknown_opcode_reaches_the_extension_set() {
let raw = RawMessage::new(0xff, vec![1, 2, 3]);
let decoded = ClientMessage::<RawMessage>::decode(&raw).unwrap();
assert_eq!(decoded, ClientMessage::Custom(raw));
}
#[test]
fn short_handshake_is_malformed() {
let error = decode_client(0x01, &[1]).unwrap_err();
assert!(matches!(
error,
MessageSetError::Malformed {
opcode: 0x01,
source: DecodeError::UnexpectedEof,
}
));
}
#[test]
fn trailing_bytes_are_malformed() {
let error = decode_client(0x05, &[1]).unwrap_err();
assert!(matches!(
error,
MessageSetError::Malformed {
opcode: 0x05,
source: DecodeError::TrailingBytes { remaining: 1 },
}
));
}
#[test]
fn authentication_with_invalid_utf8_is_malformed() {
let mut payload = vec![2];
payload.extend(&[0xff, 0xff]);
payload.push(3);
payload.extend(b"bar");
payload.push(4);
payload.extend(&[127, 0, 0, 1]);
assert!(decode_client(0x03, &payload).is_err());
}
#[test]
fn authentication_with_invalid_utf8_secret_is_malformed() {
let mut payload = vec![3];
payload.extend(b"foo");
payload.push(2);
payload.extend(&[0xff, 0xff]);
payload.push(4);
payload.extend(&[127, 0, 0, 1]);
assert!(decode_client(0x03, &payload).is_err());
}
#[test]
fn authentication_with_invalid_ip_version_is_malformed() {
let mut payload = vec![3];
payload.extend(b"foo");
payload.push(3);
payload.extend(b"bar");
payload.push(0xff);
payload.extend(&[1, 2, 3, 4]);
assert!(decode_client(0x03, &payload).is_err());
}
#[test]
fn bloop_with_invalid_uid_length_is_malformed() {
assert!(decode_client(0x08, &[5, 1, 2, 3, 4]).is_err());
}
#[test]
fn retrieve_audio_with_short_uuid_is_malformed() {
assert!(decode_client(0x0a, &[0; 15]).is_err());
}
fn encode_server(message: ServerMessage) -> RawMessage {
message.encode().unwrap()
}
#[test]
fn error_response_encodes() {
let raw = encode_server(ServerMessage::Error(ErrorResponse::InvalidCredentials));
assert_eq!(raw.message_type, 0x00);
assert_eq!(raw.payload, [3]);
}
#[test]
fn server_handshake_encodes() {
let raw = encode_server(ServerMessage::Handshake(ServerHandshake {
accepted_version: 7,
capabilities: Capabilities::none(),
}));
assert_eq!(raw.message_type, 0x02);
assert_eq!(raw.payload, [7, 0, 0, 0, 0, 0, 0, 0, 0]);
}
#[test]
fn empty_server_messages_encode() {
let raw = encode_server(ServerMessage::AuthenticationAccepted(
AuthenticationAccepted,
));
assert_eq!(raw.message_type, 0x04);
assert!(raw.payload.is_empty());
let raw = encode_server(ServerMessage::Pong(Pong));
assert_eq!(raw.message_type, 0x06);
assert!(raw.payload.is_empty());
let raw = encode_server(ServerMessage::PreloadMatch(PreloadMatch));
assert_eq!(raw.message_type, 0x0d);
assert!(raw.payload.is_empty());
}
#[test]
fn bloop_accepted_encodes_with_achievements() {
let uuid = Uuid::from_bytes([7; 16]);
let raw = encode_server(ServerMessage::BloopAccepted(BloopAccepted {
achievements: vec![AchievementRecord {
id: uuid,
audio_hash: None,
}],
}));
assert_eq!(raw.message_type, 0x09);
assert_eq!(raw.payload[0], 1);
assert_eq!(&raw.payload[1..17], uuid.as_bytes());
assert_eq!(raw.payload.len(), 1 + 16 + 1);
assert_eq!(raw.payload[17], 0);
}
#[test]
fn audio_data_encodes() {
let raw = encode_server(ServerMessage::AudioData(AudioData {
data: vec![1, 2, 3, 4, 5],
}));
assert_eq!(raw.message_type, 0x0b);
assert_eq!(raw.payload, [5, 0, 0, 0, 1, 2, 3, 4, 5]);
}
#[test]
fn preload_mismatch_encodes() {
let uuid = Uuid::from_bytes([3; 16]);
let raw = encode_server(ServerMessage::PreloadMismatch(PreloadMismatch {
audio_manifest_hash: DataHash::try_from(vec![1u8; 16]).unwrap(),
achievements: vec![AchievementRecord {
id: uuid,
audio_hash: None,
}],
}));
assert_eq!(raw.message_type, 0x0e);
assert_eq!(raw.payload[0], 16);
assert_eq!(&raw.payload[1..17], &[1; 16]);
assert_eq!(
u32::from_le_bytes(raw.payload[17..21].try_into().unwrap()),
1
);
assert_eq!(&raw.payload[21..37], uuid.as_bytes());
}
#[test]
fn custom_server_message_passes_through() {
let original = RawMessage::new(0xab, vec![9, 8, 7]);
let raw = ServerMessage::Custom(original.clone()).encode().unwrap();
assert_eq!(raw, original);
}
#[test]
fn server_messages_round_trip() {
let messages: Vec<ServerMessage> = vec![
ErrorResponse::UnknownNfcUid.into(),
ServerHandshake {
accepted_version: 3,
capabilities: Capabilities::PreloadCheck,
}
.into(),
BloopAccepted {
achievements: vec![AchievementRecord {
id: Uuid::from_bytes([1; 16]),
audio_hash: Some(DataHash::try_from(vec![2u8; 16]).unwrap()),
}],
}
.into(),
AudioData {
data: vec![1, 2, 3],
}
.into(),
];
for message in messages {
let raw = message.clone().encode().unwrap();
assert_eq!(ServerMessage::<NoExtension>::decode(&raw).unwrap(), message);
}
}
#[test]
fn client_messages_round_trip() {
let messages: Vec<ClientMessage> = vec![
ClientHandshake {
min_version: 3,
max_version: 3,
}
.into(),
Authentication {
client_id: "client".to_string(),
client_secret: "secret".to_string(),
ip_address: IpAddr::V4(Ipv4Addr::new(10, 0, 0, 7)),
}
.into(),
Bloop {
nfc_uid: NfcUid::try_from(&[1u8, 2, 3, 4, 5, 6, 7][..]).unwrap(),
}
.into(),
PreloadCheck {
audio_manifest_hash: Some(DataHash::try_from(vec![9u8; 16]).unwrap()),
}
.into(),
];
for message in messages {
let raw = message.clone().encode().unwrap();
assert_eq!(ClientMessage::<NoExtension>::decode(&raw).unwrap(), message);
}
}
}