Skip to main content

imessage_database/error/
table.rs

1/*!
2 Errors that can happen when extracting data from a `SQLite` table.
3*/
4
5use std::{
6    fmt::{Display, Formatter, Result},
7    path::PathBuf,
8};
9
10use crate::{tables::table::DEFAULT_PATH_IOS, util::path_hint::PathHint};
11
12/// Errors that can happen when extracting data from a `SQLite` table
13#[derive(Debug)]
14pub enum TableError {
15    /// Error when querying the table
16    QueryError(rusqlite::Error),
17    /// Error when connecting to the database
18    CannotConnect(TableConnectError),
19    /// Error when reading from the database file
20    CannotRead(std::io::Error),
21}
22
23/// Reasons why the database could not be connected to or read from
24#[derive(Debug)]
25pub enum TableConnectError {
26    /// Failure to open the file with `SQLite`
27    Permissions(rusqlite::Error),
28    /// Failure to inspect or open the path: missing Full Disk Access on macOS, or a path
29    /// that cannot be parsed by the OS
30    Inaccessible(PathBuf, std::io::Error),
31    /// Directory supplied in place of a database file, with hints from its contents
32    IsDirectory(PathBuf, Vec<PathHint>),
33    /// Directory with no Messages database at the iOS backup path, with hints from its contents
34    MissingBackupDatabase(PathBuf, Vec<PathHint>),
35    /// File without the `SQLite` header
36    NotADatabase(PathBuf),
37    /// `SQLite` database without a `message` table
38    NotMessagesDatabase(PathBuf),
39    /// Missing path, with hints from the nearest existing ancestor
40    DoesNotExist(PathBuf, Vec<PathHint>),
41    /// Not a backup root directory
42    NotBackupRoot,
43}
44
45/// Full Disk Access guidance for permission errors on macOS.
46const FULL_DISK_ACCESS: &str = if cfg!(target_os = "macos") {
47    "\nEnsure full disk access is enabled for your terminal emulator in System Settings > Privacy & Security > Full Disk Access"
48} else {
49    ""
50};
51
52fn write_hints(fmt: &mut Formatter<'_>, hints: &[PathHint]) -> Result {
53    for hint in hints {
54        write!(fmt, "\n{hint}")?;
55    }
56    Ok(())
57}
58
59impl Display for TableConnectError {
60    fn fmt(&self, fmt: &mut Formatter<'_>) -> Result {
61        match self {
62            TableConnectError::Permissions(why) => write!(
63                fmt,
64                "Unable to read from chat database: {why}{FULL_DISK_ACCESS}"
65            ),
66            TableConnectError::Inaccessible(path, why) => write!(
67                fmt,
68                "Unable to access `{}`: {why}{FULL_DISK_ACCESS}",
69                path.display()
70            ),
71            TableConnectError::IsDirectory(path, hints) => {
72                write!(
73                    fmt,
74                    "Specified path `{}` is a directory, not a database file!",
75                    path.display()
76                )?;
77                write_hints(fmt, hints)
78            }
79            TableConnectError::MissingBackupDatabase(path, hints) => {
80                write!(
81                    fmt,
82                    "Specified path `{}` is a directory, but it contains no Messages database at `{DEFAULT_PATH_IOS}`!",
83                    path.display()
84                )?;
85                write_hints(fmt, hints)
86            }
87            TableConnectError::NotADatabase(path) => write!(
88                fmt,
89                "Specified path `{}` is not a SQLite database file!",
90                path.display()
91            ),
92            TableConnectError::NotMessagesDatabase(path) => write!(
93                fmt,
94                "`{}` is a SQLite database, but it has no `message` table, so it is not a Messages database!",
95                path.display()
96            ),
97            TableConnectError::DoesNotExist(path, hints) => {
98                write!(
99                    fmt,
100                    "Database file `{}` does not exist at the specified path!",
101                    path.display()
102                )?;
103                write_hints(fmt, hints)
104            }
105            TableConnectError::NotBackupRoot => write!(
106                fmt,
107                "The path provided points to a database inside of an iOS backup, not the root of the backup."
108            ),
109        }
110    }
111}
112
113impl Display for TableError {
114    fn fmt(&self, fmt: &mut Formatter<'_>) -> Result {
115        match self {
116            TableError::CannotConnect(why) => write!(fmt, "{why}"),
117            TableError::CannotRead(why) => write!(fmt, "Cannot read from filesystem: {why}"),
118            TableError::QueryError(error) => write!(fmt, "Failed to query table: {error}"),
119        }
120    }
121}
122
123impl std::error::Error for TableError {
124    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
125        match self {
126            TableError::QueryError(e) => Some(e),
127            TableError::CannotConnect(e) => Some(e),
128            TableError::CannotRead(e) => Some(e),
129        }
130    }
131}
132
133impl std::error::Error for TableConnectError {
134    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
135        match self {
136            TableConnectError::Permissions(e) => Some(e),
137            TableConnectError::Inaccessible(_, e) => Some(e),
138            _ => None,
139        }
140    }
141}
142
143impl From<std::io::Error> for TableError {
144    fn from(err: std::io::Error) -> Self {
145        TableError::CannotRead(err)
146    }
147}
148
149impl From<rusqlite::Error> for TableError {
150    fn from(err: rusqlite::Error) -> Self {
151        TableError::QueryError(err)
152    }
153}