github.com/andrewsun2898/u-root@v6.0.1-0.20200616011413-4b2895c1b815+incompatible/pkg/boot/grub/grub.go (about)

     1  // Copyright 2017-2020 the u-root Authors. All rights reserved
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // Package grub implements a grub config file parser.
     6  //
     7  // See the grub manual https://www.gnu.org/software/grub/manual/grub/ for
     8  // a reference of the configuration format
     9  // In particular the following pages:
    10  // - https://www.gnu.org/software/grub/manual/grub/html_node/Shell_002dlike-scripting.html
    11  // - https://www.gnu.org/software/grub/manual/grub/html_node/Commands.html
    12  //
    13  // Currently, only the linux[16|efi], initrd[16|efi], menuentry and set
    14  // directives are partially supported.
    15  package grub
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"io"
    21  	"log"
    22  	"net/url"
    23  	"path/filepath"
    24  	"strconv"
    25  	"strings"
    26  
    27  	"github.com/u-root/u-root/pkg/boot"
    28  	"github.com/u-root/u-root/pkg/boot/multiboot"
    29  	"github.com/u-root/u-root/pkg/curl"
    30  	"github.com/u-root/u-root/pkg/shlex"
    31  	"github.com/u-root/u-root/pkg/uio"
    32  )
    33  
    34  var probeGrubFiles = []string{
    35  	"boot/grub/grub.cfg",
    36  	"grub/grub.cfg",
    37  	"grub2/grub.cfg",
    38  }
    39  
    40  // ParseLocalConfig looks for a GRUB config in the disk partition mounted at
    41  // diskDir and parses out OSes to boot.
    42  //
    43  // This... is at best crude, at worst totally wrong, since we fundamentally
    44  // assume that the kernels we boot are only on this one partition. But so is
    45  // this whole parser.
    46  func ParseLocalConfig(ctx context.Context, diskDir string) ([]boot.OSImage, error) {
    47  	wd := &url.URL{
    48  		Scheme: "file",
    49  		Path:   diskDir,
    50  	}
    51  
    52  	// This is a hack. GRUB should stop caring about URLs at least in the
    53  	// way we use them today, because GRUB has additional path encoding
    54  	// methods. Sorry.
    55  	//
    56  	// Normally, stuff like this will be in EFI/BOOT/grub.cfg, but some
    57  	// distro's have their own directory in this EFI namespace. Just check
    58  	// 'em all.
    59  	files, err := filepath.Glob(filepath.Join(diskDir, "EFI", "*", "grub.cfg"))
    60  	if err != nil {
    61  		log.Printf("Could not glob for %s/EFI/*/grub.cfg: %v", diskDir, err)
    62  	}
    63  	var relNames []string
    64  	for _, file := range files {
    65  		base, err := filepath.Rel(diskDir, file)
    66  		if err == nil {
    67  			relNames = append(relNames, base)
    68  		}
    69  	}
    70  
    71  	for _, relname := range append(relNames, probeGrubFiles...) {
    72  		c, err := ParseConfigFile(ctx, curl.DefaultSchemes, relname, wd)
    73  		if curl.IsURLError(err) {
    74  			continue
    75  		}
    76  		return c, err
    77  	}
    78  	return nil, fmt.Errorf("no valid grub config found")
    79  }
    80  
    81  // ParseConfigFile parses a grub configuration as specified in
    82  // https://www.gnu.org/software/grub/manual/grub/
    83  //
    84  // Currently, only the linux[16|efi], initrd[16|efi], menuentry and set
    85  // directives are partially supported.
    86  //
    87  // `wd` is the default scheme, host, and path for any files named as a
    88  // relative path - e.g. kernel, include, and initramfs paths are requested
    89  // relative to the wd.
    90  func ParseConfigFile(ctx context.Context, s curl.Schemes, configFile string, wd *url.URL) ([]boot.OSImage, error) {
    91  	p := newParser(wd, s)
    92  	if err := p.appendFile(ctx, configFile); err != nil {
    93  		return nil, err
    94  	}
    95  
    96  	// Don't add entries twice.
    97  	//
    98  	// Multiple labels can refer to the same image, so we have to dedup by pointer.
    99  	seenLinux := make(map[*boot.LinuxImage]struct{})
   100  	seenMB := make(map[*boot.MultibootImage]struct{})
   101  
   102  	if len(p.defaultEntry) > 0 {
   103  		p.labelOrder = append([]string{p.defaultEntry}, p.labelOrder...)
   104  	}
   105  
   106  	var images []boot.OSImage
   107  	for _, label := range p.labelOrder {
   108  		if img, ok := p.linuxEntries[label]; ok {
   109  			if _, ok := seenLinux[img]; !ok {
   110  				images = append(images, img)
   111  				seenLinux[img] = struct{}{}
   112  			}
   113  		}
   114  
   115  		if img, ok := p.mbEntries[label]; ok {
   116  			if _, ok := seenMB[img]; !ok {
   117  				images = append(images, img)
   118  				seenMB[img] = struct{}{}
   119  			}
   120  		}
   121  	}
   122  	return images, nil
   123  }
   124  
   125  type parser struct {
   126  	linuxEntries map[string]*boot.LinuxImage
   127  	mbEntries    map[string]*boot.MultibootImage
   128  
   129  	labelOrder   []string
   130  	defaultEntry string
   131  
   132  	W io.Writer
   133  
   134  	// parser internals.
   135  	numEntry int
   136  
   137  	// curEntry is the current entry number as a string.
   138  	curEntry string
   139  
   140  	// curLabel is the last parsed label from a "menuentry".
   141  	curLabel string
   142  
   143  	wd      *url.URL
   144  	schemes curl.Schemes
   145  }
   146  
   147  // newParser returns a new grub parser using working directory `wd`
   148  // and schemes `s`.
   149  //
   150  // If a path encountered in a configuration file is relative instead of a full
   151  // URL, `wd` is used as the "working directory" of that relative path; the
   152  // resulting URL is roughly `wd.String()/path`.
   153  //
   154  // `s` is used to get files referred to by URLs.
   155  func newParser(wd *url.URL, s curl.Schemes) *parser {
   156  	return &parser{
   157  		linuxEntries: make(map[string]*boot.LinuxImage),
   158  		mbEntries:    make(map[string]*boot.MultibootImage),
   159  		wd:           wd,
   160  		schemes:      s,
   161  	}
   162  }
   163  
   164  func parseURL(surl string, wd *url.URL) (*url.URL, error) {
   165  	u, err := url.Parse(surl)
   166  	if err != nil {
   167  		return nil, fmt.Errorf("could not parse URL %q: %v", surl, err)
   168  	}
   169  
   170  	if len(u.Scheme) == 0 {
   171  		u.Scheme = wd.Scheme
   172  
   173  		if len(u.Host) == 0 {
   174  			// If this is not there, it was likely just a path.
   175  			u.Host = wd.Host
   176  			u.Path = filepath.Join(wd.Path, filepath.Clean(u.Path))
   177  		}
   178  	}
   179  	return u, nil
   180  }
   181  
   182  // getFile parses `url` relative to the config's working directory and returns
   183  // an io.Reader for the requested url.
   184  //
   185  // If url is just a relative path and not a full URL, c.wd is used as the
   186  // "working directory" of that relative path; the resulting URL is roughly
   187  // path.Join(wd.String(), url).
   188  func (c *parser) getFile(url string) (io.ReaderAt, error) {
   189  	u, err := parseURL(url, c.wd)
   190  	if err != nil {
   191  		return nil, err
   192  	}
   193  
   194  	return c.schemes.LazyFetch(u)
   195  }
   196  
   197  // appendFile parses the config file downloaded from `url` and adds it to `c`.
   198  func (c *parser) appendFile(ctx context.Context, url string) error {
   199  	u, err := parseURL(url, c.wd)
   200  	if err != nil {
   201  		return err
   202  	}
   203  
   204  	log.Printf("Fetching %s", u)
   205  
   206  	r, err := c.schemes.Fetch(ctx, u)
   207  	if err != nil {
   208  		return err
   209  	}
   210  
   211  	config, err := uio.ReadAll(r)
   212  	if err != nil {
   213  		return err
   214  	}
   215  	if len(config) > 500 {
   216  		// Avoid flooding the console on real systems
   217  		// TODO: do we want to pass a verbose flag or a logger?
   218  		log.Printf("Got config file %s", r)
   219  	} else {
   220  		log.Printf("Got config file %s:\n%s\n", r, string(config))
   221  	}
   222  	return c.append(ctx, string(config))
   223  }
   224  
   225  // CmdlineQuote quotes the command line as grub-core/lib/cmdline.c does
   226  func cmdlineQuote(args []string) string {
   227  	q := make([]string, len(args))
   228  	for i, s := range args {
   229  		s = strings.Replace(s, `\`, `\\`, -1)
   230  		s = strings.Replace(s, `'`, `\'`, -1)
   231  		s = strings.Replace(s, `"`, `\"`, -1)
   232  		if strings.ContainsRune(s, ' ') {
   233  			s = `"` + s + `"`
   234  		}
   235  		q[i] = s
   236  	}
   237  	return strings.Join(q, " ")
   238  }
   239  
   240  // append parses `config` and adds the respective configuration to `c`.
   241  //
   242  // NOTE: This parser has outlived its usefulness already, given that it doesn't
   243  // even understand the {} scoping in GRUB. But let's get the tests to pass, and
   244  // then we can do a rewrite.
   245  func (c *parser) append(ctx context.Context, config string) error {
   246  	// Here's a shitty parser.
   247  	for _, line := range strings.Split(config, "\n") {
   248  		kv := shlex.Argv(line)
   249  		if len(kv) < 1 {
   250  			continue
   251  		}
   252  		directive := strings.ToLower(kv[0])
   253  		// Used by tests (allow no parameters here)
   254  		if c.W != nil && directive == "echo" {
   255  			fmt.Fprintf(c.W, "echo:%#v\n", kv[1:])
   256  		}
   257  
   258  		if len(kv) <= 1 {
   259  			continue
   260  		}
   261  		arg := kv[1]
   262  
   263  		switch directive {
   264  		case "set":
   265  			vals := strings.SplitN(arg, "=", 2)
   266  			if len(vals) == 2 {
   267  				//TODO handle vars? bootVars[vals[0]] = vals[1]
   268  				//log.Printf("grubvar: %s=%s", vals[0], vals[1])
   269  				if vals[0] == "default" {
   270  					c.defaultEntry = vals[1]
   271  				}
   272  			}
   273  
   274  		case "configfile":
   275  			// TODO test that
   276  			if err := c.appendFile(ctx, arg); err != nil {
   277  				return err
   278  			}
   279  
   280  		case "menuentry":
   281  			c.curEntry = strconv.Itoa(c.numEntry)
   282  			c.curLabel = arg
   283  			c.numEntry++
   284  			c.labelOrder = append(c.labelOrder, c.curEntry, c.curLabel)
   285  
   286  		case "linux", "linux16", "linuxefi":
   287  			k, err := c.getFile(arg)
   288  			if err != nil {
   289  				return err
   290  			}
   291  			// from grub manual: "Any initrd must be reloaded after using this command" so we can replace the entry
   292  			entry := &boot.LinuxImage{
   293  				Name:    c.curLabel,
   294  				Kernel:  k,
   295  				Cmdline: cmdlineQuote(kv[2:]),
   296  			}
   297  			c.linuxEntries[c.curEntry] = entry
   298  			c.linuxEntries[c.curLabel] = entry
   299  
   300  		case "initrd", "initrd16", "initrdefi":
   301  			if e, ok := c.linuxEntries[c.curEntry]; ok {
   302  				i, err := c.getFile(arg)
   303  				if err != nil {
   304  					return err
   305  				}
   306  				e.Initrd = i
   307  			}
   308  
   309  		case "multiboot":
   310  			// TODO handle --quirk-* arguments ? (change parsing)
   311  			k, err := c.getFile(arg)
   312  			if err != nil {
   313  				return err
   314  			}
   315  			// from grub manual: "Any initrd must be reloaded after using this command" so we can replace the entry
   316  			entry := &boot.MultibootImage{
   317  				Name:    c.curLabel,
   318  				Kernel:  k,
   319  				Cmdline: cmdlineQuote(kv[2:]),
   320  			}
   321  			c.mbEntries[c.curEntry] = entry
   322  			c.mbEntries[c.curLabel] = entry
   323  
   324  		case "module":
   325  			// TODO handle --nounzip arguments ? (change parsing)
   326  			if e, ok := c.mbEntries[c.curEntry]; ok {
   327  				// The only allowed arg
   328  				cmdline := kv[1:]
   329  				if arg == "--nounzip" {
   330  					arg = kv[2]
   331  					cmdline = kv[2:]
   332  				}
   333  
   334  				m, err := c.getFile(arg)
   335  				if err != nil {
   336  					return err
   337  				}
   338  				// TODO: Lasy tryGzipFilter(m)
   339  				mod := multiboot.Module{
   340  					Module:  m,
   341  					Name:    arg,
   342  					CmdLine: cmdlineQuote(cmdline),
   343  				}
   344  				e.Modules = append(e.Modules, mod)
   345  			}
   346  		}
   347  	}
   348  	return nil
   349  
   350  }