Skip to main content

opentele2_core/
error.rs

1//! The single error type of the core and its stable codes.
2
3/// Result alias used by every fallible operation in the core.
4pub type Result<T> = std::result::Result<T, Error>;
5
6/// What an [`Error::InvalidMagic`] was raised for.
7#[derive(Debug, Clone, PartialEq, Eq, Hash)]
8#[non_exhaustive]
9pub enum MagicContext {
10    /// A `TDF$` file started with other bytes (`Storage.ReadFile`).
11    File {
12        /// The file name without its suffix, as the message reports it.
13        file: String,
14        /// The bytes found where the magic belongs, at most four.
15        magic: Vec<u8>,
16    },
17    /// The MTP data held a block id the reader does not know.
18    MtpDataBlock {
19        /// The unknown block id.
20        block_id: u32,
21    },
22}
23
24/// What an [`Error::ReadMapDataFailed`] was raised for (`MapData.read`).
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
26#[non_exhaustive]
27pub enum MapReadFailure {
28    /// The `maps` file is missing or is not a valid `TDF$` file.
29    Open,
30    /// The legacy salt of the `maps` file has the wrong size.
31    BadSalt {
32        /// The size of the salt found, in bytes.
33        size: usize,
34    },
35    /// The map does not decrypt under the local key.
36    Decrypt,
37}
38
39/// What an [`Error::KeyDataInvalid`] was raised for ([`crate::tdata::key_data`]
40/// and [`crate::tdata::TData::load`]).
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
42#[non_exhaustive]
43pub enum KeyDataFailure {
44    /// The parameters of a passcode wrap cannot make a passcode key
45    /// (`create_passcode_key`), or the passcode is empty.
46    UnusableWrapParameters {
47        /// KDF family.
48        kind: u32,
49        /// Argon2id memory in KiB, or scrypt `N`.
50        memory: u32,
51        /// Argon2id passes, or scrypt `r`.
52        time: u32,
53        /// Argon2id lanes, or scrypt `p`.
54        parallelism: u32,
55    },
56    /// The open salt of the key file has the wrong size.
57    BadSalt {
58        /// The size of the salt found, in bytes.
59        size: usize,
60    },
61    /// The trailer does not start with the key data magic.
62    BadTrailerMagic,
63    /// The trailer holds more wraps than its format allows.
64    BadWrapCount {
65        /// The wrap count found.
66        count: u32,
67    },
68    /// A wrap within the cost caps has parameters its KDF does not accept.
69    BadKdfFamily {
70        /// The KDF kind of the wrap.
71        kind: u32,
72    },
73    /// A wrap salt is shorter than the passcode salt minimum.
74    BadWrapSaltSize {
75        /// The size of the wrap salt, in bytes.
76        size: usize,
77    },
78    /// Two wraps share a generation.
79    DuplicateWrapGeneration {
80        /// The repeated generation.
81        generation: u32,
82    },
83    /// The accounts info does not decrypt under the local key.
84    AccountsInfoDecrypt,
85    /// The accounts info lists no account (raised as a plain
86    /// `OpenTeleException` with this text).
87    BadAccountsCount {
88        /// The count found.
89        count: i32,
90    },
91}
92
93/// What an [`Error::BadConfigData`] was raised for: the MTP config checks,
94/// and the endpoint, session and self-user checks that report the same
95/// code.
96#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
97#[non_exhaustive]
98pub enum ConfigFailure {
99    /// A DC options endpoint declares an IP size outside 1 to 45
100    /// (`constructFromSerialized`).
101    BadIpSize {
102        /// The IP size found.
103        size: i32,
104    },
105    /// A DC options endpoint declares a secret size outside 0 to 32
106    /// (`constructFromSerialized`).
107    BadSecretSize {
108        /// The secret size found.
109        size: i32,
110    },
111    /// A serialized DC options IP holds a byte outside ASCII (reported with
112    /// the Python `UnicodeDecodeError` text).
113    NonAsciiIp {
114        /// The first byte outside ASCII.
115        byte: u8,
116        /// Its offset within the IP.
117        position: usize,
118    },
119    /// An endpoint IP to serialize is not ASCII.
120    UnencodableIp,
121    /// A serialized config has a version other than 1
122    /// (`MTP.Config.FromSerialized`).
123    BadVersion {
124        /// The version found.
125        version: i32,
126    },
127    /// A serialized config has an environment other than 0 and 1 (reported
128    /// with the Python `ValueError` text).
129    BadEnvironment {
130        /// The environment code found.
131        code: i32,
132    },
133    /// A DC id has no endpoints in the config or the built-in table.
134    DcNotFound {
135        /// The DC id looked for.
136        dc_id: i32,
137    },
138    /// The main DC has no TCP endpoint (`FromTDesktop`).
139    NoTcpEndpoint,
140    /// An endpoint IP does not parse as an IPv4 or IPv6 address.
141    BadEndpointIp,
142    /// A DC id does not fit the session format.
143    DcIdOutOfRange {
144        /// The DC id found.
145        dc_id: i64,
146    },
147    /// A port does not fit 16 bits.
148    PortOutOfRange {
149        /// The port found.
150        port: i64,
151    },
152    /// A column of a Telethon session row is NULL.
153    NullSessionColumn {
154        /// The column name.
155        column: &'static str,
156    },
157    /// No cached user of a session is the self user.
158    NoSelfUser,
159    /// Cached users with different ids are the self user.
160    ConflictingSelfUsers,
161    /// The self user id is not a tdata user id.
162    BadUserId {
163        /// The user id found.
164        user_id: i64,
165    },
166}
167
168/// Every failure the core can report.
169///
170/// Variants map 1:1 onto the tdata exceptions of the Python package
171/// (`opentele2.exception`), plus [`Error::BadLength`], [`Error::InvalidInput`]
172/// and [`Error::Io`]. Bindings
173/// dispatch on [`Error::code`], which is stable across releases; the
174/// `Display` text is for humans and may change. [`Error::legacy_message`]
175/// is the message of the matching exception, built from the context the
176/// variants carry.
177#[derive(Debug, thiserror::Error)]
178#[non_exhaustive]
179pub enum Error {
180    /// A required tdata file does not exist (`TFileNotFound`).
181    #[error("tdata file not found")]
182    FileNotFound {
183        /// The file name without its suffix, or the path that was looked
184        /// for.
185        file: String,
186    },
187
188    /// A tdata file does not start with the `TDF$` magic, or the MTP data
189    /// holds an unknown block (`TDataInvalidMagic`).
190    #[error("tdata file has an invalid magic")]
191    InvalidMagic {
192        /// Where the bad magic was found.
193        context: MagicContext,
194    },
195
196    /// A tdata file's MD5 checksum does not match its contents
197    /// (`TDataInvalidCheckSum`).
198    #[error("tdata file has an invalid checksum")]
199    InvalidCheckSum {
200        /// The file name without its suffix; empty when the bytes were
201        /// parsed without a name ([`crate::tdf::TdfFile::parse`]).
202        file: String,
203        /// The MD5 computed over the contents, which the stored one does not
204        /// match.
205        computed: [u8; 16],
206    },
207
208    /// Decryption produced data that does not match its key (`TDataBadDecryptKey`).
209    #[error("tdata decrypt key is bad")]
210    BadDecryptKey,
211
212    /// The local passcode does not unlock the tdata (`TDataWrongPasscode`).
213    #[error("tdata passcode is wrong")]
214    WrongPasscode,
215
216    /// Encrypted data has a size that is not valid for AES-IGE
217    /// (`TDataBadEncryptedDataSize`).
218    #[error("tdata encrypted data has a bad size")]
219    BadEncryptedDataSize {
220        /// The size of the encrypted data, in bytes.
221        size: usize,
222    },
223
224    /// Decrypted data declares a size that does not fit the buffer
225    /// (`TDataBadDecryptedDataSize`).
226    #[error("tdata decrypted data has a bad size")]
227    BadDecryptedDataSize {
228        /// The size of the encrypted data, in bytes.
229        encrypted_size: usize,
230        /// The encrypted size without the 16-byte message key.
231        full_len: usize,
232        /// The size of the decrypted buffer, in bytes.
233        decrypted_size: usize,
234    },
235
236    /// The config block of a tdata file cannot be parsed (`TDataBadConfigData`).
237    #[error("tdata config data is bad")]
238    BadConfigData {
239        /// Which check of the config failed.
240        context: ConfigFailure,
241    },
242
243    /// A `QDataStream` read or write failed (`QDataStreamFailed`).
244    #[error("QDataStream operation failed")]
245    QDataStreamFailed {
246        /// The message of the failed read, when the read has a message of
247        /// its own; `None` gives the default `Could not stream data`.
248        context: Option<&'static str>,
249    },
250
251    /// The account map data cannot be read (`TDataReadMapDataFailed`).
252    #[error("failed to read tdata map data")]
253    ReadMapDataFailed {
254        /// Which step of the read failed.
255        context: MapReadFailure,
256    },
257
258    /// The account map data was read with an incorrect passcode
259    /// (`TDataReadMapDataIncorrectPasscode`).
260    #[error("tdata map data passcode is incorrect")]
261    ReadMapDataIncorrectPasscode,
262
263    /// No authorization key was found in the tdata (`TDataAuthKeyNotFound`).
264    #[error("tdata auth key not found")]
265    AuthKeyNotFound,
266
267    /// The account limit of Telegram Desktop is reached (`MaxAccountLimit`).
268    #[error("maximum number of accounts reached")]
269    MaxAccountLimit,
270
271    /// Writing the tdata failed (`TDataSaveFailed`).
272    #[error("failed to save tdata")]
273    SaveFailed,
274
275    /// The `key_<dataName>` file is malformed or uses parameters that cannot
276    /// make a passcode key (`TDataKeyDataInvalid`).
277    #[error("tdata key data is invalid")]
278    KeyDataInvalid {
279        /// Which check of the key data failed.
280        context: KeyDataFailure,
281    },
282
283    /// A buffer, key, or IV has the wrong length.
284    #[error("{what} has a bad length: expected {expected}, got {actual}")]
285    BadLength {
286        /// Name of the value whose length is wrong.
287        what: &'static str,
288        /// Length the operation requires, in bytes.
289        expected: usize,
290        /// Length the caller supplied, in bytes.
291        actual: usize,
292    },
293
294    /// A caller-supplied argument is outside what the operation accepts, for
295    /// a reason other than its length (that is [`Error::BadLength`]).
296    #[error("Invalid {what}: {reason}")]
297    InvalidInput {
298        /// Name of the argument that was rejected.
299        what: &'static str,
300        /// Why the argument was rejected.
301        reason: String,
302    },
303
304    /// An I/O operation on a caller-supplied path failed.
305    #[error("I/O error: {0}")]
306    Io(#[from] std::io::Error),
307}
308
309impl Error {
310    /// Stable `SCREAMING_SNAKE` code identifying the variant, for example
311    /// `"TDATA_WRONG_PASSCODE"`. Bindings map this code onto their own error
312    /// types; it never changes for an existing variant.
313    #[must_use]
314    pub fn code(&self) -> &'static str {
315        match self {
316            Self::FileNotFound { .. } => "TFILE_NOT_FOUND",
317            Self::InvalidMagic { .. } => "TDATA_INVALID_MAGIC",
318            Self::InvalidCheckSum { .. } => "TDATA_INVALID_CHECKSUM",
319            Self::BadDecryptKey => "TDATA_BAD_DECRYPT_KEY",
320            Self::WrongPasscode => "TDATA_WRONG_PASSCODE",
321            Self::BadEncryptedDataSize { .. } => "TDATA_BAD_ENCRYPTED_DATA_SIZE",
322            Self::BadDecryptedDataSize { .. } => "TDATA_BAD_DECRYPTED_DATA_SIZE",
323            Self::BadConfigData { .. } => "TDATA_BAD_CONFIG_DATA",
324            Self::QDataStreamFailed { .. } => "QDATASTREAM_FAILED",
325            Self::ReadMapDataFailed { .. } => "TDATA_READ_MAP_DATA_FAILED",
326            Self::ReadMapDataIncorrectPasscode => "TDATA_READ_MAP_DATA_INCORRECT_PASSCODE",
327            Self::AuthKeyNotFound => "TDATA_AUTH_KEY_NOT_FOUND",
328            Self::MaxAccountLimit => "MAX_ACCOUNT_LIMIT",
329            Self::SaveFailed => "TDATA_SAVE_FAILED",
330            Self::KeyDataInvalid { .. } => "TDATA_KEY_DATA_INVALID",
331            Self::BadLength { .. } => "BAD_LENGTH",
332            Self::InvalidInput { .. } => "INVALID_INPUT",
333            Self::Io(_) => "IO",
334        }
335    }
336
337    /// Stable `SCREAMING_SNAKE` code of the context of an error whose
338    /// context a binding raises differently from the rest of its
339    /// [`Error::code`], else `None`. It never changes once given.
340    ///
341    /// - `"BAD_ACCOUNTS_COUNT"`: [`Error::KeyDataInvalid`] with
342    ///   [`KeyDataFailure::BadAccountsCount`], which is raised as a plain
343    ///   `OpenTeleException` rather than `TDataKeyDataInvalid`.
344    ///
345    /// Every other error has no detail code.
346    #[must_use]
347    pub fn detail_code(&self) -> Option<&'static str> {
348        match self {
349            Self::KeyDataInvalid {
350                context: KeyDataFailure::BadAccountsCount { .. },
351            } => Some("BAD_ACCOUNTS_COUNT"),
352            _ => None,
353        }
354    }
355
356    /// The message of the matching exception (its `message` attribute,
357    /// without the class name and caller `str()` adds), for
358    /// example `Invalid magic b'ABCD' in file maps`.
359    ///
360    /// - [`Error::FileNotFound`]: `Could not open {file}`.
361    /// - [`Error::InvalidMagic`]: `Invalid magic {magic!r} in file {file}`,
362    ///   the magic as a Python `bytes` literal; for an MTP data block
363    ///   `Unsupported block id in MtpData: {block_id}`.
364    /// - [`Error::InvalidCheckSum`]: `Invalid checksum {md5 hex} in file
365    ///   {file}`.
366    /// - [`Error::BadEncryptedDataSize`] and [`Error::BadDecryptedDataSize`]:
367    ///   the `Storage.DecryptLocal` messages with their sizes.
368    /// - [`Error::BadDecryptKey`]: the `Storage.DecryptLocal` message, for
369    ///   key-data steps too.
370    /// - [`Error::QDataStreamFailed`]: the context, else the default
371    ///   `Could not stream data`.
372    /// - [`Error::ReadMapDataFailed`], [`Error::ReadMapDataIncorrectPasscode`],
373    ///   [`Error::AuthKeyNotFound`] and [`Error::MaxAccountLimit`]: the
374    ///   `MapData.read`, `_findMainAuthKey` and `FromTelethon` messages.
375    /// - [`Error::KeyDataInvalid`]: the `read_key_data`,
376    ///   `create_passcode_key` and `__loadFromTData` message of its context.
377    /// - [`Error::SaveFailed`]: `Could not save tdata, something went wrong`,
378    ///   the one message of `SaveTData`.
379    /// - [`Error::BadConfigData`]: the `constructFromSerialized`,
380    ///   `MTP.Config.FromSerialized`, `MTP.Config.endpoints` and
381    ///   `FromTDesktop` message of its context (for the IP and environment
382    ///   checks the Python 3.8 `UnicodeDecodeError` and `ValueError` text),
383    ///   and a plain description for the endpoint, session and self-user
384    ///   checks.
385    ///
386    /// [`Error::WrongPasscode`], [`Error::BadLength`],
387    /// [`Error::InvalidInput`] (`Invalid {what}: {reason}`) and [`Error::Io`]
388    /// give their `Display` text.
389    #[must_use]
390    pub fn legacy_message(&self) -> String {
391        match self {
392            Self::FileNotFound { file } => format!("Could not open {file}"),
393            Self::InvalidMagic {
394                context: MagicContext::File { file, magic },
395            } => format!("Invalid magic {} in file {file}", py_bytes_repr(magic)),
396            Self::InvalidMagic {
397                context: MagicContext::MtpDataBlock { block_id },
398            } => format!("Unsupported block id in MtpData: {block_id}"),
399            Self::InvalidCheckSum { file, computed } => {
400                let mut hex = String::with_capacity(2 * computed.len());
401                for &byte in computed {
402                    push_hex(&mut hex, byte);
403                }
404                format!("Invalid checksum {hex} in file {file}")
405            }
406            Self::BadDecryptKey => {
407                "Bad decrypt key, data not decrypted - incorrect password?".to_owned()
408            }
409            Self::BadEncryptedDataSize { size } => format!("Bad encrypted part size: {size}"),
410            Self::BadDecryptedDataSize {
411                encrypted_size,
412                full_len,
413                decrypted_size,
414            } => format!(
415                "Bad decrypted part size: {encrypted_size}, fullLen: {full_len}, \
416                 decrypted size: {decrypted_size}"
417            ),
418            Self::QDataStreamFailed { context } => {
419                context.unwrap_or("Could not stream data").to_owned()
420            }
421            Self::ReadMapDataFailed { context } => match context {
422                MapReadFailure::Open => {
423                    "Could not read map data, file not found or couldn't be opened".to_owned()
424                }
425                MapReadFailure::BadSalt { size } => {
426                    format!("Bad salt in map file, size: {size}")
427                }
428                MapReadFailure::Decrypt => "Could not decrypt map data".to_owned(),
429            },
430            Self::ReadMapDataIncorrectPasscode => {
431                "Could not decrypt pass-protected key from map file, maybe bad password..."
432                    .to_owned()
433            }
434            Self::AuthKeyNotFound => "Could not find the main authKey".to_owned(),
435            Self::MaxAccountLimit => {
436                "You can't have more than 6 accounts in one TDesktop client".to_owned()
437            }
438            Self::KeyDataInvalid { context } => key_data_message(*context),
439            Self::SaveFailed => "Could not save tdata, something went wrong".to_owned(),
440            Self::BadConfigData { context } => config_message(*context),
441            Self::WrongPasscode
442            | Self::BadLength { .. }
443            | Self::InvalidInput { .. }
444            | Self::Io(_) => self.to_string(),
445        }
446    }
447
448    /// The class name of the exception raised for the failure, the
449    /// single source the bindings name their errors after (the Python
450    /// `CORE_ERROR_TYPES` table, the Node error `name`).
451    ///
452    /// Each tdata variant gives the `opentele2.exception` class its doc
453    /// names, for example `TDataWrongPasscode`; [`Error::KeyDataInvalid`] with
454    /// [`KeyDataFailure::BadAccountsCount`] ([`Error::detail_code`]
455    /// `BAD_ACCOUNTS_COUNT`) gives `OpenTeleException`. The variants with no
456    /// opentele2 class of their own give a Python builtin: `ValueError` for
457    /// [`Error::BadLength`] (as an AES-IGE backend raises) and
458    /// [`Error::InvalidInput`] (as `hashlib` does), `OSError` for
459    /// [`Error::Io`].
460    #[must_use]
461    pub fn legacy_class(&self) -> &'static str {
462        match self {
463            Self::FileNotFound { .. } => "TFileNotFound",
464            Self::InvalidMagic { .. } => "TDataInvalidMagic",
465            Self::InvalidCheckSum { .. } => "TDataInvalidCheckSum",
466            Self::BadDecryptKey => "TDataBadDecryptKey",
467            Self::WrongPasscode => "TDataWrongPasscode",
468            Self::BadEncryptedDataSize { .. } => "TDataBadEncryptedDataSize",
469            Self::BadDecryptedDataSize { .. } => "TDataBadDecryptedDataSize",
470            Self::BadConfigData { .. } => "TDataBadConfigData",
471            Self::QDataStreamFailed { .. } => "QDataStreamFailed",
472            Self::ReadMapDataFailed { .. } => "TDataReadMapDataFailed",
473            Self::ReadMapDataIncorrectPasscode => "TDataReadMapDataIncorrectPasscode",
474            Self::AuthKeyNotFound => "TDataAuthKeyNotFound",
475            Self::MaxAccountLimit => "MaxAccountLimit",
476            Self::SaveFailed => "TDataSaveFailed",
477            Self::KeyDataInvalid {
478                context: KeyDataFailure::BadAccountsCount { .. },
479            } => "OpenTeleException",
480            Self::KeyDataInvalid { .. } => "TDataKeyDataInvalid",
481            Self::BadLength { .. } | Self::InvalidInput { .. } => "ValueError",
482            Self::Io(_) => "OSError",
483        }
484    }
485}
486
487/// The message of an [`Error::BadConfigData`].
488fn config_message(context: ConfigFailure) -> String {
489    match context {
490        ConfigFailure::BadIpSize { .. } => "Bad ipSize data".to_owned(),
491        ConfigFailure::BadSecretSize { .. } => "Bad secretSize data".to_owned(),
492        ConfigFailure::NonAsciiIp { byte, position } => {
493            let mut hex = String::with_capacity(2);
494            push_hex(&mut hex, byte);
495            format!(
496                "'ascii' codec can't decode byte 0x{hex} in position {position}: \
497                 ordinal not in range(128)"
498            )
499        }
500        ConfigFailure::UnencodableIp => "Endpoint IP is not ASCII".to_owned(),
501        ConfigFailure::BadVersion { .. } => "version != kVersion, something went wrong".to_owned(),
502        ConfigFailure::BadEnvironment { code } => format!("{code} is not a valid Environment"),
503        ConfigFailure::DcNotFound { dc_id } => format!("No endpoints for dc id {dc_id}"),
504        ConfigFailure::NoTcpEndpoint => {
505            "Couldn't find endpoint for this account, something went wrong?".to_owned()
506        }
507        ConfigFailure::BadEndpointIp => "Endpoint IP is not an IPv4 or IPv6 address".to_owned(),
508        ConfigFailure::DcIdOutOfRange { dc_id } => format!("DC id out of range: {dc_id}"),
509        ConfigFailure::PortOutOfRange { port } => format!("Port out of range: {port}"),
510        ConfigFailure::NullSessionColumn { column } => format!("Session column {column} is NULL"),
511        ConfigFailure::NoSelfUser => "No cached user is the self user".to_owned(),
512        ConfigFailure::ConflictingSelfUsers => {
513            "Cached users with different ids are the self user".to_owned()
514        }
515        ConfigFailure::BadUserId { user_id } => format!("Bad user id: {user_id}"),
516    }
517}
518
519/// The message of an [`Error::KeyDataInvalid`].
520fn key_data_message(context: KeyDataFailure) -> String {
521    match context {
522        KeyDataFailure::UnusableWrapParameters {
523            kind,
524            memory,
525            time,
526            parallelism,
527        } => format!(
528            "Unusable passcode wrap parameters: PasscodeKdf(kind={kind}, memory={memory}, \
529             time={time}, parallel={parallelism})"
530        ),
531        KeyDataFailure::BadSalt { size } => format!("Bad salt in key data, size: {size}"),
532        KeyDataFailure::BadTrailerMagic => "Bad key data trailer magic".to_owned(),
533        KeyDataFailure::BadWrapCount { count } => format!("Bad key data wrap count: {count}"),
534        KeyDataFailure::BadKdfFamily { kind } => format!("Bad key data KDF family: {kind}"),
535        KeyDataFailure::BadWrapSaltSize { size } => {
536            format!("Bad key data wrap salt size: {size}")
537        }
538        KeyDataFailure::DuplicateWrapGeneration { generation } => {
539            format!("Duplicate key data wrap generation: {generation}")
540        }
541        KeyDataFailure::AccountsInfoDecrypt => "Could not decrypt the accounts info".to_owned(),
542        KeyDataFailure::BadAccountsCount { count } => {
543            format!("Bad accounts count {count}, the data might have been corrupted")
544        }
545    }
546}
547
548/// Python `repr(bytes)`: `b'…'`, or `b"…"` when the bytes hold a single
549/// quote and no double quote; `\\`, the quote, `\t`, `\n` and `\r` are
550/// escaped, other bytes outside printable ASCII are `\xNN`.
551fn py_bytes_repr(bytes: &[u8]) -> String {
552    let quote = if bytes.contains(&b'\'') && !bytes.contains(&b'"') {
553        '"'
554    } else {
555        '\''
556    };
557    let mut out = format!("b{quote}");
558    for &byte in bytes {
559        match byte {
560            b'\\' => out.push_str("\\\\"),
561            b'\t' => out.push_str("\\t"),
562            b'\n' => out.push_str("\\n"),
563            b'\r' => out.push_str("\\r"),
564            _ if char::from(byte) == quote => {
565                out.push('\\');
566                out.push(quote);
567            }
568            0x20..=0x7e => out.push(char::from(byte)),
569            _ => {
570                out.push_str("\\x");
571                push_hex(&mut out, byte);
572            }
573        }
574    }
575    out.push(quote);
576    out
577}
578
579/// Appends `byte` as two lowercase hex digits.
580fn push_hex(out: &mut String, byte: u8) {
581    const DIGITS: &[u8; 16] = b"0123456789abcdef";
582    out.push(char::from(DIGITS[usize::from(byte >> 4)]));
583    out.push(char::from(DIGITS[usize::from(byte & 0x0f)]));
584}
585
586#[cfg(test)]
587mod tests {
588    use super::{ConfigFailure, Error, KeyDataFailure, MagicContext, MapReadFailure};
589    use std::collections::HashSet;
590
591    /// One value of every variant; the exhaustive match below fails to
592    /// compile when a variant is added without extending this list.
593    fn every_variant() -> Vec<Error> {
594        let all = vec![
595            Error::FileNotFound {
596                file: "map".to_owned(),
597            },
598            Error::InvalidMagic {
599                context: MagicContext::File {
600                    file: "maps".to_owned(),
601                    magic: b"ABCD".to_vec(),
602                },
603            },
604            Error::InvalidCheckSum {
605                file: "ck".to_owned(),
606                computed: [0; 16],
607            },
608            Error::BadDecryptKey,
609            Error::WrongPasscode,
610            Error::BadEncryptedDataSize { size: 16 },
611            Error::BadDecryptedDataSize {
612                encrypted_size: 48,
613                full_len: 32,
614                decrypted_size: 32,
615            },
616            Error::BadConfigData {
617                context: ConfigFailure::BadIpSize { size: 0 },
618            },
619            Error::QDataStreamFailed { context: None },
620            Error::ReadMapDataFailed {
621                context: MapReadFailure::Open,
622            },
623            Error::ReadMapDataIncorrectPasscode,
624            Error::AuthKeyNotFound,
625            Error::MaxAccountLimit,
626            Error::SaveFailed,
627            Error::KeyDataInvalid {
628                context: KeyDataFailure::BadTrailerMagic,
629            },
630            Error::BadLength {
631                what: "key",
632                expected: 32,
633                actual: 16,
634            },
635            Error::InvalidInput {
636                what: "PBKDF2 iterations",
637                reason: "must be at least 1".to_owned(),
638            },
639            Error::Io(std::io::Error::other("disk gone")),
640        ];
641        for error in &all {
642            match error {
643                Error::FileNotFound { .. }
644                | Error::InvalidMagic { .. }
645                | Error::InvalidCheckSum { .. }
646                | Error::BadDecryptKey
647                | Error::WrongPasscode
648                | Error::BadEncryptedDataSize { .. }
649                | Error::BadDecryptedDataSize { .. }
650                | Error::BadConfigData { .. }
651                | Error::QDataStreamFailed { .. }
652                | Error::ReadMapDataFailed { .. }
653                | Error::ReadMapDataIncorrectPasscode
654                | Error::AuthKeyNotFound
655                | Error::MaxAccountLimit
656                | Error::SaveFailed
657                | Error::KeyDataInvalid { .. }
658                | Error::BadLength { .. }
659                | Error::InvalidInput { .. }
660                | Error::Io(_) => {}
661            }
662        }
663        all
664    }
665
666    #[test]
667    fn codes_are_unique() {
668        let all = every_variant();
669        let codes: HashSet<&str> = all.iter().map(Error::code).collect();
670        assert_eq!(codes.len(), all.len(), "duplicate code in {codes:?}");
671    }
672
673    #[test]
674    fn codes_are_non_empty_screaming_snake() {
675        for error in every_variant() {
676            let code = error.code();
677            assert!(!code.is_empty(), "{error:?} has an empty code");
678            assert!(
679                code.bytes()
680                    .all(|b| b.is_ascii_uppercase() || b.is_ascii_digit() || b == b'_'),
681                "{code} is not SCREAMING_SNAKE"
682            );
683        }
684    }
685
686    #[test]
687    fn only_bad_accounts_count_has_a_detail_code() {
688        for error in every_variant() {
689            assert_eq!(error.detail_code(), None, "{error:?}");
690        }
691        let error = Error::KeyDataInvalid {
692            context: KeyDataFailure::BadAccountsCount { count: 0 },
693        };
694        assert_eq!(error.detail_code(), Some("BAD_ACCOUNTS_COUNT"));
695        assert_eq!(error.code(), "TDATA_KEY_DATA_INVALID");
696    }
697
698    #[test]
699    fn wrong_passcode_code_is_stable() {
700        assert_eq!(Error::WrongPasscode.code(), "TDATA_WRONG_PASSCODE");
701    }
702
703    /// The codes and `Display` texts bindings dispatch on and show are fixed
704    /// per variant, whatever context the variant carries.
705    #[test]
706    fn codes_and_display_texts_are_fixed() {
707        let expected = [
708            ("TFILE_NOT_FOUND", "tdata file not found"),
709            ("TDATA_INVALID_MAGIC", "tdata file has an invalid magic"),
710            (
711                "TDATA_INVALID_CHECKSUM",
712                "tdata file has an invalid checksum",
713            ),
714            ("TDATA_BAD_DECRYPT_KEY", "tdata decrypt key is bad"),
715            ("TDATA_WRONG_PASSCODE", "tdata passcode is wrong"),
716            (
717                "TDATA_BAD_ENCRYPTED_DATA_SIZE",
718                "tdata encrypted data has a bad size",
719            ),
720            (
721                "TDATA_BAD_DECRYPTED_DATA_SIZE",
722                "tdata decrypted data has a bad size",
723            ),
724            ("TDATA_BAD_CONFIG_DATA", "tdata config data is bad"),
725            ("QDATASTREAM_FAILED", "QDataStream operation failed"),
726            (
727                "TDATA_READ_MAP_DATA_FAILED",
728                "failed to read tdata map data",
729            ),
730            (
731                "TDATA_READ_MAP_DATA_INCORRECT_PASSCODE",
732                "tdata map data passcode is incorrect",
733            ),
734            ("TDATA_AUTH_KEY_NOT_FOUND", "tdata auth key not found"),
735            ("MAX_ACCOUNT_LIMIT", "maximum number of accounts reached"),
736            ("TDATA_SAVE_FAILED", "failed to save tdata"),
737            ("TDATA_KEY_DATA_INVALID", "tdata key data is invalid"),
738            ("BAD_LENGTH", "key has a bad length: expected 32, got 16"),
739            (
740                "INVALID_INPUT",
741                "Invalid PBKDF2 iterations: must be at least 1",
742            ),
743            ("IO", "I/O error: disk gone"),
744        ];
745        let all = every_variant();
746        assert_eq!(all.len(), expected.len());
747        for (error, (code, display)) in all.iter().zip(expected) {
748            assert_eq!(error.code(), code);
749            assert_eq!(error.to_string(), display);
750        }
751    }
752
753    /// Variants without a message of their own give their `Display` text.
754    #[test]
755    fn legacy_message_falls_back_to_display() {
756        let error = Error::WrongPasscode;
757        assert_eq!(error.legacy_message(), error.to_string());
758    }
759
760    /// Every config context keeps the one code and `Display` text, and its
761    /// message is not the `Display` fallback.
762    #[test]
763    fn config_contexts_share_code_and_display() {
764        let contexts = [
765            ConfigFailure::BadIpSize { size: 46 },
766            ConfigFailure::BadSecretSize { size: -1 },
767            ConfigFailure::NonAsciiIp {
768                byte: 0x80,
769                position: 3,
770            },
771            ConfigFailure::UnencodableIp,
772            ConfigFailure::BadVersion { version: 2 },
773            ConfigFailure::BadEnvironment { code: 2 },
774            ConfigFailure::DcNotFound { dc_id: 9 },
775            ConfigFailure::NoTcpEndpoint,
776            ConfigFailure::BadEndpointIp,
777            ConfigFailure::DcIdOutOfRange { dc_id: 256 },
778            ConfigFailure::PortOutOfRange { port: 65536 },
779            ConfigFailure::NullSessionColumn { column: "port" },
780            ConfigFailure::NoSelfUser,
781            ConfigFailure::ConflictingSelfUsers,
782            ConfigFailure::BadUserId { user_id: -5 },
783        ];
784        let mut messages = HashSet::new();
785        for context in contexts {
786            let error = Error::BadConfigData { context };
787            assert_eq!(error.code(), "TDATA_BAD_CONFIG_DATA");
788            assert_eq!(error.to_string(), "tdata config data is bad");
789            let message = error.legacy_message();
790            assert_ne!(message, error.to_string(), "{context:?}");
791            assert!(messages.insert(message), "{context:?} repeats a message");
792        }
793    }
794
795    #[test]
796    fn io_error_converts_and_displays() {
797        let error: Error = std::io::Error::other("disk gone").into();
798        assert_eq!(error.code(), "IO");
799        assert_eq!(error.to_string(), "I/O error: disk gone");
800    }
801
802    #[test]
803    fn invalid_input_message_is_its_display() {
804        let error = Error::InvalidInput {
805            what: "pq",
806            reason: "7 is not a product of two factors greater than 1".to_owned(),
807        };
808        assert_eq!(error.code(), "INVALID_INPUT");
809        assert_eq!(
810            error.legacy_message(),
811            "Invalid pq: 7 is not a product of two factors greater than 1"
812        );
813        assert_eq!(error.legacy_message(), error.to_string());
814    }
815
816    #[test]
817    fn bad_length_displays_its_fields() {
818        let error = Error::BadLength {
819            what: "iv",
820            expected: 32,
821            actual: 31,
822        };
823        assert_eq!(
824            error.to_string(),
825            "iv has a bad length: expected 32, got 31"
826        );
827    }
828}