1use crate::ffi;
2use std::cell::RefCell;
3use std::collections::HashMap;
4use std::rc::Rc;
5
6const FILE_STATUS_SUCCESS: u32 = 1;
7const FILE_STATUS_CANCELLED: u32 = 2;
8
9const FILE_CAPABILITY_OPEN: u32 = 1 << 0;
10const FILE_CAPABILITY_READ: u32 = 1 << 1;
11const FILE_CAPABILITY_SAVE: u32 = 1 << 2;
12const FILE_CAPABILITY_CHUNKED_READ: u32 = 1 << 3;
13const FILE_CAPABILITY_CHUNKED_WRITE: u32 = 1 << 4;
14const FILE_CAPABILITY_NATIVE_SAVE_PICKER: u32 = 1 << 5;
15const FILE_CAPABILITY_PROCESS_WORKER_SAVE: u32 = 1 << 6;
16
17type FileOpenCallback = Rc<dyn Fn(FileOpenEventArgs)>;
18type FileErrorCallback = Rc<dyn Fn(FileErrorEventArgs)>;
19type FileReadCallback = Rc<dyn Fn(FileReadChunk)>;
20type FileSaveCallback = Rc<dyn Fn(FileSaveResult)>;
21type FileWriteCallback = Rc<dyn Fn(FileWriteProgress)>;
22type FileWriterCreatedCallback = Rc<dyn Fn(BrowserFileWriter)>;
23type FileWorkerChunkCallback = Rc<dyn Fn(FileReadChunk)>;
24type FileWorkerProgressCallback = Rc<dyn Fn(FileWorkerProcessProgress)>;
25type FileWorkerCompleteCallback = Rc<dyn Fn(FileWorkerProcessResult)>;
26
27thread_local! {
28 static NEXT_FILE_REQUEST_ID: RefCell<u32> = const { RefCell::new(1) };
29 static BROWSER_FILES: RefCell<HashMap<String, Rc<RefCell<BrowserFileState>>>> = RefCell::new(HashMap::new());
30 static PENDING_OPEN_REQUESTS: RefCell<HashMap<u32, PendingOpenRequest>> = RefCell::new(HashMap::new());
31 static PENDING_READ_REQUESTS: RefCell<HashMap<u32, PendingReadRequest>> = RefCell::new(HashMap::new());
32 static PENDING_SAVE_REQUESTS: RefCell<HashMap<u32, PendingSaveRequest>> = RefCell::new(HashMap::new());
33 static PENDING_WRITER_CREATE_REQUESTS: RefCell<HashMap<u32, PendingWriterCreateRequest>> = RefCell::new(HashMap::new());
34 static PENDING_WRITER_WRITE_REQUESTS: RefCell<HashMap<u32, PendingWriterWriteRequest>> = RefCell::new(HashMap::new());
35 static PENDING_WRITER_FINISH_REQUESTS: RefCell<HashMap<u32, PendingWriterFinishRequest>> = RefCell::new(HashMap::new());
36 static ACTIVE_WORKER_PROCESS_REQUESTS: RefCell<HashMap<u32, Rc<RefCell<FileWorkerProcessRequestState>>>> = RefCell::new(HashMap::new());
37}
38
39fn with_utf8(value: &str, callback: impl FnOnce(usize, u32)) {
40 let bytes = value.as_bytes();
41 callback(
42 if bytes.is_empty() {
43 0
44 } else {
45 bytes.as_ptr() as usize
46 },
47 bytes.len() as u32,
48 );
49}
50
51fn read_utf8(ptr: *const u8, len: u32) -> String {
52 if ptr.is_null() || len == 0 {
53 return String::new();
54 }
55 let bytes = unsafe { std::slice::from_raw_parts(ptr, len as usize) };
56 String::from_utf8_lossy(bytes).into_owned()
57}
58
59fn next_request_id() -> u32 {
60 NEXT_FILE_REQUEST_ID.with(|slot| {
61 let mut slot = slot.borrow_mut();
62 let id = *slot;
63 *slot += 1;
64 id
65 })
66}
67
68fn describe_file_failure(status: u32, fallback: &str) -> String {
69 if status == FILE_STATUS_CANCELLED {
70 return "File operation was cancelled.".to_string();
71 }
72 fallback.to_string()
73}
74
75fn dispatch_file_error(callback: Option<FileErrorCallback>, message: String) {
76 if let Some(callback) = callback {
77 callback(FileErrorEventArgs { message });
78 return;
79 }
80 crate::logger::warn("File", &message);
81}
82
83fn decode_length_prefixed_text(bytes: &[u8], cursor: &mut usize) -> Option<String> {
84 if *cursor + 4 > bytes.len() {
85 return None;
86 }
87 let len = u32::from_le_bytes(bytes[*cursor..*cursor + 4].try_into().ok()?) as usize;
88 *cursor += 4;
89 if *cursor + len > bytes.len() {
90 return None;
91 }
92 let value = String::from_utf8_lossy(&bytes[*cursor..*cursor + len]).into_owned();
93 *cursor += len;
94 Some(value)
95}
96
97fn decode_file_list_payload(bytes: &[u8]) -> Vec<BrowserFile> {
98 if bytes.len() < 4 {
99 return Vec::new();
100 }
101 let mut cursor = 0usize;
102 let count = u32::from_le_bytes(bytes[cursor..cursor + 4].try_into().unwrap_or([0; 4])) as usize;
103 cursor += 4;
104 let mut files = Vec::with_capacity(count);
105 for _ in 0..count {
106 let Some(id) = decode_length_prefixed_text(bytes, &mut cursor) else {
107 break;
108 };
109 if cursor + 16 > bytes.len() {
110 break;
111 }
112 let size_bytes = u64::from_le_bytes(bytes[cursor..cursor + 8].try_into().unwrap_or([0; 8]));
113 cursor += 8;
114 let last_modified_ms =
115 u64::from_le_bytes(bytes[cursor..cursor + 8].try_into().unwrap_or([0; 8]));
116 cursor += 8;
117 let Some(name) = decode_length_prefixed_text(bytes, &mut cursor) else {
118 break;
119 };
120 let Some(mime_type) = decode_length_prefixed_text(bytes, &mut cursor) else {
121 break;
122 };
123 files.push(register_browser_file(
124 id,
125 name,
126 if mime_type.is_empty() {
127 None
128 } else {
129 Some(mime_type)
130 },
131 size_bytes,
132 last_modified_ms,
133 ));
134 }
135 files
136}
137
138fn decode_writer_payload(bytes: &[u8]) -> Option<(FileSaveMode, String, Option<String>)> {
139 if bytes.len() < 4 {
140 return None;
141 }
142 let mut cursor = 0usize;
143 let mode_raw = u32::from_le_bytes(bytes[cursor..cursor + 4].try_into().ok()?);
144 cursor += 4;
145 let first = decode_length_prefixed_text(bytes, &mut cursor)?;
146 let second = if cursor < bytes.len() {
147 decode_length_prefixed_text(bytes, &mut cursor)
148 } else {
149 None
150 };
151 Some((FileSaveMode::from_raw(mode_raw), first, second))
152}
153
154fn split_worker_process_complete_payload(text: String) -> (Option<String>, Option<String>) {
155 let mut parts = text.splitn(2, '\0');
156 let output_file_name = parts.next().unwrap_or_default().to_string();
157 let worker_result = parts.next().unwrap_or_default().to_string();
158 (
159 if output_file_name.is_empty() {
160 None
161 } else {
162 Some(output_file_name)
163 },
164 if worker_result.is_empty() {
165 None
166 } else {
167 Some(worker_result)
168 },
169 )
170}
171
172#[derive(Clone, Copy, Debug, PartialEq, Eq)]
173enum FileRequestKind {
174 Open,
175 Read,
176 Save,
177 WriterCreate,
178 WriterWrite,
179 WriterFinish,
180}
181
182#[must_use = "dropping the guard unregisters the pending file callback"]
183pub struct FileRequestGuard {
184 kind: FileRequestKind,
185 request_id: u32,
186}
187
188impl FileRequestGuard {
189 fn inactive(kind: FileRequestKind) -> Self {
190 Self {
191 kind,
192 request_id: 0,
193 }
194 }
195
196 pub fn cancel(&mut self) {
197 if self.request_id == 0 {
198 return;
199 }
200 let request_id = self.request_id;
201 self.request_id = 0;
202 match self.kind {
203 FileRequestKind::Open => {
204 PENDING_OPEN_REQUESTS.with(|requests| {
205 requests.borrow_mut().remove(&request_id);
206 });
207 }
208 FileRequestKind::Read => {
209 PENDING_READ_REQUESTS.with(|requests| {
210 requests.borrow_mut().remove(&request_id);
211 });
212 }
213 FileRequestKind::Save => {
214 PENDING_SAVE_REQUESTS.with(|requests| {
215 requests.borrow_mut().remove(&request_id);
216 });
217 }
218 FileRequestKind::WriterCreate => {
219 PENDING_WRITER_CREATE_REQUESTS.with(|requests| {
220 requests.borrow_mut().remove(&request_id);
221 });
222 }
223 FileRequestKind::WriterWrite => {
224 PENDING_WRITER_WRITE_REQUESTS.with(|requests| {
225 requests.borrow_mut().remove(&request_id);
226 });
227 }
228 FileRequestKind::WriterFinish => {
229 PENDING_WRITER_FINISH_REQUESTS.with(|requests| {
230 requests.borrow_mut().remove(&request_id);
231 });
232 }
233 }
234 }
235}
236
237impl Drop for FileRequestGuard {
238 fn drop(&mut self) {
239 self.cancel();
240 }
241}
242
243#[derive(Clone, Copy, Debug, PartialEq, Eq)]
244pub enum FileSaveMode {
245 Download = 1,
246 NativePicker = 2,
247}
248
249impl FileSaveMode {
250 fn from_raw(value: u32) -> Self {
251 match value {
252 2 => Self::NativePicker,
253 _ => Self::Download,
254 }
255 }
256}
257
258#[derive(Clone, Copy, Debug, PartialEq, Eq)]
259pub struct FileCapabilities {
260 pub can_pick_open: bool,
261 pub can_read: bool,
262 pub can_save: bool,
263 pub can_read_chunks: bool,
264 pub can_write_chunks: bool,
265 pub can_use_native_save_picker: bool,
266 pub can_process_in_worker_to_picked_file: bool,
267}
268
269impl FileCapabilities {
270 fn from_bits(bits: u32) -> Self {
271 Self {
272 can_pick_open: (bits & FILE_CAPABILITY_OPEN) != 0,
273 can_read: (bits & FILE_CAPABILITY_READ) != 0,
274 can_save: (bits & FILE_CAPABILITY_SAVE) != 0,
275 can_read_chunks: (bits & FILE_CAPABILITY_CHUNKED_READ) != 0,
276 can_write_chunks: (bits & FILE_CAPABILITY_CHUNKED_WRITE) != 0,
277 can_use_native_save_picker: (bits & FILE_CAPABILITY_NATIVE_SAVE_PICKER) != 0,
278 can_process_in_worker_to_picked_file: (bits & FILE_CAPABILITY_PROCESS_WORKER_SAVE) != 0,
279 }
280 }
281}
282
283#[derive(Clone, Debug, PartialEq, Eq)]
284pub struct FileOpenEventArgs {
285 pub files: Vec<BrowserFile>,
286}
287
288#[derive(Clone, Debug, PartialEq, Eq)]
289pub struct FileErrorEventArgs {
290 pub message: String,
291}
292
293#[derive(Clone, Debug, PartialEq, Eq)]
294pub struct FileReadChunk {
295 pub offset_bytes: u64,
296 pub file_size_bytes: u64,
297 pub bytes: Vec<u8>,
298}
299
300impl FileReadChunk {
301 pub fn next_offset_bytes(&self) -> u64 {
302 self.offset_bytes + self.bytes.len() as u64
303 }
304
305 pub fn reached_eof(&self) -> bool {
306 self.next_offset_bytes() >= self.file_size_bytes
307 }
308}
309
310#[derive(Clone, Debug, PartialEq, Eq)]
311pub struct FileWriteProgress {
312 pub written_bytes: u64,
313 pub total_written_bytes: u64,
314}
315
316#[derive(Clone, Debug, PartialEq, Eq)]
317pub struct FileSaveResult {
318 pub file_name: String,
319 pub mode: FileSaveMode,
320 pub written_bytes: u64,
321}
322
323#[derive(Clone, Debug, PartialEq, Eq)]
324pub struct FileWorkerProcessProgress {
325 pub processed_bytes: u64,
326 pub total_bytes: u64,
327 pub output_file_name: Option<String>,
328}
329
330#[derive(Clone, Debug, PartialEq, Eq)]
331pub struct FileWorkerProcessResult {
332 pub processed_bytes: u64,
333 pub output_file_name: Option<String>,
334 pub worker_result: Option<String>,
335}
336
337#[derive(Debug, PartialEq, Eq)]
338struct BrowserFileState {
339 id: String,
340 name: String,
341 mime_type: Option<String>,
342 size_bytes: u64,
343 last_modified_ms: u64,
344}
345
346#[derive(Clone, Debug)]
347pub struct BrowserFile {
348 inner: Rc<RefCell<BrowserFileState>>,
349}
350
351impl Default for BrowserFile {
352 fn default() -> Self {
353 Self {
354 inner: Rc::new(RefCell::new(BrowserFileState {
355 id: String::new(),
356 name: String::new(),
357 mime_type: None,
358 size_bytes: 0,
359 last_modified_ms: 0,
360 })),
361 }
362 }
363}
364
365impl PartialEq for BrowserFile {
366 fn eq(&self, other: &Self) -> bool {
367 self.id() == other.id()
368 }
369}
370
371impl Eq for BrowserFile {}
372
373impl BrowserFile {
374 pub fn id(&self) -> String {
375 self.inner.borrow().id.clone()
376 }
377
378 pub fn name(&self) -> String {
379 self.inner.borrow().name.clone()
380 }
381
382 pub fn mime_type(&self) -> Option<String> {
383 self.inner.borrow().mime_type.clone()
384 }
385
386 pub fn size_bytes(&self) -> u64 {
387 self.inner.borrow().size_bytes
388 }
389
390 pub fn last_modified_ms(&self) -> u64 {
391 self.inner.borrow().last_modified_ms
392 }
393
394 pub fn read_bytes_chunk(
395 &self,
396 offset_bytes: u64,
397 max_bytes: u32,
398 on_complete: impl Fn(FileReadChunk) + 'static,
399 ) -> FileRequestGuard {
400 self.read_bytes_chunk_with_error(
401 offset_bytes,
402 max_bytes,
403 on_complete,
404 None::<fn(FileErrorEventArgs)>,
405 )
406 }
407
408 pub fn read_bytes_chunk_with_error(
409 &self,
410 offset_bytes: u64,
411 max_bytes: u32,
412 on_complete: impl Fn(FileReadChunk) + 'static,
413 on_error: Option<impl Fn(FileErrorEventArgs) + 'static>,
414 ) -> FileRequestGuard {
415 if max_bytes == 0 {
416 dispatch_file_error(
417 on_error.map(|handler| Rc::new(handler) as FileErrorCallback),
418 "BrowserFile.read_bytes_chunk: max_bytes must be greater than zero.".to_string(),
419 );
420 return FileRequestGuard::inactive(FileRequestKind::Read);
421 }
422 let request_id = next_request_id();
423 PENDING_READ_REQUESTS.with(|requests| {
424 requests.borrow_mut().insert(
425 request_id,
426 PendingReadRequest {
427 on_complete: Rc::new(on_complete),
428 on_error: on_error.map(|handler| Rc::new(handler) as FileErrorCallback),
429 },
430 );
431 });
432 let file_id = self.id();
433 with_utf8(&file_id, |file_id_ptr, file_id_len| unsafe {
434 ffi::fui_file_read_chunk(
435 request_id,
436 file_id_ptr,
437 file_id_len,
438 offset_bytes,
439 max_bytes,
440 );
441 });
442 FileRequestGuard {
443 kind: FileRequestKind::Read,
444 request_id,
445 }
446 }
447}
448
449struct PendingOpenRequest {
450 on_complete: FileOpenCallback,
451 on_error: Option<FileErrorCallback>,
452}
453
454struct PendingReadRequest {
455 on_complete: FileReadCallback,
456 on_error: Option<FileErrorCallback>,
457}
458
459struct PendingSaveRequest {
460 on_complete: FileSaveCallback,
461 on_error: Option<FileErrorCallback>,
462}
463
464struct PendingWriterCreateRequest {
465 on_complete: FileWriterCreatedCallback,
466 on_error: Option<FileErrorCallback>,
467}
468
469struct PendingWriterWriteRequest {
470 on_complete: FileWriteCallback,
471 on_error: Option<FileErrorCallback>,
472}
473
474struct PendingWriterFinishRequest {
475 on_complete: FileSaveCallback,
476 on_error: Option<FileErrorCallback>,
477}
478
479#[derive(Clone, Debug, PartialEq, Eq)]
480pub struct BrowserFileWriter {
481 writer_id: String,
482 pub file_name: String,
483 pub mode: FileSaveMode,
484}
485
486impl BrowserFileWriter {
487 pub fn write_text_chunk(
488 &self,
489 text: impl Into<String>,
490 on_complete: impl Fn(FileWriteProgress) + 'static,
491 ) -> FileRequestGuard {
492 self.write_text_chunk_with_error(text, on_complete, None::<fn(FileErrorEventArgs)>)
493 }
494
495 pub fn write_text_chunk_with_error(
496 &self,
497 text: impl Into<String>,
498 on_complete: impl Fn(FileWriteProgress) + 'static,
499 on_error: Option<impl Fn(FileErrorEventArgs) + 'static>,
500 ) -> FileRequestGuard {
501 let text = text.into();
502 let request_id = next_request_id();
503 PENDING_WRITER_WRITE_REQUESTS.with(|requests| {
504 requests.borrow_mut().insert(
505 request_id,
506 PendingWriterWriteRequest {
507 on_complete: Rc::new(on_complete),
508 on_error: on_error.map(|handler| Rc::new(handler) as FileErrorCallback),
509 },
510 );
511 });
512 with_utf8(&self.writer_id, |writer_id_ptr, writer_id_len| {
513 with_utf8(&text, |text_ptr, text_len| unsafe {
514 ffi::fui_file_writer_write_text(
515 request_id,
516 writer_id_ptr,
517 writer_id_len,
518 text_ptr,
519 text_len,
520 );
521 })
522 });
523 FileRequestGuard {
524 kind: FileRequestKind::WriterWrite,
525 request_id,
526 }
527 }
528
529 pub fn write_bytes_chunk(
530 &self,
531 bytes: &[u8],
532 on_complete: impl Fn(FileWriteProgress) + 'static,
533 ) -> FileRequestGuard {
534 self.write_bytes_chunk_with_error(bytes, on_complete, None::<fn(FileErrorEventArgs)>)
535 }
536
537 pub fn write_bytes_chunk_with_error(
538 &self,
539 bytes: &[u8],
540 on_complete: impl Fn(FileWriteProgress) + 'static,
541 on_error: Option<impl Fn(FileErrorEventArgs) + 'static>,
542 ) -> FileRequestGuard {
543 let owned = bytes.to_vec();
544 let request_id = next_request_id();
545 PENDING_WRITER_WRITE_REQUESTS.with(|requests| {
546 requests.borrow_mut().insert(
547 request_id,
548 PendingWriterWriteRequest {
549 on_complete: Rc::new(on_complete),
550 on_error: on_error.map(|handler| Rc::new(handler) as FileErrorCallback),
551 },
552 );
553 });
554 with_utf8(&self.writer_id, |writer_id_ptr, writer_id_len| unsafe {
555 ffi::fui_file_writer_write_bytes(
556 request_id,
557 writer_id_ptr,
558 writer_id_len,
559 if owned.is_empty() {
560 0
561 } else {
562 owned.as_ptr() as usize
563 },
564 owned.len() as u32,
565 );
566 });
567 FileRequestGuard {
568 kind: FileRequestKind::WriterWrite,
569 request_id,
570 }
571 }
572
573 pub fn finish(&self, on_complete: impl Fn(FileSaveResult) + 'static) -> FileRequestGuard {
574 self.finish_with_error(on_complete, None::<fn(FileErrorEventArgs)>)
575 }
576
577 pub fn finish_with_error(
578 &self,
579 on_complete: impl Fn(FileSaveResult) + 'static,
580 on_error: Option<impl Fn(FileErrorEventArgs) + 'static>,
581 ) -> FileRequestGuard {
582 let request_id = next_request_id();
583 PENDING_WRITER_FINISH_REQUESTS.with(|requests| {
584 requests.borrow_mut().insert(
585 request_id,
586 PendingWriterFinishRequest {
587 on_complete: Rc::new(on_complete),
588 on_error: on_error.map(|handler| Rc::new(handler) as FileErrorCallback),
589 },
590 );
591 });
592 with_utf8(&self.writer_id, |writer_id_ptr, writer_id_len| unsafe {
593 ffi::fui_file_writer_finish(request_id, writer_id_ptr, writer_id_len);
594 });
595 FileRequestGuard {
596 kind: FileRequestKind::WriterFinish,
597 request_id,
598 }
599 }
600}
601
602#[derive(Clone, Debug, Default)]
603pub struct FileOpenRequest {
604 accept: String,
605 multiple: bool,
606}
607
608impl FileOpenRequest {
609 pub fn accept(mut self, value: impl Into<String>) -> Self {
610 self.accept = value.into();
611 self
612 }
613
614 pub fn multiple(mut self, flag: bool) -> Self {
615 self.multiple = flag;
616 self
617 }
618
619 pub fn pick(self, on_complete: impl Fn(FileOpenEventArgs) + 'static) -> FileRequestGuard {
620 self.pick_with_error(on_complete, None::<fn(FileErrorEventArgs)>)
621 }
622
623 pub fn pick_with_error(
624 self,
625 on_complete: impl Fn(FileOpenEventArgs) + 'static,
626 on_error: Option<impl Fn(FileErrorEventArgs) + 'static>,
627 ) -> FileRequestGuard {
628 let request_id = next_request_id();
629 PENDING_OPEN_REQUESTS.with(|requests| {
630 requests.borrow_mut().insert(
631 request_id,
632 PendingOpenRequest {
633 on_complete: Rc::new(on_complete),
634 on_error: on_error.map(|handler| Rc::new(handler) as FileErrorCallback),
635 },
636 );
637 });
638 with_utf8(&self.accept, |accept_ptr, accept_len| unsafe {
639 ffi::fui_file_pick(request_id, accept_ptr, accept_len, self.multiple);
640 });
641 FileRequestGuard {
642 kind: FileRequestKind::Open,
643 request_id,
644 }
645 }
646}
647
648#[derive(Clone, Debug, Default)]
649pub struct FileSaveRequest {
650 suggested_name: String,
651 mime_type: String,
652 file_extension: String,
653}
654
655impl FileSaveRequest {
656 pub fn suggested_name(mut self, value: impl Into<String>) -> Self {
657 self.suggested_name = value.into();
658 self
659 }
660
661 pub fn mime_type(mut self, value: impl Into<String>) -> Self {
662 self.mime_type = value.into();
663 self
664 }
665
666 pub fn file_extension(mut self, value: impl Into<String>) -> Self {
667 self.file_extension = value.into();
668 self
669 }
670
671 pub fn save_text(
672 self,
673 text: impl Into<String>,
674 on_complete: impl Fn(FileSaveResult) + 'static,
675 ) -> FileRequestGuard {
676 self.save_text_with_error(text, on_complete, None::<fn(FileErrorEventArgs)>)
677 }
678
679 pub fn save_text_with_error(
680 self,
681 text: impl Into<String>,
682 on_complete: impl Fn(FileSaveResult) + 'static,
683 on_error: Option<impl Fn(FileErrorEventArgs) + 'static>,
684 ) -> FileRequestGuard {
685 let text = text.into();
686 let request_id = next_request_id();
687 PENDING_SAVE_REQUESTS.with(|requests| {
688 requests.borrow_mut().insert(
689 request_id,
690 PendingSaveRequest {
691 on_complete: Rc::new(on_complete),
692 on_error: on_error.map(|handler| Rc::new(handler) as FileErrorCallback),
693 },
694 );
695 });
696 with_utf8(
697 &self.suggested_name,
698 |suggested_name_ptr, suggested_name_len| {
699 with_utf8(&self.mime_type, |mime_type_ptr, mime_type_len| {
700 with_utf8(
701 &self.file_extension,
702 |file_extension_ptr, file_extension_len| {
703 with_utf8(&text, |text_ptr, text_len| unsafe {
704 ffi::fui_file_save_text(
705 request_id,
706 suggested_name_ptr,
707 suggested_name_len,
708 mime_type_ptr,
709 mime_type_len,
710 file_extension_ptr,
711 file_extension_len,
712 text_ptr,
713 text_len,
714 );
715 })
716 },
717 )
718 })
719 },
720 );
721 FileRequestGuard {
722 kind: FileRequestKind::Save,
723 request_id,
724 }
725 }
726
727 pub fn save_bytes(
728 self,
729 bytes: &[u8],
730 on_complete: impl Fn(FileSaveResult) + 'static,
731 ) -> FileRequestGuard {
732 self.save_bytes_with_error(bytes, on_complete, None::<fn(FileErrorEventArgs)>)
733 }
734
735 pub fn save_bytes_with_error(
736 self,
737 bytes: &[u8],
738 on_complete: impl Fn(FileSaveResult) + 'static,
739 on_error: Option<impl Fn(FileErrorEventArgs) + 'static>,
740 ) -> FileRequestGuard {
741 let owned = bytes.to_vec();
742 let request_id = next_request_id();
743 PENDING_SAVE_REQUESTS.with(|requests| {
744 requests.borrow_mut().insert(
745 request_id,
746 PendingSaveRequest {
747 on_complete: Rc::new(on_complete),
748 on_error: on_error.map(|handler| Rc::new(handler) as FileErrorCallback),
749 },
750 );
751 });
752 with_utf8(
753 &self.suggested_name,
754 |suggested_name_ptr, suggested_name_len| {
755 with_utf8(&self.mime_type, |mime_type_ptr, mime_type_len| {
756 with_utf8(
757 &self.file_extension,
758 |file_extension_ptr, file_extension_len| unsafe {
759 ffi::fui_file_save_bytes(
760 request_id,
761 suggested_name_ptr,
762 suggested_name_len,
763 mime_type_ptr,
764 mime_type_len,
765 file_extension_ptr,
766 file_extension_len,
767 if owned.is_empty() {
768 0
769 } else {
770 owned.as_ptr() as usize
771 },
772 owned.len() as u32,
773 );
774 },
775 )
776 })
777 },
778 );
779 FileRequestGuard {
780 kind: FileRequestKind::Save,
781 request_id,
782 }
783 }
784
785 pub fn create_writer(
786 self,
787 on_complete: impl Fn(BrowserFileWriter) + 'static,
788 ) -> FileRequestGuard {
789 self.create_writer_with_error(on_complete, None::<fn(FileErrorEventArgs)>)
790 }
791
792 pub fn create_writer_with_error(
793 self,
794 on_complete: impl Fn(BrowserFileWriter) + 'static,
795 on_error: Option<impl Fn(FileErrorEventArgs) + 'static>,
796 ) -> FileRequestGuard {
797 let request_id = next_request_id();
798 PENDING_WRITER_CREATE_REQUESTS.with(|requests| {
799 requests.borrow_mut().insert(
800 request_id,
801 PendingWriterCreateRequest {
802 on_complete: Rc::new(on_complete),
803 on_error: on_error.map(|handler| Rc::new(handler) as FileErrorCallback),
804 },
805 );
806 });
807 with_utf8(
808 &self.suggested_name,
809 |suggested_name_ptr, suggested_name_len| {
810 with_utf8(&self.mime_type, |mime_type_ptr, mime_type_len| {
811 with_utf8(
812 &self.file_extension,
813 |file_extension_ptr, file_extension_len| unsafe {
814 ffi::fui_file_create_writer(
815 request_id,
816 suggested_name_ptr,
817 suggested_name_len,
818 mime_type_ptr,
819 mime_type_len,
820 file_extension_ptr,
821 file_extension_len,
822 );
823 },
824 )
825 })
826 },
827 );
828 FileRequestGuard {
829 kind: FileRequestKind::WriterCreate,
830 request_id,
831 }
832 }
833}
834
835#[derive(Default)]
836struct FileWorkerProcessRequestState {
837 file: BrowserFile,
838 worker_wasm_path: String,
839 worker_entry_name: String,
840 suggested_name: String,
841 save_to_picked_file: bool,
842 chunk_bytes: u32,
843 on_chunk: Option<FileWorkerChunkCallback>,
844 on_progress: Option<FileWorkerProgressCallback>,
845 on_complete: Option<FileWorkerCompleteCallback>,
846 on_error: Option<FileErrorCallback>,
847 request_id: u32,
848 started: bool,
849 finished: bool,
850}
851
852#[derive(Clone, Default)]
853pub struct FileWorkerProcessRequest {
854 inner: Rc<RefCell<FileWorkerProcessRequestState>>,
855}
856
857impl FileWorkerProcessRequest {
858 fn new(file: BrowserFile) -> Self {
859 Self {
860 inner: Rc::new(RefCell::new(FileWorkerProcessRequestState {
861 suggested_name: file.name(),
862 file,
863 chunk_bytes: 64 * 1024,
864 ..FileWorkerProcessRequestState::default()
865 })),
866 }
867 }
868
869 pub fn suggested_name(self, value: impl Into<String>) -> Self {
870 self.inner.borrow_mut().suggested_name = value.into();
871 self
872 }
873
874 pub fn worker(self, wasm_path: impl Into<String>, entry_name: impl Into<String>) -> Self {
875 let mut inner = self.inner.borrow_mut();
876 inner.worker_wasm_path = wasm_path.into();
877 inner.worker_entry_name = entry_name.into();
878 drop(inner);
879 self
880 }
881
882 pub fn save_to_picked_file(self, value: impl Into<String>) -> Self {
883 let mut inner = self.inner.borrow_mut();
884 inner.save_to_picked_file = true;
885 inner.suggested_name = value.into();
886 drop(inner);
887 self
888 }
889
890 pub fn chunk_bytes(self, value: u32) -> Self {
891 self.inner.borrow_mut().chunk_bytes = value;
892 self
893 }
894
895 pub fn on_chunk(self, handler: impl Fn(FileReadChunk) + 'static) -> Self {
896 self.inner.borrow_mut().on_chunk = Some(Rc::new(handler));
897 self
898 }
899
900 pub fn on_progress(self, handler: impl Fn(FileWorkerProcessProgress) + 'static) -> Self {
901 self.inner.borrow_mut().on_progress = Some(Rc::new(handler));
902 self
903 }
904
905 pub fn on_complete(self, handler: impl Fn(FileWorkerProcessResult) + 'static) -> Self {
906 self.inner.borrow_mut().on_complete = Some(Rc::new(handler));
907 self
908 }
909
910 pub fn on_error(self, handler: impl Fn(FileErrorEventArgs) + 'static) -> Self {
911 self.inner.borrow_mut().on_error = Some(Rc::new(handler));
912 self
913 }
914
915 pub fn start(self) -> Self {
916 let (
917 request_id,
918 worker_wasm_path,
919 worker_entry_name,
920 file_id,
921 suggested_name,
922 chunk_bytes,
923 save_to_picked_file,
924 on_chunk,
925 on_error,
926 already_finished,
927 already_started,
928 ) = {
929 let inner = self.inner.borrow();
930 (
931 inner.request_id,
932 inner.worker_wasm_path.clone(),
933 inner.worker_entry_name.clone(),
934 inner.file.id(),
935 inner.suggested_name.clone(),
936 inner.chunk_bytes,
937 inner.save_to_picked_file,
938 inner.on_chunk.is_some(),
939 inner.on_error.clone(),
940 inner.finished,
941 inner.started,
942 )
943 };
944 if already_finished {
945 crate::logger::warn(
946 "File",
947 "FileWorkerProcessRequest.start ignored after the worker process already finished.",
948 );
949 return self;
950 }
951 if already_started {
952 crate::logger::warn(
953 "File",
954 "FileWorkerProcessRequest.start ignored because the request already started.",
955 );
956 return self;
957 }
958 if chunk_bytes == 0 {
959 dispatch_file_error(
960 on_error,
961 "FileWorkerProcessRequest.start: chunk_bytes must be greater than zero."
962 .to_string(),
963 );
964 return self;
965 }
966 if worker_wasm_path.is_empty() || worker_entry_name.is_empty() {
967 dispatch_file_error(
968 on_error,
969 "FileWorkerProcessRequest.start: worker(wasm_path, entry_name) is required."
970 .to_string(),
971 );
972 return self;
973 }
974 if !save_to_picked_file && !on_chunk {
975 dispatch_file_error(
976 on_error,
977 "FileWorkerProcessRequest.start: either save_to_picked_file(...) or on_chunk(...) is required.".to_string(),
978 );
979 return self;
980 }
981 let request_id = if request_id == 0 {
982 let request_id = next_request_id();
983 let mut inner = self.inner.borrow_mut();
984 inner.request_id = request_id;
985 inner.started = true;
986 drop(inner);
987 ACTIVE_WORKER_PROCESS_REQUESTS.with(|requests| {
988 requests.borrow_mut().insert(request_id, self.inner.clone());
989 });
990 request_id
991 } else {
992 request_id
993 };
994 with_utf8(
995 &worker_wasm_path,
996 |worker_wasm_path_ptr, worker_wasm_path_len| {
997 with_utf8(&worker_entry_name, |worker_entry_ptr, worker_entry_len| {
998 with_utf8(&file_id, |file_id_ptr, file_id_len| {
999 with_utf8(
1000 &suggested_name,
1001 |suggested_name_ptr, suggested_name_len| unsafe {
1002 ffi::fui_file_process_worker_start(
1003 request_id,
1004 worker_wasm_path_ptr,
1005 worker_wasm_path_len,
1006 worker_entry_ptr,
1007 worker_entry_len,
1008 file_id_ptr,
1009 file_id_len,
1010 suggested_name_ptr,
1011 suggested_name_len,
1012 chunk_bytes,
1013 save_to_picked_file,
1014 );
1015 },
1016 )
1017 })
1018 })
1019 },
1020 );
1021 self
1022 }
1023
1024 pub fn cancel(&self) {
1025 let request_id = {
1026 let mut inner = self.inner.borrow_mut();
1027 if inner.finished {
1028 return;
1029 }
1030 let request_id = inner.request_id;
1031 if request_id == 0 {
1032 inner.finished = true;
1033 return;
1034 }
1035 inner.request_id = 0;
1036 inner.started = false;
1037 inner.finished = true;
1038 inner.on_chunk = None;
1039 inner.on_progress = None;
1040 inner.on_complete = None;
1041 inner.on_error = None;
1042 request_id
1043 };
1044 ACTIVE_WORKER_PROCESS_REQUESTS.with(|requests| {
1045 requests.borrow_mut().remove(&request_id);
1046 });
1047 unsafe {
1048 ffi::fui_file_process_worker_cancel(request_id);
1049 }
1050 }
1051}
1052
1053impl Drop for FileWorkerProcessRequest {
1054 fn drop(&mut self) {
1055 self.cancel();
1056 }
1057}
1058
1059pub struct File;
1060
1061impl File {
1062 pub fn open() -> FileOpenRequest {
1063 FileOpenRequest::default()
1064 }
1065
1066 pub fn save() -> FileSaveRequest {
1067 FileSaveRequest::default()
1068 }
1069
1070 pub fn process_file_in_worker(file: BrowserFile) -> FileWorkerProcessRequest {
1071 FileWorkerProcessRequest::new(file)
1072 }
1073
1074 pub fn capabilities() -> FileCapabilities {
1075 FileCapabilities::from_bits(unsafe { ffi::fui_file_capabilities() })
1076 }
1077
1078 pub fn try_get_file(id: &str) -> Option<BrowserFile> {
1079 BROWSER_FILES.with(|files| {
1080 files
1081 .borrow()
1082 .get(id)
1083 .cloned()
1084 .map(|inner| BrowserFile { inner })
1085 })
1086 }
1087}
1088
1089pub(crate) fn register_browser_file(
1090 id: String,
1091 name: String,
1092 mime_type: Option<String>,
1093 size_bytes: u64,
1094 last_modified_ms: u64,
1095) -> BrowserFile {
1096 BROWSER_FILES.with(|files| {
1097 let mut files = files.borrow_mut();
1098 if let Some(existing) = files.get(&id).cloned() {
1099 let mut existing = existing.borrow_mut();
1100 existing.name = name;
1101 existing.mime_type = mime_type;
1102 existing.size_bytes = size_bytes;
1103 existing.last_modified_ms = last_modified_ms;
1104 drop(existing);
1105 return BrowserFile {
1106 inner: files.get(&id).unwrap().clone(),
1107 };
1108 }
1109 let inner = Rc::new(RefCell::new(BrowserFileState {
1110 id: id.clone(),
1111 name,
1112 mime_type,
1113 size_bytes,
1114 last_modified_ms,
1115 }));
1116 files.insert(id, inner.clone());
1117 BrowserFile { inner }
1118 })
1119}
1120
1121fn finish_worker_process(
1122 request_id: u32,
1123) -> Option<(
1124 Option<FileWorkerCompleteCallback>,
1125 Option<FileErrorCallback>,
1126)> {
1127 let request = ACTIVE_WORKER_PROCESS_REQUESTS
1128 .with(|requests| requests.borrow_mut().remove(&request_id))?;
1129 let mut inner = request.borrow_mut();
1130 if inner.finished {
1131 return None;
1132 }
1133 inner.started = false;
1134 inner.finished = true;
1135 inner.request_id = 0;
1136 let on_complete = inner.on_complete.take();
1137 let on_error = inner.on_error.take();
1138 inner.on_chunk = None;
1139 inner.on_progress = None;
1140 Some((on_complete, on_error))
1141}
1142
1143pub fn reset_file_runtime() {
1144 let active_requests = ACTIVE_WORKER_PROCESS_REQUESTS
1145 .with(|requests| requests.borrow().values().cloned().collect::<Vec<_>>());
1146 for request in active_requests {
1147 FileWorkerProcessRequest { inner: request }.cancel();
1148 }
1149 BROWSER_FILES.with(|files| files.borrow_mut().clear());
1150 PENDING_OPEN_REQUESTS.with(|requests| requests.borrow_mut().clear());
1151 PENDING_READ_REQUESTS.with(|requests| requests.borrow_mut().clear());
1152 PENDING_SAVE_REQUESTS.with(|requests| requests.borrow_mut().clear());
1153 PENDING_WRITER_CREATE_REQUESTS.with(|requests| requests.borrow_mut().clear());
1154 PENDING_WRITER_WRITE_REQUESTS.with(|requests| requests.borrow_mut().clear());
1155 PENDING_WRITER_FINISH_REQUESTS.with(|requests| requests.borrow_mut().clear());
1156 ACTIVE_WORKER_PROCESS_REQUESTS.with(|requests| requests.borrow_mut().clear());
1157 NEXT_FILE_REQUEST_ID.with(|slot| *slot.borrow_mut() = 1);
1158}
1159
1160#[cfg_attr(not(feature = "worker-runtime"), no_mangle)]
1161pub unsafe extern "C" fn __fui_on_file_pick_result(
1164 request_id: u32,
1165 status: u32,
1166 payload_ptr: *const u8,
1167 payload_len: u32,
1168) {
1169 let request = PENDING_OPEN_REQUESTS.with(|requests| requests.borrow_mut().remove(&request_id));
1170 let Some(request) = request else {
1171 return;
1172 };
1173 if status == FILE_STATUS_SUCCESS {
1174 let payload = if payload_ptr.is_null() || payload_len == 0 {
1175 &[][..]
1176 } else {
1177 unsafe { std::slice::from_raw_parts(payload_ptr, payload_len as usize) }
1178 };
1179 let files = decode_file_list_payload(payload);
1180 (request.on_complete)(FileOpenEventArgs { files });
1181 return;
1182 }
1183 dispatch_file_error(
1184 request.on_error,
1185 if payload_ptr.is_null() || payload_len == 0 {
1186 describe_file_failure(status, "File picker failed.")
1187 } else {
1188 read_utf8(payload_ptr, payload_len)
1189 },
1190 );
1191}
1192
1193#[cfg_attr(not(feature = "worker-runtime"), no_mangle)]
1194pub unsafe extern "C" fn __fui_on_file_read_result(
1197 request_id: u32,
1198 status: u32,
1199 offset_bytes: u64,
1200 file_size_bytes: u64,
1201 payload_ptr: *const u8,
1202 payload_len: u32,
1203) {
1204 let request = PENDING_READ_REQUESTS.with(|requests| requests.borrow_mut().remove(&request_id));
1205 let Some(request) = request else {
1206 return;
1207 };
1208 if status == FILE_STATUS_SUCCESS {
1209 let bytes = if payload_ptr.is_null() || payload_len == 0 {
1210 Vec::new()
1211 } else {
1212 unsafe { std::slice::from_raw_parts(payload_ptr, payload_len as usize) }.to_vec()
1213 };
1214 (request.on_complete)(FileReadChunk {
1215 offset_bytes,
1216 file_size_bytes,
1217 bytes,
1218 });
1219 return;
1220 }
1221 dispatch_file_error(
1222 request.on_error,
1223 if payload_ptr.is_null() || payload_len == 0 {
1224 describe_file_failure(status, "File read failed.")
1225 } else {
1226 read_utf8(payload_ptr, payload_len)
1227 },
1228 );
1229}
1230
1231#[cfg_attr(not(feature = "worker-runtime"), no_mangle)]
1232pub unsafe extern "C" fn __fui_on_file_save_result(
1235 request_id: u32,
1236 status: u32,
1237 written_bytes: u64,
1238 payload_ptr: *const u8,
1239 payload_len: u32,
1240) {
1241 let request = PENDING_SAVE_REQUESTS.with(|requests| requests.borrow_mut().remove(&request_id));
1242 let Some(request) = request else {
1243 return;
1244 };
1245 if status == FILE_STATUS_SUCCESS {
1246 let payload = if payload_ptr.is_null() || payload_len == 0 {
1247 &[][..]
1248 } else {
1249 unsafe { std::slice::from_raw_parts(payload_ptr, payload_len as usize) }
1250 };
1251 if let Some((mode, file_name, _)) = decode_writer_payload(payload) {
1252 (request.on_complete)(FileSaveResult {
1253 file_name,
1254 mode,
1255 written_bytes,
1256 });
1257 return;
1258 }
1259 }
1260 dispatch_file_error(
1261 request.on_error,
1262 if payload_ptr.is_null() || payload_len == 0 {
1263 describe_file_failure(status, "File save failed.")
1264 } else {
1265 read_utf8(payload_ptr, payload_len)
1266 },
1267 );
1268}
1269
1270#[cfg_attr(not(feature = "worker-runtime"), no_mangle)]
1271pub unsafe extern "C" fn __fui_on_file_writer_created(
1274 request_id: u32,
1275 status: u32,
1276 payload_ptr: *const u8,
1277 payload_len: u32,
1278) {
1279 let request =
1280 PENDING_WRITER_CREATE_REQUESTS.with(|requests| requests.borrow_mut().remove(&request_id));
1281 let Some(request) = request else {
1282 return;
1283 };
1284 if status == FILE_STATUS_SUCCESS {
1285 let payload = if payload_ptr.is_null() || payload_len == 0 {
1286 &[][..]
1287 } else {
1288 unsafe { std::slice::from_raw_parts(payload_ptr, payload_len as usize) }
1289 };
1290 if let Some((mode, writer_id, Some(file_name))) = decode_writer_payload(payload) {
1291 (request.on_complete)(BrowserFileWriter {
1292 writer_id,
1293 file_name,
1294 mode,
1295 });
1296 return;
1297 }
1298 }
1299 dispatch_file_error(
1300 request.on_error,
1301 if payload_ptr.is_null() || payload_len == 0 {
1302 describe_file_failure(status, "Creating a file writer failed.")
1303 } else {
1304 read_utf8(payload_ptr, payload_len)
1305 },
1306 );
1307}
1308
1309#[cfg_attr(not(feature = "worker-runtime"), no_mangle)]
1310pub unsafe extern "C" fn __fui_on_file_write_result(
1313 request_id: u32,
1314 status: u32,
1315 written_bytes: u64,
1316 total_written_bytes: u64,
1317 payload_ptr: *const u8,
1318 payload_len: u32,
1319) {
1320 let request =
1321 PENDING_WRITER_WRITE_REQUESTS.with(|requests| requests.borrow_mut().remove(&request_id));
1322 let Some(request) = request else {
1323 return;
1324 };
1325 if status == FILE_STATUS_SUCCESS {
1326 (request.on_complete)(FileWriteProgress {
1327 written_bytes,
1328 total_written_bytes,
1329 });
1330 return;
1331 }
1332 dispatch_file_error(
1333 request.on_error,
1334 if payload_ptr.is_null() || payload_len == 0 {
1335 describe_file_failure(status, "File write failed.")
1336 } else {
1337 read_utf8(payload_ptr, payload_len)
1338 },
1339 );
1340}
1341
1342#[cfg_attr(not(feature = "worker-runtime"), no_mangle)]
1343pub unsafe extern "C" fn __fui_on_file_finish_result(
1346 request_id: u32,
1347 status: u32,
1348 written_bytes: u64,
1349 payload_ptr: *const u8,
1350 payload_len: u32,
1351) {
1352 let request =
1353 PENDING_WRITER_FINISH_REQUESTS.with(|requests| requests.borrow_mut().remove(&request_id));
1354 let Some(request) = request else {
1355 return;
1356 };
1357 if status == FILE_STATUS_SUCCESS {
1358 let payload = if payload_ptr.is_null() || payload_len == 0 {
1359 &[][..]
1360 } else {
1361 unsafe { std::slice::from_raw_parts(payload_ptr, payload_len as usize) }
1362 };
1363 if let Some((mode, file_name, _)) = decode_writer_payload(payload) {
1364 (request.on_complete)(FileSaveResult {
1365 file_name,
1366 mode,
1367 written_bytes,
1368 });
1369 return;
1370 }
1371 }
1372 dispatch_file_error(
1373 request.on_error,
1374 if payload_ptr.is_null() || payload_len == 0 {
1375 describe_file_failure(status, "Finishing the file writer failed.")
1376 } else {
1377 read_utf8(payload_ptr, payload_len)
1378 },
1379 );
1380}
1381
1382#[cfg_attr(not(feature = "worker-runtime"), no_mangle)]
1383pub unsafe extern "C" fn __fui_on_file_worker_process_progress(
1386 request_id: u32,
1387 processed_bytes: u64,
1388 total_bytes: u64,
1389 payload_ptr: *const u8,
1390 payload_len: u32,
1391) {
1392 let text = if payload_ptr.is_null() || payload_len == 0 {
1393 String::new()
1394 } else {
1395 read_utf8(payload_ptr, payload_len)
1396 };
1397 let request =
1398 ACTIVE_WORKER_PROCESS_REQUESTS.with(|requests| requests.borrow().get(&request_id).cloned());
1399 let Some(request) = request else {
1400 return;
1401 };
1402 let callback = request.borrow().on_progress.clone();
1403 if let Some(callback) = callback {
1404 callback(FileWorkerProcessProgress {
1405 processed_bytes,
1406 total_bytes,
1407 output_file_name: if text.is_empty() { None } else { Some(text) },
1408 });
1409 }
1410}
1411
1412#[cfg_attr(not(feature = "worker-runtime"), no_mangle)]
1413pub unsafe extern "C" fn __fui_on_file_worker_process_chunk(
1416 request_id: u32,
1417 offset_bytes: u64,
1418 file_size_bytes: u64,
1419 payload_ptr: *const u8,
1420 payload_len: u32,
1421) {
1422 let request =
1423 ACTIVE_WORKER_PROCESS_REQUESTS.with(|requests| requests.borrow().get(&request_id).cloned());
1424 let Some(request) = request else {
1425 return;
1426 };
1427 let callback = request.borrow().on_chunk.clone();
1428 if let Some(callback) = callback {
1429 let bytes = if payload_ptr.is_null() || payload_len == 0 {
1430 Vec::new()
1431 } else {
1432 unsafe { std::slice::from_raw_parts(payload_ptr, payload_len as usize) }.to_vec()
1433 };
1434 callback(FileReadChunk {
1435 offset_bytes,
1436 file_size_bytes,
1437 bytes,
1438 });
1439 }
1440}
1441
1442#[cfg_attr(not(feature = "worker-runtime"), no_mangle)]
1443pub unsafe extern "C" fn __fui_on_file_worker_process_complete(
1446 request_id: u32,
1447 processed_bytes: u64,
1448 payload_ptr: *const u8,
1449 payload_len: u32,
1450) {
1451 let text = if payload_ptr.is_null() || payload_len == 0 {
1452 String::new()
1453 } else {
1454 read_utf8(payload_ptr, payload_len)
1455 };
1456 let (output_file_name, worker_result) = split_worker_process_complete_payload(text);
1457 let Some((on_complete, _)) = finish_worker_process(request_id) else {
1458 return;
1459 };
1460 if let Some(callback) = on_complete {
1461 callback(FileWorkerProcessResult {
1462 processed_bytes,
1463 output_file_name,
1464 worker_result,
1465 });
1466 }
1467}
1468
1469#[cfg_attr(not(feature = "worker-runtime"), no_mangle)]
1470pub unsafe extern "C" fn __fui_on_file_worker_process_error(
1473 request_id: u32,
1474 status: u32,
1475 payload_ptr: *const u8,
1476 payload_len: u32,
1477) {
1478 let message = if payload_ptr.is_null() || payload_len == 0 {
1479 describe_file_failure(status, "Worker file processing failed.")
1480 } else {
1481 read_utf8(payload_ptr, payload_len)
1482 };
1483 let Some((_, on_error)) = finish_worker_process(request_id) else {
1484 return;
1485 };
1486 dispatch_file_error(on_error, message);
1487}
1488
1489#[cfg(test)]
1490mod tests {
1491 use super::*;
1492 use crate::ffi::{self, Call};
1493 use std::cell::RefCell;
1494 use std::rc::Rc;
1495
1496 fn writer_payload(mode: u32, first: &str, second: Option<&str>) -> Vec<u8> {
1497 let mut bytes = Vec::new();
1498 bytes.extend_from_slice(&mode.to_le_bytes());
1499 bytes.extend_from_slice(&(first.len() as u32).to_le_bytes());
1500 bytes.extend_from_slice(first.as_bytes());
1501 if let Some(second) = second {
1502 bytes.extend_from_slice(&(second.len() as u32).to_le_bytes());
1503 bytes.extend_from_slice(second.as_bytes());
1504 }
1505 bytes
1506 }
1507
1508 #[test]
1509 fn open_request_emits_host_call_and_pick_result_registers_files() {
1510 reset_file_runtime();
1511 ffi::test::reset();
1512 let picked = Rc::new(RefCell::new(Vec::<BrowserFile>::new()));
1513 let picked_clone = picked.clone();
1514 let _guard = File::open()
1515 .accept(".txt")
1516 .multiple(true)
1517 .pick(move |event| {
1518 picked_clone.replace(event.files);
1519 });
1520 let calls = ffi::test::take_calls();
1521 assert!(calls.iter().any(|call| matches!(call, Call::FilePick { request_id, accept, multiple } if *request_id == 1 && accept == ".txt" && *multiple)));
1522
1523 let mut payload = Vec::new();
1524 payload.extend_from_slice(&1u32.to_le_bytes());
1525 payload.extend_from_slice(&5u32.to_le_bytes());
1526 payload.extend_from_slice(b"file-");
1527 payload.extend_from_slice(&12u64.to_le_bytes());
1528 payload.extend_from_slice(&34u64.to_le_bytes());
1529 payload.extend_from_slice(&8u32.to_le_bytes());
1530 payload.extend_from_slice(b"note.txt");
1531 payload.extend_from_slice(&10u32.to_le_bytes());
1532 payload.extend_from_slice(b"text/plain");
1533 unsafe {
1534 super::__fui_on_file_pick_result(
1535 1,
1536 FILE_STATUS_SUCCESS,
1537 payload.as_ptr(),
1538 payload.len() as u32,
1539 );
1540 }
1541
1542 let picked = picked.borrow();
1543 assert_eq!(picked.len(), 1);
1544 assert_eq!(picked[0].name(), "note.txt");
1545 assert_eq!(picked[0].mime_type(), Some("text/plain".to_string()));
1546 assert_eq!(picked[0].size_bytes(), 12);
1547 }
1548
1549 #[test]
1550 fn browser_file_read_chunk_emits_host_call_and_callback_receives_bytes() {
1551 reset_file_runtime();
1552 ffi::test::reset();
1553 let file =
1554 register_browser_file("picked-1".to_string(), "demo.bin".to_string(), None, 100, 0);
1555 let chunk = Rc::new(RefCell::new(None::<FileReadChunk>));
1556 let chunk_clone = chunk.clone();
1557 let _guard = file.read_bytes_chunk(10, 8, move |value| {
1558 chunk_clone.replace(Some(value));
1559 });
1560 let calls = ffi::test::take_calls();
1561 assert!(calls.iter().any(|call| matches!(call, Call::FileReadChunk { request_id, file_id, offset_bytes, max_bytes } if *request_id == 1 && file_id == "picked-1" && *offset_bytes == 10 && *max_bytes == 8)));
1562
1563 let bytes = b"1234".to_vec();
1564 unsafe {
1565 super::__fui_on_file_read_result(
1566 1,
1567 FILE_STATUS_SUCCESS,
1568 10,
1569 100,
1570 bytes.as_ptr(),
1571 bytes.len() as u32,
1572 );
1573 }
1574 let chunk = chunk.borrow().clone().expect("chunk");
1575 assert_eq!(chunk.offset_bytes, 10);
1576 assert_eq!(chunk.file_size_bytes, 100);
1577 assert_eq!(chunk.bytes, b"1234");
1578 }
1579
1580 #[test]
1581 fn save_text_emits_host_call() {
1582 reset_file_runtime();
1583 ffi::test::reset();
1584 let _guard = File::save()
1585 .suggested_name("report")
1586 .mime_type("text/plain")
1587 .file_extension(".txt")
1588 .save_text("hello", |_| {});
1589 let calls = ffi::test::take_calls();
1590 assert!(calls.iter().any(|call| matches!(call, Call::FileSaveText { request_id, suggested_name, mime_type, file_extension, text } if *request_id == 1 && suggested_name == "report" && mime_type == "text/plain" && file_extension == ".txt" && text == "hello")));
1591 }
1592
1593 #[test]
1594 fn writer_created_and_finish_callbacks_decode_payload() {
1595 reset_file_runtime();
1596 ffi::test::reset();
1597 let writer = Rc::new(RefCell::new(None::<BrowserFileWriter>));
1598 let writer_clone = writer.clone();
1599 let _guard = File::save()
1600 .suggested_name("report")
1601 .create_writer(move |value| {
1602 writer_clone.replace(Some(value));
1603 });
1604 let payload = writer_payload(
1605 FileSaveMode::NativePicker as u32,
1606 "writer-1",
1607 Some("report.txt"),
1608 );
1609 unsafe {
1610 super::__fui_on_file_writer_created(
1611 1,
1612 FILE_STATUS_SUCCESS,
1613 payload.as_ptr(),
1614 payload.len() as u32,
1615 );
1616 }
1617 let writer = writer.borrow().clone().expect("writer");
1618 assert_eq!(writer.file_name, "report.txt");
1619 assert_eq!(writer.mode, FileSaveMode::NativePicker);
1620
1621 ffi::test::reset();
1622 let finished = Rc::new(RefCell::new(None::<FileSaveResult>));
1623 let finished_clone = finished.clone();
1624 let _guard = writer.finish(move |value| {
1625 finished_clone.replace(Some(value));
1626 });
1627 let calls = ffi::test::take_calls();
1628 assert!(calls.iter().any(|call| matches!(call, Call::FileWriterFinish { request_id, writer_id } if *request_id == 2 && writer_id == "writer-1")));
1629 let payload = writer_payload(FileSaveMode::NativePicker as u32, "report.txt", None);
1630 unsafe {
1631 super::__fui_on_file_finish_result(
1632 2,
1633 FILE_STATUS_SUCCESS,
1634 44,
1635 payload.as_ptr(),
1636 payload.len() as u32,
1637 );
1638 }
1639 let finished = finished.borrow().clone().expect("finish");
1640 assert_eq!(finished.file_name, "report.txt");
1641 assert_eq!(finished.written_bytes, 44);
1642 }
1643
1644 #[test]
1645 fn worker_process_requires_worker_and_sink() {
1646 reset_file_runtime();
1647 ffi::test::reset();
1648 let file =
1649 register_browser_file("picked-1".to_string(), "demo.bin".to_string(), None, 100, 0);
1650 let error = Rc::new(RefCell::new(String::new()));
1651 let error_clone = error.clone();
1652 let _request = File::process_file_in_worker(file)
1653 .on_error(move |event| {
1654 error_clone.replace(event.message);
1655 })
1656 .start();
1657 assert!(error
1658 .borrow()
1659 .contains("worker(wasm_path, entry_name) is required"));
1660 }
1661
1662 #[test]
1663 fn worker_process_start_emits_host_call_and_callbacks_receive_payloads() {
1664 reset_file_runtime();
1665 ffi::test::reset();
1666 let file =
1667 register_browser_file("picked-1".to_string(), "demo.bin".to_string(), None, 100, 0);
1668 let progress = Rc::new(RefCell::new(None::<FileWorkerProcessProgress>));
1669 let result = Rc::new(RefCell::new(None::<FileWorkerProcessResult>));
1670 let progress_clone = progress.clone();
1671 let result_clone = result.clone();
1672 let _request = File::process_file_in_worker(file)
1673 .worker("./workers/file_worker.wasm", "entry")
1674 .save_to_picked_file("copy.bin")
1675 .on_progress(move |value| {
1676 progress_clone.replace(Some(value));
1677 })
1678 .on_complete(move |value| {
1679 result_clone.replace(Some(value));
1680 })
1681 .start();
1682 let calls = ffi::test::take_calls();
1683 assert!(calls.iter().any(|call| matches!(call, Call::FileProcessWorkerStart { request_id, worker_wasm_path, worker_entry_name, file_id, suggested_name, chunk_bytes, save_to_picked_file } if *request_id == 1 && worker_wasm_path == "./workers/file_worker.wasm" && worker_entry_name == "entry" && file_id == "picked-1" && suggested_name == "copy.bin" && *chunk_bytes == 64 * 1024 && *save_to_picked_file)));
1684
1685 unsafe {
1686 super::__fui_on_file_worker_process_progress(1, 20, 100, b"copy.bin".as_ptr(), 8);
1687 }
1688 let progress = progress.borrow().clone().expect("progress");
1689 assert_eq!(progress.processed_bytes, 20);
1690 assert_eq!(progress.output_file_name, Some("copy.bin".to_string()));
1691
1692 let payload = b"copy.bin\0sha256".to_vec();
1693 unsafe {
1694 super::__fui_on_file_worker_process_complete(
1695 1,
1696 100,
1697 payload.as_ptr(),
1698 payload.len() as u32,
1699 );
1700 }
1701 let result = result.borrow().clone().expect("result");
1702 assert_eq!(result.processed_bytes, 100);
1703 assert_eq!(result.output_file_name, Some("copy.bin".to_string()));
1704 assert_eq!(result.worker_result, Some("sha256".to_string()));
1705 }
1706}