/* Copyright (C) 2006 Free Software Foundation. Written by Johan Rydberg This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation. 51 Franklin Street, Fifth Floor Boston, MA 02110-1301, USA. */ #include #include #include "boot/readline.h" /* Definitions. */ typedef struct command command_t; /* Evaluate to nonzero if C is between '0' and '9'. */ #define ISNUM(c) (c >= '0' && c <= '9') /* Evaluate to nonzero if C is between 'a' and 'z'. */ #define ISALPHA(c) (c >= 'a' && c <= 'z') /* Structures. */ struct command { command_t *next; /* Name of the command, that will be used when the command is invoked. */ efi_char16_t *name; /* Documentation string of the command. First line should be self-explaining. */ efi_char16_t *doc; /* List of child commands. */ command_t **prefix_list; /* Commandline complete generator. */ int (*completer) (efi_char16_t **text, efi_char16_t ***matches); /* Function that will be called when the command is invoked. */ void (*func) (efi_char16_t *); }; /* List of global commands. */ static command_t *cmdlist = 0; /* Create a new command and put it on command list LIST. The command will be invoked by typing in NAME. FN will be called with the argument string. DOC is string that documents the effects of the command. First line of the document string should be self explaining. PREFIX_LIST can point to a sub-list of commands. */ static command_t * command_add (efi_char16_t *name, void (*fn) (efi_char16_t *), command_t **prefix_list, command_t **list, efi_char16_t *doc) { command_t *c; c = efi_xmalloc (sizeof *c); c->next = 0; c->name = name; c->doc = doc; c->prefix_list = prefix_list; c->func = fn; c->completer = 0; if (*list) { do list = &(*list)->next; while (*list); } *list = c; return c; } static efi_char16_t * skip_whitespaces (efi_char16_t *s) { while (*s == ' ' || *s == '\t') s++; return s; } static efi_char16_t * eat_whitespaces (efi_char16_t *s) { s = skip_whitespaces (s); while (s[efi_wstrlen (s) - 1] == ' ') s[efi_wstrlen (s) - 1] = '\0'; return s; } static void split_cmdline (efi_char16_t **s, efi_char16_t **e) { *s = skip_whitespaces (*s); for (*e = *s; **e && (**e != ' ' && **e != '\t');) (*e)++; } /* Lookup command in list LIST. Found command will be returned or -1 if more than one command matched the command line. *SP will be updated on success to point to the next command or argument. */ static command_t * command_lookup_1 (command_t *list, efi_char16_t **sp) { efi_char16_t *p, *s; s = skip_whitespaces (*sp); for (p = s; *p && (*p != ' ' && *p != '\t');) p++; if (p == s) return 0; else { efi_char16_t name[p - s + 1]; command_t *c; command_t *found = 0; efi_wstrncpy0 (name, s, p - s); for (c = list; c; c = c->next) { if (!efi_wstrncmp (c->name, name, p - s)) { if (found) return (command_t *) -1; found = c; } } *sp = p; return found; } } /* Lookup command *SP in list LIST. Returns pointer to matching command, and *SP will point to the first argument. -1 is returned if more than one command matches. */ static command_t * command_lookup (command_t *list, efi_char16_t **sp) { command_t *found = 0; while (1) { command_t *c = command_lookup_1 (list, sp); if (c == (command_t *) -1) /* Several commands matching. */ return (command_t *) -1; else if (c) { if (!c->prefix_list) return c; found = c; list = *c->prefix_list; } else { return found; } } } /* Completion generator function for the command line interface. On call, *TEXT points to the command line to complete. This function will locate commands matching the text, and return an array of matching names in *MATCHES. The number of elements in the array is returned, or zero if there were no matches. On return, *TEXT will be updated so that it is aligned with the matches. */ static int command_generator (efi_char16_t **text, efi_char16_t ***matches) { command_t *list = cmdlist; auto int complete (command_t *l, efi_char16_t *t, efi_char16_t ***vect); int complete (command_t *l, efi_char16_t *t, efi_char16_t ***vect) { int n = 0, i; efi_char16_t *e; efi_char16_t *v; command_t *c, *f; split_cmdline (&t, &e); v = efi_xwstrndup (t, e - t); for (c = l; c; c = c->next) { if (!efi_wstrncmp (c->name, v, e - t)) n++; } if (!vect) { efi_free_pool (v); return n; } *vect = efi_xmalloc (sizeof (efi_char16_t *) * n); for (c = l, i = 0; c; c = c->next) { if (!efi_wstrncmp (c->name, v, e - t)) { /* Add a space after the matched command name, so that the user can start completing the next argument directly without having to press space. */ (*vect)[i++] = efi_wasprintf (L"%s ", c->name); } } efi_free_pool (v); return n; } *text = skip_whitespaces (*text); while (1) { efi_char16_t *sp = *text; command_t *c; c = command_lookup_1 (list, &sp); if (c == (command_t *) -1) /* More than one command matched; gather them. */ return complete (list, *text, matches); else if (c) { /* At this point SP should point at the character directly after the matching command name. Keep completing until there is a space after the command. */ if (efi_wstrlen (c->name) != (efi_uint_t) (sp - *text) || *sp != ' ') /* The text did not match the command name completely, so return a complete with just one matche. That will fill the command line to the next argument/command. */ return complete (list, *text, matches); if (c->prefix_list) /* This is a prefix command, try to complete the next command on the command line. */ list = *c->prefix_list; else { if (c->completer) { *text = skip_whitespaces (sp); return (*c->completer) (text, matches); } return 0; } } else { if (!**text) /* Special case: If no command matched, and there were not even an effort to match anything; give a list of all available commands. */ return complete (list, *text, matches); /* No matching nothing. */ return 0; } /* Eat whitespaces till the next command. */ *text = skip_whitespaces (sp); } } /* Execute command line specified by P, picking commands from list LIST. Will print out errors messages. */ static void command_execute (command_t *list, efi_char16_t *p) { command_t *c; c = command_lookup (list, &p); if (c == (command_t *) -1) efi_wprintf (L"More than one command matching.\n"); else if (!c || !c->func) efi_wprintf (L"No such command.\n"); else { /* Skip whitespaces up to the first argument. Pass NULL instead of an empty string. */ p = skip_whitespaces (p); if (!*p) p = NULL; (*c->func) (p); } } /* Return nonzero if N is a valid disk name. */ static int valid_name (efi_char16_t *n) { return ((n[0] == 'u' || n[0] == 'h' || n[0] == 's') && n[1] == 'd' && ISNUM (n[2]) && ISALPHA (n[3]) && n[4] == '\0'); } /* The reason for not keeping disks in a list is that they might come and go dynamicly (think CD's and USB's). Instead we probe for them every time they are needed. */ /* Iterate through available disks that implement protocol specified by GUID and invoke HOOK for each one of them. */ static efi_status_t disk_iterate (efi_guid_t *guid, int (*hook) (efi_device_path_t *p, efi_char16_t *name)) { int usb_name = 0; efi_uint_t num_handles, i; efi_handle_t *handles; efi_status_t err; efi_char16_t name[5]; err = efi_locate_handle_buffer (EFI_LOCATE_SEARCH_TYPE_BY_PROTOCOL, guid, 0, &num_handles, &handles); if (err) return err; for (i = 0; i < num_handles; i++) { efi_device_path_t *path, *p, *s; path = efi_device_path_get_from_handle (handles[i]); if (!path) continue; efi_memset (name, 0, sizeof name); p = path; while ((s = efi_device_path_iterate (&p))) { switch (EFI_DEVICE_PATH_TYPE (s)) { case EFI_MESSAGING_DEVICE_PATH_TYPE: switch (EFI_DEVICE_PATH_SUBTYPE (s)) { case EFI_ATAPI_DEVICE_PATH_SUBTYPE: { efi_atapi_device_path_t *atapi = (efi_atapi_device_path_t *) s; efi_wstrcpy (name, L"hd"); name[2] = ('0' + atapi->primary_secondary * 2 + atapi->slave_master); break; } case EFI_SCSI_DEVICE_PATH_SUBTYPE: /* FIXME: implement */ break; } break; case EFI_MEDIA_DEVICE_PATH_TYPE: switch (EFI_DEVICE_PATH_SUBTYPE (s)) { case EFI_HARD_DRIVE_DEVICE_PATH_SUBTYPE: { efi_hard_drive_device_path_t *hard_drive = (efi_hard_drive_device_path_t *) s; name[3] = ('a' + hard_drive->partition_number); break; } } break; } } if (valid_name (name)) { if ((*hook) (path, name)) goto done; } } done: efi_free_pool (handles); return EFI_SUCCESS; } static efi_char16_t * efi_wstrchr (efi_char16_t *s, efi_char16_t c) { while (*s) { if (*s == c) return s; s++; } return NULL; } /* Open file system that is attached to the handle that can be found at the device path specified by PATH. The root node of the file system will be returned. */ static efi_file_t * open_file_system (efi_device_path_t *path) { efi_simple_file_system_t *fs; efi_handle_t h; efi_status_t err; efi_file_t *file = 0; err = efi_locate_device_path (&EFI_SIMPLE_FILE_SYSTEM_GUID, &path, &h); if (err) goto done; err = efi_handle_protocol (h, &EFI_SIMPLE_FILE_SYSTEM_GUID, (void **) &fs); if (err) goto done; err = (*fs->open_volume) (fs, &file); if (err) /* Can not be sure that the file system handler leaves the pointer alone. */ file = 0; done: return file; } /* Split filepath *FILEPATH into filename and device path of the disk in the filepath. The device path will be returned in *PATH. On success, *FILEPATH will point to the first character in the filename. */ static efi_status_t split_filepath (efi_char16_t **filepath, efi_device_path_t **path) { efi_char16_t *fp = *filepath; efi_char16_t *sp; sp = efi_wstrchr (*filepath, ':'); if (!sp) /* FIXME: A better solution for this case might be to just return the filename and set *PATH to NULL. */ return EFI_NOT_FOUND; else { efi_char16_t disk_name[sp - *filepath + 1]; auto int match_disk (efi_device_path_t *p, efi_char16_t *name); int match_disk (efi_device_path_t *p, efi_char16_t *name) { if (efi_wstrcmp (name, disk_name) == 0) *path = efi_device_path_duplicate (p); return 0; } efi_wstrncpy0 (disk_name, *filepath, sp - *filepath); /* Check for a match by setting *PATH to the matched path. */ *path = NULL; disk_iterate (&EFI_SIMPLE_FILE_SYSTEM_GUID, match_disk); if (*path == NULL) return EFI_NOT_FOUND; *filepath = (sp + 1); return EFI_SUCCESS; } } static efi_device_path_t * make_filepath_device_path (efi_device_path_t *p, efi_char16_t *fn) { efi_file_path_device_path_t *fp; efi_uint_t len; len = (efi_wstrlen (fn) + 1) * sizeof (efi_char16_t); fp = (efi_file_path_device_path_t *) efi_device_path_create_node (EFI_MEDIA_DEVICE_PATH_TYPE, EFI_FILE_PATH_DEVICE_PATH_SUBTYPE, sizeof *fp + len); efi_memcpy (fp->path_name, fn, len); return efi_device_path_append_node (p, (efi_device_path_t *) fp); } static efi_device_path_t * make_filepath (efi_char16_t *filepath) { efi_device_path_t *path; if (split_filepath (&filepath, &path)) return NULL; return make_filepath_device_path (path, filepath); } /* On the final completion, add a type character to ease further completions. For directories, add a '\\' and a space for files (so that the user can continue with the next argument.) */ #define TYPE_CHAR(fi) (EFI_FILE_INFO_DIRECTORY_P (fi) ? '\\' : ' ') /* Filepath completer. This completer completes paths in the form of DISK:\PATH. */ static int filepath_completer (efi_char16_t **text, efi_char16_t ***matches) { int num_matches = 0; efi_char16_t *t, *s; efi_status_t err; auto void add_match (efi_char16_t *match); void add_match (efi_char16_t *match) { int last_size = num_matches++; efi_char16_t **new_m; new_m = efi_xmalloc (num_matches * sizeof (efi_char16_t *)); efi_memcpy (new_m, *matches, last_size * sizeof (efi_char16_t *)); new_m[last_size] = match; *matches = new_m; } t = skip_whitespaces (*text); s = efi_wstrchr (t, ':'); if (!s) /* Disk name completion. */ { int match_disk (efi_device_path_t *dp, efi_char16_t *name) { if (!efi_wstrncmp (name, t, efi_wstrlen (t))) add_match (efi_wasprintf (L"%s:\\", name)); return 0; } disk_iterate (&EFI_SIMPLE_FILE_SYSTEM_GUID, match_disk); } else { efi_device_path_t *path; efi_file_t *curr_dir, *next_dir; err = split_filepath (&t, &path); if (err) goto done; curr_dir = open_file_system (path); if (!curr_dir) goto done; while (1) { efi_file_info_t *info; /* Make sure that completion is done in a directory and nothing else. */ info = efi_file_get_file_info (curr_dir); if (! EFI_FILE_INFO_DIRECTORY_P (info)) { (*curr_dir->close) (curr_dir); efi_free_pool (info); goto done; } efi_free_pool (info); while (*t == '\\') t++; s = efi_wstrchr (t, '\\'); if (!s) { /* Final component; read directory and try to complete against matching entries. */ while ((info = efi_file_readdir (curr_dir))) { if (!efi_wstrncmp (info->file_name, t, efi_wstrlen (t))) { /* Add a type character after the file name so that user can continue to complete with having to add a space or a backslash. */ add_match (efi_wasprintf (L"%s%c", info->file_name, TYPE_CHAR (info))); } efi_free_pool (info); } *text = t; (*curr_dir->close) (curr_dir); goto done; } else { efi_char16_t comp[s - t + 1]; efi_wstrncpy0 (comp, t, s - t); err = (*curr_dir->open) (curr_dir, &next_dir, comp, EFI_FILE_MODE_READ, 0); if (err) { (*curr_dir->close) (curr_dir); goto done; } t = s; (*curr_dir->close) (curr_dir); curr_dir = next_dir; } } } done: return num_matches; } /* Load and start application or driver. */ void load_command (efi_char16_t *arg) { efi_device_path_t *dp; efi_status_t err; efi_handle_t h; efi_char16_t *reason; efi_uint_t size; if (!arg) { efi_wprintf (L"load: No path specified.\n"); return; } arg = eat_whitespaces (arg); dp = make_filepath (arg); if (!dp) { err = EFI_NOT_FOUND; goto error; } err = efi_load_image (0, _efi_image_handle, dp, 0, 0, &h); if (err) goto error; err = efi_start_image (h, &size, &reason); if (err) { if (size) { efi_wprintf (L"(exit reason; %s)\n", reason); efi_free_pool (reason); } goto error; } error: if (err) efi_wprintf (L"load: %s: %s\n", arg, efi_wstrerror (err)); } /* Display version information. */ static void show_version (void) { efi_puts (L"\ gnufi " VERSION "\n\ Copyright 2006 Free Software Foundation, Inc.\n\ gnufi is free software, covered by the GNU General Public License, and you are\n\ welcome to change it and/or distribute copies of it under certain conditions.\n\n"); } void shell (void) { efi_char16_t cmdline[128]; command_t *c; c = command_add (L"load", load_command, 0, &cmdlist, L"Load and start program or driver."); c->completer = filepath_completer; show_version (); while (1) { cmdline[0] = '\0'; efi_readline (L"(gnufi) ", cmdline, 128, command_generator); command_execute (cmdlist, cmdline); } } efi_status_t efi_main (efi_handle_t image) { shell (); return 0; /* make GCC happy. */ }