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(
1161    any(not(feature = "worker-runtime"), feature = "native-runtime"),
1162    no_mangle
1163)]
1164/// # Safety
1165/// `payload_ptr` must be null for an empty payload or point to `payload_len` readable bytes.
1166pub 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)]
1200/// # Safety
1201/// `payload_ptr` must be null for an empty payload or point to `payload_len` readable bytes.
1202pub 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)]
1241/// # Safety
1242/// `payload_ptr` must be null for an empty payload or point to `payload_len` readable bytes.
1243pub 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)]
1283/// # Safety
1284/// `payload_ptr` must be null for an empty payload or point to `payload_len` readable bytes.
1285pub 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)]
1325/// # Safety
1326/// `payload_ptr` must be null for an empty payload or point to `payload_len` readable bytes.
1327pub 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)]
1361/// # Safety
1362/// `payload_ptr` must be null for an empty payload or point to `payload_len` readable bytes.
1363pub 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)]
1404/// # Safety
1405/// `payload_ptr` must be null for an empty payload or point to `payload_len` readable bytes.
1406pub 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)]
1437/// # Safety
1438/// `payload_ptr` must be null for an empty payload or point to `payload_len` readable bytes.
1439pub 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)]
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_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)]
1500/// # Safety
1501/// `payload_ptr` must be null for an empty payload or point to `payload_len` readable bytes.
1502pub 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}