Skip to main content

cacophony/
errors.rs

1use std::{
2    fmt,
3    net::{AddrParseError, SocketAddr},
4    str::Utf8Error,
5    time::Duration,
6};
7
8use dave::{
9    Codec, CreateKeyPackageError, DecryptError, EncryptError, FrameDecryptError, InitError,
10    ProcessCommitError, ProcessProposalsError, ProcessWelcomeError, SetExternalSenderError,
11    UpdateRatchetsError,
12};
13use thiserror::Error as ThisError;
14
15use crate::{
16    observer::{ConnectStage, ReceiveDecodeErrorKind},
17    rtp::RtpPayloadType,
18    state::{EncryptionMode, OfferedEncryptionMode},
19};
20
21#[derive(Debug)]
22pub enum Error {
23    InvalidInput(InvalidInputError),
24    Pcm(PcmError),
25    Protocol(ProtocolError),
26    Timeout {
27        stage: Option<ConnectStage>,
28        duration: Duration,
29    },
30    Io(std::io::Error),
31    Json(serde_json::Error),
32    WebSocket(tokio_tungstenite::tungstenite::Error),
33    Opus(OpusError),
34    UnsupportedCodec(UnsupportedCodecError),
35    Dave(DaveError),
36    Rtp(RtpError),
37    TransportCrypto(TransportCryptoError),
38    PayloadTooLarge {
39        kind: PayloadKind,
40        len: usize,
41        max_len: usize,
42    },
43    Backpressure(BackpressureError),
44    Closed,
45    Join(ConnectionJoinError),
46}
47
48impl Error {
49    pub fn receive_decode_kind(&self) -> Option<ReceiveDecodeErrorKind> {
50        match self {
51            Self::Rtp(_) => Some(ReceiveDecodeErrorKind::MalformedRtp),
52            Self::UnsupportedCodec(_) => Some(ReceiveDecodeErrorKind::UnsupportedCodec),
53            Self::TransportCrypto(_) => Some(ReceiveDecodeErrorKind::TransportDecryptFailed),
54            Self::Dave(DaveError::Decrypt(error)) => Some(error.receive_decode_kind()),
55            Self::Opus(_) => Some(ReceiveDecodeErrorKind::OpusDecodeFailed),
56            _ => None,
57        }
58    }
59}
60
61impl fmt::Display for Error {
62    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63        match self {
64            Self::InvalidInput(error) => write!(f, "{error}"),
65            Self::Pcm(error) => write!(f, "{error}"),
66            Self::Protocol(error) => write!(f, "{error}"),
67            Self::Timeout { stage, duration } => {
68                if let Some(stage) = stage {
69                    write!(f, "voice {} timed out after {duration:?}", stage.label())
70                } else {
71                    write!(f, "voice operation timed out after {duration:?}")
72                }
73            }
74            Self::Io(error) => write!(f, "{error}"),
75            Self::Json(error) => write!(f, "{error}"),
76            Self::WebSocket(error) => write!(f, "{error}"),
77            Self::Opus(error) => write!(f, "{error}"),
78            Self::UnsupportedCodec(error) => write!(f, "{error}"),
79            Self::Dave(error) => write!(f, "{error}"),
80            Self::Rtp(error) => write!(f, "{error}"),
81            Self::TransportCrypto(error) => write!(f, "{error}"),
82            Self::PayloadTooLarge { kind, len, max_len } => {
83                write!(
84                    f,
85                    "voice {kind} is {len} bytes, exceeding requested max_len {max_len}"
86                )
87            }
88            Self::Backpressure(error) => write!(f, "{error}"),
89            Self::Closed => f.write_str("voice connection is closed"),
90            Self::Join(error) => write!(f, "{error}"),
91        }
92    }
93}
94
95impl std::error::Error for Error {
96    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
97        match self {
98            Self::Pcm(error) => Some(error),
99            Self::Protocol(error) => error.source(),
100            Self::Io(error) => Some(error),
101            Self::Json(error) => Some(error),
102            Self::WebSocket(error) => Some(error),
103            Self::UnsupportedCodec(error) => Some(error),
104            Self::Dave(error) => Some(error),
105            Self::Rtp(error) => Some(error),
106            Self::TransportCrypto(error) => Some(error),
107            Self::Join(error) => error.source(),
108            _ => None,
109        }
110    }
111}
112
113impl From<std::io::Error> for Error {
114    fn from(error: std::io::Error) -> Self {
115        Self::Io(error)
116    }
117}
118
119impl From<serde_json::Error> for Error {
120    fn from(error: serde_json::Error) -> Self {
121        Self::Json(error)
122    }
123}
124
125impl From<tokio_tungstenite::tungstenite::Error> for Error {
126    fn from(error: tokio_tungstenite::tungstenite::Error) -> Self {
127        Self::WebSocket(error)
128    }
129}
130
131impl From<DaveError> for Error {
132    fn from(error: DaveError) -> Self {
133        Self::Dave(error)
134    }
135}
136
137impl From<UnsupportedCodecError> for Error {
138    fn from(error: UnsupportedCodecError) -> Self {
139        Self::UnsupportedCodec(error)
140    }
141}
142
143impl From<PcmError> for Error {
144    fn from(error: PcmError) -> Self {
145        Self::Pcm(error)
146    }
147}
148
149impl From<RtpError> for Error {
150    fn from(error: RtpError) -> Self {
151        Self::Rtp(error)
152    }
153}
154
155impl From<TransportCryptoError> for Error {
156    fn from(error: TransportCryptoError) -> Self {
157        Self::TransportCrypto(error)
158    }
159}
160
161pub type Result<T> = std::result::Result<T, Error>;
162
163#[derive(Clone, Debug, PartialEq, Eq, ThisError)]
164pub enum PcmError {
165    #[error("PCM sample rate must be greater than zero")]
166    SampleRateZero,
167    #[error("PCM channel count must be greater than zero")]
168    ChannelCountZero,
169    #[error("{encoding} PCM byte length {byte_len} is not aligned to {sample_bytes}-byte samples")]
170    SampleAlignment {
171        encoding: &'static str,
172        byte_len: usize,
173        sample_bytes: usize,
174    },
175    #[error("PCM sample count {samples} is not aligned to {channels} channels")]
176    ChannelAlignment { channels: usize, samples: usize },
177    #[error("PCM encoding `{0}` is not supported")]
178    UnsupportedEncoding(String),
179    #[error("PCM resampler chunk frame count must be greater than zero")]
180    ResamplerChunkFramesZero,
181    #[error("PCM resampler is not initialized")]
182    ResamplerNotInitialized,
183    #[error("PCM resampler error: {0}")]
184    Resampler(String),
185}
186
187#[derive(Debug, ThisError)]
188pub enum InvalidInputError {
189    #[error("connection tuning field {field} must be greater than zero")]
190    ConnectionTuningZero { field: &'static str },
191    #[error("connection tuning field {field} must be at least {min}, got {actual}")]
192    ConnectionTuningTooSmall {
193        field: &'static str,
194        min: usize,
195        actual: usize,
196    },
197    #[error("connection tuning duration {field} must be nonzero")]
198    ConnectionTuningDurationZero { field: &'static str },
199    #[error("Discord voice gateway version {version} is unsupported")]
200    UnsupportedGatewayVersion { version: u8 },
201    #[error("{} cannot be configured as a Discord video codec", codec.as_str())]
202    VideoCodecPreferenceNotVideo { codec: Codec },
203    #[error("{} appears more than once in Discord video codec preferences", codec.as_str())]
204    DuplicateVideoCodecPreference { codec: Codec },
205    #[error("max_len must be greater than zero")]
206    ZeroMaxLen,
207    #[error("{} frame must not be empty", codec.as_str())]
208    EmptyPayload { codec: Codec },
209    #[error("Discord PCM block must contain {expected} interleaved f32 samples, got {actual}")]
210    PcmBlockSampleCount { expected: usize, actual: usize },
211    #[error("Discord PCM block must contain {expected} samples per channel, got {actual}")]
212    PcmBlockFrameCount { expected: usize, actual: usize },
213    #[error("captured PCM audio must not be empty")]
214    PcmArchiveEmpty,
215    #[error("Ogg Opus vendor must not be empty")]
216    OggOpusVendorEmpty,
217    #[error("Ogg Opus archive encoding does not support {sample_rate_hz} Hz captured audio")]
218    OggOpusUnsupportedSampleRate { sample_rate_hz: u32 },
219    #[error("Discord PCM playback received mixed sample rates: {existing} and {actual}")]
220    DiscordPcmMixedSampleRates { existing: u32, actual: u32 },
221    #[error("Discord PCM playback received mixed channel counts: {existing} and {actual}")]
222    DiscordPcmMixedChannelCounts { existing: usize, actual: usize },
223    #[error("Discord PCM playback received mixed PCM encodings")]
224    DiscordPcmMixedEncoding,
225    #[error("Discord PCM encoder was not initialized")]
226    DiscordPcmEncoderUninitialized,
227    #[error("Opus bitrate {bitrate_bps} bps exceeds the encoder limit")]
228    OpusBitrateTooLarge { bitrate_bps: u32 },
229    #[error("Opus output buffer must be at least {min_len} bytes, got {len}")]
230    OpusOutputBufferTooSmall { min_len: usize, len: usize },
231}
232
233#[derive(Debug, ThisError)]
234pub enum ProtocolError {
235    #[error("DAVE MLS key package and commit/welcome use binary websocket frames")]
236    TextPayloadRequiresBinaryDaveMlsCommand,
237    #[error("voice discovery packet must be at least {min_len} bytes, got {len}")]
238    UdpDiscoveryPacketTooShort { len: usize, min_len: usize },
239    #[error(
240        "unexpected voice discovery packet type {packet_type}; expected {expected_packet_type}"
241    )]
242    UnexpectedUdpDiscoveryPacketType {
243        packet_type: u16,
244        expected_packet_type: u16,
245    },
246    #[error(
247        "unexpected voice discovery packet length {packet_len}; expected {expected_packet_len}"
248    )]
249    UnexpectedUdpDiscoveryPacketLen {
250        packet_len: u16,
251        expected_packet_len: u16,
252    },
253    #[error("invalid voice discovery ip: {0}")]
254    InvalidUdpDiscoveryIp(#[source] Utf8Error),
255    #[error("voice ready payload did not include encryption modes")]
256    ReadyMissingEncryptionModes,
257    #[error("required voice encryption mode {required_mode} was not offered: {modes:?}")]
258    RequiredEncryptionModeUnavailable {
259        required_mode: EncryptionMode,
260        modes: Vec<OfferedEncryptionMode>,
261    },
262    #[error("voice ready payload did not include a supported encryption mode: {modes:?}")]
263    ReadyMissingSupportedEncryptionMode { modes: Vec<OfferedEncryptionMode> },
264    #[error("resolve voice websocket endpoint {host}:{port}: {source}")]
265    ResolveWebSocketEndpoint {
266        host: String,
267        port: u16,
268        #[source]
269        source: std::io::Error,
270    },
271    #[error("voice websocket endpoint {host}:{port} did not resolve to any addresses")]
272    WebSocketEndpointNoAddresses { host: String, port: u16 },
273    #[error("voice websocket connect to {address} timed out after {duration:?}")]
274    WebSocketAddressConnectTimeout {
275        address: SocketAddr,
276        duration: Duration,
277    },
278    #[error(
279        "voice websocket connect to {host}:{port} failed across {address_count} resolved addresses: {}",
280        errors.join("; ")
281    )]
282    WebSocketAllAddressesFailed {
283        host: String,
284        port: u16,
285        address_count: usize,
286        errors: Vec<String>,
287    },
288    #[error("tcp connect {address}: {source}")]
289    TcpConnect {
290        address: SocketAddr,
291        #[source]
292        source: std::io::Error,
293    },
294    #[error("voice websocket URL did not include a host")]
295    WebSocketUrlMissingHost,
296    #[error("voice websocket URL did not include a usable scheme")]
297    WebSocketUrlMissingUsableScheme,
298    #[error("invalid Discord voice UDP IP {remote_ip:?}: {source}")]
299    InvalidDiscordVoiceUdpIp {
300        remote_ip: String,
301        #[source]
302        source: AddrParseError,
303    },
304    #[error("voice heartbeat ACK timed out")]
305    HeartbeatAckTimeout,
306    #[error("missing voice session description")]
307    MissingSessionDescription,
308    #[error("Discord voice session did not negotiate {} transmission; negotiated {negotiated_codec:?}", codec.as_str())]
309    MediaCodecNotNegotiated {
310        codec: Codec,
311        negotiated_codec: Option<Codec>,
312    },
313    #[error("Discord voice ready payload did not include a video SSRC for {}", codec.as_str())]
314    MissingVideoSsrc { codec: Codec },
315}
316
317#[derive(Clone, Copy, Debug, PartialEq, Eq)]
318pub enum OpusOperation {
319    CreateEncoder,
320    EncodeFrame,
321    CreateMonoDecoder,
322    CreateDecoder,
323    DecodeFrame,
324}
325
326impl fmt::Display for OpusOperation {
327    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
328        match self {
329            Self::CreateEncoder => f.write_str("create Opus encoder"),
330            Self::EncodeFrame => f.write_str("encode Opus frame"),
331            Self::CreateMonoDecoder => f.write_str("create mono Opus decoder"),
332            Self::CreateDecoder => f.write_str("create Opus decoder"),
333            Self::DecodeFrame => f.write_str("decode Opus frame"),
334        }
335    }
336}
337
338#[derive(Clone, Debug, PartialEq, Eq, ThisError)]
339pub enum OpusError {
340    #[error("failed to {operation}: {reason}")]
341    OperationFailed {
342        operation: OpusOperation,
343        reason: &'static str,
344    },
345    #[error("unsupported voice codec {}", codec.as_str())]
346    UnsupportedVoiceCodec { codec: Codec },
347    #[error("unsupported Opus channel count {channels}")]
348    UnsupportedChannelCount { channels: usize },
349    #[error("invalid Opus packet: {reason}")]
350    InvalidPacket { reason: &'static str },
351    #[error(
352        "Discord Opus packet must contain {expected_samples_per_channel} samples per channel, got {actual_samples_per_channel}"
353    )]
354    UnsupportedDiscordPacketDuration {
355        expected_samples_per_channel: usize,
356        actual_samples_per_channel: usize,
357    },
358    #[error("Opus resampler is not initialized")]
359    ResamplerNotInitialized,
360    #[error("Opus resampler error: {0}")]
361    Resampler(String),
362    #[error("Opus frame is empty")]
363    EmptyFrame,
364    #[error("Ogg Opus asset is empty")]
365    OggOpusEmpty,
366    #[error("failed to read Ogg Opus packet: {0}")]
367    OggOpusRead(String),
368    #[error("Ogg Opus asset contains no packets")]
369    OggOpusNoPackets,
370    #[error("first Ogg Opus packet is not marked as the start of stream")]
371    OggOpusHeaderNotStart,
372    #[error("Ogg stream is not Opus")]
373    OggOpusMissingHead,
374    #[error("unsupported OpusHead version {version}")]
375    OggOpusUnsupportedVersion { version: u8 },
376    #[error("Ogg Opus channel count must be 1 or 2, got {channels}")]
377    OggOpusUnsupportedChannelCount { channels: u8 },
378    #[error("Ogg Opus mapping family must be 0, got {mapping_family}")]
379    OggOpusUnsupportedMappingFamily { mapping_family: u8 },
380    #[error("Ogg Opus asset is missing OpusTags packet")]
381    OggOpusMissingTags,
382    #[error("Ogg Opus asset contains multiple logical streams")]
383    OggOpusMultipleLogicalStreams,
384    #[error("Ogg Opus asset contains no audio packets")]
385    OggOpusNoAudioPackets,
386    #[error("Ogg Opus asset contains no audio after pre-skip")]
387    OggOpusNoAudioAfterPreSkip,
388    #[error("Ogg Opus asset decoded to empty audio")]
389    OggOpusDecodedEmpty,
390    #[error("Ogg Opus output sample rate {sample_rate_hz} exceeds the decoder limit")]
391    OggOpusOutputSampleRateTooLarge { sample_rate_hz: u32 },
392}
393
394#[derive(Clone, Copy, Debug, PartialEq, Eq, ThisError)]
395pub enum BackpressureError {
396    #[error("voice connection command queue is full")]
397    CommandQueueFull,
398    #[error("voice connection media queue is full")]
399    MediaQueueFull,
400    #[error("voice connection already has an active Opus playout")]
401    ActiveOpusPlayout,
402    #[error("voice connection already has an active media frame stream")]
403    ActiveFrameStream,
404}
405
406#[derive(Debug, ThisError)]
407pub enum ConnectionJoinError {
408    #[error("voice control task join failed: {0}")]
409    ControlTaskJoinFailed(#[source] tokio::task::JoinError),
410    #[error("voice join task is closed")]
411    JoinTaskClosed,
412    #[error("voice join task stopped before replying")]
413    JoinTaskStoppedBeforeReply,
414}
415
416#[derive(Clone, Copy, Debug, PartialEq, Eq)]
417pub enum PayloadKind {
418    RawUdpPacket,
419    RtpPacket,
420    Frame,
421}
422
423#[derive(Clone, Debug, PartialEq, Eq, ThisError)]
424pub enum UnsupportedCodecError {
425    #[error("unsupported Discord voice audio codec {codec:?}; only Opus is supported")]
426    UnsupportedAudioCodec { codec: String },
427    #[error("unsupported Discord voice video codec {codec:?}")]
428    UnsupportedVideoCodec { codec: String },
429    #[error(
430        "unsupported Discord voice RTP payload type {payload_type}; expected one of {expected_payload_types:?}"
431    )]
432    UnsupportedRtpPayloadType {
433        payload_type: RtpPayloadType,
434        expected_payload_types: Vec<RtpPayloadType>,
435    },
436    #[error(
437        "Discord voice RTP payload type {payload_type} is {}; negotiated payload types are {expected_payload_types:?}",
438        codec.as_str()
439    )]
440    UnexpectedRtpPayloadCodec {
441        payload_type: RtpPayloadType,
442        codec: Codec,
443        expected_payload_types: Vec<RtpPayloadType>,
444    },
445}
446
447impl fmt::Display for PayloadKind {
448    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
449        match self {
450            Self::RawUdpPacket => f.write_str("raw UDP packet"),
451            Self::RtpPacket => f.write_str("RTP packet"),
452            Self::Frame => f.write_str("voice frame"),
453        }
454    }
455}
456
457#[derive(Clone, Debug, PartialEq, Eq, ThisError)]
458pub enum RtpError {
459    #[error("voice RTP packet is shorter than 12 bytes: {len}")]
460    PacketTooShort { len: usize },
461    #[error("unsupported voice RTP version {version}; expected 2")]
462    UnsupportedVersion { version: u8 },
463    #[error(
464        "voice RTP packet has truncated CSRC list: {len} bytes for {expected_header_len} byte header"
465    )]
466    TruncatedCsrcList {
467        len: usize,
468        expected_header_len: usize,
469    },
470    #[error(
471        "voice RTP packet has truncated extension header: {len} bytes for {expected_header_len} byte header"
472    )]
473    TruncatedExtensionHeader {
474        len: usize,
475        expected_header_len: usize,
476    },
477    #[error(
478        "voice RTP packet has truncated extension payload: {len} bytes for {expected_header_len} byte header"
479    )]
480    TruncatedExtensionPayload {
481        len: usize,
482        expected_header_len: usize,
483    },
484    #[error("voice RTP packet has truncated encrypted extension")]
485    TruncatedEncryptedExtension,
486    #[error("voice RTP packet has empty padded payload")]
487    EmptyPaddedPayload,
488    #[error(
489        "voice RTP packet has invalid padding length {padding} for payload length {payload_len}"
490    )]
491    InvalidPadding { padding: usize, payload_len: usize },
492    #[error(
493        "{} RTP payload is {payload_len} bytes, exceeding the per-packet media payload limit {max_payload_len}",
494        codec.as_str()
495    )]
496    PayloadTooLarge {
497        codec: Codec,
498        payload_len: usize,
499        max_payload_len: usize,
500    },
501    #[error("malformed {} RTP payload: {reason}", codec.as_str())]
502    MalformedPayload { codec: Codec, reason: &'static str },
503}
504
505#[derive(Clone, Debug, PartialEq, Eq, ThisError)]
506pub enum TransportCryptoError {
507    #[error("voice secret_key must be 32 bytes, got {len}")]
508    InvalidSecretKeyLen { len: usize },
509    #[error(
510        "voice RTP packet is missing the RTP-size nonce suffix: {packet_len} bytes for minimum {min_len}"
511    )]
512    MissingRtpSizeNonce { packet_len: usize, min_len: usize },
513    #[error("failed to initialize voice nonce from OS randomness")]
514    NonceRandomnessUnavailable,
515    #[error("invalid AES-GCM voice secret key")]
516    InvalidAesGcmKey,
517    #[error("invalid XChaCha20-Poly1305 voice secret key")]
518    InvalidXChaCha20Poly1305Key,
519    #[error("failed to decrypt AES-GCM voice packet")]
520    AesGcmDecryptFailed,
521    #[error("failed to encrypt AES-GCM voice packet")]
522    AesGcmEncryptFailed,
523    #[error("failed to decrypt XChaCha20-Poly1305 voice packet")]
524    XChaCha20Poly1305DecryptFailed,
525    #[error("failed to encrypt XChaCha20-Poly1305 voice packet")]
526    XChaCha20Poly1305EncryptFailed,
527}
528
529#[derive(Clone, Copy, Debug, PartialEq, Eq)]
530pub enum TransportCryptoDirection {
531    Session,
532    Send,
533    Receive,
534}
535
536impl fmt::Display for TransportCryptoDirection {
537    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
538        match self {
539            Self::Session => f.write_str("session"),
540            Self::Send => f.write_str("send"),
541            Self::Receive => f.write_str("receive"),
542        }
543    }
544}
545
546#[derive(Debug, ThisError)]
547pub enum DaveError {
548    #[error("unsupported DAVE protocol version {version}")]
549    InvalidProtocolVersion { version: u16 },
550    #[error("failed to create DAVE session: {0}")]
551    CreateSession(#[source] InitError),
552    #[error("failed to set DAVE external sender: {0}")]
553    SetExternalSender(#[source] SetExternalSenderError),
554    #[error("failed to create DAVE key package: {0}")]
555    CreateKeyPackage(#[source] CreateKeyPackageError),
556    #[error("failed to process DAVE proposals: {0}")]
557    ProcessProposals(#[source] ProcessProposalsError),
558    #[error("failed to process DAVE welcome: {0}")]
559    ProcessWelcome(#[source] ProcessWelcomeError),
560    #[error("failed to process DAVE commit: {0}")]
561    ProcessCommit(#[source] ProcessCommitError),
562    #[error("failed to update DAVE media ratchets: {0}")]
563    UpdateRatchets(#[source] UpdateRatchetsError),
564    #[error(
565        "failed to recover invalid DAVE group after {operation} error ({original}): {recovery}"
566    )]
567    RecoverInvalidGroup {
568        operation: &'static str,
569        #[source]
570        original: Box<DaveError>,
571        recovery: Box<Error>,
572    },
573    #[error("invalid DAVE gateway payload: {0}")]
574    InvalidGatewayPayload(#[source] DaveGatewayPayloadError),
575    #[error("invalid DAVE proposals payload: {0}")]
576    InvalidProposalsPayload(#[source] DaveProposalsPayloadError),
577    #[error("{0}")]
578    Encrypt(#[source] EncryptError),
579    #[error("{0}")]
580    Decrypt(#[source] DaveDecryptError),
581}
582
583impl DaveError {
584    pub(crate) fn recover_invalid_group(
585        operation: &'static str,
586        original: Self,
587        recovery: Error,
588    ) -> Error {
589        match recovery {
590            Error::Dave(_) => Self::RecoverInvalidGroup {
591                operation,
592                original: Box::new(original),
593                recovery: Box::new(recovery),
594            }
595            .into(),
596            _ => recovery,
597        }
598    }
599}
600
601#[derive(Clone, Debug, PartialEq, Eq, ThisError)]
602pub enum DaveGatewayPayloadError {
603    #[error("opcode {opcode} payload is {len} bytes, expected at least {min_len}")]
604    PayloadTooShort {
605        opcode: u8,
606        len: usize,
607        min_len: usize,
608    },
609}
610
611#[derive(Clone, Debug, PartialEq, Eq, ThisError)]
612pub enum DaveProposalsPayloadError {
613    #[error("missing proposals operation byte")]
614    MissingOperation,
615    #[error("invalid proposals operation byte {operation}")]
616    InvalidOperation { operation: u8 },
617}
618
619#[derive(Clone, Debug, PartialEq, Eq, ThisError)]
620pub enum DaveDecryptError {
621    #[error("DAVE frame decrypt requires mapped user_id")]
622    MissingUser,
623    #[error("{0}")]
624    Source(#[source] DecryptError),
625}
626
627impl DaveDecryptError {
628    pub(crate) fn receive_decode_kind(&self) -> ReceiveDecodeErrorKind {
629        match self {
630            Self::MissingUser => ReceiveDecodeErrorKind::MissingDaveUser,
631            Self::Source(DecryptError::NoDecryptorForUser { .. }) => {
632                ReceiveDecodeErrorKind::DaveNoDecryptorForUser
633            }
634            Self::Source(DecryptError::Frame(FrameDecryptError::NoValidCryptor { .. })) => {
635                ReceiveDecodeErrorKind::DaveNoValidCryptor
636            }
637            Self::Source(DecryptError::Frame(FrameDecryptError::PassthroughDisabled)) => {
638                ReceiveDecodeErrorKind::DaveUnencryptedWhenPassthroughDisabled
639            }
640            Self::Source(DecryptError::Frame(
641                FrameDecryptError::MalformedFrame
642                | FrameDecryptError::ReplayedNonce
643                | FrameDecryptError::MissingCryptor { .. }
644                | FrameDecryptError::Aead { .. }
645                | FrameDecryptError::InvalidKey,
646            )) => ReceiveDecodeErrorKind::DaveOtherDecryptError,
647        }
648    }
649
650    pub(crate) fn is_no_valid_cryptor(&self) -> bool {
651        matches!(
652            self,
653            Self::Source(DecryptError::Frame(
654                FrameDecryptError::NoValidCryptor { .. }
655            ))
656        )
657    }
658}
659
660impl From<DecryptError> for DaveDecryptError {
661    fn from(error: DecryptError) -> Self {
662        Self::Source(error)
663    }
664}