Stdin software keyboard implementation

A simple stdin swkbd for the purpose of adding several missing features
from the existing implementation. Allows users to type into the console
for text input, runs several of the validation checks, and returns the
correct result codes.
This commit is contained in:
James Rowe 2016-10-24 23:24:38 -06:00
parent 9dc43d3720
commit da07fdc93c
5 changed files with 362 additions and 30 deletions

View File

@ -2,7 +2,11 @@
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.
#include <algorithm>
#include <cctype>
#include <cstring>
#include <functional>
#include <iostream>
#include <string>
#include "common/assert.h"
#include "common/logging/log.h"
@ -21,6 +25,10 @@
namespace HLE {
namespace Applets {
const static std::array<std::string, 1> swkbd_default_1_button = {"Ok"};
const static std::array<std::string, 2> swkbd_default_2_button = {"Cancel", "Ok"};
const static std::array<std::string, 3> swkbd_default_3_button = {"Cancel", "I Forgot", "Ok"};
ResultCode SoftwareKeyboard::ReceiveParameter(Service::APT::MessageParameter const& parameter) {
if (parameter.signal != static_cast<u32>(Service::APT::SignalType::Request)) {
LOG_ERROR(Service_APT, "unsupported signal %u", parameter.signal);
@ -42,7 +50,7 @@ ResultCode SoftwareKeyboard::ReceiveParameter(Service::APT::MessageParameter con
heap_memory = std::make_shared<std::vector<u8>>(capture_info.size);
// Create a SharedMemory that directly points to this heap block.
framebuffer_memory = Kernel::SharedMemory::CreateForApplet(
heap_memory, 0, heap_memory->size(), MemoryPermission::ReadWrite,
heap_memory, 0, static_cast<u32>(heap_memory->size()), MemoryPermission::ReadWrite,
MemoryPermission::ReadWrite, "SoftwareKeyboard Memory");
// Send the response message with the newly created SharedMemory
@ -74,18 +82,192 @@ ResultCode SoftwareKeyboard::StartImpl(Service::APT::AppletStartupParameter cons
return RESULT_SUCCESS;
}
static bool ValidateFilters(const u32 filters, const std::string& input) {
bool valid = true;
bool local_filter = true;
if ((filters & SWKBDFILTER_DIGITS) == SWKBDFILTER_DIGITS) {
valid &= local_filter =
std::all_of(input.begin(), input.end(), [](const char c) { return !std::isdigit(c); });
if (!local_filter) {
std::cout << "Input must not contain any digits" << std::endl;
}
}
if ((filters & SWKBDFILTER_AT) == SWKBDFILTER_AT) {
valid &= local_filter = input.find("@") == std::string::npos;
if (!local_filter) {
std::cout << "Input must not contain the @ symbol" << std::endl;
}
}
if ((filters & SWKBDFILTER_PERCENT) == SWKBDFILTER_PERCENT) {
valid &= local_filter = input.find("%") == std::string::npos;
if (!local_filter) {
std::cout << "Input must not contain the % symbol" << std::endl;
}
}
if ((filters & SWKBDFILTER_BACKSLASH) == SWKBDFILTER_BACKSLASH) {
valid &= local_filter = input.find("\\") == std::string::npos;
if (!local_filter) {
std::cout << "Input must not contain the \\ symbol" << std::endl;
}
}
if ((filters & SWKBDFILTER_PROFANITY) == SWKBDFILTER_PROFANITY) {
// TODO: check the profanity filter
LOG_WARNING(Service_APT, "App requested profanity filter, but its not implemented.");
}
if ((filters & SWKBDFILTER_CALLBACK) == SWKBDFILTER_CALLBACK) {
// TODO: check the callback
LOG_WARNING(Service_APT, "App requested a callback check, but its not implemented.");
}
return valid;
}
static bool ValidateInput(const SoftwareKeyboardConfig& config, const std::string input) {
// TODO(jroweboy): Is max_text_length inclusive or exclusive?
if (input.size() > config.max_text_length) {
std::cout << Common::StringFromFormat("Input is longer than the maximum length. Max: %u",
config.max_text_length)
<< std::endl;
return false;
}
// return early if the text is filtered
if (config.filter_flags && !ValidateFilters(config.filter_flags, input)) {
return false;
}
bool valid;
switch (config.valid_input) {
case SwkbdValidInput::FIXEDLEN:
valid = input.size() == config.max_text_length;
if (!valid) {
std::cout << Common::StringFromFormat("Input must be exactly %u characters.",
config.max_text_length)
<< std::endl;
}
break;
case SwkbdValidInput::NOTEMPTY_NOTBLANK:
case SwkbdValidInput::NOTBLANK:
valid =
std::any_of(input.begin(), input.end(), [](const char c) { return !std::isspace(c); });
if (!valid) {
std::cout << "Input must not be blank." << std::endl;
}
break;
case SwkbdValidInput::NOTEMPTY:
valid = input.empty();
if (!valid) {
std::cout << "Input must not be empty." << std::endl;
}
break;
case SwkbdValidInput::ANYTHING:
valid = true;
break;
default:
// TODO(jroweboy): What does hardware do in this case?
LOG_CRITICAL(Service_APT, "Application requested unknown validation method. Method: %u",
static_cast<u32>(config.valid_input));
UNREACHABLE();
}
return valid;
}
static bool ValidateButton(u32 num_buttons, const std::string& input) {
// check that the input is a valid number
bool valid = false;
try {
u32 num = std::stoul(input);
valid = num <= num_buttons;
if (!valid) {
std::cout << Common::StringFromFormat("Please choose a number between 0 and %u",
num_buttons)
<< std::endl;
}
} catch (const std::invalid_argument& e) {
(void)e;
std::cout << "Unable to parse input as a number." << std::endl;
} catch (const std::out_of_range& e) {
(void)e;
std::cout << "Input number is not valid." << std::endl;
}
return valid;
}
void SoftwareKeyboard::Update() {
// TODO(Subv): Handle input using the touch events from the HID module
// Until then, just read input from the terminal
std::string input;
std::cout << "SOFTWARE KEYBOARD" << std::endl;
// Display hint text
std::u16string hint(reinterpret_cast<char16_t*>(config.hint_text));
if (!hint.empty()) {
std::cout << "Hint text: " << Common::UTF16ToUTF8(hint) << std::endl;
}
do {
std::cout << "Enter the text you will send to the application:" << std::endl;
std::getline(std::cin, input);
} while (!ValidateInput(config, input));
// TODO(Subv): Remove this hardcoded text
std::u16string text = Common::UTF8ToUTF16("Citra");
memcpy(text_memory->GetPointer(), text.c_str(), text.length() * sizeof(char16_t));
std::string option_text;
// convert all of the button texts into something we can output
// num_buttons is in the range of 0-2 so use <= instead of <
u32 num_buttons = static_cast<u32>(config.num_buttons_m1);
for (u32 i = 0; i <= num_buttons; ++i) {
std::string final_text;
// apps are allowed to set custom text to display on the button
std::u16string custom_button_text(reinterpret_cast<char16_t*>(config.button_text[i]));
if (custom_button_text.empty()) {
// Use the system default text for that button
if (num_buttons == 0) {
final_text = swkbd_default_1_button[i];
} else if (num_buttons == 1) {
final_text = swkbd_default_2_button[i];
} else {
final_text = swkbd_default_3_button[i];
}
} else {
final_text = Common::UTF16ToUTF8(custom_button_text);
}
option_text += Common::StringFromFormat("\t(%u) %s\t", i, final_text);
}
std::string option;
do {
std::cout << "\nPlease type the number of the button you will press: \n"
<< option_text << std::endl;
std::getline(std::cin, option);
} while (!ValidateButton(static_cast<u32>(config.num_buttons_m1), option));
// TODO(Subv): Ask for input and write it to the shared memory
// TODO(Subv): Find out what are the possible values for the return code,
// some games seem to check for a hardcoded 2
config.return_code = 2;
config.text_length = 6;
s32 button = std::stol(option);
switch (config.num_buttons_m1) {
case SwkbdButtonConfig::SINGLE_BUTTON:
config.return_code = SwkbdResult::D0_CLICK;
break;
case SwkbdButtonConfig::DUAL_BUTTON:
if (button == 0) {
config.return_code = SwkbdResult::D1_CLICK0;
} else {
config.return_code = SwkbdResult::D1_CLICK1;
}
break;
case SwkbdButtonConfig::TRIPLE_BUTTON:
if (button == 0) {
config.return_code = SwkbdResult::D2_CLICK0;
} else if (button == 1) {
config.return_code = SwkbdResult::D2_CLICK1;
} else {
config.return_code = SwkbdResult::D2_CLICK2;
}
break;
default:
// TODO: what does the hardware do
LOG_WARNING(Service_APT, "Unknown option for num_buttons_m1: %u",
static_cast<u32>(config.num_buttons_m1));
config.return_code = SwkbdResult::NONE;
break;
}
std::u16string utf16_input = Common::UTF8ToUTF16(input);
memcpy(text_memory->GetPointer(), utf16_input.c_str(), utf16_input.length() * sizeof(char16_t));
config.text_length = static_cast<u16>(utf16_input.size());
config.text_offset = 0;
// TODO(Subv): We're finalizing the applet immediately after it's started,

View File

@ -15,33 +15,153 @@
namespace HLE {
namespace Applets {
/// Maximum number of buttons that can be in the keyboard.
#define SWKBD_MAX_BUTTON 3
/// Maximum button text length, in UTF-16 code units.
#define SWKBD_MAX_BUTTON_TEXT_LEN 16
/// Maximum hint text length, in UTF-16 code units.
#define SWKBD_MAX_HINT_TEXT_LEN 64
/// Maximum filter callback error message length, in UTF-16 code units.
#define SWKBD_MAX_CALLBACK_MSG_LEN 256
/// Keyboard types
enum class SwkbdType : u32 {
NORMAL = 0, ///< Normal keyboard with several pages (QWERTY/accents/symbol/mobile)
QWERTY, ///< QWERTY keyboard only.
NUMPAD, ///< Number pad.
WESTERN, ///< On JPN systems, a text keyboard without Japanese input capabilities,
/// otherwise same as SWKBD_TYPE_NORMAL.
};
/// Keyboard dialog buttons.
enum class SwkbdButtonConfig : u32 {
SINGLE_BUTTON = 0, ///< Ok button
DUAL_BUTTON, ///< Cancel | Ok buttons
TRIPLE_BUTTON, ///< Cancel | I Forgot | Ok buttons
NO_BUTTON, ///< No button (returned by swkbdInputText in special cases)
};
/// Accepted input types.
enum class SwkbdValidInput : u32 {
ANYTHING = 0, ///< All inputs are accepted.
NOTEMPTY, ///< Empty inputs are not accepted.
NOTEMPTY_NOTBLANK, ///< Empty or blank inputs (consisting solely of whitespace) are not
/// accepted.
NOTBLANK, ///< Blank inputs (consisting solely of whitespace) are not accepted, but empty
/// inputs are.
FIXEDLEN, ///< The input must have a fixed length (specified by maxTextLength in
/// swkbdInit).
};
/// Keyboard password modes.
enum class SwkbdPasswordMode : u32 {
NONE = 0, ///< Characters are not concealed.
HIDE, ///< Characters are concealed immediately.
HIDE_DELAY, ///< Characters are concealed a second after they've been typed.
};
/// Keyboard input filtering flags.
enum SwkbdFilter {
SWKBDFILTER_DIGITS =
1, ///< Disallow the use of more than a certain number of digits (0 or more)
SWKBDFILTER_AT = 1 << 1, ///< Disallow the use of the @ sign.
SWKBDFILTER_PERCENT = 1 << 2, ///< Disallow the use of the % sign.
SWKBDFILTER_BACKSLASH = 1 << 3, ///< Disallow the use of the \ sign.
SWKBDFILTER_PROFANITY = 1 << 4, ///< Disallow profanity using Nintendo's profanity filter.
SWKBDFILTER_CALLBACK = 1 << 5, ///< Use a callback in order to check the input.
};
/// Keyboard features.
enum SwkbdFeatures {
SWKBDFEATURES_PARENTAL = 1, ///< Parental PIN mode.
SWKBDFEATURES_DARKEN_TOP_SCREEN = 1 << 1, ///< Darken the top screen when the keyboard is shown.
SWKBDFEATURES_PREDICTIVE_INPUT =
1 << 2, ///< Enable predictive input (necessary for Kanji input in JPN systems).
SWKBDFEATURES_MULTILINE = 1 << 3, ///< Enable multiline input.
SWKBDFEATURES_FIXED_WIDTH = 1 << 4, ///< Enable fixed-width mode.
SWKBDFEATURES_ALLOW_HOME = 1 << 5, ///< Allow the usage of the HOME button.
SWKBDFEATURES_ALLOW_RESET = 1 << 6, ///< Allow the usage of a software-reset combination.
SWKBDFEATURES_ALLOW_POWER = 1 << 7, ///< Allow the usage of the POWER button.
SWKBDFEATURES_DEFAULT_QWERTY = 1
<< 9, ///< Default to the QWERTY page when the keyboard is shown.
};
/// Keyboard filter callback return values.
enum class SwkbdCallbackResult : u32 {
OK = 0, ///< Specifies that the input is valid.
CLOSE, ///< Displays an error message, then closes the keyboard.
CONTINUE, ///< Displays an error message and continues displaying the keyboard.
};
/// Keyboard return values.
enum class SwkbdResult : s32 {
NONE = -1, ///< Dummy/unused.
INVALID_INPUT = -2, ///< Invalid parameters to swkbd.
OUTOFMEM = -3, ///< Out of memory.
D0_CLICK = 0, ///< The button was clicked in 1-button dialogs.
D1_CLICK0, ///< The left button was clicked in 2-button dialogs.
D1_CLICK1, ///< The right button was clicked in 2-button dialogs.
D2_CLICK0, ///< The left button was clicked in 3-button dialogs.
D2_CLICK1, ///< The middle button was clicked in 3-button dialogs.
D2_CLICK2, ///< The right button was clicked in 3-button dialogs.
HOMEPRESSED = 10, ///< The HOME button was pressed.
RESETPRESSED, ///< The soft-reset key combination was pressed.
POWERPRESSED, ///< The POWER button was pressed.
PARENTAL_OK = 20, ///< The parental PIN was verified successfully.
PARENTAL_FAIL, ///< The parental PIN was incorrect.
BANNED_INPUT = 30, ///< The filter callback returned SWKBD_CALLBACK_CLOSE.
};
struct SoftwareKeyboardConfig {
INSERT_PADDING_WORDS(0x8);
SwkbdType type;
SwkbdButtonConfig num_buttons_m1;
SwkbdValidInput valid_input;
SwkbdPasswordMode password_mode;
s32 is_parental_screen;
s32 darken_top_screen;
u32 filter_flags;
u32 save_state_flags;
u16 max_text_length; ///< Maximum length of the input text
u16 dict_word_count;
u16 max_digits;
u16 button_text[SWKBD_MAX_BUTTON][SWKBD_MAX_BUTTON_TEXT_LEN + 1];
u16 numpad_keys[2];
u16 hint_text[SWKBD_MAX_HINT_TEXT_LEN + 1]; ///< Text to display when asking the user for input
bool predictive_input;
bool multiline;
bool fixed_width;
bool allow_home;
bool allow_reset;
bool allow_power;
bool unknown; // XX: what is this supposed to do? "communicateWithOtherRegions"
bool default_qwerty;
bool button_submits_text[4];
u16 language; // XX: not working? supposedly 0 = use system language, CFG_Language+1 = pick
// language
INSERT_PADDING_BYTES(0x6E);
char16_t display_text[65]; ///< Text to display when asking the user for input
INSERT_PADDING_BYTES(0xE);
u32 default_text_offset; ///< Offset of the default text in the output SharedMemory
INSERT_PADDING_WORDS(0x3);
u32 initial_text_offset; ///< Offset of the default text in the output SharedMemory
u32 dict_offset;
u32 initial_status_offset;
u32 initial_learning_offset;
u32 shared_memory_size; ///< Size of the SharedMemory
u32 version;
INSERT_PADDING_WORDS(0x1);
SwkbdResult return_code;
u32 return_code; ///< Return code of the SoftwareKeyboard, usually 2, other values are unknown
INSERT_PADDING_WORDS(0x2);
u32 status_offset;
u32 learning_offset;
u32 text_offset; ///< Offset in the SharedMemory where the output text starts
u16 text_length; ///< Length in characters of the output text
INSERT_PADDING_BYTES(0x2B6);
int callback_result;
u16 callback_msg[SWKBD_MAX_CALLBACK_MSG_LEN + 1];
bool skip_at_check;
INSERT_PADDING_BYTES(0xAB);
};
/**

View File

@ -40,8 +40,12 @@ static ScreencapPostPermission screen_capture_post_permission;
/// Parameter data to be returned in the next call to Glance/ReceiveParameter
static MessageParameter next_parameter;
/// stores the state for CancelParameter
static bool cancelled = false;
void SendParameter(const MessageParameter& parameter) {
next_parameter = parameter;
cancelled = false;
// Signal the event to let the application know that a new parameter is ready to be read
parameter_event->Signal();
}
@ -208,6 +212,11 @@ void ReceiveParameter(Service::Interface* self) {
u32 buffer_size = cmd_buff[2];
VAddr buffer = cmd_buff[0x104 >> 2];
if (cancelled) {
cmd_buff[1] = -1;
return;
}
cmd_buff[1] = RESULT_SUCCESS.raw; // No error
cmd_buff[2] = next_parameter.sender_id;
cmd_buff[3] = next_parameter.signal; // Signal type
@ -230,6 +239,11 @@ void GlanceParameter(Service::Interface* self) {
u32 buffer_size = cmd_buff[2];
VAddr buffer = cmd_buff[0x104 >> 2];
if (cancelled) {
cmd_buff[1] = -1;
return;
}
cmd_buff[1] = RESULT_SUCCESS.raw; // No error
cmd_buff[2] = next_parameter.sender_id;
cmd_buff[3] = next_parameter.signal; // Signal type
@ -254,6 +268,8 @@ void CancelParameter(Service::Interface* self) {
u32 flag2 = cmd_buff[3];
u32 app_id = cmd_buff[4];
cancelled = true;
cmd_buff[1] = RESULT_SUCCESS.raw; // No error
cmd_buff[2] = 1; // Set to Success
@ -493,6 +509,13 @@ void CheckNew3DS(Service::Interface* self) {
LOG_WARNING(Service_APT, "(STUBBED) called");
}
void ReplySleepQuery(Service::Interface* self) {
u32* cmd_buff = Kernel::GetCommandBuffer();
cmd_buff[1] = RESULT_SUCCESS.raw;
LOG_WARNING(Service_APT, "(STUBBED) ReplySleepQuery called");
}
void Init() {
AddService(new APT_A_Interface);
AddService(new APT_S_Interface);

View File

@ -461,6 +461,13 @@ void CheckNew3DSApp(Service::Interface* self);
*/
void CheckNew3DS(Service::Interface* self);
/**
* APT::ReplySleepQuery service function
* Outputs:
* 1: Result code
*/
void ReplySleepQuery(Service::Interface* self);
/// Initialize the APT service
void Init();

View File

@ -20,10 +20,10 @@ const Interface::FunctionInfo FunctionTable[] = {
{0x00090040, nullptr, "IsRegistered"},
{0x000A0040, nullptr, "GetAttribute"},
{0x000B0040, InquireNotification, "InquireNotification"},
{0x000C0104, nullptr, "SendParameter"},
{0x000C0104, SendParameter, "SendParameter"},
{0x000D0080, ReceiveParameter, "ReceiveParameter"},
{0x000E0080, GlanceParameter, "GlanceParameter"},
{0x000F0100, nullptr, "CancelParameter"},
{0x000F0100, CancelParameter, "CancelParameter"},
{0x001000C2, nullptr, "DebugFunc"},
{0x001100C0, nullptr, "MapProgramIdForDebug"},
{0x00120040, nullptr, "SetHomeMenuAppletIdForDebug"},
@ -38,7 +38,7 @@ const Interface::FunctionInfo FunctionTable[] = {
{0x001B00C4, nullptr, "StartApplication"},
{0x001C0000, nullptr, "WakeupApplication"},
{0x001D0000, nullptr, "CancelApplication"},
{0x001E0084, nullptr, "StartLibraryApplet"},
{0x001E0084, StartLibraryApplet, "StartLibraryApplet"},
{0x001F0084, nullptr, "StartSystemApplet"},
{0x00200044, nullptr, "StartNewestHomeMenu"},
{0x00210000, nullptr, "OrderToCloseApplication"},
@ -70,7 +70,7 @@ const Interface::FunctionInfo FunctionTable[] = {
{0x003B0040, nullptr, "CancelLibraryApplet"},
{0x003C0042, nullptr, "SendDspSleep"},
{0x003D0042, nullptr, "SendDspWakeUp"},
{0x003E0080, nullptr, "ReplySleepQuery"},
{0x003E0080, ReplySleepQuery, "ReplySleepQuery"},
{0x003F0040, nullptr, "ReplySleepNotificationComplete"},
{0x00400042, nullptr, "SendCaptureBufferInfo"},
{0x00410040, nullptr, "ReceiveCaptureBufferInfo"},