diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 358f2f7..f9c2388 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ jobs: matrix: toolchain: # Run against a "known good" nightly. Rustc version is 1 day behind the toolchain date - - nightly-2023-06-01 + - nightly-2024-02-18 # Check for breakage on latest nightly - nightly @@ -53,7 +53,7 @@ jobs: strategy: matrix: toolchain: - - nightly-2023-06-01 + - nightly-2024-02-18 - nightly continue-on-error: ${{ matrix.toolchain == 'nightly' }} runs-on: ubuntu-latest diff --git a/ctru-rs/Cargo.toml b/ctru-rs/Cargo.toml index 4d6b363..ddd15db 100644 --- a/ctru-rs/Cargo.toml +++ b/ctru-rs/Cargo.toml @@ -10,7 +10,7 @@ categories = ["os", "api-bindings", "hardware-support"] exclude = ["examples"] license = "Zlib" edition = "2021" -rust-version = "1.70" +rust-version = "1.73" [lib] crate-type = ["rlib"] @@ -24,6 +24,7 @@ shim-3ds = { git = "https://github.com/rust3ds/shim-3ds.git" } pthread-3ds = { git = "https://github.com/rust3ds/pthread-3ds.git" } libc = "0.2.121" bitflags = "2.3.3" +widestring = "1.0.2" [build-dependencies] toml = "0.5" diff --git a/ctru-rs/examples/file-explorer.rs b/ctru-rs/examples/file-explorer.rs index 5c158ac..14f3474 100644 --- a/ctru-rs/examples/file-explorer.rs +++ b/ctru-rs/examples/file-explorer.rs @@ -165,7 +165,7 @@ impl<'a> FileExplorer<'a> { fn get_input_and_run(&mut self, action: impl FnOnce(&mut Self, String)) { let mut keyboard = SoftwareKeyboard::default(); - match keyboard.get_string(2048, self.apt, self.gfx) { + match keyboard.launch(self.apt, self.gfx) { Ok((path, Button::Right)) => { // Clicked "OK". action(self, path); diff --git a/ctru-rs/examples/software-keyboard.rs b/ctru-rs/examples/software-keyboard.rs index 1d24a7d..1b58da5 100644 --- a/ctru-rs/examples/software-keyboard.rs +++ b/ctru-rs/examples/software-keyboard.rs @@ -44,9 +44,9 @@ fn main() { // Check if the user request to write some input. if hid.keys_down().contains(KeyPad::A) { - // Raise the software keyboard. You can perform different actions depending on which + // Launch the software keyboard. You can perform different actions depending on which // software button the user pressed. - match keyboard.get_string(2048, &apt, &gfx) { + match keyboard.launch(&apt, &gfx) { Ok((text, Button::Right)) => println!("You entered: {text}"), Ok((_, Button::Left)) => println!("Cancelled"), Ok((_, Button::Middle)) => println!("How did you even press this?"), diff --git a/ctru-rs/src/applets/swkbd.rs b/ctru-rs/src/applets/swkbd.rs index 9dd75d1..93f69ec 100644 --- a/ctru-rs/src/applets/swkbd.rs +++ b/ctru-rs/src/applets/swkbd.rs @@ -4,10 +4,14 @@ #![doc(alias = "keyboard")] use crate::services::{apt::Apt, gfx::Gfx}; -use ctru_sys::{self, SwkbdState}; +use ctru_sys::{ + aptLaunchLibraryApplet, aptSetMessageCallback, envGetAptAppId, svcCloseHandle, + svcCreateMemoryBlock, APT_SendParameter, SwkbdButton, SwkbdDictWord, SwkbdExtra, + SwkbdLearningData, SwkbdState, SwkbdStatusData, APPID_SOFTWARE_KEYBOARD, APTCMD_MESSAGE, + NS_APPID, +}; use bitflags::bitflags; -use libc; use std::ffi::{CStr, CString}; use std::fmt::Display; @@ -110,7 +114,7 @@ pub enum ButtonConfig { LeftMiddleRight = 3, } -/// Error returned by an unsuccessful [`SoftwareKeyboard::get_string()`]. +/// Error returned by an unsuccessful [`SoftwareKeyboard::launch()`]. #[doc(alias = "SwkbdResult")] #[derive(Copy, Clone, Debug, PartialEq, Eq)] #[repr(i32)] @@ -207,6 +211,13 @@ bitflags! { } } +// Internal book-keeping struct used to send data to `aptSetMessageCallback` when calling the software keyboard. +// We only need this because libctru doesn't keep a pointer to the shared memory block in `SwkbdExtra` for whatever reason +struct MessageCallbackData { + extra: *mut SwkbdExtra, + swkbd_shared_mem_ptr: *mut libc::c_void, +} + impl SoftwareKeyboard { /// Initialize a new configuration for the Software Keyboard applet depending on how many "exit" buttons are available to the user (1, 2 or 3). /// @@ -241,11 +252,6 @@ impl SoftwareKeyboard { /// Launches the applet based on the given configuration and returns a string containing the text input. /// - /// # Notes - /// - /// The text received from the keyboard will be truncated if it is longer than `max_bytes`. - /// Use [`SoftwareKeyboard::set_max_text_len()`] to make sure the buffer can contain the input text. - /// /// # Example /// /// ``` @@ -260,67 +266,15 @@ impl SoftwareKeyboard { /// use ctru::applets::swkbd::SoftwareKeyboard; /// let mut keyboard = SoftwareKeyboard::default(); /// - /// let (text, button) = keyboard.get_string(2048, &apt, &gfx)?; + /// let (text, button) = keyboard.launch(&apt, &gfx)?; /// # /// # Ok(()) /// # } /// ``` #[doc(alias = "swkbdInputText")] - pub fn get_string( - &mut self, - max_bytes: usize, - apt: &Apt, - gfx: &Gfx, - ) -> Result<(String, Button), Error> { - // Unfortunately the libctru API doesn't really provide a way to get the exact length - // of the string that it receieves from the software keyboard. Instead it expects you - // to pass in a buffer and hope that it's big enough to fit the entire string, so - // you have to set some upper limit on the potential size of the user's input. - let mut tmp = vec![0u8; max_bytes]; - let button = self.write_exact(&mut tmp, apt, gfx)?; - - // libctru does, however, seem to ensure that the buffer will always contain a properly - // terminated UTF-8 sequence even if the input has to be truncated, so these operations - // should be safe. - let len = unsafe { libc::strlen(tmp.as_ptr()) }; - tmp.truncate(len); - - let res = unsafe { String::from_utf8_unchecked(tmp) }; - - Ok((res, button)) - } + pub fn launch(&mut self, _apt: &Apt, _gfx: &Gfx) -> Result<(String, Button), Error> { + let mut output = String::new(); - /// Fills the provided buffer with a UTF-8 encoded, NUL-terminated sequence of bytes from - /// this software keyboard. - /// - /// # Notes - /// - /// If the buffer is too small to contain the entire sequence received from the keyboard, - /// the output will be truncated. - /// - /// # Example - /// - /// ``` - /// # let _runner = test_runner::GdbRunner::default(); - /// # use std::error::Error; - /// # fn main() -> Result<(), Box> { - /// # use ctru::services::{apt::Apt, gfx::Gfx}; - /// # - /// # let gfx = Gfx::new().unwrap(); - /// # let apt = Apt::new().unwrap(); - /// # - /// use ctru::applets::swkbd::SoftwareKeyboard; - /// let mut keyboard = SoftwareKeyboard::default(); - /// - /// let mut buffer = vec![0; 100]; - /// - /// let button = keyboard.write_exact(&mut buffer, &apt, &gfx)?; - /// # - /// # Ok(()) - /// # } - /// ``` - #[doc(alias = "swkbdInputText")] - pub fn write_exact(&mut self, buf: &mut [u8], _apt: &Apt, _gfx: &Gfx) -> Result { unsafe { // The filter callback gets reset every time the SoftwareKeyboard is used. ctru_sys::swkbdSetFilterCallback( @@ -329,11 +283,11 @@ impl SoftwareKeyboard { (self as *mut Self).cast(), ); - match ctru_sys::swkbdInputText(self.state.as_mut(), buf.as_mut_ptr(), buf.len()) { + match self.swkbd_input_text(&mut output) { ctru_sys::SWKBD_BUTTON_NONE => Err(self.state.result.into()), - ctru_sys::SWKBD_BUTTON_LEFT => Ok(Button::Left), - ctru_sys::SWKBD_BUTTON_MIDDLE => Ok(Button::Middle), - ctru_sys::SWKBD_BUTTON_RIGHT => Ok(Button::Right), + ctru_sys::SWKBD_BUTTON_LEFT => Ok((output, Button::Left)), + ctru_sys::SWKBD_BUTTON_MIDDLE => Ok((output, Button::Middle)), + ctru_sys::SWKBD_BUTTON_RIGHT => Ok((output, Button::Right)), _ => unreachable!(), } } @@ -654,7 +608,7 @@ impl SoftwareKeyboard { /// /// Keyboard input is converted from UTF-16 to UTF-8 before being handed to Rust, /// so this code point limit does not necessarily equal the max number of UTF-8 code points - /// receivable by [`SoftwareKeyboard::get_string()`] and [`SoftwareKeyboard::write_exact()`]. + /// receivable by [`SoftwareKeyboard::launch()`]. /// /// # Example /// @@ -675,6 +629,293 @@ impl SoftwareKeyboard { // Activate the specific validation rule for maximum length. self.state.valid_input = ValidInput::FixedLen.into(); } + + // A reimplementation of `swkbdInputText` from `libctru/source/applets/swkbd.c`. Allows us to + // get text from the software keyboard and put it directly into a `String` without requiring + // an intermediate fixed-size buffer + fn swkbd_input_text(&mut self, output: &mut String) -> SwkbdButton { + use ctru_sys::{ + MEMPERM_READ, MEMPERM_WRITE, R_FAILED, SWKBD_BUTTON_LEFT, SWKBD_BUTTON_MIDDLE, + SWKBD_BUTTON_NONE, SWKBD_BUTTON_RIGHT, SWKBD_D0_CLICK, SWKBD_D1_CLICK0, + SWKBD_D1_CLICK1, SWKBD_D2_CLICK0, SWKBD_D2_CLICK1, SWKBD_D2_CLICK2, + SWKBD_FILTER_CALLBACK, SWKBD_OUTOFMEM, + }; + + let swkbd = self.state.as_mut(); + let mut extra = unsafe { swkbd.__bindgen_anon_1.extra }; + + // Calculate shared mem size + let mut shared_mem_size = 0; + + shared_mem_size += (std::mem::size_of::() * (swkbd.max_text_len as usize + 1)) + .next_multiple_of(std::mem::size_of::()); + + let dict_off = shared_mem_size; + + shared_mem_size += (std::mem::size_of::() * swkbd.dict_word_count as usize) + .next_multiple_of(std::mem::size_of::()); + + let status_off = shared_mem_size; + + shared_mem_size += if swkbd.initial_learning_offset >= 0 { + std::mem::size_of::() + } else { + 0 + }; + + let learning_off = shared_mem_size; + + shared_mem_size += if swkbd.initial_learning_offset >= 0 { + std::mem::size_of::() + } else { + 0 + }; + + if swkbd.save_state_flags & (1 << 0) != 0 { + swkbd.status_offset = shared_mem_size as _; + shared_mem_size += std::mem::size_of::(); + } + + if swkbd.save_state_flags & (1 << 1) != 0 { + swkbd.learning_offset = shared_mem_size as _; + shared_mem_size += std::mem::size_of::(); + } + + shared_mem_size = shared_mem_size.next_multiple_of(0x1000); + + swkbd.shared_memory_size = shared_mem_size; + + // Allocate shared mem + let swkbd_shared_mem_ptr = unsafe { libc::memalign(0x1000, shared_mem_size) }; + + let mut swkbd_shared_mem_handle = 0; + + if swkbd_shared_mem_ptr.is_null() { + swkbd.result = SWKBD_OUTOFMEM; + return SWKBD_BUTTON_NONE; + } + + let res = unsafe { + svcCreateMemoryBlock( + &mut swkbd_shared_mem_handle, + swkbd_shared_mem_ptr as _, + shared_mem_size as _, + MEMPERM_READ | MEMPERM_WRITE, + MEMPERM_READ | MEMPERM_WRITE, + ) + }; + + if R_FAILED(res) { + unsafe { + libc::free(swkbd_shared_mem_ptr); + swkbd.result = SWKBD_OUTOFMEM; + return SWKBD_BUTTON_NONE; + } + } + + // Copy stuff to shared mem + if !extra.initial_text.is_null() { + swkbd.initial_text_offset = 0; + + unsafe { + let utf16_iter = + str::from_utf8_unchecked(CStr::from_ptr(extra.initial_text).to_bytes()) + .encode_utf16() + .take(swkbd.max_text_len as _) + .chain(once(0)); + + let mut initial_text_cursor = swkbd_shared_mem_ptr.cast(); + + for code_unit in utf16_iter { + *initial_text_cursor = code_unit; + initial_text_cursor = initial_text_cursor.add(1); + } + } + } + + if !extra.dict.is_null() { + swkbd.dict_offset = dict_off as _; + unsafe { + std::ptr::copy_nonoverlapping( + extra.dict, + swkbd_shared_mem_ptr.add(dict_off).cast(), + swkbd.dict_word_count as _, + ) + }; + } + + if swkbd.initial_status_offset >= 0 { + swkbd.initial_status_offset = status_off as _; + unsafe { + std::ptr::copy_nonoverlapping( + extra.status_data, + swkbd_shared_mem_ptr.add(status_off).cast(), + 1, + ) + }; + } + + if swkbd.initial_learning_offset >= 0 { + swkbd.initial_learning_offset = learning_off as _; + unsafe { + std::ptr::copy_nonoverlapping( + extra.learning_data, + swkbd_shared_mem_ptr.add(learning_off).cast(), + 1, + ) + }; + } + + if extra.callback.is_some() { + swkbd.filter_flags |= SWKBD_FILTER_CALLBACK; + } else { + swkbd.filter_flags &= !SWKBD_FILTER_CALLBACK; + } + + // Launch swkbd + unsafe { + swkbd.__bindgen_anon_1.reserved.fill(0); + + let mut callback_data = MessageCallbackData { + extra: std::ptr::addr_of_mut!(extra), + swkbd_shared_mem_ptr, + }; + + if extra.callback.is_some() { + aptSetMessageCallback( + Some(Self::swkbd_message_callback), + std::ptr::addr_of_mut!(callback_data).cast(), + ); + } + + aptLaunchLibraryApplet( + APPID_SOFTWARE_KEYBOARD, + (swkbd as *mut SwkbdState).cast(), + std::mem::size_of::(), + swkbd_shared_mem_handle, + ); + + if extra.callback.is_some() { + aptSetMessageCallback(None, std::ptr::null_mut()); + } + + let _ = svcCloseHandle(swkbd_shared_mem_handle); + } + + let button = match swkbd.result { + SWKBD_D1_CLICK0 | SWKBD_D2_CLICK0 => SWKBD_BUTTON_LEFT, + SWKBD_D2_CLICK1 => SWKBD_BUTTON_MIDDLE, + SWKBD_D0_CLICK | SWKBD_D1_CLICK1 | SWKBD_D2_CLICK2 => SWKBD_BUTTON_RIGHT, + _ => SWKBD_BUTTON_NONE, + }; + + if swkbd.text_length > 0 { + let text16 = unsafe { + widestring::Utf16Str::from_slice_unchecked(std::slice::from_raw_parts( + swkbd_shared_mem_ptr.add(swkbd.text_offset as _).cast(), + swkbd.text_length as _, + )) + }; + + *output = text16.to_string(); + } + + if swkbd.save_state_flags & (1 << 0) != 0 { + unsafe { + std::ptr::copy_nonoverlapping( + swkbd_shared_mem_ptr.add(swkbd.status_offset as _).cast(), + extra.status_data, + 1, + ) + }; + } + + if swkbd.save_state_flags & (1 << 1) != 0 { + unsafe { + std::ptr::copy_nonoverlapping( + swkbd_shared_mem_ptr.add(swkbd.learning_offset as _).cast(), + extra.learning_data, + 1, + ) + }; + } + + unsafe { libc::free(swkbd_shared_mem_ptr) }; + + button + } + + // A reimplementation of `swkbdMessageCallback` from `libctru/source/applets/swkbd.c`. + // This function sets up and then calls the callback set by `swkbdSetFilterCallback` + #[deny(unsafe_op_in_unsafe_fn)] + unsafe extern "C" fn swkbd_message_callback( + user: *mut libc::c_void, + sender: NS_APPID, + msg: *mut libc::c_void, + msg_size: libc::size_t, + ) { + let data = unsafe { &mut *user.cast::() }; + let swkbd = unsafe { &mut *msg.cast::() }; + let extra = unsafe { &mut *data.extra }; + + if sender != ctru_sys::APPID_SOFTWARE_KEYBOARD + || msg_size != std::mem::size_of::() + { + return; + } + + let text16 = unsafe { + widestring::Utf16Str::from_slice_unchecked(std::slice::from_raw_parts( + data.swkbd_shared_mem_ptr.add(swkbd.text_offset as _).cast(), + swkbd.text_length as usize + 1, + )) + }; + + let text8 = text16.to_string(); + + let mut retmsg = std::ptr::null(); + + if let Some(cb) = extra.callback { + swkbd.callback_result = unsafe { + cb( + extra.callback_user, + &mut retmsg, + text8.as_ptr(), + text8.len(), + ) + } as _ + }; + + let retmsg = if !retmsg.is_null() { + unsafe { + let len = libc::strlen(retmsg) + 1; + std::str::from_utf8_unchecked(std::slice::from_raw_parts(retmsg, len)) + } + } else { + "\0" + }; + + let callback_msg = &mut swkbd.callback_msg; + + for (idx, code_unit) in retmsg + .encode_utf16() + .take(callback_msg.len() - 1) + .enumerate() + { + callback_msg[idx] = code_unit; + } + + let _ = unsafe { + APT_SendParameter( + envGetAptAppId(), + sender, + APTCMD_MESSAGE, + (swkbd as *mut SwkbdState).cast(), + std::mem::size_of::() as _, + 0, + ) + }; + } } impl ParentalLock {