1use std::borrow::{Borrow, Cow};
45use std::collections::HashMap;
46use std::fmt;
47
48use async_trait::async_trait;
49
50use crate::generated::api_types::{
51 SessionFsError, SessionFsErrorCode, SessionFsReaddirWithTypesEntry,
52 SessionFsReaddirWithTypesEntryType, SessionFsSetProviderConventions, SessionFsStatResult,
53};
54pub use crate::generated::api_types::{
55 SessionFsSqliteQueryType, SessionFsSqliteTransactionErrorClass,
56 SessionFsSqliteTransactionStatement,
57};
58use crate::{Custom, Repr};
59
60#[non_exhaustive]
62#[derive(Debug, Clone, Default)]
63pub struct SessionFsCapabilities {
64 pub sqlite: bool,
66}
67
68impl SessionFsCapabilities {
69 pub fn new() -> Self {
71 Self::default()
72 }
73
74 pub fn with_sqlite(mut self, sqlite: bool) -> Self {
76 self.sqlite = sqlite;
77 self
78 }
79}
80
81#[non_exhaustive]
87#[derive(Debug, Clone)]
88pub struct SessionFsConfig {
89 pub initial_cwd: String,
91 pub session_state_path: String,
94 pub conventions: SessionFsConventions,
96 pub capabilities: Option<SessionFsCapabilities>,
98}
99
100impl SessionFsConfig {
101 pub fn new(
103 initial_cwd: impl Into<String>,
104 session_state_path: impl Into<String>,
105 conventions: SessionFsConventions,
106 ) -> Self {
107 Self {
108 initial_cwd: initial_cwd.into(),
109 session_state_path: session_state_path.into(),
110 conventions,
111 capabilities: None,
112 }
113 }
114
115 pub fn with_capabilities(mut self, capabilities: SessionFsCapabilities) -> Self {
117 self.capabilities = Some(capabilities);
118 self
119 }
120}
121
122#[derive(Debug, Clone, Copy, PartialEq, Eq)]
129pub enum SessionFsConventions {
130 Posix,
132 Windows,
134}
135
136impl SessionFsConventions {
137 pub(crate) fn into_wire(self) -> SessionFsSetProviderConventions {
138 match self {
139 Self::Posix => SessionFsSetProviderConventions::Posix,
140 Self::Windows => SessionFsSetProviderConventions::Windows,
141 }
142 }
143}
144
145#[derive(Clone, Debug, PartialEq, Eq)]
150#[non_exhaustive]
151pub enum FsErrorKind {
152 NotFound(String),
154
155 Other,
157}
158
159impl fmt::Display for FsErrorKind {
160 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
161 match self {
162 FsErrorKind::NotFound(path) => write!(f, "not found: {path}"),
163 FsErrorKind::Other => write!(f, "filesystem error"),
164 }
165 }
166}
167
168#[derive(Debug)]
173pub struct FsError {
174 repr: Repr<FsErrorKind>,
175}
176
177impl FsError {
178 pub fn new<E>(kind: FsErrorKind, error: E) -> Self
180 where
181 E: Into<Box<dyn std::error::Error + Send + Sync>>,
182 {
183 Self {
184 repr: Repr::Custom(Custom {
185 kind,
186 error: error.into(),
187 }),
188 }
189 }
190
191 pub fn kind(&self) -> &FsErrorKind {
193 match &self.repr {
194 Repr::Simple(k) | Repr::SimpleMessage(k, ..) | Repr::Custom(Custom { kind: k, .. }) => {
195 k
196 }
197 }
198 }
199
200 pub fn message(&self) -> Option<&str> {
202 match &self.repr {
203 Repr::SimpleMessage(_, m) => Some(m.borrow()),
204 _ => None,
205 }
206 }
207
208 #[must_use]
210 pub fn with_message<C>(kind: FsErrorKind, message: C) -> Self
211 where
212 C: Into<Cow<'static, str>>,
213 {
214 Self {
215 repr: Repr::SimpleMessage(kind, message.into()),
216 }
217 }
218
219 pub(crate) fn into_wire(self) -> SessionFsError {
220 match self.kind() {
221 FsErrorKind::NotFound(message) => SessionFsError {
222 code: SessionFsErrorCode::ENOENT,
223 message: Some(message.clone()),
224 },
225 FsErrorKind::Other => SessionFsError {
226 code: SessionFsErrorCode::UNKNOWN,
227 message: Some(self.to_string()),
228 },
229 }
230 }
231}
232
233impl fmt::Display for FsError {
234 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
235 match &self.repr {
236 Repr::Simple(k) => write!(f, "{k}"),
237 Repr::SimpleMessage(_, m) => write!(f, "{m}"),
238 Repr::Custom(Custom { error, .. }) => write!(f, "{error}"),
239 }
240 }
241}
242
243impl std::error::Error for FsError {
244 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
245 match &self.repr {
246 Repr::Custom(Custom { error, .. }) => Some(&**error),
247 _ => None,
248 }
249 }
250}
251
252impl From<FsErrorKind> for FsError {
253 fn from(kind: FsErrorKind) -> Self {
254 Self {
255 repr: Repr::Simple(kind),
256 }
257 }
258}
259
260impl From<std::io::Error> for FsError {
261 fn from(err: std::io::Error) -> Self {
262 match err.kind() {
263 std::io::ErrorKind::NotFound => Self::new(FsErrorKind::NotFound(err.to_string()), err),
264 _ => Self::new(FsErrorKind::Other, err),
265 }
266 }
267}
268
269#[non_exhaustive]
273#[derive(Debug, Clone)]
274pub struct FileInfo {
275 pub is_file: bool,
277 pub is_directory: bool,
279 pub size: i64,
281 pub mtime: String,
283 pub birthtime: String,
285}
286
287impl FileInfo {
288 pub fn new(
292 is_file: bool,
293 is_directory: bool,
294 size: i64,
295 mtime: impl Into<String>,
296 birthtime: impl Into<String>,
297 ) -> Self {
298 Self {
299 is_file,
300 is_directory,
301 size,
302 mtime: mtime.into(),
303 birthtime: birthtime.into(),
304 }
305 }
306
307 pub(crate) fn into_wire(self) -> SessionFsStatResult {
308 SessionFsStatResult {
309 is_file: self.is_file,
310 is_directory: self.is_directory,
311 size: self.size,
312 mtime: self.mtime,
313 birthtime: self.birthtime,
314 error: None,
315 }
316 }
317}
318
319#[derive(Debug, Clone, Copy, PartialEq, Eq)]
325pub enum DirEntryKind {
326 File,
328 Directory,
330}
331
332impl DirEntryKind {
333 fn into_wire(self) -> SessionFsReaddirWithTypesEntryType {
334 match self {
335 Self::File => SessionFsReaddirWithTypesEntryType::File,
336 Self::Directory => SessionFsReaddirWithTypesEntryType::Directory,
337 }
338 }
339}
340
341#[non_exhaustive]
344#[derive(Debug, Clone)]
345pub struct DirEntry {
346 pub name: String,
348 pub kind: DirEntryKind,
350}
351
352impl DirEntry {
353 pub fn new(name: impl Into<String>, kind: DirEntryKind) -> Self {
355 Self {
356 name: name.into(),
357 kind,
358 }
359 }
360
361 pub(crate) fn into_wire(self) -> SessionFsReaddirWithTypesEntry {
362 SessionFsReaddirWithTypesEntry {
363 name: self.name,
364 r#type: self.kind.into_wire(),
365 }
366 }
367}
368
369#[async_trait]
392pub trait SessionFsProvider: Send + Sync + 'static {
393 async fn read_file(&self, path: &str) -> Result<String, FsError> {
395 let _ = path;
396 Err(FsError::with_message(
397 FsErrorKind::Other,
398 "read_file not supported",
399 ))
400 }
401
402 async fn write_file(
404 &self,
405 path: &str,
406 content: &str,
407 mode: Option<i64>,
408 ) -> Result<(), FsError> {
409 let _ = (path, content, mode);
410 Err(FsError::with_message(
411 FsErrorKind::Other,
412 "write_file not supported",
413 ))
414 }
415
416 async fn append_file(
418 &self,
419 path: &str,
420 content: &str,
421 mode: Option<i64>,
422 ) -> Result<(), FsError> {
423 let _ = (path, content, mode);
424 Err(FsError::with_message(
425 FsErrorKind::Other,
426 "append_file not supported",
427 ))
428 }
429
430 async fn exists(&self, path: &str) -> Result<bool, FsError> {
434 let _ = path;
435 Err(FsError::with_message(
436 FsErrorKind::Other,
437 "exists not supported",
438 ))
439 }
440
441 async fn stat(&self, path: &str) -> Result<FileInfo, FsError> {
443 let _ = path;
444 Err(FsError::with_message(
445 FsErrorKind::Other,
446 "stat not supported",
447 ))
448 }
449
450 async fn mkdir(&self, path: &str, recursive: bool, mode: Option<i64>) -> Result<(), FsError> {
452 let _ = (path, recursive, mode);
453 Err(FsError::with_message(
454 FsErrorKind::Other,
455 "mkdir not supported",
456 ))
457 }
458
459 async fn readdir(&self, path: &str) -> Result<Vec<String>, FsError> {
461 let _ = path;
462 Err(FsError::with_message(
463 FsErrorKind::Other,
464 "readdir not supported",
465 ))
466 }
467
468 async fn readdir_with_types(&self, path: &str) -> Result<Vec<DirEntry>, FsError> {
470 let _ = path;
471 Err(FsError::with_message(
472 FsErrorKind::Other,
473 "readdir_with_types not supported",
474 ))
475 }
476
477 async fn rm(&self, path: &str, recursive: bool, force: bool) -> Result<(), FsError> {
480 let _ = (path, recursive, force);
481 Err(FsError::with_message(
482 FsErrorKind::Other,
483 "rm not supported",
484 ))
485 }
486
487 async fn rename(&self, src: &str, dest: &str) -> Result<(), FsError> {
489 let _ = (src, dest);
490 Err(FsError::with_message(
491 FsErrorKind::Other,
492 "rename not supported",
493 ))
494 }
495
496 fn sqlite(&self) -> Option<&dyn SessionFsSqliteProvider> {
501 None
502 }
503}
504
505#[async_trait]
525pub trait SessionFsSqliteProvider: Send + Sync {
526 async fn sqlite_query(
528 &self,
529 query_type: SessionFsSqliteQueryType,
530 query: &str,
531 params: Option<&HashMap<String, serde_json::Value>>,
532 ) -> Result<Option<SessionFsSqliteQueryResult>, FsError>;
533
534 async fn sqlite_transaction(
542 &self,
543 _statements: &[SessionFsSqliteTransactionStatement],
544 ) -> Result<Vec<SessionFsSqliteQueryResult>, SessionFsSqliteTransactionError> {
545 Err(SessionFsSqliteTransactionError::fatal(
546 "SQLite transactions are not supported by this SessionFs provider",
547 ))
548 }
549
550 async fn sqlite_exists(&self) -> Result<bool, FsError>;
552}
553
554#[derive(Debug, Clone)]
557pub struct SessionFsSqliteTransactionError {
558 pub error_class: SessionFsSqliteTransactionErrorClass,
560 pub message: String,
562}
563
564impl SessionFsSqliteTransactionError {
565 pub fn fatal(message: impl Into<String>) -> Self {
567 Self {
568 error_class: SessionFsSqliteTransactionErrorClass::Fatal,
569 message: message.into(),
570 }
571 }
572
573 pub fn busy_or_locked(message: impl Into<String>) -> Self {
575 Self {
576 error_class: SessionFsSqliteTransactionErrorClass::BusyOrLocked,
577 message: message.into(),
578 }
579 }
580
581 pub fn post_commit_ambiguous(message: impl Into<String>) -> Self {
583 Self {
584 error_class: SessionFsSqliteTransactionErrorClass::PostCommitAmbiguous,
585 message: message.into(),
586 }
587 }
588}
589
590impl std::fmt::Display for SessionFsSqliteTransactionError {
591 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
592 f.write_str(&self.message)
593 }
594}
595
596impl std::error::Error for SessionFsSqliteTransactionError {}
597
598impl From<FsError> for SessionFsSqliteTransactionError {
599 fn from(error: FsError) -> Self {
600 Self::fatal(error.to_string())
601 }
602}
603
604#[derive(Debug, Clone, Default)]
609pub struct SessionFsSqliteQueryResult {
610 pub columns: Vec<String>,
612 pub rows: Vec<HashMap<String, serde_json::Value>>,
614 pub rows_affected: i64,
616 pub last_insert_rowid: Option<i64>,
618}
619
620#[cfg(test)]
621mod tests {
622 use super::*;
623
624 #[test]
625 fn fs_error_maps_io_not_found_to_enoent() {
626 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "missing.txt");
627 let fs_err: FsError = io_err.into();
628 assert!(
629 matches!(fs_err.kind(), FsErrorKind::NotFound(message) if message == "missing.txt")
630 );
631 let wire = fs_err.into_wire();
632 assert_eq!(wire.code, SessionFsErrorCode::ENOENT);
633 }
634
635 #[test]
636 fn fs_error_maps_other_io_to_unknown() {
637 let io_err = std::io::Error::other("disk full");
638 let fs_err: FsError = io_err.into();
639 assert!(matches!(fs_err.kind(), FsErrorKind::Other));
640 let wire = fs_err.into_wire();
641 assert_eq!(wire.code, SessionFsErrorCode::UNKNOWN);
642 assert!(wire.message.unwrap().contains("disk full"));
643 }
644
645 #[test]
646 fn conventions_maps_to_wire() {
647 assert_eq!(
648 SessionFsConventions::Posix.into_wire(),
649 SessionFsSetProviderConventions::Posix
650 );
651 assert_eq!(
652 SessionFsConventions::Windows.into_wire(),
653 SessionFsSetProviderConventions::Windows
654 );
655 }
656
657 struct DefaultProvider;
658 #[async_trait]
659 impl SessionFsProvider for DefaultProvider {}
660
661 #[tokio::test]
662 async fn default_impls_return_unsupported() {
663 let p = DefaultProvider;
664 let err = p.read_file("/x").await.unwrap_err();
665 assert!(
666 matches!(err.kind(), FsErrorKind::Other) && err.to_string().contains("not supported")
667 );
668 }
669}