diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f53f4c2..4a040d47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,8 +9,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ### Added +- `flipperzero::dialogs::DialogFileBrowserOptions` + ### Changed +- `flipperzero::dialogs::DialogFileBrowserOptions` now uses native initialization function. + ### Removed ## [0.12.0] @@ -225,20 +229,37 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Initial release! [Unreleased]: https://github.com/flipperzero-rs/flipperzero/compare/v0.12.0...HEAD + [0.12.0]: https://github.com/flipperzero-rs/flipperzero/releases/tag/v0.12.0 + [0.11.0]: https://github.com/flipperzero-rs/flipperzero/releases/tag/v0.11.0 + [0.10.0]: https://github.com/flipperzero-rs/flipperzero/releases/tag/v0.10.0 + [0.9.0]: https://github.com/flipperzero-rs/flipperzero/releases/tag/v0.9.0 + [0.8.0]: https://github.com/flipperzero-rs/flipperzero/releases/tag/v0.8.0 + [0.7.2]: https://github.com/flipperzero-rs/flipperzero/releases/tag/v0.7.2 + [0.7.1]: https://github.com/flipperzero-rs/flipperzero/releases/tag/v0.7.1 + [0.7.0]: https://github.com/flipperzero-rs/flipperzero/releases/tag/v0.7.0 + [0.6.1]: https://github.com/flipperzero-rs/flipperzero/releases/tag/v0.6.1 + [0.6.0]: https://github.com/flipperzero-rs/flipperzero/releases/tag/v0.6.0 + [0.5.0]: https://github.com/flipperzero-rs/flipperzero/releases/tag/v0.5.0 + [0.4.1]: https://github.com/flipperzero-rs/flipperzero/releases/tag/v0.4.1 + [0.4.0]: https://github.com/flipperzero-rs/flipperzero/releases/tag/v0.4.0 + [0.3.1]: https://github.com/flipperzero-rs/flipperzero/releases/tag/v0.3.1 + [0.3.0]: https://github.com/flipperzero-rs/flipperzero/releases/tag/v0.3.0 + [0.2.0]: https://github.com/flipperzero-rs/flipperzero/releases/tag/v0.2.0 + [0.1.0]: https://github.com/flipperzero-rs/flipperzero/releases/tag/v0.1.0 diff --git a/crates/flipperzero/src/dialogs/mod.rs b/crates/flipperzero/src/dialogs/mod.rs index da6c39db..6c22eb28 100644 --- a/crates/flipperzero/src/dialogs/mod.rs +++ b/crates/flipperzero/src/dialogs/mod.rs @@ -5,6 +5,7 @@ use alloc::ffi::CString; use core::ffi::{c_void, CStr}; use core::marker::PhantomData; +use core::mem::MaybeUninit; use core::ptr::{self, NonNull}; use flipperzero_sys as sys; @@ -25,6 +26,7 @@ pub struct DialogMessage<'a> { } /// A dialog file browser options. +#[repr(transparent)] pub struct DialogFileBrowserOptions<'a> { data: sys::DialogsFileBrowserOptions, _phantom: PhantomData<&'a ()>, @@ -77,7 +79,7 @@ impl DialogsApp { options, ) } - .then_some(result_path) + .then_some(result_path) } } @@ -215,40 +217,108 @@ impl<'a> Default for DialogFileBrowserOptions<'a> { } impl<'a> DialogFileBrowserOptions<'a> { - /// Creates a new dialog file browser options and initializes to default values. pub fn new() -> Self { + // SAFETY: the string is a valid UTF-8 + unsafe { Self::with_extension(c"*") } + } + + /// Creates a new dialog file browser options and initializes to default values. + /// + /// # Safety + /// + /// `extension` should be a valid UTF-8 string + /// + /// # Compatibility + /// + /// This function's signature may change in the future to make it safe. + /// + /// # Examples + /// + /// Basic usage: + /// + /// ``` + /// # use flipperzero::dialogs::DialogFileBrowserOptions; + /// let options = DialogFileBrowserOptions::new(c"*"); + /// ``` + /// + /// ## Lifetime covariance: + /// + /// Even if `'static` lifetime is involved in the creation of options, + /// the resulting lifetime will be the most applicable one: + /// + /// ``` + /// # use core::ffi::CStr; + /// # use flipperzero::dialogs::DialogFileBrowserOptions; + /// // has `'static` lifetime + /// const EXTENSION: &CStr = c"txt"; + /// // has "local" lifetime, aka `'a` + /// let base_path_bytes = [b'/', b'r', b'o', b'o', b't']; + /// let base_path = CStr::from_bytes_with_nul(&base_path_bytes).unwrap(); + /// // the most appropriate lifetime `'a` is used + /// // SAFETY: `EXTENSION` is a valid UTF-8 string + /// let mut options = unsafe { DialogFileBrowserOptions::new(EXTENSION) } + /// .set_base_path(base_path); + /// ``` + /// + /// Still this should not allow the options to outlive its components: + /// + /// ```compile_fail + /// # use core::ffi::CStr; + /// # use flipperzero::dialogs::DialogFileBrowserOptions; + /// # use flipperzero_sys::{cstr, DialogsFileBrowserOptions}; + /// const EXTENSION: &CStr = cstr!("*"); + /// // SAFETY: `EXTENSION` is a valid UTF-8 string + /// let mut options = unsafe { DialogFileBrowserOptions::new(EXTENSION) }; + /// { + /// let base_path_bytes = [b'/', b'r', b'o', b'o', b't']; + /// let base_path = CStr::from_bytes_with_nul(&base_path_bytes).unwrap(); + /// options = options.set_base_path(base_path); + /// } + /// ``` + pub unsafe fn with_extension(extension: &'a CStr) -> Self { + let mut options = MaybeUninit::::uninit(); + let uninit_options = options.as_mut_ptr(); + let extension = extension.as_ptr(); + // TODO: as for now, we stick to default (NULL) icon, + // although we may want to make it customizable via this function's parameter + // once there are safe Icon-related APIs + let icon = ptr::null(); + // SAFETY: all pointers are valid (`icon` is allowed to be NULL) + // and options is intentionally uninitialized + // since it is the called function's job to do it + unsafe { sys::dialog_file_browser_set_basic_options(uninit_options, extension, icon) }; Self { - // default values from sys::dialog_file_browser_set_basic_options() - data: sys::DialogsFileBrowserOptions { - extension: c"*".as_ptr(), - base_path: ptr::null(), - skip_assets: true, - hide_dot_files: false, - icon: ptr::null(), - hide_ext: true, - item_loader_callback: None, - item_loader_context: ptr::null_mut(), - }, + // SAFETY: data has just been initialized fully + // as guaranteed by the previously called function's contract + data: unsafe { options.assume_init() }, _phantom: PhantomData, } } /// Set file extension to be offered for selection. - pub fn set_extension( - mut self, - // FIXME: this is unsound for non-UTF8 string - extension: &'a CStr, - ) -> Self { + /// + /// # Safety + /// + /// `extension` should be a valid UTF-8 string + /// + /// # Compatibility + /// + /// This function's signature may change in the future to make it safe. + pub unsafe fn set_extension(mut self, extension: &'a CStr) -> Self { self.data.extension = extension.as_ptr(); self } /// Set root folder path for navigation with back key. - pub fn set_base_path( - mut self, - // FIXME: this is unsound for non-UTF8 string - base_path: &'a CStr, - ) -> Self { + /// + /// # Safety + /// + /// `base_path` should be a valid UTF-8 string + /// + /// # Compatibility + /// + /// This function's signature may change in the future to make it safe. + pub unsafe fn set_base_path(mut self, base_path: &'a CStr) -> Self { self.data.base_path = base_path.as_ptr(); self }