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(
1161 any(not(feature = "worker-runtime"), feature = "native-runtime"),
1162 no_mangle
1163)]
1164pub unsafe extern "C" fn __fui_on_file_pick_result(
1167 request_id: u32,
1168 status: u32,
1169 payload_ptr: *const u8,
1170 payload_len: u32,
1171) {
1172 let request = PENDING_OPEN_REQUESTS.with(|requests| requests.borrow_mut().remove(&request_id));
1173 let Some(request) = request else {
1174 return;
1175 };
1176 if status == FILE_STATUS_SUCCESS {
1177 let payload = if payload_ptr.is_null() || payload_len == 0 {
1178 &[][..]
1179 } else {
1180 unsafe { std::slice::from_raw_parts(payload_ptr, payload_len as usize) }
1181 };
1182 let files = decode_file_list_payload(payload);
1183 (request.on_complete)(FileOpenEventArgs { files });
1184 return;
1185 }
1186 dispatch_file_error(
1187 request.on_error,
1188 if payload_ptr.is_null() || payload_len == 0 {
1189 describe_file_failure(status, "File picker failed.")
1190 } else {
1191 read_utf8(payload_ptr, payload_len)
1192 },
1193 );
1194}
1195
1196#[cfg_attr(
1197 any(not(feature = "worker-runtime"), feature = "native-runtime"),
1198 no_mangle
1199)]
1200pub unsafe extern "C" fn __fui_on_file_read_result(
1203 request_id: u32,
1204 status: u32,
1205 offset_bytes: u64,
1206 file_size_bytes: u64,
1207 payload_ptr: *const u8,
1208 payload_len: u32,
1209) {
1210 let request = PENDING_READ_REQUESTS.with(|requests| requests.borrow_mut().remove(&request_id));
1211 let Some(request) = request else {
1212 return;
1213 };
1214 if status == FILE_STATUS_SUCCESS {
1215 let bytes = if payload_ptr.is_null() || payload_len == 0 {
1216 Vec::new()
1217 } else {
1218 unsafe { std::slice::from_raw_parts(payload_ptr, payload_len as usize) }.to_vec()
1219 };
1220 (request.on_complete)(FileReadChunk {
1221 offset_bytes,
1222 file_size_bytes,
1223 bytes,
1224 });
1225 return;
1226 }
1227 dispatch_file_error(
1228 request.on_error,
1229 if payload_ptr.is_null() || payload_len == 0 {
1230 describe_file_failure(status, "File read failed.")
1231 } else {
1232 read_utf8(payload_ptr, payload_len)
1233 },
1234 );
1235}
1236
1237#[cfg_attr(
1238 any(not(feature = "worker-runtime"), feature = "native-runtime"),
1239 no_mangle
1240)]
1241pub unsafe extern "C" fn __fui_on_file_save_result(
1244 request_id: u32,
1245 status: u32,
1246 written_bytes: u64,
1247 payload_ptr: *const u8,
1248 payload_len: u32,
1249) {
1250 let request = PENDING_SAVE_REQUESTS.with(|requests| requests.borrow_mut().remove(&request_id));
1251 let Some(request) = request else {
1252 return;
1253 };
1254 if status == FILE_STATUS_SUCCESS {
1255 let payload = if payload_ptr.is_null() || payload_len == 0 {
1256 &[][..]
1257 } else {
1258 unsafe { std::slice::from_raw_parts(payload_ptr, payload_len as usize) }
1259 };
1260 if let Some((mode, file_name, _)) = decode_writer_payload(payload) {
1261 (request.on_complete)(FileSaveResult {
1262 file_name,
1263 mode,
1264 written_bytes,
1265 });
1266 return;
1267 }
1268 }
1269 dispatch_file_error(
1270 request.on_error,
1271 if payload_ptr.is_null() || payload_len == 0 {
1272 describe_file_failure(status, "File save failed.")
1273 } else {
1274 read_utf8(payload_ptr, payload_len)
1275 },
1276 );
1277}
1278
1279#[cfg_attr(
1280 any(not(feature = "worker-runtime"), feature = "native-runtime"),
1281 no_mangle
1282)]
1283pub unsafe extern "C" fn __fui_on_file_writer_created(
1286 request_id: u32,
1287 status: u32,
1288 payload_ptr: *const u8,
1289 payload_len: u32,
1290) {
1291 let request =
1292 PENDING_WRITER_CREATE_REQUESTS.with(|requests| requests.borrow_mut().remove(&request_id));
1293 let Some(request) = request else {
1294 return;
1295 };
1296 if status == FILE_STATUS_SUCCESS {
1297 let payload = if payload_ptr.is_null() || payload_len == 0 {
1298 &[][..]
1299 } else {
1300 unsafe { std::slice::from_raw_parts(payload_ptr, payload_len as usize) }
1301 };
1302 if let Some((mode, writer_id, Some(file_name))) = decode_writer_payload(payload) {
1303 (request.on_complete)(BrowserFileWriter {
1304 writer_id,
1305 file_name,
1306 mode,
1307 });
1308 return;
1309 }
1310 }
1311 dispatch_file_error(
1312 request.on_error,
1313 if payload_ptr.is_null() || payload_len == 0 {
1314 describe_file_failure(status, "Creating a file writer failed.")
1315 } else {
1316 read_utf8(payload_ptr, payload_len)
1317 },
1318 );
1319}
1320
1321#[cfg_attr(
1322 any(not(feature = "worker-runtime"), feature = "native-runtime"),
1323 no_mangle
1324)]
1325pub unsafe extern "C" fn __fui_on_file_write_result(
1328 request_id: u32,
1329 status: u32,
1330 written_bytes: u64,
1331 total_written_bytes: u64,
1332 payload_ptr: *const u8,
1333 payload_len: u32,
1334) {
1335 let request =
1336 PENDING_WRITER_WRITE_REQUESTS.with(|requests| requests.borrow_mut().remove(&request_id));
1337 let Some(request) = request else {
1338 return;
1339 };
1340 if status == FILE_STATUS_SUCCESS {
1341 (request.on_complete)(FileWriteProgress {
1342 written_bytes,
1343 total_written_bytes,
1344 });
1345 return;
1346 }
1347 dispatch_file_error(
1348 request.on_error,
1349 if payload_ptr.is_null() || payload_len == 0 {
1350 describe_file_failure(status, "File write failed.")
1351 } else {
1352 read_utf8(payload_ptr, payload_len)
1353 },
1354 );
1355}
1356
1357#[cfg_attr(
1358 any(not(feature = "worker-runtime"), feature = "native-runtime"),
1359 no_mangle
1360)]
1361pub unsafe extern "C" fn __fui_on_file_finish_result(
1364 request_id: u32,
1365 status: u32,
1366 written_bytes: u64,
1367 payload_ptr: *const u8,
1368 payload_len: u32,
1369) {
1370 let request =
1371 PENDING_WRITER_FINISH_REQUESTS.with(|requests| requests.borrow_mut().remove(&request_id));
1372 let Some(request) = request else {
1373 return;
1374 };
1375 if status == FILE_STATUS_SUCCESS {
1376 let payload = if payload_ptr.is_null() || payload_len == 0 {
1377 &[][..]
1378 } else {
1379 unsafe { std::slice::from_raw_parts(payload_ptr, payload_len as usize) }
1380 };
1381 if let Some((mode, file_name, _)) = decode_writer_payload(payload) {
1382 (request.on_complete)(FileSaveResult {
1383 file_name,
1384 mode,
1385 written_bytes,
1386 });
1387 return;
1388 }
1389 }
1390 dispatch_file_error(
1391 request.on_error,
1392 if payload_ptr.is_null() || payload_len == 0 {
1393 describe_file_failure(status, "Finishing the file writer failed.")
1394 } else {
1395 read_utf8(payload_ptr, payload_len)
1396 },
1397 );
1398}
1399
1400#[cfg_attr(
1401 any(not(feature = "worker-runtime"), feature = "native-runtime"),
1402 no_mangle
1403)]
1404pub unsafe extern "C" fn __fui_on_file_worker_process_progress(
1407 request_id: u32,
1408 processed_bytes: u64,
1409 total_bytes: u64,
1410 payload_ptr: *const u8,
1411 payload_len: u32,
1412) {
1413 let text = if payload_ptr.is_null() || payload_len == 0 {
1414 String::new()
1415 } else {
1416 read_utf8(payload_ptr, payload_len)
1417 };
1418 let request =
1419 ACTIVE_WORKER_PROCESS_REQUESTS.with(|requests| requests.borrow().get(&request_id).cloned());
1420 let Some(request) = request else {
1421 return;
1422 };
1423 let callback = request.borrow().on_progress.clone();
1424 if let Some(callback) = callback {
1425 callback(FileWorkerProcessProgress {
1426 processed_bytes,
1427 total_bytes,
1428 output_file_name: if text.is_empty() { None } else { Some(text) },
1429 });
1430 }
1431}
1432
1433#[cfg_attr(
1434 any(not(feature = "worker-runtime"), feature = "native-runtime"),
1435 no_mangle
1436)]
1437pub unsafe extern "C" fn __fui_on_file_worker_process_chunk(
1440 request_id: u32,
1441 offset_bytes: u64,
1442 file_size_bytes: u64,
1443 payload_ptr: *const u8,
1444 payload_len: u32,
1445) {
1446 let request =
1447 ACTIVE_WORKER_PROCESS_REQUESTS.with(|requests| requests.borrow().get(&request_id).cloned());
1448 let Some(request) = request else {
1449 return;
1450 };
1451 let callback = request.borrow().on_chunk.clone();
1452 if let Some(callback) = callback {
1453 let bytes = if payload_ptr.is_null() || payload_len == 0 {
1454 Vec::new()
1455 } else {
1456 unsafe { std::slice::from_raw_parts(payload_ptr, payload_len as usize) }.to_vec()
1457 };
1458 callback(FileReadChunk {
1459 offset_bytes,
1460 file_size_bytes,
1461 bytes,
1462 });
1463 }
1464}
1465
1466#[cfg_attr(
1467 any(not(feature = "worker-runtime"), feature = "native-runtime"),
1468 no_mangle
1469)]
1470pub unsafe extern "C" fn __fui_on_file_worker_process_complete(
1473 request_id: u32,
1474 processed_bytes: u64,
1475 payload_ptr: *const u8,
1476 payload_len: u32,
1477) {
1478 let text = if payload_ptr.is_null() || payload_len == 0 {
1479 String::new()
1480 } else {
1481 read_utf8(payload_ptr, payload_len)
1482 };
1483 let (output_file_name, worker_result) = split_worker_process_complete_payload(text);
1484 let Some((on_complete, _)) = finish_worker_process(request_id) else {
1485 return;
1486 };
1487 if let Some(callback) = on_complete {
1488 callback(FileWorkerProcessResult {
1489 processed_bytes,
1490 output_file_name,
1491 worker_result,
1492 });
1493 }
1494}
1495
1496#[cfg_attr(
1497 any(not(feature = "worker-runtime"), feature = "native-runtime"),
1498 no_mangle
1499)]
1500pub unsafe extern "C" fn __fui_on_file_worker_process_error(
1503 request_id: u32,
1504 status: u32,
1505 payload_ptr: *const u8,
1506 payload_len: u32,
1507) {
1508 let message = if payload_ptr.is_null() || payload_len == 0 {
1509 describe_file_failure(status, "Worker file processing failed.")
1510 } else {
1511 read_utf8(payload_ptr, payload_len)
1512 };
1513 let Some((_, on_error)) = finish_worker_process(request_id) else {
1514 return;
1515 };
1516 dispatch_file_error(on_error, message);
1517}
1518
1519#[cfg(test)]
1520mod tests {
1521 use super::*;
1522 use crate::ffi::{self, Call};
1523 use std::cell::RefCell;
1524 use std::rc::Rc;
1525
1526 fn writer_payload(mode: u32, first: &str, second: Option<&str>) -> Vec<u8> {
1527 let mut bytes = Vec::new();
1528 bytes.extend_from_slice(&mode.to_le_bytes());
1529 bytes.extend_from_slice(&(first.len() as u32).to_le_bytes());
1530 bytes.extend_from_slice(first.as_bytes());
1531 if let Some(second) = second {
1532 bytes.extend_from_slice(&(second.len() as u32).to_le_bytes());
1533 bytes.extend_from_slice(second.as_bytes());
1534 }
1535 bytes
1536 }
1537
1538 #[test]
1539 fn open_request_emits_host_call_and_pick_result_registers_files() {
1540 reset_file_runtime();
1541 ffi::test::reset();
1542 let picked = Rc::new(RefCell::new(Vec::<BrowserFile>::new()));
1543 let picked_clone = picked.clone();
1544 let _guard = File::open()
1545 .accept(".txt")
1546 .multiple(true)
1547 .pick(move |event| {
1548 picked_clone.replace(event.files);
1549 });
1550 let calls = ffi::test::take_calls();
1551 assert!(calls.iter().any(|call| matches!(call, Call::FilePick { request_id, accept, multiple } if *request_id == 1 && accept == ".txt" && *multiple)));
1552
1553 let mut payload = Vec::new();
1554 payload.extend_from_slice(&1u32.to_le_bytes());
1555 payload.extend_from_slice(&5u32.to_le_bytes());
1556 payload.extend_from_slice(b"file-");
1557 payload.extend_from_slice(&12u64.to_le_bytes());
1558 payload.extend_from_slice(&34u64.to_le_bytes());
1559 payload.extend_from_slice(&8u32.to_le_bytes());
1560 payload.extend_from_slice(b"note.txt");
1561 payload.extend_from_slice(&10u32.to_le_bytes());
1562 payload.extend_from_slice(b"text/plain");
1563 unsafe {
1564 super::__fui_on_file_pick_result(
1565 1,
1566 FILE_STATUS_SUCCESS,
1567 payload.as_ptr(),
1568 payload.len() as u32,
1569 );
1570 }
1571
1572 let picked = picked.borrow();
1573 assert_eq!(picked.len(), 1);
1574 assert_eq!(picked[0].name(), "note.txt");
1575 assert_eq!(picked[0].mime_type(), Some("text/plain".to_string()));
1576 assert_eq!(picked[0].size_bytes(), 12);
1577 }
1578
1579 #[test]
1580 fn browser_file_read_chunk_emits_host_call_and_callback_receives_bytes() {
1581 reset_file_runtime();
1582 ffi::test::reset();
1583 let file =
1584 register_browser_file("picked-1".to_string(), "demo.bin".to_string(), None, 100, 0);
1585 let chunk = Rc::new(RefCell::new(None::<FileReadChunk>));
1586 let chunk_clone = chunk.clone();
1587 let _guard = file.read_bytes_chunk(10, 8, move |value| {
1588 chunk_clone.replace(Some(value));
1589 });
1590 let calls = ffi::test::take_calls();
1591 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)));
1592
1593 let bytes = b"1234".to_vec();
1594 unsafe {
1595 super::__fui_on_file_read_result(
1596 1,
1597 FILE_STATUS_SUCCESS,
1598 10,
1599 100,
1600 bytes.as_ptr(),
1601 bytes.len() as u32,
1602 );
1603 }
1604 let chunk = chunk.borrow().clone().expect("chunk");
1605 assert_eq!(chunk.offset_bytes, 10);
1606 assert_eq!(chunk.file_size_bytes, 100);
1607 assert_eq!(chunk.bytes, b"1234");
1608 }
1609
1610 #[test]
1611 fn save_text_emits_host_call() {
1612 reset_file_runtime();
1613 ffi::test::reset();
1614 let _guard = File::save()
1615 .suggested_name("report")
1616 .mime_type("text/plain")
1617 .file_extension(".txt")
1618 .save_text("hello", |_| {});
1619 let calls = ffi::test::take_calls();
1620 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")));
1621 }
1622
1623 #[test]
1624 fn writer_created_and_finish_callbacks_decode_payload() {
1625 reset_file_runtime();
1626 ffi::test::reset();
1627 let writer = Rc::new(RefCell::new(None::<BrowserFileWriter>));
1628 let writer_clone = writer.clone();
1629 let _guard = File::save()
1630 .suggested_name("report")
1631 .create_writer(move |value| {
1632 writer_clone.replace(Some(value));
1633 });
1634 let payload = writer_payload(
1635 FileSaveMode::NativePicker as u32,
1636 "writer-1",
1637 Some("report.txt"),
1638 );
1639 unsafe {
1640 super::__fui_on_file_writer_created(
1641 1,
1642 FILE_STATUS_SUCCESS,
1643 payload.as_ptr(),
1644 payload.len() as u32,
1645 );
1646 }
1647 let writer = writer.borrow().clone().expect("writer");
1648 assert_eq!(writer.file_name, "report.txt");
1649 assert_eq!(writer.mode, FileSaveMode::NativePicker);
1650
1651 ffi::test::reset();
1652 let finished = Rc::new(RefCell::new(None::<FileSaveResult>));
1653 let finished_clone = finished.clone();
1654 let _guard = writer.finish(move |value| {
1655 finished_clone.replace(Some(value));
1656 });
1657 let calls = ffi::test::take_calls();
1658 assert!(calls.iter().any(|call| matches!(call, Call::FileWriterFinish { request_id, writer_id } if *request_id == 2 && writer_id == "writer-1")));
1659 let payload = writer_payload(FileSaveMode::NativePicker as u32, "report.txt", None);
1660 unsafe {
1661 super::__fui_on_file_finish_result(
1662 2,
1663 FILE_STATUS_SUCCESS,
1664 44,
1665 payload.as_ptr(),
1666 payload.len() as u32,
1667 );
1668 }
1669 let finished = finished.borrow().clone().expect("finish");
1670 assert_eq!(finished.file_name, "report.txt");
1671 assert_eq!(finished.written_bytes, 44);
1672 }
1673
1674 #[test]
1675 fn worker_process_requires_worker_and_sink() {
1676 reset_file_runtime();
1677 ffi::test::reset();
1678 let file =
1679 register_browser_file("picked-1".to_string(), "demo.bin".to_string(), None, 100, 0);
1680 let error = Rc::new(RefCell::new(String::new()));
1681 let error_clone = error.clone();
1682 let _request = File::process_file_in_worker(file)
1683 .on_error(move |event| {
1684 error_clone.replace(event.message);
1685 })
1686 .start();
1687 assert!(error
1688 .borrow()
1689 .contains("worker(wasm_path, entry_name) is required"));
1690 }
1691
1692 #[test]
1693 fn worker_process_start_emits_host_call_and_callbacks_receive_payloads() {
1694 reset_file_runtime();
1695 ffi::test::reset();
1696 let file =
1697 register_browser_file("picked-1".to_string(), "demo.bin".to_string(), None, 100, 0);
1698 let progress = Rc::new(RefCell::new(None::<FileWorkerProcessProgress>));
1699 let result = Rc::new(RefCell::new(None::<FileWorkerProcessResult>));
1700 let progress_clone = progress.clone();
1701 let result_clone = result.clone();
1702 let _request = File::process_file_in_worker(file)
1703 .worker("./workers/file_worker.wasm", "entry")
1704 .save_to_picked_file("copy.bin")
1705 .on_progress(move |value| {
1706 progress_clone.replace(Some(value));
1707 })
1708 .on_complete(move |value| {
1709 result_clone.replace(Some(value));
1710 })
1711 .start();
1712 let calls = ffi::test::take_calls();
1713 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)));
1714
1715 unsafe {
1716 super::__fui_on_file_worker_process_progress(1, 20, 100, b"copy.bin".as_ptr(), 8);
1717 }
1718 let progress = progress.borrow().clone().expect("progress");
1719 assert_eq!(progress.processed_bytes, 20);
1720 assert_eq!(progress.output_file_name, Some("copy.bin".to_string()));
1721
1722 let payload = b"copy.bin\0sha256".to_vec();
1723 unsafe {
1724 super::__fui_on_file_worker_process_complete(
1725 1,
1726 100,
1727 payload.as_ptr(),
1728 payload.len() as u32,
1729 );
1730 }
1731 let result = result.borrow().clone().expect("result");
1732 assert_eq!(result.processed_bytes, 100);
1733 assert_eq!(result.output_file_name, Some("copy.bin".to_string()));
1734 assert_eq!(result.worker_result, Some("sha256".to_string()));
1735 }
1736}