1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2025-02-04 03:40:58 +00:00
trezor-firmware/core/embed/rtl/cli.c

672 lines
17 KiB
C

#include <trezor_rtl.h>
#include <rtl/cli.h>
#include <rtl/mini_printf.h>
#include <ctype.h>
#include <stdarg.h>
#define ESC_COLOR_GREEN "\e[32m"
#define ESC_COLOR_RED "\e[31m"
#define ESC_COLOR_GRAY "\e[37m"
#define ESC_COLOR_RESET "\e[39m"
bool cli_init(cli_t* cli, cli_read_cb_t read, cli_write_cb_t write,
void* callback_context) {
memset(cli, 0, sizeof(cli_t));
cli->read = read;
cli->write = write;
cli->callback_context = callback_context;
return true;
}
void cli_set_commands(cli_t* cli, const cli_command_t* cmd_array,
size_t cmd_count) {
cli->cmd_array = cmd_array;
cli->cmd_count = cmd_count;
}
static void cli_vprintf(cli_t* cli, const char* format, va_list args) {
char buffer[CLI_LINE_BUFFER_SIZE];
mini_vsnprintf(buffer, sizeof(buffer), format, args);
cli->write(cli->callback_context, buffer, strlen(buffer));
}
static void cli_printf(cli_t* cli, const char* format, ...) {
va_list args;
va_start(args, format);
cli_vprintf(cli, format, args);
va_end(args);
}
void cli_vtrace(cli_t* cli, const char* format, va_list args) {
if (cli->interactive) {
cli_printf(cli, ESC_COLOR_GRAY);
}
cli_printf(cli, "#");
if (cli->interactive) {
cli_printf(cli, ESC_COLOR_RESET);
}
if (format != NULL && format[0] != '\0') {
// Print the formatted message
cli_printf(cli, " ");
cli_vprintf(cli, format, args);
}
cli_printf(cli, "\r\n");
}
void cli_trace(cli_t* cli, const char* format, ...) {
va_list args;
va_start(args, format);
cli_vtrace(cli, format, args);
va_end(args);
}
void cli_ok(cli_t* cli, const char* format, ...) {
va_list args;
if (cli->interactive) {
cli_printf(cli, ESC_COLOR_GREEN);
}
cli_printf(cli, "OK");
if (cli->interactive) {
cli_printf(cli, ESC_COLOR_RESET);
}
if (format != NULL && format[0] != '\0') {
// Print the formatted message
cli_printf(cli, " ");
va_start(args, format);
cli_vprintf(cli, format, args);
va_end(args);
}
cli_printf(cli, "\r\n");
cli->final_status = true;
}
// Write OK response with hex-encoded data
void cli_ok_hexdata(cli_t* cli, const void* data, size_t size) {
if (cli->interactive) {
cli_printf(cli, ESC_COLOR_GREEN);
}
cli_printf(cli, "OK");
if (cli->interactive) {
cli_printf(cli, ESC_COLOR_RESET);
}
if (size > 0) {
cli_printf(cli, " ");
for (size_t i = 0; i < size; i++) {
cli_printf(cli, "%02X", ((uint8_t*)data)[i]);
}
}
cli_printf(cli, "\r\n");
cli->final_status = true;
}
static void cli_verror(cli_t* cli, const char* code, const char* format,
va_list args) {
if (cli->interactive) {
cli_printf(cli, ESC_COLOR_RED);
}
cli_printf(cli, "ERROR");
if (cli->interactive) {
cli_printf(cli, ESC_COLOR_RESET);
}
cli_printf(cli, " %s", code);
if (format != NULL && format[0] != '\0') {
cli_printf(cli, " \"");
// Print the formatted message
cli_vprintf(cli, format, args);
cli_printf(cli, "\"");
}
cli_printf(cli, "\r\n");
cli->final_status = true;
}
void cli_error(cli_t* cli, const char* code, const char* format, ...) {
va_list args;
va_start(args, format);
cli_verror(cli, code, format, args);
va_end(args);
}
void cli_error_arg(cli_t* cli, const char* format, ...) {
if (cli->interactive && cli->current_cmd != NULL) {
const cli_command_t* cmd = cli->current_cmd;
if (cmd->args != NULL) {
cli_trace(cli, "USAGE: %s %s", cmd->name, cmd->args);
} else {
cli_trace(cli, "USAGE: %s", cmd->name);
}
}
va_list args;
va_start(args, format);
cli_verror(cli, CLI_ERROR_INVALID_ARG, format, args);
va_end(args);
}
void cli_error_arg_count(cli_t* cli) {
cli_error_arg(cli, "Unexpected trailing input.");
}
void cli_progress(cli_t* cli, const char* format, ...) {
va_list args;
va_start(args, format);
cli_printf(cli, "PROGRESS");
if (format != NULL && format[0] != '\0') {
cli_printf(cli, " ");
// Print the formatted message
cli_vprintf(cli, format, args);
}
cli_printf(cli, "\r\n");
va_end(args);
}
void cli_abort(cli_t* cli) { cli->aborted = true; }
bool cli_aborted(cli_t* cli) { return cli->aborted; }
// Finds a command record by name
//
// Returns NULL if the command is not found
static const cli_command_t* cli_find_command(cli_t* cli, const char* cmd) {
for (size_t i = 0; i < cli->cmd_count; i++) {
if (strcmp(cmd, cli->cmd_array[i].name) == 0) {
return &cli->cmd_array[i];
}
}
return NULL;
}
#define INDEX_ADD(index, offset) \
(((index) + CLI_HISTORY_DEPTH + (offset)) % CLI_HISTORY_DEPTH)
static void cli_history_add(cli_t* cli, const char* line) {
size_t line_len = strlen(line);
if (line_len == 0 || line_len >= CLI_HISTORY_LINE_SIZE) {
// Skip empty or too long lines
return;
}
for (int i = 0; i < CLI_HISTORY_DEPTH; i++) {
if (strcmp(cli->history[i], line) == 0) {
// Duplicate line => Move it to the top
for (; i != INDEX_ADD(cli->history_head, -1); i = INDEX_ADD(i, 1)) {
strcpy(cli->history[i], cli->history[INDEX_ADD(i, 1)]);
}
strcpy(cli->history[i], line);
return;
}
}
// Add the new line to the history
strcpy(cli->history[cli->history_head], line);
cli->history_head = (cli->history_head + 1) % CLI_HISTORY_DEPTH;
}
// Searches the history for the previous command that starts with the prefix
// provided in the `line` buffer.
//
// `idx` is the index of the current command in the history
//
// Returns NULL if there are no more commands
static const char* cli_history_rev(cli_t* cli, int* idx, char* line,
int prefix) {
for (int i = *idx + 1; i <= CLI_HISTORY_DEPTH; i++) {
const char* hist_line = cli->history[INDEX_ADD(cli->history_head, -i)];
if (*hist_line == '\0') break;
if (strlen(hist_line) >= prefix && strncmp(hist_line, line, prefix) == 0) {
*idx = i;
return hist_line;
}
}
return NULL;
}
// Searches the history for the next command that starts with the prefix
// provided in the `line` buffer.
//
// `idx` is the index of the current command in the history
//
// Returns NULL if there are no more commands
static const char* cli_history_fwd(cli_t* cli, int* idx, char* line,
int prefix) {
for (int i = *idx - 1; i > 0; i--) {
const char* hist_line = cli->history[INDEX_ADD(cli->history_head, -i)];
if (strlen(hist_line) >= prefix && strncmp(hist_line, line, prefix) == 0) {
*idx = i;
return hist_line;
}
}
*idx = 0;
return NULL;
}
#define ESC_SEQ(ch) (0x200 + (ch))
// Reads a character from the console input and return it.
// Comple escape sequences translates into ESC_SEQ values
// - ESC[<letter> => ESC_SEQ(letter), e.g. ESC[A => ESC_SEQ('A')
// - ESC[<number>~ => ESC_SEQ(number) , e.g. ESC[3~ => ESC_SEQ(3)
static int cli_readch(cli_t* cli) {
int esc_len = 0; // >0 if we are in the middle of an escape sequence
int esc_code = 0; // numeric code of the escape sequence
for (;;) {
char ch;
cli->read(cli->callback_context, &ch, 1);
if (ch == '\e') {
// Escape sequence start
esc_len = 1;
} else if (esc_len == 1) {
if (ch == '\e') {
return 'e';
} else if (ch == '[') {
// Control sequence introducer
esc_len = 2;
esc_code = 0;
} else {
esc_len = 0;
}
} else if (esc_len == 2 && ch >= 'A' && ch <= 'Z') {
// XTERM sequences - ESC[<letter>
return ESC_SEQ(ch);
} else if (esc_len >= 2 && ch >= '0' && ch <= '9') {
// VT sequences - ESC[<number>~
esc_code = esc_code * 10 + (ch - '0');
esc_len++;
} else if (esc_len >= 3 && ch == '~') {
// End of VT sequence
return ESC_SEQ(esc_code);
} else if (esc_len >= 3) {
// Invalid VT sequence
esc_len = 0;
} else {
// Non-escape character
return ch;
}
}
}
// Finds the next character that can be used for autocomplete.
// Returns '\0' if there are no more characters.
static char cli_autocomplete(cli_t* cli, const char* prefix) {
char next_char = '\0';
size_t prefix_len = strlen(prefix);
for (size_t i = 0; i < cli->cmd_count; i++) {
const char* cmd = cli->cmd_array[i].name;
if (cstr_starts_with(cmd, prefix)) {
char ch = cmd[prefix_len];
if (next_char == '\0') {
next_char = ch;
} else if (ch != next_char) {
return '\0';
}
}
}
return next_char;
}
// Reads a line from the console input and stores it in the `cli->line` buffer
//
// Returns false if the input line is too long
static bool cli_readln(cli_t* cli) {
char* buf = cli->line_buffer;
int len = 0; // number of characters in the buffer (excluding '\0')
int cursor = 0; // cursor position in the buffer
int hist_idx = 0;
int hist_prefix = 0;
buf[0] = '\0';
for (;;) {
int ch = cli_readch(cli);
switch (ch) {
case ESC_SEQ('A'): // ESC[A
// Up arrow - search history backwards
if (hist_idx == 0) {
hist_prefix = len;
}
const char* hist_line =
cli_history_rev(cli, &hist_idx, buf, hist_prefix);
if (hist_line != NULL) {
if (cursor > 0) {
// Move the cursor to the beginning of the line
cli_printf(cli, "\e[%dD", cursor);
}
// Replace original text
strcpy(buf, hist_line);
len = cursor = strlen(buf);
cli_printf(cli, "%s\e[K", buf);
}
continue;
case ESC_SEQ('B'): // ESC[B
// Down arrow - search history forwards
if (hist_idx > 0) {
const char* hist_line =
cli_history_fwd(cli, &hist_idx, buf, hist_prefix);
if (hist_line != NULL) {
if (cursor > 0) {
// Move the cursor to the beginning of the line
cli_printf(cli, "\e[%dD", cursor);
}
// Replace original text
strcpy(buf, hist_line);
len = cursor = strlen(buf);
cli_printf(cli, "%s\e[K", buf);
} else {
if (cursor > hist_prefix) {
cli_printf(cli, "\e[%dD", cursor - hist_prefix);
}
cli_printf(cli, "\e[K");
len = cursor = hist_prefix;
buf[len] = '\0';
}
}
continue;
}
// Reset the history index, if the user types something else
hist_idx = 0;
switch (ch) {
case ESC_SEQ('C'): // ESC[C
// Right arrow
if (cursor < len) {
if (cli->interactive) {
cli_printf(cli, "\e[C");
}
cursor++;
}
break;
case ESC_SEQ('D'): // ESC[D
// Left arrow
if (cursor > 0) {
if (cli->interactive) {
cli_printf(cli, "\e[D");
}
cursor--;
}
break;
case '\b':
case 0x7F:
// backspace => delete last character
if (cursor == 0) break;
if (cli->interactive) {
// Move the cursor left
cli_printf(cli, "\e[D");
}
--cursor;
// do not break, fall through
case ESC_SEQ(3): // ESC[3~
// Delete
if (cursor < len) {
// Delete the character at the cursor
memmove(&buf[cursor], &buf[cursor + 1], len - cursor);
--len;
if (cli->interactive) {
// Print the rest of the line and move the cursor back
cli_printf(cli, "%s \b", &buf[cursor]);
if (cursor < len) {
cli_printf(cli, "\e[%dD", len - cursor);
}
}
}
break;
case '\r':
case '\n':
// end of line
if (cli->interactive) {
cli_printf(cli, "\r\n");
}
return len < CLI_LINE_BUFFER_SIZE;
case '\t':
// tab => autocomplete
if (cli->interactive && len == cursor) {
char ch;
while ((ch = cli_autocomplete(cli, buf)) != '\0') {
if (len < CLI_LINE_BUFFER_SIZE - 1) {
cli_printf(cli, "%c", ch);
buf[len++] = ch;
buf[len] = '\0';
cursor++;
}
}
}
break;
default:
if (ch >= 0x20 && ch <= 0x7E) {
// Printable character
if (len < CLI_LINE_BUFFER_SIZE - 1) {
// Insert the character at the cursor
++len;
memmove(&buf[cursor + 1], &buf[cursor], len - cursor);
buf[cursor] = ch;
// Print new character and the rest of the line
if (cli->interactive) {
cli_printf(cli, "%s", &buf[cursor]);
}
++cursor;
if (cli->interactive && cursor < len) {
// Move the cursor back
cli_printf(cli, "\e[%dD", len - cursor);
}
}
}
}
}
}
// Splits the command line into arguments
// Returns false if there are too many arguments
static const char* cstr_token(char** str) {
char* p = *str;
// Skip leading whitespace
p = (char*)cstr_skip_whitespace(p);
// Start of token
const char* token = p;
// Find the end of the token
while (*p != '\0' && !isspace((unsigned char)*p)) {
++p;
}
// Terminate the token
if (*p != '\0') {
*p++ = '\0';
}
*str = p;
return token;
}
static bool cli_split_args(cli_t* cli) {
char* buf = cli->line_buffer;
cli->cmd_name = cstr_token(&buf);
cli->args_count = 0;
while (*buf != '\0' && cli->args_count < CLI_MAX_ARGS) {
const char* arg = cstr_token(&buf);
if (*arg != '\0') {
cli->args[cli->args_count++] = arg;
}
}
return *cstr_skip_whitespace(buf) == '\0';
}
void cli_run_loop(cli_t* cli) {
while (true) {
if (cli->interactive) {
if (cli->final_status) {
// Finalize the last command with an empty line
cli_printf(cli, "\r\n");
}
// Print the prompt
cli_printf(cli, "> ");
}
cli->final_status = false;
cli->aborted = false;
// Read the next line
if (!cli_readln(cli)) {
cli_error(cli, CLI_ERROR_FATAL, "Input line too long.");
continue;
}
cli_history_add(cli, cli->line_buffer);
// Split command line into arguments
if (!cli_split_args(cli)) {
cli_error(cli, CLI_ERROR_FATAL, "Too many arguments.");
continue;
}
// Empty line?
if (*cli->cmd_name == '\0') {
// Switch to interactive mode if two empty lines are entered
if (++cli->empty_lines >= 2 && !cli->interactive) {
cli->interactive = true;
// Print the welcome message
const cli_command_t* cmd = cli_find_command(cli, "$intro");
if (cmd != NULL) {
cmd->func(cli);
}
}
continue;
}
cli->empty_lines = 0;
// Find the command handler
cli->current_cmd = cli_find_command(cli, cli->cmd_name);
if (cli->current_cmd == NULL) {
cli_error(cli, CLI_ERROR_INVALID_CMD, "Invalid command '%s', try 'help'.",
cli->cmd_name);
continue;
}
// Call the command handler
cli->current_cmd->func(cli);
if (!cli->final_status) {
// Command handler hasn't send final status
if (cli->aborted) {
cli_error(cli, CLI_ERROR_ABORT, "");
} else {
cli_error(cli, CLI_ERROR_FATAL,
"Command handler didn't finish properly.");
}
}
}
}
// Return position of the argument with the given name in
// the command definition.
//
// Returns -1 if the argument is not present.
static int find_arg(const cli_command_t* cmd, const char* name) {
if (cmd->args == NULL) {
return -1;
}
const char* p = cmd->args;
int index = 0;
while (*p != '\0') {
// Skip '<' or '[>'
while (*p != '\0' && (*p == ' ' || *p == '<' || *p == '[')) {
p++;
}
// Extract argument name
const char* s = p;
while (*p != '\0' && (*p != '>' && *p != ']')) {
p++;
}
if (strlen(name) == (p - s) && strncmp(s, name, p - s) == 0) {
return index;
}
// Skip ']' or '>'
while (*p != '\0' && (*p == ']' || *p == '>')) {
p++;
}
index++;
}
return -1;
}
size_t cli_arg_count(cli_t* cli) { return cli->args_count; }
bool cli_has_nth_arg(cli_t* cli, int n) {
return n >= 0 && n < cli->args_count;
}
bool cli_has_arg(cli_t* cli, const char* name) {
return cli_has_nth_arg(cli, find_arg(cli->current_cmd, name));
}
const char* cli_nth_arg(cli_t* cli, int n) {
if (n >= 0 && n < cli->args_count) {
return cli->args[n];
} else {
return "";
}
}
const char* cli_arg(cli_t* cli, const char* name) {
return cli_nth_arg(cli, find_arg(cli->current_cmd, name));
}
bool cli_nth_arg_uint32(cli_t* cli, int n, uint32_t* result) {
const char* arg = cli_nth_arg(cli, n);
return cstr_parse_uint32(arg, 0, result);
}
bool cli_arg_uint32(cli_t* cli, const char* name, uint32_t* result) {
const char* arg = cli_arg(cli, name);
return cstr_parse_uint32(arg, 0, result);
}
bool cli_arg_hex(cli_t* cli, const char* name, uint8_t* dst, size_t dst_len,
size_t* bytes_written) {
const char* arg = cli_arg(cli, name);
return cstr_decode_hex(arg, dst, dst_len, bytes_written);
}