Skip to main content

fui/
file.rs

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)]
1161/// # Safety
1162/// `payload_ptr` must be null for an empty payload or point to `payload_len` readable bytes.
1163pub 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)]
1194/// # Safety
1195/// `payload_ptr` must be null for an empty payload or point to `payload_len` readable bytes.
1196pub 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)]
1232/// # Safety
1233/// `payload_ptr` must be null for an empty payload or point to `payload_len` readable bytes.
1234pub 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)]
1271/// # Safety
1272/// `payload_ptr` must be null for an empty payload or point to `payload_len` readable bytes.
1273pub 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)]
1310/// # Safety
1311/// `payload_ptr` must be null for an empty payload or point to `payload_len` readable bytes.
1312pub 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)]
1343/// # Safety
1344/// `payload_ptr` must be null for an empty payload or point to `payload_len` readable bytes.
1345pub 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)]
1383/// # Safety
1384/// `payload_ptr` must be null for an empty payload or point to `payload_len` readable bytes.
1385pub 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)]
1413/// # Safety
1414/// `payload_ptr` must be null for an empty payload or point to `payload_len` readable bytes.
1415pub 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)]
1443/// # Safety
1444/// `payload_ptr` must be null for an empty payload or point to `payload_len` readable bytes.
1445pub 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)]
1470/// # Safety
1471/// `payload_ptr` must be null for an empty payload or point to `payload_len` readable bytes.
1472pub 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}