gitee.com/mirrors_u-root/u-root@v7.0.0+incompatible/pkg/boot/syslinux/syslinux.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 syslinux implements a syslinux config file parser.
     6  //
     7  // See http://www.syslinux.org/wiki/index.php?title=Config for general syslinux
     8  // config features.
     9  //
    10  // Currently, only the APPEND, INCLUDE, KERNEL, LABEL, DEFAULT, and INITRD
    11  // directives are partially supported.
    12  package syslinux
    13  
    14  import (
    15  	"context"
    16  	"fmt"
    17  	"io"
    18  	"log"
    19  	"net/url"
    20  	"path"
    21  	"path/filepath"
    22  	"strings"
    23  
    24  	"github.com/u-root/u-root/pkg/boot"
    25  	"github.com/u-root/u-root/pkg/boot/multiboot"
    26  	"github.com/u-root/u-root/pkg/curl"
    27  	"github.com/u-root/u-root/pkg/uio"
    28  )
    29  
    30  func probeIsolinuxFiles() []string {
    31  	files := make([]string, 0, 10)
    32  	// search order from the syslinux wiki
    33  	// http://wiki.syslinux.org/wiki/index.php?title=Config
    34  	// TODO: do we want to handle extlinux too ?
    35  	dirs := []string{
    36  		"boot/isolinux",
    37  		"isolinux",
    38  		"boot/syslinux",
    39  		"syslinux",
    40  		"",
    41  	}
    42  	confs := []string{
    43  		"isolinux.cfg",
    44  		"syslinux.cfg",
    45  	}
    46  	for _, dir := range dirs {
    47  		for _, conf := range confs {
    48  			if dir == "" {
    49  				files = append(files, conf)
    50  			} else {
    51  				files = append(files, filepath.Join(dir, conf))
    52  			}
    53  		}
    54  	}
    55  	return files
    56  }
    57  
    58  // ParseLocalConfig treats diskDir like a mount point on the local file system
    59  // and finds an isolinux config under there.
    60  func ParseLocalConfig(ctx context.Context, diskDir string) ([]boot.OSImage, error) {
    61  	rootdir := &url.URL{
    62  		Scheme: "file",
    63  		Path:   diskDir,
    64  	}
    65  
    66  	for _, relname := range probeIsolinuxFiles() {
    67  		dir, name := filepath.Split(relname)
    68  
    69  		// "When booting, the initial working directory for SYSLINUX /
    70  		// ISOLINUX will be the directory containing the initial
    71  		// configuration file."
    72  		//
    73  		// https://wiki.syslinux.org/wiki/index.php?title=Config#Working_directory
    74  		imgs, err := ParseConfigFile(ctx, curl.DefaultSchemes, name, rootdir, dir)
    75  		if curl.IsURLError(err) {
    76  			continue
    77  		}
    78  		return imgs, err
    79  	}
    80  	return nil, fmt.Errorf("no valid syslinux config found on %s", diskDir)
    81  }
    82  
    83  // ParseConfigFile parses a Syslinux configuration as specified in
    84  // http://www.syslinux.org/wiki/index.php?title=Config
    85  //
    86  // Currently, only the APPEND, INCLUDE, KERNEL, LABEL, DEFAULT, and INITRD
    87  // directives are partially supported.
    88  //
    89  // `s` is used to fetch any files that must be parsed or provided.
    90  //
    91  // rootdir is the partition mount point that syslinux is operating under.
    92  // Parsed absolute paths will be interpreted relative to the rootdir.
    93  //
    94  // wd is a directory within rootdir that is the current working directory.
    95  // Parsed relative paths will be interpreted relative to rootdir + "/" + wd.
    96  //
    97  // For PXE clients, rootdir will be the the URL without the path, and wd the
    98  // path component of the URL (e.g. rootdir = http://foobar.com, wd =
    99  // barfoo/pxelinux.cfg/).
   100  func ParseConfigFile(ctx context.Context, s curl.Schemes, configFile string, rootdir *url.URL, wd string) ([]boot.OSImage, error) {
   101  	p := newParser(rootdir, wd, s)
   102  	if err := p.appendFile(ctx, configFile); err != nil {
   103  		return nil, err
   104  	}
   105  
   106  	// Assign the right label to display to users.
   107  	for label, displayLabel := range p.menuLabel {
   108  		if e, ok := p.linuxEntries[label]; ok {
   109  			e.Name = displayLabel
   110  		}
   111  		if e, ok := p.mbEntries[label]; ok {
   112  			e.Name = displayLabel
   113  		}
   114  	}
   115  
   116  	// Intended order:
   117  	//
   118  	// 1. nerfDefaultEntry
   119  	// 2. defaultEntry
   120  	// 3. labels in order they appeared in config
   121  	if len(p.labelOrder) == 0 {
   122  		return nil, nil
   123  	}
   124  	if len(p.defaultEntry) > 0 {
   125  		p.labelOrder = append([]string{p.defaultEntry}, p.labelOrder...)
   126  	}
   127  	if len(p.nerfDefaultEntry) > 0 {
   128  		p.labelOrder = append([]string{p.nerfDefaultEntry}, p.labelOrder...)
   129  	}
   130  	p.labelOrder = dedupStrings(p.labelOrder)
   131  
   132  	var images []boot.OSImage
   133  	for _, label := range p.labelOrder {
   134  		if img, ok := p.linuxEntries[label]; ok && img.Kernel != nil {
   135  			images = append(images, img)
   136  		}
   137  		if img, ok := p.mbEntries[label]; ok && img.Kernel != nil {
   138  			images = append(images, img)
   139  		}
   140  	}
   141  	return images, nil
   142  }
   143  
   144  func dedupStrings(list []string) []string {
   145  	var newList []string
   146  	seen := make(map[string]struct{})
   147  	for _, s := range list {
   148  		if _, ok := seen[s]; !ok {
   149  			seen[s] = struct{}{}
   150  			newList = append(newList, s)
   151  		}
   152  	}
   153  	return newList
   154  }
   155  
   156  type parser struct {
   157  	// linuxEntries is a map of label name -> label configuration.
   158  	linuxEntries map[string]*boot.LinuxImage
   159  	mbEntries    map[string]*boot.MultibootImage
   160  
   161  	// labelOrder is the order of label entries in linuxEntries.
   162  	labelOrder []string
   163  
   164  	// menuLabel are human-readable labels defined by the "menu label" directive.
   165  	menuLabel map[string]string
   166  
   167  	defaultEntry     string
   168  	nerfDefaultEntry string
   169  
   170  	// parser internals.
   171  	globalAppend string
   172  	scope        scope
   173  	curEntry     string
   174  	wd           string
   175  	rootdir      *url.URL
   176  	schemes      curl.Schemes
   177  }
   178  
   179  type scope uint8
   180  
   181  const (
   182  	scopeGlobal scope = iota
   183  	scopeEntry
   184  )
   185  
   186  // newParser returns a new PXE parser using working directory `wd`
   187  // and schemes `s`.
   188  //
   189  // If a path encountered in a configuration file is relative instead of a full
   190  // URL, `wd` is used as the "working directory" of that relative path; the
   191  // resulting URL is roughly `wd.String()/path`.
   192  //
   193  // `s` is used to get files referred to by URLs.
   194  func newParser(rootdir *url.URL, wd string, s curl.Schemes) *parser {
   195  	return &parser{
   196  		linuxEntries: make(map[string]*boot.LinuxImage),
   197  		mbEntries:    make(map[string]*boot.MultibootImage),
   198  		scope:        scopeGlobal,
   199  		wd:           wd,
   200  		rootdir:      rootdir,
   201  		schemes:      s,
   202  		menuLabel:    make(map[string]string),
   203  	}
   204  }
   205  
   206  func parseURL(name string, rootdir *url.URL, wd string) (*url.URL, error) {
   207  	u, err := url.Parse(name)
   208  	if err != nil {
   209  		return nil, fmt.Errorf("could not parse URL %q: %v", name, err)
   210  	}
   211  
   212  	// If it parsed, but it didn't have a Scheme or Host, use the working
   213  	// directory's values.
   214  	if len(u.Scheme) == 0 && rootdir != nil {
   215  		u.Scheme = rootdir.Scheme
   216  
   217  		if len(u.Host) == 0 {
   218  			// If this is not there, it was likely just a path.
   219  			u.Host = rootdir.Host
   220  
   221  			// Absolute file names don't get the parent
   222  			// directories, just the host and scheme.
   223  			//
   224  			// "All (paths to) file names inside the configuration
   225  			// file are relative to the Working Directory, unless
   226  			// preceded with a slash."
   227  			//
   228  			// https://wiki.syslinux.org/wiki/index.php?title=Config#Working_directory
   229  			if path.IsAbs(name) {
   230  				u.Path = path.Join(rootdir.Path, path.Clean(u.Path))
   231  			} else {
   232  				u.Path = path.Join(rootdir.Path, wd, path.Clean(u.Path))
   233  			}
   234  		}
   235  	}
   236  	return u, nil
   237  }
   238  
   239  // getFile parses `url` relative to the config's working directory and returns
   240  // an io.Reader for the requested url.
   241  //
   242  // If url is just a relative path and not a full URL, c.wd is used as the
   243  // "working directory" of that relative path; the resulting URL is roughly
   244  // path.Join(wd.String(), url).
   245  func (c *parser) getFile(url string) (io.ReaderAt, error) {
   246  	u, err := parseURL(url, c.rootdir, c.wd)
   247  	if err != nil {
   248  		return nil, err
   249  	}
   250  
   251  	return c.schemes.LazyFetch(u)
   252  }
   253  
   254  // appendFile parses the config file downloaded from `url` and adds it to `c`.
   255  func (c *parser) appendFile(ctx context.Context, url string) error {
   256  	u, err := parseURL(url, c.rootdir, c.wd)
   257  	if err != nil {
   258  		return err
   259  	}
   260  
   261  	r, err := c.schemes.Fetch(ctx, u)
   262  	if err != nil {
   263  		return err
   264  	}
   265  	config, err := uio.ReadAll(r)
   266  	if err != nil {
   267  		return err
   268  	}
   269  	log.Printf("Got config file %s:\n%s\n", r, string(config))
   270  	return c.append(ctx, string(config))
   271  }
   272  
   273  // Append parses `config` and adds the respective configuration to `c`.
   274  func (c *parser) append(ctx context.Context, config string) error {
   275  	// Here's a shitty parser.
   276  	for _, line := range strings.Split(config, "\n") {
   277  		// This is stupid. There should be a FieldsN(...).
   278  		kv := strings.Fields(line)
   279  		if len(kv) <= 1 {
   280  			continue
   281  		}
   282  		directive := strings.ToLower(kv[0])
   283  		var arg string
   284  		if len(kv) == 2 {
   285  			arg = kv[1]
   286  		} else {
   287  			arg = strings.Join(kv[1:], " ")
   288  		}
   289  
   290  		switch directive {
   291  		case "default":
   292  			c.defaultEntry = arg
   293  
   294  		case "nerfdefault":
   295  			c.nerfDefaultEntry = arg
   296  
   297  		case "include":
   298  			if err := c.appendFile(ctx, arg); curl.IsURLError(err) {
   299  				log.Printf("failed to parse %s: %v", arg, err)
   300  				// Means we didn't find the file. Just ignore
   301  				// it.
   302  				// TODO(hugelgupf): plumb a logger through here.
   303  				continue
   304  			} else if err != nil {
   305  				return err
   306  			}
   307  
   308  		case "menu":
   309  			opt := strings.Fields(arg)
   310  			if len(opt) < 1 {
   311  				continue
   312  			}
   313  			switch strings.ToLower(opt[0]) {
   314  			case "label":
   315  				// Note that "menu label" only changes the
   316  				// displayed label, not the identifier for this
   317  				// entry.
   318  				//
   319  				// We track these separately because "menu
   320  				// label" directives may happen before we know
   321  				// whether this is a Linux or Multiboot entry.
   322  				c.menuLabel[c.curEntry] = strings.Join(opt[1:], " ")
   323  
   324  			case "default":
   325  				// Are we in label scope?
   326  				//
   327  				// "Only valid after a LABEL statement" -syslinux wiki.
   328  				if c.scope == scopeEntry {
   329  					c.defaultEntry = c.curEntry
   330  				}
   331  			}
   332  
   333  		case "label":
   334  			// We forever enter label scope.
   335  			c.scope = scopeEntry
   336  			c.curEntry = arg
   337  			c.linuxEntries[c.curEntry] = &boot.LinuxImage{
   338  				Cmdline: c.globalAppend,
   339  				Name:    c.curEntry,
   340  			}
   341  			c.labelOrder = append(c.labelOrder, c.curEntry)
   342  
   343  		case "kernel":
   344  			// I hate special cases like these, but we aren't gonna
   345  			// implement syslinux modules.
   346  			if arg == "mboot.c32" {
   347  				// Prepare for a multiboot kernel.
   348  				delete(c.linuxEntries, c.curEntry)
   349  				c.mbEntries[c.curEntry] = &boot.MultibootImage{
   350  					Name: c.curEntry,
   351  				}
   352  			}
   353  			fallthrough
   354  
   355  		case "linux":
   356  			if e, ok := c.linuxEntries[c.curEntry]; ok {
   357  				k, err := c.getFile(arg)
   358  				if err != nil {
   359  					return err
   360  				}
   361  				e.Kernel = k
   362  			}
   363  
   364  		case "initrd":
   365  			if e, ok := c.linuxEntries[c.curEntry]; ok {
   366  				// TODO: append "initrd=$arg" to the cmdline.
   367  				//
   368  				// For how this interacts with global appends,
   369  				// read
   370  				// https://wiki.syslinux.org/wiki/index.php?title=Directives/append
   371  				// Multiple initrds are comma-separated
   372  				var initrds []io.ReaderAt
   373  				for _, f := range strings.Split(arg, ",") {
   374  					i, err := c.getFile(f)
   375  					if err != nil {
   376  						return err
   377  					}
   378  					initrds = append(initrds, i)
   379  				}
   380  				e.Initrd = boot.CatInitrds(initrds...)
   381  			}
   382  
   383  		case "append":
   384  			switch c.scope {
   385  			case scopeGlobal:
   386  				c.globalAppend = arg
   387  
   388  			case scopeEntry:
   389  				if e, ok := c.mbEntries[c.curEntry]; ok {
   390  					modules := strings.Split(arg, "---")
   391  					// The first module is special -- the kernel.
   392  					if len(modules) > 0 {
   393  						kernel := strings.Fields(modules[0])
   394  						k, err := c.getFile(kernel[0])
   395  						if err != nil {
   396  							return err
   397  						}
   398  						e.Kernel = k
   399  						if len(kernel) > 1 {
   400  							e.Cmdline = strings.Join(kernel[1:], " ")
   401  						}
   402  						modules = modules[1:]
   403  					}
   404  					for _, cmdline := range modules {
   405  						m := strings.Fields(cmdline)
   406  						if len(m) == 0 {
   407  							continue
   408  						}
   409  						file, err := c.getFile(m[0])
   410  						if err != nil {
   411  							return err
   412  						}
   413  						e.Modules = append(e.Modules, multiboot.Module{
   414  							Cmdline: strings.TrimSpace(cmdline),
   415  							Module:  file,
   416  						})
   417  					}
   418  				}
   419  				if e, ok := c.linuxEntries[c.curEntry]; ok {
   420  					if arg == "-" {
   421  						e.Cmdline = ""
   422  					} else {
   423  						// Yes, we explicitly _override_, not
   424  						// concatenate. If a specific append
   425  						// directive is present, a global
   426  						// append directive is ignored.
   427  						//
   428  						// Also, "If you enter multiple APPEND
   429  						// statements in a single LABEL entry,
   430  						// only the last one will be used".
   431  						//
   432  						// https://wiki.syslinux.org/wiki/index.php?title=Directives/append
   433  						e.Cmdline = arg
   434  					}
   435  				}
   436  			}
   437  		}
   438  	}
   439  
   440  	// Go through all labels and download the initrds.
   441  	for _, label := range c.linuxEntries {
   442  		// If the initrd was set via the INITRD directive, don't
   443  		// overwrite that.
   444  		//
   445  		// TODO(hugelgupf): Is this really what syslinux does? Does
   446  		// INITRD trump cmdline? Does it trump global? What if both the
   447  		// directive and cmdline initrd= are set? Does it depend on the
   448  		// order in the config file? (My current best guess: order.)
   449  		//
   450  		// Answer: Normally, the INITRD directive appends to the
   451  		// cmdline, and the _last_ effective initrd= parameter is used
   452  		// for loading initrd files.
   453  		if label.Initrd != nil {
   454  			continue
   455  		}
   456  
   457  		for _, opt := range strings.Fields(label.Cmdline) {
   458  			optkv := strings.Split(opt, "=")
   459  			if optkv[0] != "initrd" {
   460  				continue
   461  			}
   462  
   463  			i, err := c.getFile(optkv[1])
   464  			if err != nil {
   465  				return err
   466  			}
   467  			label.Initrd = i
   468  		}
   469  	}
   470  	return nil
   471  
   472  }