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 }