root/usr/src/lib/libgrubmgmt/common/libgrub_menu.c
/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License (the "License").
 * You may not use this file except in compliance with the License.
 *
 * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
 * or http://www.opensolaris.org/os/licensing.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information: Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 */
/*
 * Copyright 2009 Sun Microsystems, Inc.  All rights reserved.
 * Use is subject to license terms.
 */

/*
 * This file contains functions for manipulating the GRUB menu.
 */
#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/mount.h>
#include <stdarg.h>
#include <assert.h>
#include <ctype.h>

#include "libgrub_impl.h"

static const grub_cmd_desc_t grub_cmd_descs[GRBM_CMD_NUM] = {
#define menu_cmd(cmd, num, flag, parsef)        {cmd, num, flag},
#include "libgrub_cmd.def"
};

static void
append_line(grub_menu_t *mp, grub_line_t *lp)
{
        if (mp->gm_start == NULL) {
                mp->gm_start = lp;
        } else {
                mp->gm_end->gl_next = lp;
                lp->gl_prev = mp->gm_end;
        }
        mp->gm_end = lp;
        lp->gl_line_num = ++mp->gm_line_num;
        lp->gl_entry_num = GRUB_ENTRY_DEFAULT;
}

static void
process_line(grub_menu_t *mp)
{
        int     n;
        grub_line_t     *lp;

        lp = mp->gm_end;
        n = sizeof (grub_cmd_descs) / sizeof (grub_cmd_descs[0]);

        /* search through the table of known commands */
        while (n-- != 0 && strcmp(lp->gl_cmd, grub_cmd_descs[n].gcd_cmd) != 0)
                ;

        /* unknown command */
        if (n < 0)
                return;

        /* we found command, fill lp fields */
        lp->gl_flags = grub_cmd_descs[n].gcd_flags;
        lp->gl_cmdtp = grub_cmd_descs[n].gcd_num;
}


static void
check_entry(grub_entry_t *ent)
{
        int i;
        uint_t emask;
        grub_line_t *lp;
        const grub_line_t * const lend = ent->ge_end->gl_next;

        emask = 0;
        for (i = 0, lp = ent->ge_start; lend != lp; lp = lp->gl_next, ++i) {
                lp->gl_entry_num = ent->ge_entry_num;
                if (lp->gl_flags == GRUB_LINE_INVALID ||
                    lp->gl_flags == GRUB_LINE_GLOBAL) {
                        emask |= 1 << i;
                        lp->gl_flags = GRUB_LINE_INVALID;
                }
        }

        if ((ent->ge_emask = emask) == 0)
                ent->ge_flags |= GRBM_VALID_FLAG;
}

static int
add_entry(grub_menu_t *mp, grub_line_t *start, grub_line_t *end)
{
        grub_entry_t *ent;

        if ((ent = calloc(1, sizeof (*ent))) == NULL)
                return (errno);

        ent->ge_start = start;
        ent->ge_end = end;

        if (mp->gm_ent_end == NULL) {
                mp->gm_ent_start = ent;
        } else {
                mp->gm_ent_end->ge_next = ent;
                ent->ge_prev = mp->gm_ent_end;
        }
        mp->gm_ent_end = ent;
        ent->ge_entry_num = mp->gm_entry_num++;
        ent->ge_menu = mp;
        return (0);
}

static void
default_entry(grub_menu_t *mp)
{
        uint_t defent;
        grub_line_t *lp;
        grub_entry_t *ent;

        defent = 0;
        lp = mp->gm_curdefault;

        if (lp != NULL && lp->gl_flags == GRUB_LINE_GLOBAL &&
            lp->gl_cmdtp == GRBM_DEFAULT_CMD) {
                defent  = strtoul(lp->gl_arg, NULL, 0);
                if (defent >= mp->gm_entry_num)
                        defent = 0;
        }

        for (ent = mp->gm_ent_start; ent != NULL && defent != ent->ge_entry_num;
            ent = ent->ge_next)
                ;

        mp->gm_ent_default = ent;
}

static void
free_line(grub_line_t *lp)
{
        if (lp == NULL)
                return;

        free(lp->gl_cmd);
        free(lp->gl_sep);
        free(lp->gl_arg);
        free(lp->gl_line);
        free(lp);
}

static void
free_linelist(grub_line_t *line)
{
        grub_line_t *lp;

        if (line == NULL)
                return;

        while (line) {
                lp = line;
                line = lp->gl_next;
                free_line(lp);
        }
}

static void
free_entries(grub_menu_t *mp)
{
        grub_entry_t *ent, *tmp;

        if (mp == NULL)
                return;

        for (ent = mp->gm_ent_start; (tmp = ent) != NULL;
            ent = tmp->ge_next, free(tmp))
                ;

        mp->gm_ent_start = NULL;
        mp->gm_ent_end = NULL;
}

