Skip to content
Snippets Groups Projects
cmd_pxe.c 32.7 KiB
Newer Older
  • Learn to ignore specific revisions
  • Jason Hobbs's avatar
    Jason Hobbs committed
    /*
     * Copyright 2010-2011 Calxeda, Inc.
     *
     * 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 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, see <http://www.gnu.org/licenses/>.
     */
    #include <common.h>
    #include <command.h>
    #include <malloc.h>
    #include <linux/string.h>
    #include <linux/ctype.h>
    #include <errno.h>
    #include <linux/list.h>
    
    #include "menu.h"
    
    #define MAX_TFTP_PATH_LEN 127
    
    /*
     * Like getenv, but prints an error if envvar isn't defined in the
     * environment.  It always returns what getenv does, so it can be used in
     * place of getenv without changing error handling otherwise.
     */
    
    static char *from_env(const char *envvar)
    
    Jason Hobbs's avatar
    Jason Hobbs committed
    {
    	char *ret;
    
    	ret = getenv(envvar);
    
    	if (!ret)
    		printf("missing environment variable: %s\n", envvar);
    
    	return ret;
    }
    
    /*
     * Convert an ethaddr from the environment to the format used by pxelinux
     * filenames based on mac addresses. Convert's ':' to '-', and adds "01-" to
     * the beginning of the ethernet address to indicate a hardware type of
     * Ethernet. Also converts uppercase hex characters into lowercase, to match
     * pxelinux's behavior.
     *
     * Returns 1 for success, -ENOENT if 'ethaddr' is undefined in the
     * environment, or some other value < 0 on error.
     */
    static int format_mac_pxe(char *outbuf, size_t outbuf_len)
    {
    
    	uchar ethaddr[6];
    
    Jason Hobbs's avatar
    Jason Hobbs committed
    
    
    	if (outbuf_len < 21) {
    		printf("outbuf is too small (%d < 21)\n", outbuf_len);
    
    Jason Hobbs's avatar
    Jason Hobbs committed
    
    		return -EINVAL;
    	}
    
    
    	if (!eth_getenv_enetaddr_by_index("eth", eth_get_dev_index(),
    					  ethaddr))
    		return -ENOENT;
    
    Jason Hobbs's avatar
    Jason Hobbs committed
    
    
    	sprintf(outbuf, "01-%02x-%02x-%02x-%02x-%02x-%02x",
    		ethaddr[0], ethaddr[1], ethaddr[2],
    		ethaddr[3], ethaddr[4], ethaddr[5]);
    
    Jason Hobbs's avatar
    Jason Hobbs committed
    
    	return 1;
    }
    
    /*
     * Returns the directory the file specified in the bootfile env variable is
     * in. If bootfile isn't defined in the environment, return NULL, which should
     * be interpreted as "don't prepend anything to paths".
     */
    
    Rob Herring's avatar
    Rob Herring committed
    static int get_bootfile_path(const char *file_path, char *bootfile_path,
    			     size_t bootfile_path_size)
    
    Jason Hobbs's avatar
    Jason Hobbs committed
    {
    	char *bootfile, *last_slash;
    
    Rob Herring's avatar
    Rob Herring committed
    	size_t path_len = 0;
    
    	if (file_path[0] == '/')
    		goto ret;
    
    Jason Hobbs's avatar
    Jason Hobbs committed
    
    	bootfile = from_env("bootfile");
    
    
    Rob Herring's avatar
    Rob Herring committed
    	if (!bootfile)
    		goto ret;
    
    Jason Hobbs's avatar
    Jason Hobbs committed
    
    	last_slash = strrchr(bootfile, '/');
    
    
    Rob Herring's avatar
    Rob Herring committed
    	if (last_slash == NULL)
    		goto ret;
    
    Jason Hobbs's avatar
    Jason Hobbs committed
    
    	path_len = (last_slash - bootfile) + 1;
    
    	if (bootfile_path_size < path_len) {
    		printf("bootfile_path too small. (%d < %d)\n",
    				bootfile_path_size, path_len);
    
    		return -1;
    	}
    
    	strncpy(bootfile_path, bootfile, path_len);
    
    
    Rob Herring's avatar
    Rob Herring committed
     ret:
    
    Jason Hobbs's avatar
    Jason Hobbs committed
    	bootfile_path[path_len] = '\0';
    
    	return 1;
    }
    
    
    static int (*do_getfile)(const char *file_path, char *file_addr);
    
    static int do_get_tftp(const char *file_path, char *file_addr)
    
    {
    	char *tftp_argv[] = {"tftp", NULL, NULL, NULL};
    
    	tftp_argv[1] = file_addr;
    
    	tftp_argv[2] = (void *)file_path;
    
    
    	if (do_tftpb(NULL, 0, 3, tftp_argv))
    		return -ENOENT;
    
    	return 1;
    }
    
    static char *fs_argv[5];
    
    
    static int do_get_ext2(const char *file_path, char *file_addr)
    
    {
    #ifdef CONFIG_CMD_EXT2
    	fs_argv[0] = "ext2load";
    	fs_argv[3] = file_addr;
    
    	fs_argv[4] = (void *)file_path;
    
    
    	if (!do_ext2load(NULL, 0, 5, fs_argv))
    		return 1;
    #endif
    	return -ENOENT;
    }
    
    
    static int do_get_fat(const char *file_path, char *file_addr)
    
    {
    #ifdef CONFIG_CMD_FAT
    	fs_argv[0] = "fatload";
    	fs_argv[3] = file_addr;
    
    	fs_argv[4] = (void *)file_path;
    
    
    	if (!do_fat_fsload(NULL, 0, 5, fs_argv))
    		return 1;
    #endif
    	return -ENOENT;
    }
    
    
    Jason Hobbs's avatar
    Jason Hobbs committed
    /*
     * As in pxelinux, paths to files referenced from files we retrieve are
     * relative to the location of bootfile. get_relfile takes such a path and
     * joins it with the bootfile path to get the full path to the target file. If
     * the bootfile path is NULL, we use file_path as is.
     *
     * Returns 1 for success, or < 0 on error.
     */
    
    static int get_relfile(const char *file_path, void *file_addr)
    
    Jason Hobbs's avatar
    Jason Hobbs committed
    {
    	size_t path_len;
    	char relfile[MAX_TFTP_PATH_LEN+1];
    	char addr_buf[10];
    	int err;
    
    
    Rob Herring's avatar
    Rob Herring committed
    	err = get_bootfile_path(file_path, relfile, sizeof(relfile));
    
    Jason Hobbs's avatar
    Jason Hobbs committed
    
    	if (err < 0)
    		return err;
    
    	path_len = strlen(file_path);
    	path_len += strlen(relfile);
    
    	if (path_len > MAX_TFTP_PATH_LEN) {
    		printf("Base path too long (%s%s)\n",
    					relfile,
    					file_path);
    
    		return -ENAMETOOLONG;
    	}
    
    	strcat(relfile, file_path);
    
    	printf("Retrieving file: %s\n", relfile);
    
    	sprintf(addr_buf, "%p", file_addr);
    
    
    	return do_getfile(relfile, addr_buf);
    
    Jason Hobbs's avatar
    Jason Hobbs committed
    }
    
    /*
     * Retrieve the file at 'file_path' to the locate given by 'file_addr'. If
     * 'bootfile' was specified in the environment, the path to bootfile will be
     * prepended to 'file_path' and the resulting path will be used.
     *
     * Returns 1 on success, or < 0 for error.
     */
    
    static int get_pxe_file(const char *file_path, void *file_addr)
    
    Jason Hobbs's avatar
    Jason Hobbs committed
    {
    	unsigned long config_file_size;
    	char *tftp_filesize;
    	int err;
    
    	err = get_relfile(file_path, file_addr);
    
    	if (err < 0)
    		return err;
    
    	/*
    	 * the file comes without a NUL byte at the end, so find out its size
    	 * and add the NUL byte.
    	 */
    	tftp_filesize = from_env("filesize");
    
    	if (!tftp_filesize)
    		return -ENOENT;
    
    	if (strict_strtoul(tftp_filesize, 16, &config_file_size) < 0)
    		return -EINVAL;
    
    	*(char *)(file_addr + config_file_size) = '\0';
    
    	return 1;
    }
    
    #define PXELINUX_DIR "pxelinux.cfg/"
    
    /*
     * Retrieves a file in the 'pxelinux.cfg' folder. Since this uses get_pxe_file
     * to do the hard work, the location of the 'pxelinux.cfg' folder is generated
     * from the bootfile path, as described above.
     *
     * Returns 1 on success or < 0 on error.
     */
    
    static int get_pxelinux_path(const char *file, void *pxefile_addr_r)
    
    Jason Hobbs's avatar
    Jason Hobbs committed
    {
    	size_t base_len = strlen(PXELINUX_DIR);
    	char path[MAX_TFTP_PATH_LEN+1];
    
    	if (base_len + strlen(file) > MAX_TFTP_PATH_LEN) {
    		printf("path (%s%s) too long, skipping\n",
    				PXELINUX_DIR, file);
    		return -ENAMETOOLONG;
    	}
    
    	sprintf(path, PXELINUX_DIR "%s", file);
    
    	return get_pxe_file(path, pxefile_addr_r);
    }
    
    /*
     * Looks for a pxe file with a name based on the pxeuuid environment variable.
     *
     * Returns 1 on success or < 0 on error.
     */
    static int pxe_uuid_path(void *pxefile_addr_r)
    {
    	char *uuid_str;
    
    	uuid_str = from_env("pxeuuid");
    
    	if (!uuid_str)
    		return -ENOENT;
    
    	return get_pxelinux_path(uuid_str, pxefile_addr_r);
    }
    
    /*
     * Looks for a pxe file with a name based on the 'ethaddr' environment
     * variable.
     *
     * Returns 1 on success or < 0 on error.
     */
    static int pxe_mac_path(void *pxefile_addr_r)
    {
    	char mac_str[21];
    	int err;
    
    	err = format_mac_pxe(mac_str, sizeof(mac_str));
    
    	if (err < 0)
    		return err;
    
    	return get_pxelinux_path(mac_str, pxefile_addr_r);
    }
    
    /*
     * Looks for pxe files with names based on our IP address. See pxelinux
     * documentation for details on what these file names look like.  We match
     * that exactly.
     *
     * Returns 1 on success or < 0 on error.
     */
    static int pxe_ipaddr_paths(void *pxefile_addr_r)
    {
    	char ip_addr[9];
    	int mask_pos, err;
    
    	sprintf(ip_addr, "%08X", ntohl(NetOurIP));
    
    	for (mask_pos = 7; mask_pos >= 0;  mask_pos--) {
    		err = get_pxelinux_path(ip_addr, pxefile_addr_r);
    
    		if (err > 0)
    			return err;
    
    		ip_addr[mask_pos] = '\0';
    	}
    
    	return -ENOENT;
    }
    
    /*
     * Entry point for the 'pxe get' command.
     * This Follows pxelinux's rules to download a config file from a tftp server.
     * The file is stored at the location given by the pxefile_addr_r environment
     * variable, which must be set.
     *
     * UUID comes from pxeuuid env variable, if defined
     * MAC addr comes from ethaddr env variable, if defined
     * IP
     *
     * see http://syslinux.zytor.com/wiki/index.php/PXELINUX
     *
     * Returns 0 on success or 1 on error.
     */
    static int
    do_pxe_get(cmd_tbl_t *cmdtp, int flag, int argc, char * const argv[])
    {
    	char *pxefile_addr_str;
    
    	unsigned long pxefile_addr_r;
    
    Jason Hobbs's avatar
    Jason Hobbs committed
    	int err;
    
    
    	do_getfile = do_get_tftp;
    
    
    Jason Hobbs's avatar
    Jason Hobbs committed
    	if (argc != 1)
    
    Jason Hobbs's avatar
    Jason Hobbs committed
    
    	pxefile_addr_str = from_env("pxefile_addr_r");
    
    	if (!pxefile_addr_str)
    		return 1;
    
    	err = strict_strtoul(pxefile_addr_str, 16,
    				(unsigned long *)&pxefile_addr_r);
    	if (err < 0)
    		return 1;
    
    	/*
    	 * Keep trying paths until we successfully get a file we're looking
    	 * for.
    	 */
    
    	if (pxe_uuid_path((void *)pxefile_addr_r) > 0
    		|| pxe_mac_path((void *)pxefile_addr_r) > 0
    		|| pxe_ipaddr_paths((void *)pxefile_addr_r) > 0
    		|| get_pxelinux_path("default", (void *)pxefile_addr_r) > 0) {
    
    Jason Hobbs's avatar
    Jason Hobbs committed
    
    		printf("Config file found\n");
    
    		return 0;
    	}
    
    	printf("Config file not found\n");
    
    	return 1;
    }
    
    /*
     * Wrapper to make it easier to store the file at file_path in the location
     * specified by envaddr_name. file_path will be joined to the bootfile path,
     * if any is specified.
     *
     * Returns 1 on success or < 0 on error.
     */
    
    static int get_relfile_envaddr(const char *file_path, const char *envaddr_name)
    
    Jason Hobbs's avatar
    Jason Hobbs committed
    {
    
    	unsigned long file_addr;
    
    Jason Hobbs's avatar
    Jason Hobbs committed
    	char *envaddr;
    
    	envaddr = from_env(envaddr_name);
    
    	if (!envaddr)
    		return -ENOENT;
    
    
    	if (strict_strtoul(envaddr, 16, &file_addr) < 0)
    
    Jason Hobbs's avatar
    Jason Hobbs committed
    		return -EINVAL;
    
    
    	return get_relfile(file_path, (void *)file_addr);
    
    Jason Hobbs's avatar
    Jason Hobbs committed
    }
    
    /*
     * A note on the pxe file parser.
     *
     * We're parsing files that use syslinux grammar, which has a few quirks.
     * String literals must be recognized based on context - there is no
     * quoting or escaping support. There's also nothing to explicitly indicate
     * when a label section completes. We deal with that by ending a label
     * section whenever we see a line that doesn't include.
     *
     * As with the syslinux family, this same file format could be reused in the
     * future for non pxe purposes. The only action it takes during parsing that
     * would throw this off is handling of include files. It assumes we're using
     * pxe, and does a tftp download of a file listed as an include file in the
     * middle of the parsing operation. That could be handled by refactoring it to
     * take a 'include file getter' function.
     */
    
    /*
     * Describes a single label given in a pxe file.
     *
     * Create these with the 'label_create' function given below.
     *
     * name - the name of the menu as given on the 'menu label' line.
     * kernel - the path to the kernel file to use for this label.
     * append - kernel command line to use when booting this label
     * initrd - path to the initrd to use for this label.
     * attempted - 0 if we haven't tried to boot this label, 1 if we have.
     * localboot - 1 if this label specified 'localboot', 0 otherwise.
     * list - lets these form a list, which a pxe_menu struct will hold.
     */
    struct pxe_label {
    	char *name;
    
    	char *menu;
    
    Jason Hobbs's avatar
    Jason Hobbs committed
    	char *kernel;
    	char *append;
    	char *initrd;
    
    Jason Hobbs's avatar
    Jason Hobbs committed
    	int attempted;
    	int localboot;
    	struct list_head list;
    };
    
    /*
     * Describes a pxe menu as given via pxe files.
     *
     * title - the name of the menu as given by a 'menu title' line.
     * default_label - the name of the default label, if any.
     * timeout - time in tenths of a second to wait for a user key-press before
     *           booting the default label.
     * prompt - if 0, don't prompt for a choice unless the timeout period is
     *          interrupted.  If 1, always prompt for a choice regardless of
     *          timeout.
     * labels - a list of labels defined for the menu.
     */
    struct pxe_menu {
    	char *title;
    	char *default_label;
    	int timeout;
    	int prompt;
    	struct list_head labels;
    };
    
    /*
     * Allocates memory for and initializes a pxe_label. This uses malloc, so the
     * result must be free()'d to reclaim the memory.
     *
     * Returns NULL if malloc fails.
     */
    static struct pxe_label *label_create(void)
    {
    	struct pxe_label *label;
    
    	label = malloc(sizeof(struct pxe_label));
    
    	if (!label)
    		return NULL;
    
    	memset(label, 0, sizeof(struct pxe_label));
    
    	return label;
    }
    
    /*
     * Free the memory used by a pxe_label, including that used by its name,
     * kernel, append and initrd members, if they're non NULL.
     *
     * So - be sure to only use dynamically allocated memory for the members of
     * the pxe_label struct, unless you want to clean it up first. These are
     * currently only created by the pxe file parsing code.
     */
    static void label_destroy(struct pxe_label *label)
    {
    	if (label->name)
    		free(label->name);
    
    	if (label->kernel)
    		free(label->kernel);
    
    	if (label->append)
    		free(label->append);
    
    	if (label->initrd)
    		free(label->initrd);
    
    
    	if (label->fdt)
    		free(label->fdt);
    
    
    Jason Hobbs's avatar
    Jason Hobbs committed
    	free(label);
    }
    
    /*
     * Print a label and its string members if they're defined.
     *
     * This is passed as a callback to the menu code for displaying each
     * menu entry.
     */
    static void label_print(void *data)
    {
    	struct pxe_label *label = data;
    
    	const char *c = label->menu ? label->menu : label->kernel;
    
    Jason Hobbs's avatar
    Jason Hobbs committed
    
    
    	printf("%s:\t%s\n", label->name, c);
    
    Jason Hobbs's avatar
    Jason Hobbs committed
    
    	if (label->kernel)
    
    		printf("\t\tkernel: %s\n", label->kernel);
    
    Jason Hobbs's avatar
    Jason Hobbs committed
    
    	if (label->append)
    
    		printf("\t\tappend: %s\n", label->append);
    
    Jason Hobbs's avatar
    Jason Hobbs committed
    
    	if (label->initrd)
    
    		printf("\t\tinitrd: %s\n", label->initrd);
    
    
    	if (label->fdt)
    		printf("\tfdt: %s\n", label->fdt);
    
    Jason Hobbs's avatar
    Jason Hobbs committed
    }
    
    /*
     * Boot a label that specified 'localboot'. This requires that the 'localcmd'
     * environment variable is defined. Its contents will be executed as U-boot
     * command.  If the label specified an 'append' line, its contents will be
     * used to overwrite the contents of the 'bootargs' environment variable prior
     * to running 'localcmd'.
     *
     * Returns 1 on success or < 0 on error.
     */
    static int label_localboot(struct pxe_label *label)
    {
    
    Jason Hobbs's avatar
    Jason Hobbs committed
    
    	localcmd = from_env("localcmd");
    
    	if (!localcmd)
    		return -ENOENT;
    
    	if (label->append)
    		setenv("bootargs", label->append);
    
    
    	debug("running: %s\n", localcmd);
    
    Jason Hobbs's avatar
    Jason Hobbs committed
    
    
    	return run_command_list(localcmd, strlen(localcmd), 0);
    
    Jason Hobbs's avatar
    Jason Hobbs committed
    }
    
    /*
     * Boot according to the contents of a pxe_label.
     *
     * If we can't boot for any reason, we return.  A successful boot never
     * returns.
     *
     * The kernel will be stored in the location given by the 'kernel_addr_r'
     * environment variable.
     *
     * If the label specifies an initrd file, it will be stored in the location
     * given by the 'ramdisk_addr_r' environment variable.
     *
     * If the label specifies an 'append' line, its contents will overwrite that
     * of the 'bootargs' environment variable.
     */
    static void label_boot(struct pxe_label *label)
    {
    	char *bootm_argv[] = { "bootm", NULL, NULL, NULL, NULL };
    	int bootm_argc = 3;
    
    	label_print(label);
    
    	label->attempted = 1;
    
    	if (label->localboot) {
    		label_localboot(label);
    		return;
    	}
    
    	if (label->kernel == NULL) {
    		printf("No kernel given, skipping %s\n",
    				label->name);
    		return;
    	}
    
    	if (label->initrd) {
    		if (get_relfile_envaddr(label->initrd, "ramdisk_addr_r") < 0) {
    			printf("Skipping %s for failure retrieving initrd\n",
    					label->name);
    			return;
    		}
    
    		bootm_argv[2] = getenv("ramdisk_addr_r");
    	} else {
    		bootm_argv[2] = "-";
    	}
    
    	if (get_relfile_envaddr(label->kernel, "kernel_addr_r") < 0) {
    		printf("Skipping %s for failure retrieving kernel\n",
    				label->name);
    		return;
    	}
    
    	if (label->append)
    		setenv("bootargs", label->append);
    
    	bootm_argv[1] = getenv("kernel_addr_r");
    
    	/*
    
    	 * fdt usage is optional:
    	 * It handles the following scenarios. All scenarios are exclusive
    	 *
    	 * Scenario 1: If fdt_addr_r specified and "fdt" label is defined in
    	 * pxe file, retrieve fdt blob from server. Pass fdt_addr_r to bootm,
    	 * and adjust argc appropriately.
    	 *
    	 * Scenario 2: If there is an fdt_addr specified, pass it along to
    	 * bootm, and adjust argc appropriately.
    	 *
    	 * Scenario 3: fdt blob is not available.
    
    Jason Hobbs's avatar
    Jason Hobbs committed
    	 */
    
    	bootm_argv[3] = getenv("fdt_addr_r");
    
    	/* if fdt label is defined then get fdt from server */
    	if (bootm_argv[3] && label->fdt) {
    		if (get_relfile_envaddr(label->fdt, "fdt_addr_r") < 0) {
    			printf("Skipping %s for failure retrieving fdt\n",
    					label->name);
    			return;
    		}
    	} else
    		bootm_argv[3] = getenv("fdt_addr");
    
    Jason Hobbs's avatar
    Jason Hobbs committed
    
    	if (bootm_argv[3])
    		bootm_argc = 4;
    
    	do_bootm(NULL, 0, bootm_argc, bootm_argv);
    }
    
    /*
     * Tokens for the pxe file parser.
     */
    enum token_type {
    	T_EOL,
    	T_STRING,
    	T_EOF,
    	T_MENU,
    	T_TITLE,
    	T_TIMEOUT,
    	T_LABEL,
    	T_KERNEL,
    
    Jason Hobbs's avatar
    Jason Hobbs committed
    	T_APPEND,
    	T_INITRD,
    	T_LOCALBOOT,
    	T_DEFAULT,
    	T_PROMPT,
    	T_INCLUDE,
    
    Jason Hobbs's avatar
    Jason Hobbs committed
    	T_INVALID
    };
    
    /*
     * A token - given by a value and a type.
     */
    struct token {
    	char *val;
    	enum token_type type;
    };
    
    /*
     * Keywords recognized.
     */
    static const struct token keywords[] = {
    	{"menu", T_MENU},
    	{"title", T_TITLE},
    	{"timeout", T_TIMEOUT},
    	{"default", T_DEFAULT},
    	{"prompt", T_PROMPT},
    	{"label", T_LABEL},
    	{"kernel", T_KERNEL},
    
    	{"linux", T_LINUX},
    
    Jason Hobbs's avatar
    Jason Hobbs committed
    	{"localboot", T_LOCALBOOT},
    	{"append", T_APPEND},
    	{"initrd", T_INITRD},
    	{"include", T_INCLUDE},
    
    	{"fdt", T_FDT},
    
    Jason Hobbs's avatar
    Jason Hobbs committed
    	{NULL, T_INVALID}
    };
    
    /*
     * Since pxe(linux) files don't have a token to identify the start of a
     * literal, we have to keep track of when we're in a state where a literal is
     * expected vs when we're in a state a keyword is expected.
     */
    enum lex_state {
    	L_NORMAL = 0,
    	L_KEYWORD,
    	L_SLITERAL
    };
    
    /*
     * get_string retrieves a string from *p and stores it as a token in
     * *t.
     *
     * get_string used for scanning both string literals and keywords.
     *
     * Characters from *p are copied into t-val until a character equal to
     * delim is found, or a NUL byte is reached. If delim has the special value of
     * ' ', any whitespace character will be used as a delimiter.
     *
     * If lower is unequal to 0, uppercase characters will be converted to
     * lowercase in the result. This is useful to make keywords case
     * insensitive.
     *
     * The location of *p is updated to point to the first character after the end
     * of the token - the ending delimiter.
     *
     * On success, the new value of t->val is returned. Memory for t->val is
     * allocated using malloc and must be free()'d to reclaim it.  If insufficient
     * memory is available, NULL is returned.
     */
    static char *get_string(char **p, struct token *t, char delim, int lower)
    {
    	char *b, *e;
    	size_t len, i;
    
    	/*
    	 * b and e both start at the beginning of the input stream.
    	 *
    	 * e is incremented until we find the ending delimiter, or a NUL byte
    	 * is reached. Then, we take e - b to find the length of the token.
    	 */
    	b = e = *p;
    
    	while (*e) {
    		if ((delim == ' ' && isspace(*e)) || delim == *e)
    			break;
    		e++;
    	}
    
    	len = e - b;
    
    	/*
    	 * Allocate memory to hold the string, and copy it in, converting
    	 * characters to lowercase if lower is != 0.
    	 */
    	t->val = malloc(len + 1);
    	if (!t->val)
    		return NULL;
    
    	for (i = 0; i < len; i++, b++) {
    		if (lower)
    			t->val[i] = tolower(*b);
    		else
    			t->val[i] = *b;
    	}
    
    	t->val[len] = '\0';
    
    	/*
    	 * Update *p so the caller knows where to continue scanning.
    	 */
    	*p = e;
    
    	t->type = T_STRING;
    
    	return t->val;
    }
    
    /*
     * Populate a keyword token with a type and value.
     */
    static void get_keyword(struct token *t)
    {
    	int i;
    
    	for (i = 0; keywords[i].val; i++) {
    		if (!strcmp(t->val, keywords[i].val)) {
    			t->type = keywords[i].type;
    			break;
    		}
    	}
    }
    
    /*
     * Get the next token.  We have to keep track of which state we're in to know
     * if we're looking to get a string literal or a keyword.
     *
     * *p is updated to point at the first character after the current token.
     */
    static void get_token(char **p, struct token *t, enum lex_state state)
    {
    	char *c = *p;
    
    	t->type = T_INVALID;
    
    	/* eat non EOL whitespace */
    	while (isblank(*c))
    		c++;
    
    	/*
    	 * eat comments. note that string literals can't begin with #, but
    	 * can contain a # after their first character.
    	 */
    	if (*c == '#') {
    		while (*c && *c != '\n')
    			c++;
    	}
    
    	if (*c == '\n') {
    		t->type = T_EOL;
    		c++;
    	} else if (*c == '\0') {
    		t->type = T_EOF;
    		c++;
    	} else if (state == L_SLITERAL) {
    		get_string(&c, t, '\n', 0);
    	} else if (state == L_KEYWORD) {
    		/*
    		 * when we expect a keyword, we first get the next string
    		 * token delimited by whitespace, and then check if it
    		 * matches a keyword in our keyword list. if it does, it's
    		 * converted to a keyword token of the appropriate type, and
    		 * if not, it remains a string token.
    		 */
    		get_string(&c, t, ' ', 1);
    		get_keyword(t);
    	}
    
    	*p = c;
    }
    
    /*
     * Increment *c until we get to the end of the current line, or EOF.
     */
    static void eol_or_eof(char **c)
    {
    	while (**c && **c != '\n')
    		(*c)++;
    }
    
    /*
     * All of these parse_* functions share some common behavior.
     *
     * They finish with *c pointing after the token they parse, and return 1 on
     * success, or < 0 on error.
     */
    
    /*
     * Parse a string literal and store a pointer it at *dst. String literals
     * terminate at the end of the line.
     */
    static int parse_sliteral(char **c, char **dst)
    {
    	struct token t;
    	char *s = *c;
    
    	get_token(c, &t, L_SLITERAL);
    
    	if (t.type != T_STRING) {
    		printf("Expected string literal: %.*s\n", (int)(*c - s), s);
    		return -EINVAL;
    	}
    
    	*dst = t.val;
    
    	return 1;
    }
    
    /*
     * Parse a base 10 (unsigned) integer and store it at *dst.
     */
    static int parse_integer(char **c, int *dst)
    {
    	struct token t;
    	char *s = *c;
    	unsigned long temp;
    
    	get_token(c, &t, L_SLITERAL);
    
    	if (t.type != T_STRING) {
    		printf("Expected string: %.*s\n", (int)(*c - s), s);
    		return -EINVAL;
    	}
    
    	if (strict_strtoul(t.val, 10, &temp) < 0) {
    		printf("Expected unsigned integer: %s\n", t.val);
    		return -EINVAL;
    	}
    
    	*dst = (int)temp;
    
    	free(t.val);
    
    	return 1;
    }
    
    static int parse_pxefile_top(char *p, struct pxe_menu *cfg, int nest_level);
    
    /*
     * Parse an include statement, and retrieve and parse the file it mentions.
     *
     * base should point to a location where it's safe to store the file, and
     * nest_level should indicate how many nested includes have occurred. For this
     * include, nest_level has already been incremented and doesn't need to be
     * incremented here.
     */
    static int handle_include(char **c, char *base,
    				struct pxe_menu *cfg, int nest_level)
    {
    	char *include_path;
    	char *s = *c;
    	int err;
    
    	err = parse_sliteral(c, &include_path);
    
    	if (err < 0) {
    		printf("Expected include path: %.*s\n",
    				 (int)(*c - s), s);
    		return err;
    	}
    
    	err = get_pxe_file(include_path, base);
    
    	if (err < 0) {
    		printf("Couldn't retrieve %s\n", include_path);
    		return err;
    	}
    
    	return parse_pxefile_top(base, cfg, nest_level);
    }
    
    /*
     * Parse lines that begin with 'menu'.
     *
     * b and nest are provided to handle the 'menu include' case.
     *
     * b should be the address where the file currently being parsed is stored.
     *
     * nest_level should be 1 when parsing the top level pxe file, 2 when parsing
     * a file it includes, 3 when parsing a file included by that file, and so on.
     */
    static int parse_menu(char **c, struct pxe_menu *cfg, char *b, int nest_level)
    {
    	struct token t;
    	char *s = *c;
    
    Jason Hobbs's avatar
    Jason Hobbs committed
    
    	get_token(c, &t, L_KEYWORD);
    
    	switch (t.type) {
    	case T_TITLE:
    		err = parse_sliteral(c, &cfg->title);
    
    		break;
    
    	case T_INCLUDE:
    		err = handle_include(c, b + strlen(b) + 1, cfg,
    						nest_level + 1);
    		break;
    
    	default:
    		printf("Ignoring malformed menu command: %.*s\n",
    				(int)(*c - s), s);
    	}
    
    	if (err < 0)
    		return err;
    
    	eol_or_eof(c);
    
    	return 1;
    }
    
    /*
     * Handles parsing a 'menu line' when we're parsing a label.
     */
    static int parse_label_menu(char **c, struct pxe_menu *cfg,
    				struct pxe_label *label)
    {
    	struct token t;
    	char *s;
    
    	s = *c;
    
    	get_token(c, &t, L_KEYWORD);