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 }