static int
grub_menu_append_line(grub_menu_t *mp, const char *line)
{
        int rc;
        size_t n;
        grub_line_t *lp;

        if (line == NULL)
                return (EINVAL);

        rc = 0;
        lp = NULL;
        if ((lp = calloc(1, sizeof (*lp))) == NULL ||
            (lp->gl_line = strdup(line)) == NULL) {
                free(lp);
                return (errno);
        }

        /* skip initial white space */
        line += strspn(line, " \t");

        /* process comment line */
        if (line[0] == '#') {
                if ((lp->gl_cmd =
                    strdup(grub_cmd_descs[GRBM_COMMENT_CMD].gcd_cmd)) == NULL ||
                    (lp->gl_sep =
                    strdup(grub_cmd_descs[GRBM_EMPTY_CMD].gcd_cmd)) == NULL ||
                    (lp->gl_arg = strdup(line + 1)) == NULL)
                        rc = errno;
        } else {
                /* get command */
                n = strcspn(line, " \t=");
                if ((lp->gl_cmd = malloc(n + 1)) == NULL)
                        rc = errno;
                else
                        (void) strlcpy(lp->gl_cmd, line, n + 1);

                line += n;

                /* get separator */
                n = strspn(line, " \t=");
                if ((lp->gl_sep = malloc(n + 1)) == NULL)
                        rc = errno;
                else
                        (void) strlcpy(lp->gl_sep, line, n + 1);

                line += n;

                /* get arguments */
                if ((lp->gl_arg = strdup(line)) == NULL)
                        rc = errno;
        }

        if (rc != 0) {
                free_line(lp);
                return (rc);
        }

        append_line(mp, lp);
        process_line(mp);
        return (0);
}

static int
grub_menu_process(grub_menu_t *mp)
{
        int ret;
        grub_entry_t *ent;
        grub_line_t *line, *start;

        /* Free remaininig entries, if any */
        free_entries(mp);

        /*
         * Walk through lines, till first 'title' command is encountered.
         * Initialize globals.
         */
        for (line = mp->gm_start; line != NULL; line = line->gl_next) {
                if (line->gl_flags == GRUB_LINE_GLOBAL &&
                    line->gl_cmdtp == GRBM_DEFAULT_CMD)
                        mp->gm_curdefault = line;
                else if (line->gl_cmdtp == GRBM_TITLE_CMD)
                        break;
        }

        /*
         * Walk through remaining lines and recreate menu entries.
         */
        for (start = NULL; line != NULL; line = line->gl_next) {
                if (line->gl_cmdtp == GRBM_TITLE_CMD) {
                        /* is first entry */
                        if (start != NULL &&
                            (ret = add_entry(mp, start, line->gl_prev)) != 0)
                                return (ret);
                        start = line;
                }
        }

        /* Add last entry */
        if (start != NULL && (ret = add_entry(mp, start, mp->gm_end)) != 0)
                return (ret);

        for (ent = mp->gm_ent_start; NULL != ent; ent = ent->ge_next)
                check_entry(ent);

        default_entry(mp);

        return (0);
}

static int
grub_fs_init(grub_fs_t *fs)
{
        assert(fs);
        if ((fs->gf_lzfh = libzfs_init()) == NULL ||
            (fs->gf_diroot = di_init("/", DINFOCPYALL | DINFOPATH))
            == DI_NODE_NIL ||
            (fs->gf_dvlh = di_devlink_init(NULL, 0)) == DI_LINK_NIL) {
                return (EG_INITFS);
        }
        return (0);
}

static void
grub_fs_fini(grub_fs_t *fs)
{
        if (fs == NULL)
                return;

        if (fs->gf_dvlh != DI_LINK_NIL)
                (void) di_devlink_fini(&fs->gf_dvlh);
        if (fs->gf_diroot != DI_NODE_NIL)
                di_fini(fs->gf_diroot);
        if (fs->gf_lzfh != NULL)
                libzfs_fini(fs->gf_lzfh);
        (void) memset(fs, 0, sizeof (*fs));
}

/*
 * Reads and parses GRUB menu file into a grub_menu_t data structure.
 * If grub_menu_path file path is NULL, will use 'currently active'
 * GRUB menu file.
 *
 * Memory for the menu data structure is allocated within the routine.
 * Caller must call grub_menu_fini() to release memory after calling
 * grub_menu_init().
 */
