loki_file_access/
token.rs1use base64::engine::general_purpose::URL_SAFE_NO_PAD;
16use base64::Engine as _;
17use std::path::PathBuf;
18
19use crate::error::{AccessError, TokenParseError};
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23#[non_exhaustive]
24pub enum PermissionStatus {
25 Valid,
27 Revoked,
29 Unknown,
31}
32
33#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
37pub(crate) enum TokenInner {
38 Desktop {
40 path: PathBuf,
42 display_name: String,
44 },
45 Android {
47 uri: String,
49 display_name: String,
51 mime_type: Option<String>,
53 },
54 Ios {
56 bookmark: Vec<u8>,
58 display_name: String,
60 mime_type: Option<String>,
62 },
63 Wasm {
65 data: Vec<u8>,
67 name: String,
69 mime_type: Option<String>,
71 },
72}
73
74#[derive(Debug, Clone)]
80pub struct FileAccessToken {
81 pub(crate) inner: TokenInner,
82}
83
84impl FileAccessToken {
85 #[must_use = "this returns a Result that may contain an error"]
91 pub fn open_read(&self) -> Result<Box<dyn ReadSeek>, AccessError> {
92 crate::platform::open_read(&self.inner)
93 }
94
95 #[must_use = "this returns a Result that may contain an error"]
101 pub fn open_write(&self) -> Result<Box<dyn WriteSeek>, AccessError> {
102 crate::platform::open_write(&self.inner)
103 }
104
105 #[must_use]
107 pub fn display_name(&self) -> &str {
108 match &self.inner {
109 TokenInner::Desktop { display_name, .. }
110 | TokenInner::Android { display_name, .. }
111 | TokenInner::Ios { display_name, .. } => display_name,
112 TokenInner::Wasm { name, .. } => name,
113 }
114 }
115
116 #[must_use]
118 pub fn mime_type(&self) -> Option<&str> {
119 match &self.inner {
120 TokenInner::Desktop { .. } => None,
121 TokenInner::Android { mime_type, .. }
122 | TokenInner::Ios { mime_type, .. }
123 | TokenInner::Wasm { mime_type, .. } => mime_type.as_deref(),
124 }
125 }
126
127 #[must_use]
129 pub fn check_permission(&self) -> PermissionStatus {
130 crate::platform::check_permission(&self.inner)
131 }
132
133 #[must_use]
135 pub fn serialize(&self) -> String {
136 let json = match serde_json::to_string(&self.inner) {
142 Ok(j) => j,
143 Err(_) => return URL_SAFE_NO_PAD.encode(b"{}"),
144 };
145 URL_SAFE_NO_PAD.encode(json.as_bytes())
146 }
147
148 pub fn deserialize(s: &str) -> Result<Self, TokenParseError> {
154 let bytes = URL_SAFE_NO_PAD
155 .decode(s)
156 .map_err(|e| TokenParseError::InvalidBase64 {
157 message: e.to_string(),
158 })?;
159
160 let json = String::from_utf8(bytes).map_err(|e| TokenParseError::InvalidBase64 {
161 message: e.to_string(),
162 })?;
163
164 let inner: TokenInner =
165 serde_json::from_str(&json).map_err(|e| TokenParseError::InvalidJson {
166 message: e.to_string(),
167 })?;
168
169 Ok(Self { inner })
170 }
171}
172
173pub trait ReadSeek: std::io::Read + std::io::Seek + Send {}
175impl<T: std::io::Read + std::io::Seek + Send> ReadSeek for T {}
176
177pub trait WriteSeek: std::io::Write + std::io::Seek + Send {}
179impl<T: std::io::Write + std::io::Seek + Send> WriteSeek for T {}
180
181impl std::fmt::Display for FileAccessToken {
182 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
183 f.write_str(&self.serialize())
184 }
185}
186
187impl std::str::FromStr for FileAccessToken {
188 type Err = TokenParseError;
189
190 fn from_str(s: &str) -> Result<Self, Self::Err> {
191 Self::deserialize(s)
192 }
193}
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198
199 #[test]
200 fn round_trip_desktop_token() {
201 let token = FileAccessToken {
202 inner: TokenInner::Desktop {
203 path: PathBuf::from("/tmp/test.txt"),
204 display_name: "test.txt".into(),
205 },
206 };
207 let serialized = token.serialize();
208 let restored = FileAccessToken::deserialize(&serialized).unwrap();
209 assert_eq!(restored.display_name(), "test.txt");
210 assert!(restored.mime_type().is_none());
211 }
212
213 #[test]
214 fn round_trip_android_token() {
215 let token = FileAccessToken {
216 inner: TokenInner::Android {
217 uri: "content://com.example/doc/1".into(),
218 display_name: "photo.jpg".into(),
219 mime_type: Some("image/jpeg".into()),
220 },
221 };
222 let serialized = token.serialize();
223 let restored = FileAccessToken::deserialize(&serialized).unwrap();
224 assert_eq!(restored.display_name(), "photo.jpg");
225 assert_eq!(restored.mime_type(), Some("image/jpeg"));
226 }
227
228 #[test]
229 fn round_trip_ios_token() {
230 let token = FileAccessToken {
231 inner: TokenInner::Ios {
232 bookmark: vec![0xDE, 0xAD, 0xBE, 0xEF],
233 display_name: "notes.pdf".into(),
234 mime_type: Some("application/pdf".into()),
235 },
236 };
237 let serialized = token.serialize();
238 let restored = FileAccessToken::deserialize(&serialized).unwrap();
239 assert_eq!(restored.display_name(), "notes.pdf");
240 assert_eq!(restored.mime_type(), Some("application/pdf"));
241 }
242
243 #[test]
244 fn round_trip_wasm_token() {
245 let token = FileAccessToken {
246 inner: TokenInner::Wasm {
247 data: vec![1, 2, 3, 4, 5],
248 name: "data.bin".into(),
249 mime_type: Some("application/octet-stream".into()),
250 },
251 };
252 let serialized = token.serialize();
253 let restored = FileAccessToken::deserialize(&serialized).unwrap();
254 assert_eq!(restored.display_name(), "data.bin");
255 assert_eq!(restored.mime_type(), Some("application/octet-stream"));
256 }
257
258 #[test]
259 fn deserialize_invalid_base64_returns_error() {
260 let result = FileAccessToken::deserialize("not!valid!base64!!!");
261 assert!(result.is_err());
262 assert!(matches!(
263 result.unwrap_err(),
264 TokenParseError::InvalidBase64 { .. }
265 ));
266 }
267
268 #[test]
269 fn deserialize_invalid_json_returns_error() {
270 let bad = URL_SAFE_NO_PAD.encode(b"not json");
271 let result = FileAccessToken::deserialize(&bad);
272 assert!(result.is_err());
273 assert!(matches!(
274 result.unwrap_err(),
275 TokenParseError::InvalidJson { .. }
276 ));
277 }
278
279 #[test]
280 fn display_and_from_str_round_trip() {
281 let token = FileAccessToken {
282 inner: TokenInner::Desktop {
283 path: PathBuf::from("/tmp/x.txt"),
284 display_name: "x.txt".into(),
285 },
286 };
287 let s = token.to_string();
288 let restored: FileAccessToken = s.parse().unwrap();
289 assert_eq!(restored.display_name(), "x.txt");
290 }
291}