1use crate::file_dialog::FileDialogResult;
41use crate::schema::{FileDelivery, FileDialogStatus, FileOp, IoStatus};
42use std::cell::RefCell;
43use std::collections::HashMap;
44use std::fmt;
45use std::future::Future;
46use std::pin::Pin;
47use std::rc::Rc;
48use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
49use std::sync::{Arc, Mutex, MutexGuard, OnceLock, Weak};
50use std::task::{Context, Poll, Waker};
51
52#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
54pub enum Delivery {
55 Handle,
58 Upload,
60 Download,
63}
64
65impl From<FileDelivery> for Delivery {
66 fn from(value: FileDelivery) -> Self {
67 match value {
68 FileDelivery::Handle => Delivery::Handle,
69 FileDelivery::Upload => Delivery::Upload,
70 FileDelivery::Download => Delivery::Download,
71 }
72 }
73}
74
75#[non_exhaustive]
77#[derive(Clone, Debug, Eq, PartialEq)]
78pub enum FileError {
79 NotFound,
82 PermissionDenied,
84 ReadOnly,
86 NotAFolder,
88 IsAFolder,
90 TooLarge { len: u64, limit: u64 },
93 Unavailable(String),
95 Io(String),
97}
98
99impl fmt::Display for FileError {
100 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
101 match self {
102 FileError::NotFound => f.write_str("the file is not there any more"),
103 FileError::PermissionDenied => f.write_str("access to the file was refused"),
104 FileError::ReadOnly => f.write_str("the file can only be read"),
105 FileError::NotAFolder => f.write_str("the file is not a folder"),
106 FileError::IsAFolder => f.write_str("the file is a folder"),
107 FileError::TooLarge { len, limit } => write!(
108 f,
109 "the file is {len} bytes, more than the {limit} bytes one read may carry"
110 ),
111 FileError::Unavailable(reason) => write!(f, "unavailable here: {reason}"),
112 FileError::Io(reason) => f.write_str(reason),
113 }
114 }
115}
116
117impl std::error::Error for FileError {}
118
119#[derive(Clone)]
124pub struct PickedFile {
125 inner: Inner,
126}
127
128#[derive(Clone)]
129enum Inner {
130 Path(PathFile),
131 Web(Arc<WebFile>),
132}
133
134#[derive(Clone)]
135struct PathFile {
136 path: String,
137 name: String,
138 folder: OnceLock<bool>,
141}
142
143struct WebFile {
146 handle: u32,
147 name: String,
148 folder: bool,
149 delivery: Delivery,
150 released: bool,
153}
154
155impl Drop for WebFile {
156 fn drop(&mut self) {
157 if self.released {
158 return;
159 }
160 let mut handles = registry();
161 let still_held = handles
164 .get(&self.handle)
165 .and_then(|entry| entry.live.as_ref())
166 .is_some_and(|live| live.strong_count() > 0);
167 if still_held {
168 return;
169 }
170 handles.remove(&self.handle);
171 drop(handles);
172 queue_release(self.handle);
173 }
174}
175
176impl fmt::Debug for PickedFile {
177 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
178 match &self.inner {
179 Inner::Path(file) => f
180 .debug_struct("PickedFile")
181 .field("path", &file.path)
182 .finish(),
183 Inner::Web(file) => f
184 .debug_struct("PickedFile")
185 .field("handle", &file.handle)
186 .field("name", &file.name)
187 .field("folder", &file.folder)
188 .field("delivery", &file.delivery)
189 .finish(),
190 }
191 }
192}
193
194impl PartialEq for PickedFile {
195 fn eq(&self, other: &Self) -> bool {
196 match (&self.inner, &other.inner) {
197 (Inner::Path(left), Inner::Path(right)) => left.path == right.path,
198 (Inner::Web(left), Inner::Web(right)) => left.handle == right.handle,
199 _ => false,
200 }
201 }
202}
203
204impl Eq for PickedFile {}
205
206const WEB_REFERENCE_PREFIX: &str = "webfile:";
208
209const WEB_ANSWER_BYTES: u64 = crate::schema::WEB_EVENT_BUFFER_MAX_BYTES as u64 - 32;
212
213impl PickedFile {
214 pub const MAX_READ_BYTES: u64 = 256 * 1024 * 1024;
217
218 #[cfg(not(target_arch = "wasm32"))]
224 pub fn from_path(path: impl Into<String>) -> PickedFile {
225 PickedFile::at_path(path.into(), None)
226 }
227
228 fn at_path(path: String, folder: Option<bool>) -> PickedFile {
229 let name = std::path::Path::new(&path)
230 .file_name()
231 .map(|name| name.to_string_lossy().into_owned())
232 .unwrap_or_else(|| path.clone());
233 let known = OnceLock::new();
234 if let Some(folder) = folder {
235 let _ = known.set(folder);
236 }
237 PickedFile {
238 inner: Inner::Path(PathFile {
239 path,
240 name,
241 folder: known,
242 }),
243 }
244 }
245
246 pub fn name(&self) -> &str {
248 match &self.inner {
249 Inner::Path(file) => &file.name,
250 Inner::Web(file) => &file.name,
251 }
252 }
253
254 pub fn path(&self) -> Option<&str> {
256 match &self.inner {
257 Inner::Path(file) => Some(&file.path),
258 Inner::Web(_) => None,
259 }
260 }
261
262 pub fn is_folder(&self) -> bool {
267 match &self.inner {
268 Inner::Path(file) => *file
269 .folder
270 .get_or_init(|| std::path::Path::new(&file.path).is_dir()),
271 Inner::Web(file) => file.folder,
272 }
273 }
274
275 pub fn delivery(&self) -> Delivery {
277 match &self.inner {
278 Inner::Path(_) => Delivery::Handle,
279 Inner::Web(file) => file.delivery,
280 }
281 }
282
283 pub fn can_write_again(&self) -> bool {
286 self.delivery() == Delivery::Handle && !self.is_folder()
287 }
288
289 pub async fn read(&self) -> Result<Vec<u8>, FileError> {
291 match &self.inner {
292 Inner::Path(file) => {
293 let path = file.path.clone();
294 crate::runtime::with_worker(move || native::read(&path)).await
295 }
296 Inner::Web(file) => {
297 if file.released {
298 return Err(FileError::NotFound);
299 }
300 if file.folder {
301 return Err(FileError::IsAFolder);
302 }
303 match web_call(FileOp::Read, false, file.handle, String::new(), Vec::new()).await {
304 Answer::Io {
305 status: IoStatus::Ok,
306 data,
307 } => Ok(data),
308 Answer::Io { status, .. } => Err(error_of(status)),
309 Answer::Files { .. } => Err(unexpected_answer("a read")),
310 }
311 }
312 }
313 }
314
315 pub async fn write(&self, bytes: impl Into<Vec<u8>>) -> Result<(), FileError> {
322 let bytes = bytes.into();
323 match &self.inner {
324 Inner::Path(file) => {
325 let path = file.path.clone();
326 crate::runtime::with_worker(move || native::write(&path, &bytes)).await
327 }
328 Inner::Web(file) => {
329 if file.released {
330 return Err(FileError::NotFound);
331 }
332 if file.folder {
333 return Err(FileError::IsAFolder);
334 }
335 if file.delivery == Delivery::Upload {
336 return Err(FileError::ReadOnly);
337 }
338 match web_call(FileOp::Write, false, file.handle, String::new(), bytes).await {
339 Answer::Io {
340 status: IoStatus::Ok,
341 ..
342 } => Ok(()),
343 Answer::Io { status, .. } => Err(error_of(status)),
344 Answer::Files { .. } => Err(unexpected_answer("a write")),
345 }
346 }
347 }
348 }
349
350 pub async fn entries(&self) -> Result<Vec<PickedFile>, FileError> {
353 match &self.inner {
354 Inner::Path(file) => {
355 let path = file.path.clone();
356 crate::runtime::with_worker(move || native::entries(&path)).await
357 }
358 Inner::Web(file) => {
359 if file.released {
360 return Err(FileError::NotFound);
361 }
362 if !file.folder {
363 return Err(FileError::NotAFolder);
364 }
365 match web_call(
366 FileOp::Entries,
367 false,
368 file.handle,
369 String::new(),
370 Vec::new(),
371 )
372 .await
373 {
374 Answer::Files(files) => Ok(files),
375 Answer::Io { status, .. } => Err(error_of(status)),
376 }
377 }
378 }
379 }
380
381 pub async fn child(&self, name: &str, create: bool) -> Result<PickedFile, FileError> {
387 if !is_plain_name(name) {
388 return Err(FileError::Io(format!(
389 "{name:?} is not the name of one file: it is empty, holds a path separator or a NUL, or is . or .."
390 )));
391 }
392 match &self.inner {
393 Inner::Path(file) => {
394 let path = file.path.clone();
395 let name = name.to_owned();
396 crate::runtime::with_worker(move || native::child(&path, &name, create)).await
397 }
398 Inner::Web(file) => {
399 if file.released {
400 return Err(FileError::NotFound);
401 }
402 if !file.folder {
403 return Err(FileError::NotAFolder);
404 }
405 match web_call(
406 FileOp::Child,
407 create,
408 file.handle,
409 name.to_owned(),
410 Vec::new(),
411 )
412 .await
413 {
414 Answer::Files(mut files) if files.len() == 1 => Ok(files.remove(0)),
415 Answer::Files(files) => Err(FileError::Io(format!(
416 "the browser answered a request for one child with {} files",
417 files.len()
418 ))),
419 Answer::Io { status, .. } => Err(error_of(status)),
420 }
421 }
422 }
423 }
424}
425
426fn is_plain_name(name: &str) -> bool {
427 !name.is_empty() && name != "." && name != ".." && !name.contains(['/', '\\', '\0'])
428}
429
430fn unexpected_answer(what: &str) -> FileError {
431 FileError::Io(format!(
432 "the browser answered {what} with a list of files instead of bytes"
433 ))
434}
435
436fn error_of(status: IoStatus) -> FileError {
438 match status {
439 IoStatus::NotFound => FileError::NotFound,
440 IoStatus::PermissionDenied => FileError::PermissionDenied,
441 IoStatus::ReadOnly => FileError::ReadOnly,
442 IoStatus::TooLarge => FileError::TooLarge {
443 len: WEB_ANSWER_BYTES + 1,
444 limit: WEB_ANSWER_BYTES,
445 },
446 IoStatus::QuotaExceeded => {
447 FileError::Io("the browser has no room left to write the file".to_owned())
448 }
449 IoStatus::Unavailable => FileError::Unavailable(
450 "the Renderer has no way to reach this file in this run".to_owned(),
451 ),
452 IoStatus::Failed | IoStatus::Ok => {
453 FileError::Io("the browser could not finish the operation".to_owned())
454 }
455 }
456}
457
458impl FileDialogResult {
459 pub fn files(&self) -> Vec<PickedFile> {
462 match self {
463 FileDialogResult::Chosen { paths, .. } => {
464 paths.iter().map(|path| picked_from_entry(path)).collect()
465 }
466 FileDialogResult::Cancelled | FileDialogResult::Unavailable => Vec::new(),
467 }
468 }
469}
470
471fn picked_from_entry(entry: &str) -> PickedFile {
475 match parse_reference(entry) {
476 Some((handle, name)) => web_file(handle, name),
477 None => PickedFile::at_path(entry.to_owned(), None),
478 }
479}
480
481fn parse_reference(entry: &str) -> Option<(u32, &str)> {
483 let rest = entry.strip_prefix(WEB_REFERENCE_PREFIX)?;
484 let (number, name) = rest.split_once('/')?;
485 if number.is_empty() || !number.bytes().all(|digit| digit.is_ascii_digit()) {
486 return None;
487 }
488 Some((number.parse().ok()?, name))
489}
490
491fn reference(handle: u32, name: &str) -> String {
492 format!("{WEB_REFERENCE_PREFIX}{handle}/{name}")
493}
494
495struct HandleEntry {
499 name: String,
500 folder: bool,
501 delivery: Delivery,
502 live: Option<Weak<WebFile>>,
504}
505
506static REGISTRY: OnceLock<Mutex<HashMap<u32, HandleEntry>>> = OnceLock::new();
507
508fn registry() -> MutexGuard<'static, HashMap<u32, HandleEntry>> {
509 REGISTRY
510 .get_or_init(|| Mutex::new(HashMap::new()))
511 .lock()
512 .unwrap_or_else(|poison| poison.into_inner())
513}
514
515fn register(
518 first_handle: u32,
519 delivery: Delivery,
520 names: &str,
521 kinds: &str,
522) -> Vec<(u32, String, bool)> {
523 let names: Vec<&str> = if names.is_empty() {
524 Vec::new()
525 } else {
526 names.split('\0').collect()
527 };
528 let mut handles = registry();
529 names
530 .iter()
531 .zip(kinds.bytes())
532 .enumerate()
533 .map(|(index, (name, kind))| {
534 let handle = first_handle.wrapping_add(index as u32);
535 let folder = kind == b'd';
536 handles.insert(
537 handle,
538 HandleEntry {
539 name: (*name).to_owned(),
540 folder,
541 delivery,
542 live: None,
543 },
544 );
545 (handle, (*name).to_owned(), folder)
546 })
547 .collect()
548}
549
550fn web_file(handle: u32, name: &str) -> PickedFile {
552 let mut handles = registry();
553 let Some(entry) = handles.get_mut(&handle) else {
554 return PickedFile {
557 inner: Inner::Web(Arc::new(WebFile {
558 handle,
559 name: name.to_owned(),
560 folder: false,
561 delivery: Delivery::Handle,
562 released: true,
563 })),
564 };
565 };
566 if let Some(live) = entry.live.as_ref().and_then(Weak::upgrade) {
567 return PickedFile {
568 inner: Inner::Web(live),
569 };
570 }
571 let file = Arc::new(WebFile {
572 handle,
573 name: entry.name.clone(),
574 folder: entry.folder,
575 delivery: entry.delivery,
576 released: false,
577 });
578 entry.live = Some(Arc::downgrade(&file));
579 PickedFile {
580 inner: Inner::Web(file),
581 }
582}
583
584pub(crate) struct Call {
588 pub(crate) request: u32,
589 pub(crate) op: FileOp,
590 pub(crate) create: bool,
591 pub(crate) handle: u32,
592 pub(crate) name: String,
593 pub(crate) data: Vec<u8>,
594}
595
596static CALLS: Mutex<Vec<Call>> = Mutex::new(Vec::new());
597static RELEASES: Mutex<Vec<u32>> = Mutex::new(Vec::new());
598static PENDING: AtomicBool = AtomicBool::new(false);
601
602static NEXT_REQUEST: AtomicU32 = AtomicU32::new(1);
606pub(crate) const REQUEST_BIT: u32 = 0x8000_0000;
607
608fn next_request() -> u32 {
609 loop {
610 let low = NEXT_REQUEST.fetch_add(1, Ordering::Relaxed) & !REQUEST_BIT;
611 if low != 0 {
612 return REQUEST_BIT | low;
613 }
614 }
615}
616
617fn lock<T>(mutex: &'static Mutex<T>) -> MutexGuard<'static, T> {
618 mutex.lock().unwrap_or_else(|poison| poison.into_inner())
620}
621
622fn wake_batch() {
623 PENDING.store(true, Ordering::Release);
624 if !crate::boundary::in_host_call() {
626 crate::boundary::request_frame_from_worker();
627 }
628}
629
630fn queue_release(handle: u32) {
631 lock(&RELEASES).push(handle);
632 wake_batch();
633}
634
635pub(crate) fn drain(mut release: impl FnMut(u32), mut call: impl FnMut(&Call)) {
637 if !PENDING.swap(false, Ordering::AcqRel) {
638 return;
639 }
640 let releases = std::mem::take(&mut *lock(&RELEASES));
641 for handle in releases {
642 release(handle);
643 }
644 let calls = std::mem::take(&mut *lock(&CALLS));
645 for waiting in &calls {
646 call(waiting);
647 }
648 if !lock(&RELEASES).is_empty() || !lock(&CALLS).is_empty() {
649 PENDING.store(true, Ordering::Release);
651 }
652}
653
654enum Answer {
657 Io { status: IoStatus, data: Vec<u8> },
658 Files(Vec<PickedFile>),
659}
660
661#[derive(Default)]
662struct Slot {
663 answer: Option<Answer>,
664 waker: Option<Waker>,
665}
666
667thread_local! {
668 static WAITING: RefCell<HashMap<u32, Rc<RefCell<Slot>>>> = RefCell::new(HashMap::new());
670}
671
672fn answer(request: u32, answer: Answer) -> bool {
673 let Some(slot) = WAITING.with_borrow_mut(|waiting| waiting.remove(&request)) else {
674 return false;
675 };
676 let waker = {
677 let mut slot = slot.borrow_mut();
678 slot.answer = Some(answer);
679 slot.waker.take()
680 };
681 if let Some(waker) = waker {
682 waker.wake();
683 }
684 true
685}
686
687pub(crate) fn deliver_io(request: u32, status: IoStatus, data: &[u8]) -> bool {
690 if request & REQUEST_BIT == 0 {
691 return false;
692 }
693 self::answer(
694 request,
695 Answer::Io {
696 status,
697 data: data.to_vec(),
698 },
699 )
700}
701
702pub(crate) fn deliver_handles(
706 request: u32,
707 status: FileDialogStatus,
708 filter: u32,
709 delivery: FileDelivery,
710 first_handle: u32,
711 names: &str,
712 kinds: &str,
713) -> bool {
714 let delivery = Delivery::from(delivery);
715 let named = register(first_handle, delivery, names, kinds);
716 if request & REQUEST_BIT != 0 {
717 let files = named
718 .iter()
719 .map(|(handle, name, _)| web_file(*handle, name))
720 .collect();
721 return self::answer(request, Answer::Files(files));
722 }
723 if status != FileDialogStatus::Chosen {
724 return crate::file_dialog::deliver(request, FileDialogStatus::Unavailable, u32::MAX, "");
726 }
727 let paths: Vec<String> = named
728 .iter()
729 .map(|(handle, name, _)| reference(*handle, name))
730 .collect();
731 crate::file_dialog::deliver(request, status, filter, &paths.join("\0"))
732}
733
734fn web_call(op: FileOp, create: bool, handle: u32, name: String, data: Vec<u8>) -> WebCall {
738 WebCall {
739 call: Some(Call {
740 request: 0,
741 op,
742 create,
743 handle,
744 name,
745 data,
746 }),
747 slot: None,
748 }
749}
750
751struct WebCall {
752 call: Option<Call>,
753 slot: Option<Rc<RefCell<Slot>>>,
754}
755
756impl Unpin for WebCall {}
757
758impl Future for WebCall {
759 type Output = Answer;
760
761 fn poll(mut self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll<Answer> {
762 if let Some(mut call) = self.call.take() {
763 let request = next_request();
764 call.request = request;
765 let slot = Rc::new(RefCell::new(Slot {
766 answer: None,
767 waker: Some(context.waker().clone()),
768 }));
769 WAITING.with_borrow_mut(|waiting| waiting.insert(request, Rc::clone(&slot)));
770 self.slot = Some(slot);
771 lock(&CALLS).push(call);
772 wake_batch();
773 return Poll::Pending;
774 }
775 let Some(slot) = &self.slot else {
776 return Poll::Pending;
777 };
778 let mut slot = slot.borrow_mut();
779 match slot.answer.take() {
780 Some(answer) => Poll::Ready(answer),
781 None => {
782 slot.waker = Some(context.waker().clone());
783 Poll::Pending
784 }
785 }
786 }
787}
788
789mod native {
792 use super::{FileError, PickedFile};
793 use std::io::Write;
794 use std::path::Path;
795 use std::sync::atomic::{AtomicU64, Ordering};
796
797 fn error(failure: std::io::Error) -> FileError {
798 match failure.kind() {
799 std::io::ErrorKind::NotFound => FileError::NotFound,
800 std::io::ErrorKind::PermissionDenied => FileError::PermissionDenied,
801 std::io::ErrorKind::Unsupported => FileError::Unavailable(failure.to_string()),
802 _ => FileError::Io(failure.to_string()),
803 }
804 }
805
806 pub(super) fn read(path: &str) -> Result<Vec<u8>, FileError> {
807 let metadata = std::fs::metadata(path).map_err(error)?;
808 if metadata.is_dir() {
809 return Err(FileError::IsAFolder);
810 }
811 if metadata.len() > PickedFile::MAX_READ_BYTES {
812 return Err(FileError::TooLarge {
813 len: metadata.len(),
814 limit: PickedFile::MAX_READ_BYTES,
815 });
816 }
817 let bytes = std::fs::read(path).map_err(error)?;
818 if bytes.len() as u64 > PickedFile::MAX_READ_BYTES {
820 return Err(FileError::TooLarge {
821 len: bytes.len() as u64,
822 limit: PickedFile::MAX_READ_BYTES,
823 });
824 }
825 Ok(bytes)
826 }
827
828 static TEMPORARY: AtomicU64 = AtomicU64::new(0);
829
830 pub(super) fn write(path: &str, bytes: &[u8]) -> Result<(), FileError> {
831 let target = Path::new(path);
832 let existing = match std::fs::metadata(target) {
833 Ok(metadata) if metadata.is_dir() => return Err(FileError::IsAFolder),
834 Ok(metadata) => Some(metadata),
835 Err(failure) if failure.kind() == std::io::ErrorKind::NotFound => None,
836 Err(failure) => return Err(error(failure)),
837 };
838 if existing
839 .as_ref()
840 .is_some_and(|metadata| metadata.permissions().readonly())
841 {
842 return Err(FileError::ReadOnly);
843 }
844 let folder = target
845 .parent()
846 .filter(|parent| !parent.as_os_str().is_empty())
847 .unwrap_or_else(|| Path::new("."));
848 let name = target
849 .file_name()
850 .map(|name| name.to_string_lossy().into_owned())
851 .unwrap_or_default();
852 let temporary = folder.join(format!(
854 ".{name}.{}.{}.compose-rust.tmp",
855 std::process::id(),
856 TEMPORARY.fetch_add(1, Ordering::Relaxed)
857 ));
858 let written = (|| {
859 {
862 let mut file = std::fs::OpenOptions::new()
863 .write(true)
864 .create_new(true)
865 .open(&temporary)?;
866 file.write_all(bytes)?;
867 file.sync_all()?;
868 }
869 if let Some(metadata) = &existing {
871 std::fs::set_permissions(&temporary, metadata.permissions())?;
872 }
873 std::fs::rename(&temporary, target)
874 })();
875 if let Err(failure) = written {
876 let _ = std::fs::remove_file(&temporary);
877 return Err(error(failure));
878 }
879 Ok(())
880 }
881
882 fn folder_of(path: &str) -> Result<(), FileError> {
883 let metadata = std::fs::metadata(path).map_err(error)?;
884 if !metadata.is_dir() {
885 return Err(FileError::NotAFolder);
886 }
887 Ok(())
888 }
889
890 fn joined(path: &str, name: &str) -> Result<String, FileError> {
891 Path::new(path)
892 .join(name)
893 .into_os_string()
894 .into_string()
895 .map_err(|_| FileError::Io(format!("{name:?} inside {path:?} is not UTF-8")))
896 }
897
898 pub(super) fn entries(path: &str) -> Result<Vec<PickedFile>, FileError> {
899 folder_of(path)?;
900 let mut found = Vec::new();
901 for entry in std::fs::read_dir(path).map_err(error)? {
902 let entry = entry.map_err(error)?;
903 let Ok(name) = entry.file_name().into_string() else {
906 continue;
907 };
908 let folder = entry.file_type().map(|kind| kind.is_dir()).unwrap_or(false);
909 found.push((name, folder));
910 }
911 found.sort();
912 found
913 .into_iter()
914 .map(|(name, folder)| Ok(PickedFile::at_path(joined(path, &name)?, Some(folder))))
915 .collect()
916 }
917
918 pub(super) fn child(path: &str, name: &str, create: bool) -> Result<PickedFile, FileError> {
919 folder_of(path)?;
920 let child = joined(path, name)?;
921 match std::fs::metadata(&child) {
922 Ok(metadata) => Ok(PickedFile::at_path(child, Some(metadata.is_dir()))),
923 Err(failure) if failure.kind() == std::io::ErrorKind::NotFound && create => {
924 match std::fs::OpenOptions::new()
925 .write(true)
926 .create_new(true)
927 .open(&child)
928 {
929 Ok(_) => Ok(PickedFile::at_path(child, Some(false))),
930 Err(again) if again.kind() == std::io::ErrorKind::AlreadyExists => {
933 let folder = Path::new(&child).is_dir();
934 Ok(PickedFile::at_path(child, Some(folder)))
935 }
936 Err(again) => Err(error(again)),
937 }
938 }
939 Err(failure) => Err(error(failure)),
940 }
941 }
942}
943
944#[cfg(test)]
945mod tests {
946 use super::*;
947
948 #[test]
949 fn fr72_a_reference_names_its_handle_and_a_path_is_not_one() {
950 assert_eq!(
951 parse_reference("webfile:7/notes.txt"),
952 Some((7, "notes.txt"))
953 );
954 assert_eq!(parse_reference("webfile:12/"), Some((12, "")));
955 assert_eq!(parse_reference("webfile:/a"), None);
956 assert_eq!(parse_reference("webfile:x/a"), None);
957 assert_eq!(parse_reference("/home/webfile:1/a"), None);
958 assert_eq!(parse_reference("C:\\webfile:1\\a"), None);
959 assert_eq!(reference(3, "한글 파일.txt"), "webfile:3/한글 파일.txt");
960 }
961
962 #[test]
963 fn fr72_a_child_name_is_one_name() {
964 for refused in ["", ".", "..", "a/b", "a\\b", "a\0b"] {
965 assert!(!is_plain_name(refused), "{refused:?}");
966 }
967 for accepted in ["a.txt", "한글", ".hidden", "a b"] {
968 assert!(is_plain_name(accepted), "{accepted:?}");
969 }
970 }
971
972 #[test]
973 fn fr72_file_requests_never_share_a_number_with_a_dialog() {
974 for _ in 0..4 {
975 let request = next_request();
976 assert_ne!(request & REQUEST_BIT, 0);
977 assert_ne!(request & !REQUEST_BIT, 0);
978 }
979 assert!(
980 !deliver_io(5, IoStatus::Ok, b""),
981 "a dialog's number is not ours"
982 );
983 }
984}