github.com/xyproto/u-root@v6.0.1-0.20200302025726-5528e0c77a3c+incompatible/pkg/boot/syslinux/syslinux.go (about)

     1  // Copyright 2017-2018 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  	"errors"
    16  	"fmt"
    17  	"io"
    18  	"log"
    19  	"net/url"
    20  	"path/filepath"
    21  	"strings"
    22  
    23  	"github.com/u-root/u-root/pkg/boot"
    24  	"github.com/u-root/u-root/pkg/curl"
    25  	"github.com/u-root/u-root/pkg/uio"
    26  )
    27  
    28  var (
    29  	// ErrDefaultEntryNotFound is returned when the configuration file
    30  	// names a default label that is not part of the configuration.
    31  	ErrDefaultEntryNotFound = errors.New("default label not found in configuration")
    32  )
    33  
    34  // Config encapsulates a parsed Syslinux configuration file.
    35  //
    36  // See http://www.syslinux.org/wiki/index.php?title=Config for the
    37  // configuration file specification.
    38  type Config struct {
    39  	// Entries is a map of label name -> label configuration.
    40  	Entries map[string]*boot.LinuxImage
    41  
    42  	// DefaultEntry is the default label key to use.
    43  	//
    44  	// If DefaultEntry is non-empty, the label is guaranteed to exist in
    45  	// `Entries`.
    46  	DefaultEntry string
    47  }
    48  
    49  // ParseConfigFile parses a Syslinux configuration as specified in
    50  // http://www.syslinux.org/wiki/index.php?title=Config
    51  //
    52  // Currently, only the APPEND, INCLUDE, KERNEL, LABEL, DEFAULT, and INITRD
    53  // directives are partially supported.
    54  //
    55  // curl.DefaultSchemes is used to fetch any files that must be parsed or
    56  // provided.
    57  //
    58  // `wd` is the default scheme, host, and path for any files named as a
    59  // relative path - e.g. kernel, include, and initramfs paths are requested
    60  // relative to the wd. The default path for config files is assumed to be
    61  // `wd.Path`/pxelinux.cfg/.
    62  func ParseConfigFile(url string, wd *url.URL) (*Config, error) {
    63  	return ParseConfigFileWithSchemes(curl.DefaultSchemes, url, wd)
    64  }
    65  
    66  // ParseConfigFileWithSchemes is like ParseConfigFile, but uses the given
    67  // schemes explicitly.
    68  func ParseConfigFileWithSchemes(s curl.Schemes, url string, wd *url.URL) (*Config, error) {
    69  	p := newParserWithSchemes(wd, s)
    70  	if err := p.appendFile(url); err != nil {
    71  		return nil, err
    72  	}
    73  	return p.config, nil
    74  }
    75  
    76  type parser struct {
    77  	config *Config
    78  
    79  	// parser internals.
    80  	globalAppend string
    81  	scope        scope
    82  	curEntry     string
    83  	wd           *url.URL
    84  	schemes      curl.Schemes
    85  }
    86  
    87  type scope uint8
    88  
    89  const (
    90  	scopeGlobal scope = iota
    91  	scopeEntry
    92  )
    93  
    94  // newParserWithSchemes returns a new PXE parser using working directory `wd`
    95  // and schemes `s`.
    96  //
    97  // If a path encountered in a configuration file is relative instead of a full
    98  // URL, `wd` is used as the "working directory" of that relative path; the
    99  // resulting URL is roughly `wd.String()/path`.
   100  //
   101  // `s` is used to get files referred to by URLs.
   102  func newParserWithSchemes(wd *url.URL, s curl.Schemes) *parser {
   103  	return &parser{
   104  		config: &Config{
   105  			Entries: make(map[string]*boot.LinuxImage),
   106  		},
   107  		scope:   scopeGlobal,
   108  		wd:      wd,
   109  		schemes: s,
   110  	}
   111  }
   112  
   113  func parseURL(surl string, wd *url.URL) (*url.URL, error) {
   114  	u, err := url.Parse(surl)
   115  	if err != nil {
   116  		return nil, fmt.Errorf("could not parse URL %q: %v", surl, err)
   117  	}
   118  
   119  	if len(u.Scheme) == 0 {
   120  		u.Scheme = wd.Scheme
   121  
   122  		if len(u.Host) == 0 {
   123  			// If this is not there, it was likely just a path.
   124  			u.Host = wd.Host
   125  			u.Path = filepath.Join(wd.Path, filepath.Clean(u.Path))
   126  		}
   127  	}
   128  	return u, nil
   129  }
   130  
   131  // getFile parses `url` relative to the config's working directory and returns
   132  // an io.Reader for the requested url.
   133  //
   134  // If url is just a relative path and not a full URL, c.wd is used as the
   135  // "working directory" of that relative path; the resulting URL is roughly
   136  // path.Join(wd.String(), url).
   137  func (c *parser) getFile(url string) (io.ReaderAt, error) {
   138  	u, err := parseURL(url, c.wd)
   139  	if err != nil {
   140  		return nil, err
   141  	}
   142  
   143  	return c.schemes.LazyFetch(u)
   144  }
   145  
   146  // appendFile parses the config file downloaded from `url` and adds it to `c`.
   147  func (c *parser) appendFile(url string) error {
   148  	r, err := c.getFile(url)
   149  	if err != nil {
   150  		return err
   151  	}
   152  	config, err := uio.ReadAll(r)
   153  	if err != nil {
   154  		return err
   155  	}
   156  	log.Printf("Got config file %s:\n%s\n", r, string(config))
   157  	return c.append(string(config))
   158  }
   159  
   160  // Append parses `config` and adds the respective configuration to `c`.
   161  func (c *parser) append(config string) error {
   162  	// Here's a shitty parser.
   163  	for _, line := range strings.Split(config, "\n") {
   164  		// This is stupid. There should be a FieldsN(...).
   165  		kv := strings.Fields(line)
   166  		if len(kv) <= 1 {
   167  			continue
   168  		}
   169  		directive := strings.ToLower(kv[0])
   170  		var arg string
   171  		if len(kv) == 2 {
   172  			arg = kv[1]
   173  		} else {
   174  			arg = strings.Join(kv[1:], " ")
   175  		}
   176  
   177  		switch directive {
   178  		case "default":
   179  			c.config.DefaultEntry = arg
   180  
   181  		case "include":
   182  			if err := c.appendFile(arg); curl.IsURLError(err) {
   183  				// Means we didn't find the file. Just ignore
   184  				// it.
   185  				// TODO(hugelgupf): plumb a logger through here.
   186  				continue
   187  			} else if err != nil {
   188  				return err
   189  			}
   190  
   191  		case "label":
   192  			// We forever enter label scope.
   193  			c.scope = scopeEntry
   194  			c.curEntry = arg
   195  			c.config.Entries[c.curEntry] = &boot.LinuxImage{}
   196  			c.config.Entries[c.curEntry].Cmdline = c.globalAppend
   197  
   198  		case "kernel":
   199  			k, err := c.getFile(arg)
   200  			if err != nil {
   201  				return err
   202  			}
   203  			c.config.Entries[c.curEntry].Kernel = k
   204  
   205  		case "initrd":
   206  			i, err := c.getFile(arg)
   207  			if err != nil {
   208  				return err
   209  			}
   210  			c.config.Entries[c.curEntry].Initrd = i
   211  
   212  		case "append":
   213  			switch c.scope {
   214  			case scopeGlobal:
   215  				c.globalAppend = arg
   216  
   217  			case scopeEntry:
   218  				if arg == "-" {
   219  					c.config.Entries[c.curEntry].Cmdline = ""
   220  				} else {
   221  					c.config.Entries[c.curEntry].Cmdline = arg
   222  				}
   223  			}
   224  		}
   225  	}
   226  
   227  	// Go through all labels and download the initrds.
   228  	for _, label := range c.config.Entries {
   229  		// If the initrd was set via the INITRD directive, don't
   230  		// overwrite that.
   231  		//
   232  		// TODO(hugelgupf): Is this really what syslinux does? Does
   233  		// INITRD trump cmdline? Does it trump global? What if both the
   234  		// directive and cmdline initrd= are set? Does it depend on the
   235  		// order in the config file? (My current best guess: order.)
   236  		if label.Initrd != nil {
   237  			continue
   238  		}
   239  
   240  		for _, opt := range strings.Fields(label.Cmdline) {
   241  			optkv := strings.Split(opt, "=")
   242  			if optkv[0] != "initrd" {
   243  				continue
   244  			}
   245  
   246  			i, err := c.getFile(optkv[1])
   247  			if err != nil {
   248  				return err
   249  			}
   250  			label.Initrd = i
   251  		}
   252  	}
   253  
   254  	if len(c.config.DefaultEntry) > 0 {
   255  		if _, ok := c.config.Entries[c.config.DefaultEntry]; !ok {
   256  			return ErrDefaultEntryNotFound
   257  		}
   258  	}
   259  	return nil
   260  
   261  }