1use crate::file_system::{DirEntry, Directory, File, FileSystem, FileSystemError, Offset};
36use alloc::{boxed::Box, string::String, vec, vec::Vec};
37use js_sys::{Array, JsString, Promise, Reflect, TypeError, Uint8Array};
38use wasm_bindgen::prelude::*;
39use web_sys::{DomException, FileList, FileSystemDirectoryHandle};
40
41fn to_filesystem_error(value: JsValue) -> FileSystemError {
42 if value.has_type::<DomException>()
43 && Reflect::get(&value, &JsValue::from("name")).unwrap() == "NotFoundError"
44 {
45 FileSystemError::NotFound(Box::new(value))
46 } else if let Ok(s) = value.clone().dyn_into::<JsString>()
47 && s == "file not found"
48 {
49 FileSystemError::NotFound(Box::new(value))
50 } else {
51 FileSystemError::Other(Box::new(value))
52 }
53}
54
55#[wasm_bindgen(module = "/src/web/file-system.js")]
56extern "C" {
57 type DirectoryWrapper;
58 #[wasm_bindgen(constructor)]
59 fn new(inner: &JsValue) -> DirectoryWrapper;
60
61 #[wasm_bindgen(method)]
62 fn openSubdir(this: &DirectoryWrapper, name: &str) -> Promise;
63 #[wasm_bindgen(method)]
64 fn listDir(this: &DirectoryWrapper) -> Promise;
65 #[wasm_bindgen(method)]
66 fn openFile(this: &DirectoryWrapper, name: &str) -> Promise;
67
68 type FileWrapper;
69 #[wasm_bindgen(method)]
70 fn readAll(this: &FileWrapper) -> Promise;
71 #[wasm_bindgen(method)]
72 fn readSegment(this: &FileWrapper, offset: f64, length: f64) -> Promise;
73
74 type FSDirectory;
75 #[wasm_bindgen(constructor)]
76 fn new(handle: &FileSystemDirectoryHandle) -> FSDirectory;
77
78 type EntriesDirectory;
79 fn entriesDirectoryFromFileList(fileList: &FileList) -> EntriesDirectory;
80}
81
82pub struct WebFileSystem;
84impl FileSystem for WebFileSystem {
85 type File = WebFile;
86 type Directory = WebDirectory;
87}
88
89pub struct WebDirectory {
91 directory: DirectoryWrapper,
92}
93
94impl Clone for WebDirectory {
95 fn clone(&self) -> Self {
96 Self {
97 directory: self.directory.clone().dyn_into().unwrap(),
98 }
99 }
100}
101
102impl WebDirectory {
103 pub fn new(inner: &JsValue) -> Result<Self, JsError> {
108 if let Ok(handle) = inner.clone().dyn_into::<FileSystemDirectoryHandle>() {
109 let directory = FSDirectory::new(&handle);
110 Ok(Self {
111 directory: DirectoryWrapper::new(&directory),
112 })
113 } else if let Ok(file_list) = inner.clone().dyn_into::<web_sys::FileList>() {
114 let directory = entriesDirectoryFromFileList(&file_list);
115 Ok(Self {
116 directory: DirectoryWrapper::new(&directory),
117 })
118 } else {
119 Err(JsError::new(
120 "must provide either a FileSystemDirectory Handle or a FileList object",
121 ))
122 }
123 }
124}
125
126impl Directory<WebFile> for WebDirectory {
127 async fn open_subdir(&self, name: &[u8]) -> Result<Self, FileSystemError> {
128 let f = async || -> Result<Self, JsValue> {
129 let subdir: DirectoryWrapper = self
130 .directory
131 .openSubdir(str::from_utf8(name).map_err(|_| TypeError::new("name was not UTF-8"))?)
132 .await?
133 .dyn_into()?;
134 Ok(Self { directory: subdir })
135 };
136 f().await.map_err(to_filesystem_error)
137 }
138
139 async fn list_dir(&self) -> Result<Vec<DirEntry>, FileSystemError> {
140 let f = async || -> Result<Vec<DirEntry>, JsValue> {
141 let entries: Array = self.directory.listDir().await?.dyn_into()?;
142 let directories: Array = entries.at(0).dyn_into()?;
143 let files: Array = entries.at(1).dyn_into()?;
144 let mut out: Vec<DirEntry> = Vec::new();
145 for name in directories {
146 let name: JsString = name.dyn_into()?;
147 let name: String = name.into();
148 let name: Vec<u8> = name.into_bytes();
149 out.push(DirEntry::Directory(name));
150 }
151 for name in files {
152 let name: JsString = name.dyn_into()?;
153 let name: String = name.into();
154 let name: Vec<u8> = name.into_bytes();
155 out.push(DirEntry::File(name));
156 }
157 Ok(out)
158 };
159 f().await.map_err(to_filesystem_error)
160 }
161
162 async fn open_file(&self, name: &[u8]) -> Result<WebFile, FileSystemError> {
163 let f = async || -> Result<WebFile, JsValue> {
164 let js_file: FileWrapper = self
165 .directory
166 .openFile(str::from_utf8(name).map_err(|_| TypeError::new("name was not UTF-8"))?)
167 .await?
168 .dyn_into()?;
169 Ok(WebFile { file: js_file })
170 };
171 f().await.map_err(to_filesystem_error)
172 }
173}
174
175pub struct WebFile {
177 file: FileWrapper,
178}
179
180impl File for WebFile {
181 async fn read_all(&mut self) -> Result<Vec<u8>, FileSystemError> {
182 let f = async || -> Result<Vec<u8>, JsValue> {
183 let data: Uint8Array = self.file.readAll().await?.dyn_into()?;
184 let mut out = vec![0u8; data.length() as usize];
185 data.copy_to(&mut out);
186 Ok(out)
187 };
188 f().await.map_err(to_filesystem_error)
189 }
190
191 async fn read_segment(
192 &mut self,
193 offset: Offset,
194 dest: &mut [u8],
195 ) -> Result<usize, FileSystemError> {
196 let mut f = async || -> Result<usize, JsValue> {
197 assert!(offset.0 <= 2u64.pow(53), "offset not representable as f64");
198 #[allow(clippy::cast_precision_loss)]
199 let offset = offset.0 as f64;
200 assert!(
201 dest.len() as u64 <= 2u64.pow(53),
202 "length not representable as f64"
203 );
204 #[allow(clippy::cast_precision_loss)]
205 let length = dest.len() as f64;
206 let data: Uint8Array = self.file.readSegment(offset, length).await?.dyn_into()?;
207 let bytes_read = data.length() as usize;
208 data.copy_to(&mut dest[0..bytes_read]);
209 Ok(bytes_read)
210 };
211 f().await.map_err(to_filesystem_error)
212 }
213}