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 }