Skip to main content

loki_file_access/
error.rs

1// SPDX-License-Identifier: MIT
2// Copyright (c) 2026 AppThere
3
4//! Error types for the `loki-file-access` crate.
5//!
6//! This module defines all error enums used across the public API surface:
7//!
8//! - [`PickerError`] — errors originating from the platform file-picker dialog.
9//! - [`AccessError`] — errors when reading from or writing to a previously granted file.
10//! - [`TokenParseError`] — errors when deserializing a stored [`crate::FileAccessToken`].
11//!
12//! All enums are `#[non_exhaustive]` so that new variants can be added in
13//! future minor versions without breaking downstream matches.
14
15/// Errors that can occur when presenting a file-picker dialog.
16///
17/// Note that the user cancelling the dialog is **not** an error — it is
18/// represented as `Ok(None)` on the single-file methods and `Ok(vec![])` on
19/// multi-file methods.
20#[derive(Debug, thiserror::Error)]
21#[non_exhaustive]
22pub enum PickerError {
23    /// The platform returned an error from the native file-picker API.
24    #[error("platform file-picker error: {message}")]
25    Platform {
26        /// Human-readable description of the platform error.
27        message: String,
28    },
29
30    /// The current platform does not support the requested operation.
31    #[error("operation not supported on this platform: {operation}")]
32    Unsupported {
33        /// Description of the unsupported operation.
34        operation: String,
35    },
36
37    /// An internal synchronisation error occurred (e.g. a poisoned mutex).
38    #[error("internal synchronisation error: {message}")]
39    Internal {
40        /// Human-readable description of the internal error.
41        message: String,
42    },
43}
44
45/// Errors that can occur when opening or accessing a previously granted file.
46#[derive(Debug, thiserror::Error)]
47#[non_exhaustive]
48pub enum AccessError {
49    /// The permission grant for this file has been revoked by the user or OS.
50    #[error("file access permission has been revoked")]
51    PermissionRevoked,
52
53    /// An I/O error occurred while reading from or writing to the file.
54    #[error("I/O error: {source}")]
55    Io {
56        /// The underlying I/O error.
57        #[from]
58        source: std::io::Error,
59    },
60
61    /// The file descriptor or handle returned by the platform was invalid.
62    #[error("invalid file descriptor returned by platform")]
63    InvalidDescriptor,
64
65    /// The platform returned an error when attempting to open the file.
66    #[error("platform access error: {message}")]
67    Platform {
68        /// Human-readable description of the platform error.
69        message: String,
70    },
71}
72
73/// Errors that can occur when deserializing a stored [`crate::FileAccessToken`].
74#[derive(Debug, thiserror::Error)]
75#[non_exhaustive]
76pub enum TokenParseError {
77    /// The base64 encoding of the token is invalid.
78    #[error("invalid base64 encoding: {message}")]
79    InvalidBase64 {
80        /// Description of the base64 decode error.
81        message: String,
82    },
83
84    /// The JSON payload inside the token is malformed.
85    #[error("invalid JSON in token: {message}")]
86    InvalidJson {
87        /// Description of the JSON parse error.
88        message: String,
89    },
90
91    /// The token contains an unrecognised platform variant.
92    #[error("unknown token variant: {variant}")]
93    UnknownVariant {
94        /// The variant tag that was not recognised.
95        variant: String,
96    },
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102
103    #[test]
104    fn picker_error_platform_displays_message() {
105        let err = PickerError::Platform {
106            message: "dialog failed".into(),
107        };
108        let msg = err.to_string();
109        assert!(!msg.is_empty(), "display string must not be empty");
110        assert!(msg.contains("dialog failed"));
111    }
112
113    #[test]
114    fn picker_error_unsupported_displays_message() {
115        let err = PickerError::Unsupported {
116            operation: "save".into(),
117        };
118        let msg = err.to_string();
119        assert!(!msg.is_empty());
120        assert!(msg.contains("save"));
121    }
122
123    #[test]
124    fn picker_error_internal_displays_message() {
125        let err = PickerError::Internal {
126            message: "mutex poisoned".into(),
127        };
128        let msg = err.to_string();
129        assert!(!msg.is_empty());
130        assert!(msg.contains("mutex poisoned"));
131    }
132
133    #[test]
134    fn access_error_permission_revoked_displays_message() {
135        let err = AccessError::PermissionRevoked;
136        assert!(!err.to_string().is_empty());
137    }
138
139    #[test]
140    fn access_error_io_displays_message() {
141        let err = AccessError::Io {
142            source: std::io::Error::new(std::io::ErrorKind::NotFound, "gone"),
143        };
144        assert!(!err.to_string().is_empty());
145    }
146
147    #[test]
148    fn access_error_invalid_descriptor_displays_message() {
149        let err = AccessError::InvalidDescriptor;
150        assert!(!err.to_string().is_empty());
151    }
152
153    #[test]
154    fn access_error_platform_displays_message() {
155        let err = AccessError::Platform {
156            message: "fd error".into(),
157        };
158        let msg = err.to_string();
159        assert!(!msg.is_empty());
160        assert!(msg.contains("fd error"));
161    }
162
163    #[test]
164    fn token_parse_error_base64_displays_message() {
165        let err = TokenParseError::InvalidBase64 {
166            message: "bad padding".into(),
167        };
168        let msg = err.to_string();
169        assert!(!msg.is_empty());
170        assert!(msg.contains("bad padding"));
171    }
172
173    #[test]
174    fn token_parse_error_json_displays_message() {
175        let err = TokenParseError::InvalidJson {
176            message: "unexpected EOF".into(),
177        };
178        let msg = err.to_string();
179        assert!(!msg.is_empty());
180        assert!(msg.contains("unexpected EOF"));
181    }
182
183    #[test]
184    fn token_parse_error_unknown_variant_displays_message() {
185        let err = TokenParseError::UnknownVariant {
186            variant: "FuturePlatform".into(),
187        };
188        let msg = err.to_string();
189        assert!(!msg.is_empty());
190        assert!(msg.contains("FuturePlatform"));
191    }
192}