bloop-protocol 1.0.0

Core implementation of the Bloop wire protocol
//! The error response message.

use crate::codec::{Decode, DecodeError, Encode, EncodeError};
use crate::set::Payload;

/// An error reported by the server.
///
/// Error codes below `0x80` are reserved for the standard protocol; the
/// range above is available to extensions via [`ErrorResponse::Custom`].
///
/// Decoding is total over the code byte: codes this crate does not know,
/// including reserved ones a future minor protocol version may define,
/// decode as `Custom` so an older client degrades gracefully instead of
/// treating the message as malformed. As a consequence, constructing
/// `Custom` with a code that has a dedicated variant (e.g. `Custom(3)`) does
/// not round-trip: it decodes as that variant.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[non_exhaustive]
pub enum ErrorResponse {
    /// Unexpected message sent by the peer (fatal).
    UnexpectedMessage,

    /// Sent message does not follow the protocol spec (fatal).
    MalformedMessage,

    /// Requested version range is not supported (fatal).
    UnsupportedVersionRange,

    /// Invalid credentials sent in authentication (fatal).
    InvalidCredentials,

    /// The NFC UID is unknown.
    UnknownNfcUid,

    /// The NFC UID is recognized but throttled.
    NfcUidThrottled,

    /// No audio file is available for the given achievement.
    AudioUnavailable,

    /// An error code without a dedicated variant.
    Custom(u8),
}

impl ErrorResponse {
    /// Returns the wire code of the error.
    pub fn code(&self) -> u8 {
        match self {
            Self::UnexpectedMessage => 0,
            Self::MalformedMessage => 1,
            Self::UnsupportedVersionRange => 2,
            Self::InvalidCredentials => 3,
            Self::UnknownNfcUid => 4,
            Self::NfcUidThrottled => 5,
            Self::AudioUnavailable => 6,
            Self::Custom(code) => *code,
        }
    }

    /// Maps a wire code onto its error, falling back to [`Self::Custom`].
    pub fn from_code(code: u8) -> Self {
        match code {
            0 => Self::UnexpectedMessage,
            1 => Self::MalformedMessage,
            2 => Self::UnsupportedVersionRange,
            3 => Self::InvalidCredentials,
            4 => Self::UnknownNfcUid,
            5 => Self::NfcUidThrottled,
            6 => Self::AudioUnavailable,
            other => Self::Custom(other),
        }
    }
}

impl Payload for ErrorResponse {
    const OPCODE: u8 = 0x00;
}

impl Encode for ErrorResponse {
    fn encode(&self, out: &mut Vec<u8>) -> Result<(), EncodeError> {
        out.push(self.code());
        Ok(())
    }
}

impl Decode for ErrorResponse {
    fn decode(input: &mut &[u8]) -> Result<Self, DecodeError> {
        Ok(Self::from_code(u8::decode(input)?))
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::codec::{decode_payload, encode_payload};

    #[test]
    fn known_codes_round_trip() {
        for code in 0..=6 {
            let error = ErrorResponse::from_code(code);

            assert!(!matches!(error, ErrorResponse::Custom(_)));
            assert_eq!(error.code(), code);
            assert_eq!(encode_payload(&error).unwrap(), [code]);
            assert_eq!(decode_payload::<ErrorResponse>(&[code]).unwrap(), error);
        }
    }

    #[test]
    fn unknown_codes_decode_as_custom() {
        assert_eq!(
            decode_payload::<ErrorResponse>(&[0x07]).unwrap(),
            ErrorResponse::Custom(0x07)
        );
        assert_eq!(
            decode_payload::<ErrorResponse>(&[0x80]).unwrap(),
            ErrorResponse::Custom(0x80)
        );
    }

    #[test]
    fn custom_with_dedicated_code_decodes_as_variant() {
        let encoded = encode_payload(&ErrorResponse::Custom(3)).unwrap();

        assert_eq!(encoded, [3]);
        assert_eq!(
            decode_payload::<ErrorResponse>(&encoded).unwrap(),
            ErrorResponse::InvalidCredentials
        );
    }
}