int
grub_menu_init(const char *path, grub_menu_t **menup)
{
        FILE *fp;
        char *cp;
        grub_menu_t *mp;
        int len, n, ret;
        char buf[GRBM_MAXLINE];

        if (menup == NULL)
                return (EINVAL);

        /*
         * Allocate space, perform initialization
         */
        if ((mp = calloc(1, sizeof (*mp))) == NULL) {
                *menup = mp;
                return (errno);
        }

        if ((ret = grub_fs_init(&mp->gm_fs)) != 0 ||
            (ret = grub_current_root(&mp->gm_fs, &mp->gm_root)) != 0)
                goto err_out1;

        if (path == NULL) {
                /*
                 * Use default grub-menu.
                 * If top dataset is not mounted, mount it now.
                 */
                if (mp->gm_root.gr_fs[GRBM_FS_TOP].gfs_mountp[0] == 0) {
                        if ((ret = grub_fsd_mount_tmp(mp->gm_root.gr_fs +
                            GRBM_FS_TOP, mp->gm_root.gr_fstyp)) != 0)
                                goto err_out1;
                }
                (void) snprintf(mp->gm_path, sizeof (mp->gm_path),
                    "%s/%s", mp->gm_root.gr_fs[GRBM_FS_TOP].gfs_mountp,
                    GRUB_MENU);
        } else {
                (void) strlcpy(mp->gm_path, path, sizeof (mp->gm_path));
        }

        if ((fp = fopen(mp->gm_path, "r")) == NULL) {
                ret = errno;
                goto err_out1;
        }

        cp = buf;
        len = sizeof (buf);

        while (fgets(cp, len, fp) != NULL) {

                if (IS_LINE2BIG(cp, len, n)) {
                        ret = E2BIG;
                        break;
                }

                /* remove white space at the end of line */
                for (; n != 0 && isspace(cp[n - 1]); --n)
                        ;
                cp[n] = '\0';

                if (n > 0 && cp[n - 1] == '\\') {
                        len -= n - 1;
                        assert(len >= 2);
                        cp += n - 1;
                        continue;
                }
                if ((ret = grub_menu_append_line(mp, buf)) != 0)
                        break;

                cp = buf;
                len = sizeof (buf);
        }

        if (fclose(fp) == EOF)
                ret = errno;
        else if (ret == 0)
                ret = grub_menu_process(mp);

err_out1:
        grub_fsd_umount_tmp(mp->gm_root.gr_fs + GRBM_FS_TOP);
        if (0 != ret) {
                grub_menu_fini(mp);
                mp = NULL;
        }
        *menup = mp;
        return (ret);
}

void
grub_menu_fini(grub_menu_t *mp)
{
        if (mp == NULL)
                return;

        grub_fs_fini(&mp->gm_fs);
        free_entries(mp);
        free_linelist(mp->gm_start);
        free(mp);
}

grub_line_t *
grub_menu_next_line(const grub_menu_t *mp, const grub_line_t *lp)
{
        assert(mp);
        if (lp == NULL)
                return (mp->gm_start);
        else
                return (lp->gl_next);
}

grub_line_t *
grub_menu_prev_line(const grub_menu_t *mp, const grub_line_t *lp)
{
        assert(mp);
        if (lp == NULL)
                return (mp->gm_end);
        else
                return (lp->gl_prev);
}

grub_line_t *
grub_menu_get_line(const grub_menu_t *mp, int num)
{
        grub_line_t *lp;

        assert(mp);
        if (num > mp->gm_line_num)
                return (NULL);
        for (lp = mp->gm_start; lp != NULL && num != lp->gl_line_num;
            lp = lp->gl_next)
                ;
        return (lp);
}

size_t
grub_menu_get_cmdline(const grub_menu_t *mp, int num, char *cmdl, size_t size)
{
        grub_entry_t *ent;

        assert(mp);
        if ((ent = grub_menu_get_entry(mp, num)) == NULL)
                return (size_t)(-1);

        return (grub_entry_get_cmdline(ent, cmdl, size));
}

grub_entry_t *
grub_menu_next_entry(const grub_menu_t *mp, const grub_entry_t *ent)
{
        assert(mp);
        if (ent == NULL) {
                return (mp->gm_ent_start);
        } else {
                assert(mp == ent->ge_menu);
                return (ent->ge_next);
        }
}

grub_entry_t *
grub_menu_prev_entry(const grub_menu_t *mp, const grub_entry_t *ent)
{
        assert(mp);
        if (ent == NULL) {
                return (mp->gm_ent_end);
        } else {
                assert(mp == ent->ge_menu);
                return (ent->ge_prev);
        }
}

grub_entry_t *
grub_menu_get_entry(const grub_menu_t *mp, int num)
{
        grub_entry_t *ent;

        assert(mp);
        if (num == GRUB_ENTRY_DEFAULT) {
                ent = mp->gm_ent_default;
        } else if (num >= mp->gm_entry_num) {
                ent = NULL;
        } else {
                for (ent = mp->gm_ent_start;
                    ent != NULL && num != ent->ge_entry_num;
                    ent = ent->ge_next)
                        ;
        }
        return (ent);
}