// Copyright 2019 the u-root Authors. All rights reserved
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package esxi contains an ESXi boot config parser for disks and CDROMs.
//
// For CDROMs, it parses the boot.cfg found in the root directory and tries to
// boot from it.
//
// For disks, there may be multiple boot partitions:
//
// - Locates both <device>5/boot.cfg and <device>6/boot.cfg.
//
// - If parsable, chooses partition with bootstate=(0|2|empty) and greater
// updated=N.
//
// Sometimes, an ESXi partition can contain a valid boot.cfg, but not actually
// any of the named modules. Hence it is important to try fully loading ESXi
// into memory, and only then falling back to the other partition.
//
// Only boots partitions with bootstate=0, bootstate=2, bootstate=(empty) will
// boot at all.
//
// Most of the parsing logic in this package comes from
// https://github.com/vmware/esx-boot/blob/master/safeboot/bootbank.c
package esxi
import (
"bufio"
"encoding/hex"
"fmt"
"io"
"os"
"path/filepath"
"strconv"
"strings"
"unicode"
"golang.org/x/sys/unix"
"github.com/u-root/u-root/pkg/boot"
"github.com/u-root/u-root/pkg/boot/multiboot"
"github.com/u-root/u-root/pkg/mount"
"github.com/u-root/u-root/pkg/mount/gpt"
"github.com/u-root/uio/uio"
)
func partNo(device string, number int) (string, error) {
var name string
if unicode.IsDigit(rune(device[len(device)-1])) {
name = fmt.Sprintf("%sp%d", device, number)
} else {
name = fmt.Sprintf("%s%d", device, number)
}
if _, err := os.Stat(name); err != nil {
return "", err
}
return name, nil
}
// LoadDisk loads the right ESXi multiboot kernel from partitions 5 or 6 of the
// given device.
//
// The kernels are returned in the priority order according to the bootstate
// and updated values in their boot configurations.
//
// The caller should try loading all returned images in order, as some of them
// may not be valid.
//
// device5 and device6 will be mounted at temporary directories.
func LoadDisk(device string) ([]*boot.MultibootImage, []*mount.MountPoint, error) {
opts5, mp5, err5 := mountPartition(device, 5)
opts6, mp6, err6 := mountPartition(device, 6)
if err5 != nil && err6 != nil {
return nil, nil, fmt.Errorf("could not mount or read either partition 5 (%w) or partition 6 (%w)", err5, err6)
}
var mps []*mount.MountPoint
if mp5 != nil {
mps = append(mps, mp5)
}
if mp6 != nil {
mps = append(mps, mp6)
}
imgs, err := getImages(device, opts5, opts6)
if err != nil {
for _, mp := range mps {
mp.Unmount(mount.MNT_DETACH)
}
return nil, nil, err
}
return imgs, mps, nil
}
func getImages(device string, opts5, opts6 *options) ([]*boot.MultibootImage, error) {
var (
img5, img6 *boot.MultibootImage
err5, err6 error
)
if opts5 != nil {
name, _ := partNo(device, 5)
img5, err5 = getBootImage(*opts5, device, 5, name)
}
if opts6 != nil {
name, _ := partNo(device, 6)
img6, err6 = getBootImage(*opts6, device, 6, name)
}
if img5 == nil && img6 == nil {
return nil, fmt.Errorf("could not read boot configs on partition 5 (%w) or partition 6 (%w)", err5, err6)
}
if img5 != nil && img6 != nil {
if opts6.updated > opts5.updated {
return []*boot.MultibootImage{img6, img5}, nil
}
return []*boot.MultibootImage{img5, img6}, nil
} else if img5 != nil {
return []*boot.MultibootImage{img5}, nil
}
return []*boot.MultibootImage{img6}, nil
}
// LoadCDROM loads an ESXi multiboot kernel from a CDROM at device.
//
// device will be mounted at mountPoint.
func LoadCDROM(device string) (*boot.MultibootImage, *mount.MountPoint, error) {
mountPoint, err := os.MkdirTemp("", "esxi-mount-")
if err != nil {
return nil, nil, err
}
mp, err := mount.Mount(device, mountPoint, "iso9660", "", unix.MS_RDONLY|unix.MS_NOATIME)
if err != nil {
os.RemoveAll(mountPoint)
return nil, nil, err
}
opts, err := parse(filepath.Join(mountPoint, "boot.cfg"))
if err != nil {
mp.Unmount(mount.MNT_DETACH)
os.RemoveAll(mountPoint)
return nil, nil, fmt.Errorf("cannot parse config from %s: %w", device, err)
}
img, err := getBootImage(opts, "", 0, device)
if err != nil {
mp.Unmount(mount.MNT_DETACH)
os.RemoveAll(mountPoint)
return nil, nil, err
}
return img, mp, nil
}
// LoadConfig loads an ESXi configuration from configFile.
func LoadConfig(configFile string) (*boot.MultibootImage, error) {
opts, err := parse(configFile)
if err != nil {
return nil, fmt.Errorf("cannot parse config at %s: %w", configFile, err)
}
return getBootImage(opts, "", 0, fmt.Sprintf("config file %s", configFile))
}
func mountPartition(parentdev string, partition int) (*options, *mount.MountPoint, error) {
dev, err := partNo(parentdev, partition)
if err != nil {
return nil, nil, err
}
base := filepath.Base(dev)
mountPoint, err := os.MkdirTemp("", fmt.Sprintf("%s-", base))
if err != nil {
return nil, nil, err
}
mp, err := mount.Mount(dev, mountPoint, "vfat", "", unix.MS_RDONLY|unix.MS_NOATIME)
if err != nil {
os.RemoveAll(mountPoint)
return nil, nil, err
}
configFile := filepath.Join(mountPoint, "boot.cfg")
opts, err := parse(configFile)
if err != nil {
mp.Unmount(mount.MNT_DETACH)
os.RemoveAll(mountPoint)
return nil, nil, fmt.Errorf("cannot parse config at %s: %w", configFile, err)
}
return &opts, mp, nil
}
// lazyOpenModules assigns modules to be opened as files.
//
// Each module is a path followed by optional command-line arguments, e.g.
// []string{"./module arg1 arg2", "./module2 arg3 arg4"}.
func lazyOpenModules(mods []module) multiboot.Modules {
modules := make([]multiboot.Module, 0, len(mods))
for _, m := range mods {
modules = append(modules, multiboot.Module{
Cmdline: m.cmdline,
Module: uio.NewLazyFile(m.path),
})
}
return modules
}
func getBootImage(opts options, device string, partition int, name string) (*boot.MultibootImage, error) {
// Only valid and upgrading are bootable partitions.
//
// We are supposed to support the following two state transitions (only
// one transition every boot!):
//
// upgrading -> dirty
// dirty -> invalid
//
// A validly booted system will set its own bootstate to "valid" from
// "dirty".
//
// We currently don't support writing the state back to disk, which is
// fine in our manual testing.
if opts.bootstate != bootValid && opts.bootstate != bootUpgrading {
return nil, fmt.Errorf("boot state %d invalid", opts.bootstate)
}
if len(device) > 0 {
if err := opts.addUUID(device, partition); err != nil {
return nil, fmt.Errorf("cannot add boot uuid of %s: %w", device, err)
}
}
return &boot.MultibootImage{
Name: fmt.Sprintf("%s from %s", opts.title, name),
Kernel: uio.NewLazyFile(opts.kernel),
Cmdline: opts.args,
Modules: lazyOpenModules(opts.modules),
}, nil
}
type module struct {
path string
cmdline string
}
type options struct {
title string
kernel string
args string
modules []module
updated int
bootstate bootstate
}
type bootstate int
// From safeboot.c
const (
bootValid bootstate = 0
bootUpgrading bootstate = 1
bootDirty bootstate = 2
bootInvalid bootstate = 3
)
// So tests can replace this and don't have to have actual block devices.
var getBlockSize = gpt.GetBlockSize
func getUUID(device string, partition int) (string, error) {
device = strings.TrimRight(device, "/")
blockSize, err := getBlockSize(device)
if err != nil {
return "", err
}
dev, err := partNo(device, partition)
if err != nil {
return "", err
}
f, err := os.Open(dev)
if err != nil {
return "", err
}
// Boot uuid is stored in the second block of the disk
// in the following format:
//
// VMWARE FAT16 <uuid>
// <---128 bit----><128 bit>
data := make([]byte, uuidSize)
n, err := f.ReadAt(data, int64(blockSize))
if err != nil {
return "", err
}
if n != uuidSize {
return "", io.ErrUnexpectedEOF
}
if magic := string(data[:len(uuidMagic)]); magic != uuidMagic {
return "", fmt.Errorf("bad uuid magic %q, want %q", magic, uuidMagic)
}
uuid := hex.EncodeToString(data[len(uuidMagic):])
return fmt.Sprintf("bootUUID=%s", uuid), nil
}
func (o *options) addUUID(device string, partition int) error {
uuid, err := getUUID(device, partition)
if err != nil {
return err
}
o.args += " " + uuid
return nil
}
const (
comment = '#'
sep = "---"
uuidMagic = "VMWARE FAT16 "
uuidSize = 32
)
func parse(configFile string) (options, error) {
dir := filepath.Dir(configFile)
f, err := os.Open(configFile)
if err != nil {
return options{}, err
}
defer f.Close()
// An empty or missing updated value is always 0, so we can let the
// ints be initialized to 0.
//
// see esx-boot/bootlib/parse.c:parse_config_file.
opt := options{
title: "VMware ESXi",
// Default value taken from
// esx-boot/safeboot/bootbank.c:bank_scan.
bootstate: bootInvalid,
}
scanner := bufio.NewScanner(f)
for scanner.Scan() {
line := scanner.Text()
line = strings.TrimSpace(line)
if len(line) == 0 || line[0] == comment {
continue
}
tokens := strings.SplitN(line, "=", 2)
if len(tokens) != 2 {
return opt, fmt.Errorf("bad line %q", line)
}
key := strings.TrimSpace(tokens[0])
val := strings.TrimSpace(tokens[1])
switch key {
case "title":
opt.title = val
case "kernel":
opt.kernel = filepath.Join(dir, val)
// The kernel cmdline is expected to have the filename
// first, as in cmdlines[0] here:
// https://github.com/vmware/esx-boot/blob/1380fc86cffdfb83448e2913ae11f6b7f248cf23/mboot/mutiboot.c#L870
//
// Note that the kernel is module 0 in the esx-boot
// code base, but it doesn't get loaded like that into
// the info structure; see -- so don't panic like I did
// when you read that!
// https://github.com/vmware/esx-boot/blob/1380fc86cffdfb83448e2913ae11f6b7f248cf23/mboot/mutiboot.c#L578
opt.args = val + " " + opt.args
case "kernelopt":
opt.args += val
case "updated":
if len(val) == 0 {
// Explicitly setting to 0, as in
// esx-boot/bootlib/parse.c:parse_config_file,
// in case this value is specified twice.
opt.updated = 0
} else {
n, err := strconv.Atoi(val)
if err != nil {
return options{}, err
}
opt.updated = n
}
case "bootstate":
if len(val) == 0 {
// Explicitly setting to valid, as in
// esx-boot/bootlib/parse.c:parse_config_file,
// in case this value is specified twice.
opt.bootstate = bootValid
} else {
n, err := strconv.Atoi(val)
if err != nil {
return options{}, err
}
if n < 0 || n > 3 {
opt.bootstate = bootInvalid
} else {
opt.bootstate = bootstate(n)
}
}
case "modules":
for _, tok := range strings.Split(val, sep) {
// Each module is "filename arg0 arg1 arg2" and
// the filename is relative to the directory
// the module is in.
tok = strings.TrimSpace(tok)
if len(tok) > 0 {
entry := strings.Fields(tok)
opt.modules = append(opt.modules, module{
path: filepath.Join(dir, entry[0]),
cmdline: tok,
})
}
}
}
}
err = scanner.Err()
return opt, err
}
// Copyright 2020 the u-root Authors. All rights reserved
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package grub
import (
"bufio"
"bytes"
"fmt"
"io"
"sort"
"strings"
)
// GRUB block size.
const blockSize = 1024
// EnvFile is a GRUB environment file consisting of key-value pairs akin to the
// GRUB commands load_env and save_env.
type EnvFile struct {
Vars map[string]string
}
// NewEnvFile allocates a new env file.
func NewEnvFile() *EnvFile {
return &EnvFile{
Vars: make(map[string]string),
}
}
// WriteTo writes key-value pairs to a file, padded to 1024 bytes, as save_env does.
func (env *EnvFile) WriteTo(w io.Writer) (int64, error) {
var b bytes.Buffer
b.WriteString("# GRUB Environment Block\n")
// Sort keys so order is deterministic.
keys := make([]string, 0, len(env.Vars))
for k := range env.Vars {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
if len(env.Vars[k]) > 0 {
b.WriteString(k)
b.WriteString("=")
b.WriteString(env.Vars[k])
b.WriteString("\n")
}
}
length := b.Len()
// Fill up the file with # until 1024 bytes to make the file size a
// multiple of the block size.
remainder := blockSize - length%blockSize
for i := 0; i < remainder; i++ {
b.WriteByte('#')
}
return b.WriteTo(w)
}
// ParseEnvFile reads a key-value pair GRUB environment file.
//
// ParseEnvFile accepts incorrectly padded GRUB env files, as opposed to GRUB.
func ParseEnvFile(r io.Reader) (*EnvFile, error) {
s := bufio.NewScanner(r)
conf := NewEnvFile()
var replacer = strings.NewReplacer(
"\n", "",
"\r", "",
)
// Best lexer & parser in the world.
for s.Scan() {
if len(s.Text()) == 0 {
continue
}
cleanedText := replacer.Replace(s.Text())
if len(cleanedText) == 0 {
continue
}
// Comments.
if cleanedText[0] == '#' {
continue
}
tokens := strings.SplitN(cleanedText, "=", 2)
if len(tokens) != 2 {
return nil, fmt.Errorf("error parsing %q: must find = or # and key + values in each line", s.Text())
}
if tokens[0] == "" || tokens[1] == "" {
return nil, fmt.Errorf("error parsing %q: either the key or value is empty: %q = %q", s.Text(), tokens[0], tokens[1])
}
key, value := tokens[0], tokens[1]
conf.Vars[key] = value
}
if err := s.Err(); err != nil {
return nil, err
}
return conf, nil
}
// Copyright 2017-2020 the u-root Authors. All rights reserved
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package grub implements a grub config file parser.
//
// See the grub manual https://www.gnu.org/software/grub/manual/grub/ for
// a reference of the configuration format
// In particular the following pages:
// - https://www.gnu.org/software/grub/manual/grub/html_node/Shell_002dlike-scripting.html
// - https://www.gnu.org/software/grub/manual/grub/html_node/Commands.html
//
// See parser.append function for list of commands that are supported.
package grub
import (
"context"
"errors"
"fmt"
"io"
"log"
"net/url"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"github.com/spf13/pflag"
"github.com/u-root/u-root/pkg/boot"
"github.com/u-root/u-root/pkg/boot/bls"
"github.com/u-root/u-root/pkg/boot/multiboot"
"github.com/u-root/u-root/pkg/curl"
"github.com/u-root/u-root/pkg/mount"
"github.com/u-root/u-root/pkg/mount/block"
"github.com/u-root/u-root/pkg/shlex"
"github.com/u-root/u-root/pkg/ulog"
"github.com/u-root/uio/uio"
)
var probeGrubFiles = []string{
"boot/grub/grub.cfg",
"grub/grub.cfg",
"grub2/grub.cfg",
"boot/grub2/grub.cfg",
}
var probeGrubEnvFiles = []string{
"EFI/*/grubenv",
"boot/grub/grubenv",
"grub/grubenv",
"grub2/grubenv",
"boot/grub2/grubenv",
}
// Grub syntax for OpenSUSE/Fedora/RHEL has some undocumented quirks. You
// won't find it on the master branch, but instead look at the rhel and fedora
// branches for these commits:
//
// * https://github.com/rhboot/grub2/commit/7e6775e6d4a8de9baf3f4676d4e021cc2f5dd761
// * https://github.com/rhboot/grub2/commit/0c26c6f7525737962d1389ebdfbb918f52d1b3b6
//
// They add a special case to not escape hex sequences:
//
// grub> echo hello \xff \xfg
// hello \xff xfg
//
// Their default installations depend on this functionality.
var hexEscape = regexp.MustCompile(`\\x[0-9a-fA-F]{2}`)
var anyEscape = regexp.MustCompile(`\\.{0,3}`)
// mountFlags are the flags this grub interpreter uses to mount partitions.
var mountFlags = uintptr(mount.ReadOnly)
var errMissingKey = errors.New("key is not found")
// absFileScheme creates a file:/// scheme with an absolute path. Technically,
// file schemes must be absolute paths and Go makes that assumption.
func absFileScheme(path string) (*url.URL, error) {
path, err := filepath.Abs(path)
if err != nil {
return nil, err
}
return &url.URL{
Scheme: "file",
Path: path,
}, nil
}
// ParseLocalConfig looks for a GRUB config in the disk partition mounted at
// diskDir and parses out OSes to boot.
func ParseLocalConfig(ctx context.Context, diskDir string, devices block.BlockDevices, mountPool *mount.Pool) ([]boot.OSImage, error) {
root, err := absFileScheme(diskDir)
if err != nil {
return nil, err
}
// This is a hack. GRUB should stop caring about URLs at least in the
// way we use them today, because GRUB has additional path encoding
// methods. Sorry.
//
// Normally, stuff like this will be in EFI/BOOT/grub.cfg, but some
// distro's have their own directory in this EFI namespace. Just check
// 'em all.
files, err := filepath.Glob(filepath.Join(diskDir, "EFI", "*", "grub.cfg"))
if err != nil {
log.Printf("[grub] Could not glob for %s/EFI/*/grub.cfg: %v", diskDir, err)
}
var relNames []string
for _, file := range files {
base, err := filepath.Rel(diskDir, file)
if err == nil {
relNames = append(relNames, base)
}
}
for _, relname := range append(relNames, probeGrubFiles...) {
c, err := ParseConfigFile(ctx, curl.DefaultSchemes, relname, root, devices, mountPool)
if curl.IsURLError(err) {
continue
}
return c, err
}
return nil, fmt.Errorf("no valid grub config found")
}
func grubScanBLSEntries(mountPool *mount.Pool, variables map[string]string, grubDefaultSavedEntry string) ([]boot.OSImage, error) {
var images []boot.OSImage
// Scan each mounted partition for BLS entries
for _, m := range mountPool.MountPoints {
imgs, _ := bls.ScanBLSEntries(ulog.Null, m.Path, variables, grubDefaultSavedEntry)
images = append(images, imgs...)
}
if len(images) == 0 {
return nil, fmt.Errorf("no valid BLS entry found")
}
return images, nil
}
// Find and return the value of the key from a grubenv file
func findkeywordGrubEnv(file string, fsRoot string, key string) (string, error) {
FileGrubenv, err := filepath.Glob(filepath.Join(fsRoot, file))
if FileGrubenv == nil || err != nil {
return "", err
}
relNamesFile, err := os.Open(FileGrubenv[0])
if err != nil {
return "", fmt.Errorf("[grubenv]:%w", err)
}
grubenv, err := uio.ReadAll(relNamesFile)
if err != nil {
return "", fmt.Errorf("[grubenv]:%w", err)
}
for _, line := range strings.Split(string(grubenv), "\n") {
vals := strings.SplitN(line, "=", 2)
if vals[0] == key {
return vals[1], nil
}
}
return "", fmt.Errorf("%q:%w", key, errMissingKey)
}
// ParseConfigFile parses a grub configuration as specified in
// https://www.gnu.org/software/grub/manual/grub/
//
// See parser.append function for list of commands that are supported.
//
// `root` is the default scheme, host, and path for any files named as a
// relative path - e.g. kernel and initramfs paths are requested relative to
// the root.
func ParseConfigFile(ctx context.Context, s curl.Schemes, configFile string, root *url.URL, devices block.BlockDevices, mountPool *mount.Pool) ([]boot.OSImage, error) {
p := newParser(root, devices, mountPool, s)
if err := p.appendFile(ctx, configFile); err != nil {
return nil, err
}
// Don't add entries twice.
//
// Multiple labels can refer to the same image, so we have to dedup by pointer.
seenLinux := make(map[*boot.LinuxImage]struct{})
seenMB := make(map[*boot.MultibootImage]struct{})
var grubDefaultSavedEntry string
// If the value of keyword "default_saved_entry" exists, find the value of "save_entry" from grubenv files from all possible paths.
if _, ok := p.variables["default_saved_entry"]; ok {
for _, m := range mountPool.MountPoints {
for _, file := range probeGrubEnvFiles {
// Parse grubenv and return the value of 'saved_entry'.
val, _ := findkeywordGrubEnv(file, m.Path, "saved_entry")
if val != "" {
grubDefaultSavedEntry = val
}
}
}
}
if defaultEntry, ok := p.variables["default"]; ok {
p.labelOrder = append([]string{defaultEntry}, p.labelOrder...)
}
var images []boot.OSImage
if p.blscfgFound {
if imgs, err := grubScanBLSEntries(p.mountPool, p.variables, grubDefaultSavedEntry); err == nil {
images = append(images, imgs...)
}
}
for _, label := range p.labelOrder {
if img, ok := p.linuxEntries[label]; ok {
if _, ok := seenLinux[img]; !ok {
images = append(images, img)
seenLinux[img] = struct{}{}
}
}
if img, ok := p.mbEntries[label]; ok {
if _, ok := seenMB[img]; !ok {
images = append(images, img)
seenMB[img] = struct{}{}
}
}
}
return images, nil
}
type parser struct {
linuxEntries map[string]*boot.LinuxImage
mbEntries map[string]*boot.MultibootImage
labelOrder []string
W io.Writer
// parser internals.
numEntry int
// Special variables:
// * default: Default boot option.
// * root: Root "partition" as a URL.
variables map[string]string
// curEntry is the current entry number as a string.
curEntry string
// curLabel is the last parsed label from a "menuentry".
curLabel string
devices block.BlockDevices
mountPool *mount.Pool
schemes curl.Schemes
// blscfgFound is set to true when blscfg is found
blscfgFound bool
}
// newParser returns a new grub parser using `root` and schemes `s`.
//
// We are going off script here by using URLs instead of grub's device syntax.
//
// Typically, the default value for root should be the mount point containing
// the grub config, for example: "file:///tmp/sda1/". Kernel and initramfs
// files are opened relative to this path.
//
// Some grub configs may set a different local root. For this, all partitions
// must be mounted beforehand and made available to grub through `mounts`.
//
// For example, if the grub config contains `search --by-label LINUX`, this
// resolves to the device node "/dev/disk/by-partlabel/LINUX". This grub parser
// looks through mounts for a matching device number.
func newParser(root *url.URL, devices block.BlockDevices, mountPool *mount.Pool, s curl.Schemes) *parser {
return &parser{
linuxEntries: make(map[string]*boot.LinuxImage),
mbEntries: make(map[string]*boot.MultibootImage),
variables: map[string]string{
"root": root.String(),
},
devices: devices,
mountPool: mountPool,
schemes: s,
blscfgFound: false,
}
}
func parseURL(surl string, root string) (*url.URL, error) {
u, err := url.Parse(surl)
if err != nil {
return nil, fmt.Errorf("could not parse URL %q: %w", surl, err)
}
ru, err := url.Parse(root)
if err != nil {
return nil, fmt.Errorf("could not parse URL %q: %w", root, err)
}
if len(u.Scheme) == 0 {
u.Scheme = ru.Scheme
if len(u.Host) == 0 {
// If this is not there, it was likely just a path.
u.Host = ru.Host
u.Path = filepath.Join(ru.Path, filepath.Clean(u.Path))
}
}
return u, nil
}
// getFile parses `url` relative to the current root and returns an io.Reader
// for the requested url.
//
// If url is just a relative path and not a full URL, c.root is used for the
// relative path; the resulting URL is roughly path.Join(root, url).
func (c *parser) getFile(url string) (io.ReaderAt, error) {
u, err := parseURL(url, c.variables["root"])
if err != nil {
return nil, err
}
return c.schemes.LazyFetch(u)
}
// appendFile parses the config file downloaded from `url` and adds it to `c`.
func (c *parser) appendFile(ctx context.Context, url string) error {
u, err := parseURL(url, c.variables["root"])
if err != nil {
return err
}
r, err := c.schemes.Fetch(ctx, u)
if err != nil {
return err
}
config, err := uio.ReadAll(r)
if err != nil {
return err
}
if len(config) > 500 {
// Avoid flooding the console on real systems
// TODO: do we want to pass a verbose flag or a logger?
log.Printf("[grub] Got config file %s", r)
} else {
log.Printf("[grub] Got config file %s:\n%s\n", r, string(config))
}
return c.append(ctx, string(config))
}
// CmdlineQuote quotes the command line as grub-core/lib/cmdline.c does
func cmdlineQuote(args []string) string {
q := make([]string, len(args))
for i, s := range args {
// Replace \ with \\ unless it matches \xXX
s = anyEscape.ReplaceAllStringFunc(s, func(match string) string {
if hexEscape.MatchString(match) {
return match
}
return strings.Replace(match, `\`, `\\`, -1)
})
s = strings.Replace(s, `'`, `\'`, -1)
s = strings.Replace(s, `"`, `\"`, -1)
if strings.ContainsRune(s, ' ') {
s = `"` + s + `"`
}
q[i] = s
}
return strings.Join(q, " ")
}
// append parses `config` and adds the respective configuration to `c`.
//
// NOTE: This parser has outlived its usefulness already, given that it doesn't
// even understand the {} scoping in GRUB. But let's get the tests to pass, and
// then we can do a rewrite.
func (c *parser) append(ctx context.Context, config string) error {
// Here's a shitty parser.
for _, line := range strings.Split(config, "\n") {
// Add extra backslash for OpenSUSE/Fedora/RHEL use case. shlex
// will convert it back to a single backslash.
line = hexEscape.ReplaceAllString(line, `\\$0`)
kv := shlex.Argv(line)
if len(kv) < 1 {
continue
}
directive := strings.ToLower(kv[0])
// blscfg len(kv) is 1 so need to be checked here
if directive == "blscfg" {
c.blscfgFound = true
}
// Used by tests (allow no parameters here)
if c.W != nil && directive == "echo" {
fmt.Fprintf(c.W, "echo:%#v\n", kv[1:])
}
if len(kv) <= 1 {
continue
}
arg := kv[1]
switch directive {
case "search.file", "search.fs_label", "search.fs_uuid":
// Alias to regular search directive.
kv = append(
[]string{"search", map[string]string{
"search.file": "--file",
"search.fs_label": "--fs-label",
"search.fs_uuid": "--fs-uuid",
}[directive]},
kv[1:]...,
)
fallthrough
case "search":
// Parses a line with this format:
// search [--file|--label|--fs-uuid] [--set [var]] [--no-floppy] name
fs := pflag.NewFlagSet("grub.search", pflag.ContinueOnError)
searchUUID := fs.BoolP("fs-uuid", "u", false, "")
searchLabel := fs.BoolP("fs-label", "l", false, "")
searchFile := fs.BoolP("file", "f", false, "")
setVar := fs.String("set", "root", "")
// Ignored flags
fs.String("no-floppy", "", "ignored")
fs.String("hint", "", "ignored")
fs.SetNormalizeFunc(func(f *pflag.FlagSet, name string) pflag.NormalizedName {
// Everything that begins with "hint" is ignored.
if strings.HasPrefix(name, "hint") {
name = "hint"
}
return pflag.NormalizedName(name)
})
if err := fs.Parse(kv[1:]); err != nil || fs.NArg() != 1 {
log.Printf("Warning: Grub parser could not parse %q", kv)
continue
}
searchName := fs.Arg(0)
if *searchUUID && *searchLabel || *searchUUID && *searchFile || *searchLabel && *searchFile {
log.Printf("Warning: Grub parser found more than one search option in %q, skipping line", line)
continue
}
if !*searchUUID && !*searchLabel && !*searchFile {
// defaults to searchUUID
*searchUUID = true
}
switch {
case *searchUUID:
d := c.devices.FilterFSUUID(searchName)
if len(d) != 1 {
log.Printf("Error: Expected 1 device with UUID %q, found %d", searchName, len(d))
continue
}
mp, err := c.mountPool.Mount(d[0], mountFlags)
if err != nil {
log.Printf("Error: Could not mount %v: %v", d[0], err)
continue
}
setVal, err := absFileScheme(mp.Path)
if err != nil {
continue
}
c.variables[*setVar] = setVal.String()
case *searchLabel:
d := c.devices.FilterPartLabel(searchName)
if len(d) != 1 {
log.Printf("Error: Expected 1 device with label %q, found %d", searchName, len(d))
continue
}
mp, err := c.mountPool.Mount(d[0], mountFlags)
if err != nil {
log.Printf("Error: Could not mount %v: %v", d[0], err)
continue
}
setVal, err := absFileScheme(mp.Path)
if err != nil {
continue
}
c.variables[*setVar] = setVal.String()
case *searchFile:
// Make sure searchName stays in mountpoint. Remove "../" components.
cleanPath, err := filepath.Rel("/", filepath.Clean(filepath.Join("/", searchName)))
if err != nil {
log.Printf("Error: Could not clean path %q: %v", searchName, err)
continue
}
// Search through all the devices for the file.
for _, d := range c.devices {
mp, err := c.mountPool.Mount(d, mountFlags)
if err != nil {
log.Printf("Warning: Could not mount %v: %v", mp, err)
continue
}
file := filepath.Join(mp.Path, cleanPath)
if _, err := os.Stat(file); err == nil {
setVal, err := absFileScheme(mp.Path)
if err != nil {
continue
}
c.variables[*setVar] = setVal.String()
break
}
}
}
case "set":
vals := strings.SplitN(arg, "=", 2)
if len(vals) == 2 {
// TODO: We cannot parse grub device syntax.
if vals[0] == "root" {
continue
}
// here we only add the support for the case: set default="${saved_entry}".
if vals[0] == "default" {
if vals[1] == "${saved_entry}" {
c.variables["default_saved_entry"] = vals[1]
} else {
c.variables[vals[0]] = vals[1]
}
} else {
c.variables[vals[0]] = vals[1]
}
}
case "configfile":
// TODO test that
if err := c.appendFile(ctx, arg); err != nil {
return err
}
case "devicetree":
if e, ok := c.linuxEntries[c.curEntry]; ok {
dtb, err := c.getFile(arg)
if err != nil {
return err
}
e.DTB = dtb
}
case "menuentry":
c.curEntry = strconv.Itoa(c.numEntry)
c.curLabel = arg
c.numEntry++
c.labelOrder = append(c.labelOrder, c.curEntry, c.curLabel)
case "linux", "linux16", "linuxefi":
k, err := c.getFile(arg)
if err != nil {
return err
}
// from grub manual: "Any initrd must be reloaded after using this command" so we can replace the entry
entry := &boot.LinuxImage{
Name: c.curLabel,
Kernel: k,
Cmdline: cmdlineQuote(kv[2:]),
}
c.linuxEntries[c.curEntry] = entry
c.linuxEntries[c.curLabel] = entry
case "initrd", "initrd16", "initrdefi":
if e, ok := c.linuxEntries[c.curEntry]; ok {
i, err := c.getFile(arg)
if err != nil {
return err
}
e.Initrd = i
}
case "multiboot", "multiboot2":
// TODO handle --quirk-* arguments ? (change parsing)
k, err := c.getFile(arg)
if err != nil {
return err
}
// from grub manual: "Any initrd must be reloaded after using this command" so we can replace the entry
entry := &boot.MultibootImage{
Name: c.curLabel,
Kernel: k,
Cmdline: cmdlineQuote(kv[2:]),
}
c.mbEntries[c.curEntry] = entry
c.mbEntries[c.curLabel] = entry
case "module", "module2":
// TODO handle --nounzip arguments ? (change parsing)
if e, ok := c.mbEntries[c.curEntry]; ok {
// The only allowed arg
cmdline := kv[1:]
if arg == "--nounzip" {
if len(kv) < 3 {
return fmt.Errorf("no file argument given: %v", kv)
}
arg = kv[2]
cmdline = kv[2:]
}
m, err := c.getFile(arg)
if err != nil {
return err
}
// TODO: Lasy tryGzipFilter(m)
mod := multiboot.Module{
Module: m,
Cmdline: cmdlineQuote(cmdline),
}
e.Modules = append(e.Modules, mod)
}
}
}
return nil
}
// Copyright 2017-2019 the u-root Authors. All rights reserved
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package ipxe implements a trivial IPXE config file parser.
package ipxe
import (
"context"
"errors"
"fmt"
"io"
"net/url"
"os"
"path"
"strings"
"github.com/u-root/u-root/pkg/boot"
"github.com/u-root/u-root/pkg/curl"
"github.com/u-root/u-root/pkg/ulog"
"github.com/u-root/uio/uio"
)
// ErrNotIpxeScript is returned when the config file is not an
// ipxe script.
var ErrNotIpxeScript = errors.New("config file is not ipxe as it does not start with #!ipxe")
// parser encapsulates a parsed ipxe configuration file.
//
// We currently only support kernel and initrd commands.
type parser struct {
bootImage *boot.LinuxImage
// wd is the current working directory.
//
// Relative file paths are interpreted relative to this URL.
wd *url.URL
log ulog.Logger
schemes curl.Schemes
}
// ParseConfig returns a new configuration with the file at URL and default
// schemes.
//
// `s` is used to get files referred to by URLs in the configuration.
func ParseConfig(ctx context.Context, l ulog.Logger, configURL *url.URL, s curl.Schemes) (*boot.LinuxImage, error) {
c := &parser{
schemes: s,
log: l,
}
if err := c.getAndParseFile(ctx, configURL); err != nil {
return nil, err
}
return c.bootImage, nil
}
// getAndParse parses the config file downloaded from `url` and fills in `c`.
func (c *parser) getAndParseFile(ctx context.Context, u *url.URL) error {
r, err := c.schemes.Fetch(ctx, u)
if err != nil {
return err
}
data, err := uio.ReadAll(r)
if err != nil {
return err
}
config := string(data)
if !strings.HasPrefix(config, "#!ipxe") {
return ErrNotIpxeScript
}
//c.log.Printf("Got ipxe config file %s:\n%s\n", r, config)
// Parent dir of the config file.
c.wd = &url.URL{
Scheme: u.Scheme,
Host: u.Host,
Path: path.Dir(u.Path),
}
return c.parseIpxe(config)
}
// getFile parses `surl` and returns an io.Reader for the requested url.
func (c *parser) getFile(surl string) (io.ReaderAt, error) {
u, err := parseURL(surl, c.wd)
if err != nil {
return nil, fmt.Errorf("could not parse URL %q: %w", surl, err)
}
// Cache content read from http body into a tmpfs file, other
// than in heap. This cuts down ram consumption and help boot
// on board with low ram config.
return uio.NewLazyOpenerAt(surl, func() (io.ReaderAt, error) {
f, err := os.CreateTemp("", "cache-kernel")
if err != nil {
return nil, err
}
defer f.Close()
r, err := c.schemes.LazyFetchWithoutCache(u)
if err != nil {
return nil, err
}
_, err = io.Copy(f, r)
if err != nil {
return nil, err
}
if err := f.Sync(); err != nil {
return nil, err
}
// Return a read-only copy.
readOnlyF, err := os.Open(f.Name())
if err != nil {
return nil, err
}
return readOnlyF, nil
}), nil
}
func (c *parser) getFileWithoutCache(surl string) (io.Reader, error) {
u, err := parseURL(surl, c.wd)
if err != nil {
return nil, fmt.Errorf("could not parse URL %q: %w", surl, err)
}
return c.schemes.LazyFetchWithoutCache(u)
}
func parseURL(name string, wd *url.URL) (*url.URL, error) {
u, err := url.Parse(name)
if err != nil {
return nil, fmt.Errorf("could not parse URL %q: %w", name, err)
}
// If it parsed, but it didn't have a Scheme or Host, use the working
// directory's values.
if len(u.Scheme) == 0 && wd != nil {
u.Scheme = wd.Scheme
if len(u.Host) == 0 {
// If this is not there, it was likely just a path.
u.Host = wd.Host
// Absolute file names don't get the parent
// directories, just the host and scheme.
if !path.IsAbs(name) {
u.Path = path.Join(wd.Path, path.Clean(u.Path))
}
}
}
return u, nil
}
func (c *parser) createInitrd(initrds []io.Reader) {
if len(initrds) > 0 {
c.bootImage.Initrd = boot.CatInitrdsWithFileCache(initrds...)
}
}
// parseIpxe parses `config` and constructs a BootImage for `c`.
func (c *parser) parseIpxe(config string) error {
// A trivial ipxe script parser.
// Currently only supports kernel and initrd commands.
c.bootImage = &boot.LinuxImage{}
var initrds []io.Reader
for _, line := range strings.Split(config, "\n") {
// Skip blank lines and comment lines.
line = strings.TrimSpace(line)
if line == "" || line[0] == '#' {
continue
}
args := strings.Fields(line)
if len(args) == 0 {
continue
}
cmd := strings.ToLower(args[0])
switch cmd {
case "kernel":
if len(args) > 1 {
k, err := c.getFile(args[1])
if err != nil {
return err
}
c.bootImage.Kernel = k
}
// Add cmdline if there are any.
if len(args) > 2 {
c.bootImage.Cmdline = strings.Join(args[2:], " ")
}
case "initrd":
if len(args) > 1 {
for _, f := range strings.Split(args[1], ",") {
i, err := c.getFileWithoutCache(f)
if err != nil {
return err
}
initrds = append(initrds, i)
}
}
case "boot":
// Stop parsing at this point, we should go ahead and
// boot.
c.createInitrd(initrds)
return nil
default:
//c.log.Printf("Ignoring unsupported ipxe cmd: %s", line)
}
}
// EOF - we should go ahead and boot.
c.createInitrd(initrds)
return nil
}
// Copyright 2017-2020 the u-root Authors. All rights reserved
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package syslinux implements a syslinux config file parser.
//
// See http://www.syslinux.org/wiki/index.php?title=Config for general syslinux
// config features.
//
// Currently, only the APPEND, INCLUDE, KERNEL, LABEL, DEFAULT, and INITRD
// directives are partially supported.
package syslinux
import (
"context"
"fmt"
"io"
"log"
"net/url"
"path"
"path/filepath"
"strings"
"github.com/u-root/u-root/pkg/boot"
"github.com/u-root/u-root/pkg/boot/multiboot"
"github.com/u-root/u-root/pkg/curl"
"github.com/u-root/uio/uio"
)
func probeIsolinuxFiles() []string {
files := make([]string, 0, 10)
// search order from the syslinux wiki
// http://wiki.syslinux.org/wiki/index.php?title=Config
dirs := []string{
"boot/isolinux",
"isolinux",
"boot/syslinux",
"extlinux",
"syslinux",
"",
}
confs := []string{
"isolinux.cfg",
"extlinux.conf",
"syslinux.cfg",
}
for _, dir := range dirs {
for _, conf := range confs {
if dir == "" {
files = append(files, conf)
} else {
files = append(files, filepath.Join(dir, conf))
}
}
}
return files
}
// ParseLocalConfig treats diskDir like a mount point on the local file system
// and finds an isolinux config under there.
func ParseLocalConfig(ctx context.Context, diskDir string) ([]boot.OSImage, error) {
rootdir := &url.URL{
Scheme: "file",
Path: diskDir,
}
for _, relname := range probeIsolinuxFiles() {
dir, name := filepath.Split(relname)
// "When booting, the initial working directory for SYSLINUX /
// ISOLINUX will be the directory containing the initial
// configuration file."
//
// https://wiki.syslinux.org/wiki/index.php?title=Config#Working_directory
imgs, err := ParseConfigFile(ctx, curl.DefaultSchemes, name, rootdir, dir)
if curl.IsURLError(err) {
continue
}
return imgs, err
}
return nil, fmt.Errorf("no valid syslinux config found on %s", diskDir)
}
// ParseConfigFile parses a Syslinux configuration as specified in
// http://www.syslinux.org/wiki/index.php?title=Config
//
// Currently, only the APPEND, INCLUDE, KERNEL, LABEL, DEFAULT, and INITRD
// directives are partially supported.
//
// `s` is used to fetch any files that must be parsed or provided.
//
// rootdir is the partition mount point that syslinux is operating under.
// Parsed absolute paths will be interpreted relative to the rootdir.
//
// wd is a directory within rootdir that is the current working directory.
// Parsed relative paths will be interpreted relative to rootdir + "/" + wd.
//
// For PXE clients, rootdir will be the the URL without the path, and wd the
// path component of the URL (e.g. rootdir = http://foobar.com, wd =
// barfoo/pxelinux.cfg/).
func ParseConfigFile(ctx context.Context, s curl.Schemes, configFile string, rootdir *url.URL, wd string) ([]boot.OSImage, error) {
p := newParser(rootdir, wd, s)
if err := p.appendFile(ctx, configFile); err != nil {
return nil, err
}
// Assign the right label to display to users.
for label, displayLabel := range p.menuLabel {
if e, ok := p.linuxEntries[label]; ok {
e.Name = displayLabel
}
if e, ok := p.mbEntries[label]; ok {
e.Name = displayLabel
}
}
// Intended order:
//
// 1. nerfDefaultEntry
// 2. defaultEntry
// 3. labels in order they appeared in config
if len(p.labelOrder) == 0 {
return nil, nil
}
if len(p.defaultEntry) > 0 {
p.labelOrder = append([]string{p.defaultEntry}, p.labelOrder...)
}
if len(p.nerfDefaultEntry) > 0 {
p.labelOrder = append([]string{p.nerfDefaultEntry}, p.labelOrder...)
}
p.labelOrder = dedupStrings(p.labelOrder)
var images []boot.OSImage
for _, label := range p.labelOrder {
if img, ok := p.linuxEntries[label]; ok && img.Kernel != nil {
images = append(images, img)
}
if img, ok := p.mbEntries[label]; ok && img.Kernel != nil {
images = append(images, img)
}
}
return images, nil
}
func dedupStrings(list []string) []string {
var newList []string
seen := make(map[string]struct{})
for _, s := range list {
if _, ok := seen[s]; !ok {
seen[s] = struct{}{}
newList = append(newList, s)
}
}
return newList
}
type parser struct {
// linuxEntries is a map of label name -> label configuration.
linuxEntries map[string]*boot.LinuxImage
mbEntries map[string]*boot.MultibootImage
// labelOrder is the order of label entries in linuxEntries.
labelOrder []string
// menuLabel are human-readable labels defined by the "menu label" directive.
menuLabel map[string]string
defaultEntry string
nerfDefaultEntry string
// parser internals.
globalAppend string
scope scope
curEntry string
wd string
rootdir *url.URL
schemes curl.Schemes
}
type scope uint8
const (
scopeGlobal scope = iota
scopeEntry
)
// newParser returns a new PXE parser using working directory `wd`
// and schemes `s`.
//
// If a path encountered in a configuration file is relative instead of a full
// URL, `wd` is used as the "working directory" of that relative path; the
// resulting URL is roughly `wd.String()/path`.
//
// `s` is used to get files referred to by URLs.
func newParser(rootdir *url.URL, wd string, s curl.Schemes) *parser {
return &parser{
linuxEntries: make(map[string]*boot.LinuxImage),
mbEntries: make(map[string]*boot.MultibootImage),
scope: scopeGlobal,
wd: wd,
rootdir: rootdir,
schemes: s,
menuLabel: make(map[string]string),
}
}
func parseURL(name string, rootdir *url.URL, wd string) (*url.URL, error) {
u, err := url.Parse(name)
if err != nil {
return nil, fmt.Errorf("could not parse URL %q: %w", name, err)
}
// If it parsed, but it didn't have a Scheme or Host, use the working
// directory's values.
if len(u.Scheme) == 0 && rootdir != nil {
u.Scheme = rootdir.Scheme
if len(u.Host) == 0 {
// If this is not there, it was likely just a path.
u.Host = rootdir.Host
// Absolute file names don't get the parent
// directories, just the host and scheme.
//
// "All (paths to) file names inside the configuration
// file are relative to the Working Directory, unless
// preceded with a slash."
//
// https://wiki.syslinux.org/wiki/index.php?title=Config#Working_directory
if path.IsAbs(name) {
u.Path = path.Join(rootdir.Path, path.Clean(u.Path))
} else {
u.Path = path.Join(rootdir.Path, wd, path.Clean(u.Path))
}
}
}
return u, nil
}
// getFile parses `url` relative to the config's working directory and returns
// an io.Reader for the requested url.
//
// If url is just a relative path and not a full URL, c.wd is used as the
// "working directory" of that relative path; the resulting URL is roughly
// path.Join(wd.String(), url).
func (c *parser) getFile(url string) (io.ReaderAt, error) {
u, err := parseURL(url, c.rootdir, c.wd)
if err != nil {
return nil, err
}
return c.schemes.LazyFetch(u)
}
// getFileWithoutCache gets the file at `url` without caching.
func (c *parser) getFileWithoutCache(surl string) (io.Reader, error) {
u, err := parseURL(surl, c.rootdir, c.wd)
if err != nil {
return nil, fmt.Errorf("could not parse URL %q: %w", surl, err)
}
return c.schemes.LazyFetchWithoutCache(u)
}
// appendFile parses the config file downloaded from `url` and adds it to `c`.
func (c *parser) appendFile(ctx context.Context, url string) error {
u, err := parseURL(url, c.rootdir, c.wd)
if err != nil {
return err
}
r, err := c.schemes.Fetch(ctx, u)
if err != nil {
return err
}
config, err := uio.ReadAll(r)
if err != nil {
return err
}
log.Printf("Got config file %s:\n%s\n", r, string(config))
return c.append(ctx, string(config))
}
// Append parses `config` and adds the respective configuration to `c`.
func (c *parser) append(ctx context.Context, config string) error {
// Here's a shitty parser.
for _, line := range strings.Split(config, "\n") {
// This is stupid. There should be a FieldsN(...).
kv := strings.Fields(line)
if len(kv) <= 1 {
continue
}
directive := strings.ToLower(kv[0])
var arg string
if len(kv) == 2 {
arg = kv[1]
} else {
arg = strings.Join(kv[1:], " ")
}
switch directive {
case "default":
c.defaultEntry = arg
case "nerfdefault":
c.nerfDefaultEntry = arg
case "include":
if err := c.appendFile(ctx, arg); curl.IsURLError(err) {
log.Printf("failed to parse %s: %v", arg, err)
// Means we didn't find the file. Just ignore
// it.
// TODO(hugelgupf): plumb a logger through here.
continue
} else if err != nil {
return err
}
case "menu":
opt := strings.Fields(arg)
if len(opt) < 1 {
continue
}
switch strings.ToLower(opt[0]) {
case "label":
// Note that "menu label" only changes the
// displayed label, not the identifier for this
// entry.
//
// We track these separately because "menu
// label" directives may happen before we know
// whether this is a Linux or Multiboot entry.
c.menuLabel[c.curEntry] = strings.Join(opt[1:], " ")
case "default":
// Are we in label scope?
//
// "Only valid after a LABEL statement" -syslinux wiki.
if c.scope == scopeEntry {
c.defaultEntry = c.curEntry
}
}
case "label":
// We forever enter label scope.
c.scope = scopeEntry
c.curEntry = arg
c.linuxEntries[c.curEntry] = &boot.LinuxImage{
Cmdline: c.globalAppend,
Name: c.curEntry,
}
c.labelOrder = append(c.labelOrder, c.curEntry)
case "kernel":
// I hate special cases like these, but we aren't gonna
// implement syslinux modules.
if arg == "mboot.c32" {
// Prepare for a multiboot kernel.
delete(c.linuxEntries, c.curEntry)
c.mbEntries[c.curEntry] = &boot.MultibootImage{
Name: c.curEntry,
}
}
fallthrough
case "linux":
if e, ok := c.linuxEntries[c.curEntry]; ok {
k, err := c.getFile(arg)
if err != nil {
return err
}
e.Kernel = k
}
case "initrd":
if e, ok := c.linuxEntries[c.curEntry]; ok {
// TODO: append "initrd=$arg" to the cmdline.
//
// For how this interacts with global appends,
// read
// https://wiki.syslinux.org/wiki/index.php?title=Directives/append
// Multiple initrds are comma-separated
var initrds []io.Reader
for _, f := range strings.Split(arg, ",") {
i, err := c.getFileWithoutCache(f)
if err != nil {
return err
}
initrds = append(initrds, i)
}
e.Initrd = boot.CatInitrdsWithFileCache(initrds...)
}
case "fdt":
// TODO: fdtdir support
//
// The logic in u-boot is quite obscure and replies on soc/board names to select the right dtb file.
// https://gitlab.com/u-boot/u-boot/-/blob/master/boot/pxe_utils.c#L634
// Can be implemented based on data in /proc/device-tree/compatible
if e, ok := c.linuxEntries[c.curEntry]; ok {
dtb, err := c.getFile(arg)
if err != nil {
return err
}
e.DTB = dtb
}
case "append":
switch c.scope {
case scopeGlobal:
c.globalAppend = arg
case scopeEntry:
if e, ok := c.mbEntries[c.curEntry]; ok {
modules := strings.Split(arg, "---")
// The first module is special -- the kernel.
if len(modules) > 0 {
kernel := strings.Fields(modules[0])
if len(kernel) == 0 {
return fmt.Errorf("no kernel specified by %v", modules[0])
}
k, err := c.getFile(kernel[0])
if err != nil {
return err
}
e.Kernel = k
if len(kernel) > 1 {
e.Cmdline = strings.Join(kernel[1:], " ")
}
modules = modules[1:]
}
for _, cmdline := range modules {
m := strings.Fields(cmdline)
if len(m) == 0 {
continue
}
file, err := c.getFile(m[0])
if err != nil {
return err
}
e.Modules = append(e.Modules, multiboot.Module{
Cmdline: strings.TrimSpace(cmdline),
Module: file,
})
}
}
if e, ok := c.linuxEntries[c.curEntry]; ok {
if arg == "-" {
e.Cmdline = ""
} else {
// Yes, we explicitly _override_, not
// concatenate. If a specific append
// directive is present, a global
// append directive is ignored.
//
// Also, "If you enter multiple APPEND
// statements in a single LABEL entry,
// only the last one will be used".
//
// https://wiki.syslinux.org/wiki/index.php?title=Directives/append
e.Cmdline = arg
}
}
}
}
}
// Go through all labels and download the initrds.
for _, label := range c.linuxEntries {
// If the initrd was set via the INITRD directive, don't
// overwrite that.
//
// TODO(hugelgupf): Is this really what syslinux does? Does
// INITRD trump cmdline? Does it trump global? What if both the
// directive and cmdline initrd= are set? Does it depend on the
// order in the config file? (My current best guess: order.)
//
// Answer: Normally, the INITRD directive appends to the
// cmdline, and the _last_ effective initrd= parameter is used
// for loading initrd files.
if label.Initrd != nil {
continue
}
for _, opt := range strings.Fields(label.Cmdline) {
optkv := strings.Split(opt, "=")
if len(optkv) != 2 || optkv[0] != "initrd" {
continue
}
i, err := c.getFile(optkv[1])
if err != nil {
return err
}
label.Initrd = i
}
}
return nil
}
// Copyright 2013-2017 the u-root Authors. All rights reserved
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package cpio
import (
"io"
"strings"
)
// Archive is an in-memory list of files.
//
// Archive itself is a RecordWriter, and Archive.Reader() returns a new
// RecordReader for the archive starting from the first file.
type Archive struct {
// Files is a map of relative archive path -> record.
Files map[string]Record
// Order is a list of relative archive paths and represents the order
// in which Files were added.
Order []string
}
// InMemArchive returns an in-memory file archive.
func InMemArchive() *Archive {
return &Archive{
Files: make(map[string]Record),
}
}
// ArchiveFromRecords creates a new Archive from the records.
func ArchiveFromRecords(rs []Record) *Archive {
a := InMemArchive()
for _, r := range rs {
a.WriteRecord(r)
}
return a
}
// ArchiveFromReader reads records from r into a new Archive in memory.
func ArchiveFromReader(r RecordReader) (*Archive, error) {
a := InMemArchive()
if err := Concat(a, r, nil); err != nil {
return nil, err
}
return a, nil
}
// WriteRecord implements RecordWriter and adds a record to the archive.
//
// WriteRecord uses Normalize to deduplicate paths.
func (a *Archive) WriteRecord(r Record) error {
r.Name = Normalize(r.Name)
a.Files[r.Name] = r
a.Order = append(a.Order, r.Name)
return nil
}
// Empty returns whether the archive has any files in it.
func (a *Archive) Empty() bool {
return len(a.Files) == 0
}
// Contains returns true if a record matching r is in the archive.
func (a *Archive) Contains(r Record) bool {
r.Name = Normalize(r.Name)
if s, ok := a.Files[r.Name]; ok {
return Equal(r, s)
}
return false
}
// Get returns a record for the normalized path or false if there is none.
//
// The path is normalized using Normalize, so Get("/bin/bar") is the same as
// Get("bin/bar") is the same as Get("bin//bar").
func (a *Archive) Get(path string) (Record, bool) {
r, ok := a.Files[Normalize(path)]
return r, ok
}
// String implements fmt.Stringer.
//
// String lists files like ls would.
func (a *Archive) String() string {
var b strings.Builder
r := a.Reader()
for {
record, err := r.ReadRecord()
if err != nil {
return b.String()
}
b.WriteString(record.String())
b.WriteString("\n")
}
}
type archiveReader struct {
a *Archive
pos int
}
// Reader returns a RecordReader for the archive that starts at the first
// record.
func (a *Archive) Reader() RecordReader {
return &EOFReader{&archiveReader{a: a}}
}
// ReadRecord implements RecordReader.
func (ar *archiveReader) ReadRecord() (Record, error) {
if ar.pos >= len(ar.a.Order) {
return Record{}, io.EOF
}
path := ar.a.Order[ar.pos]
ar.pos++
return ar.a.Files[path], nil
}
// Copyright 2013-2017 the u-root Authors. All rights reserved
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package cpio implements utilities for reading and writing cpio archives.
//
// Currently, only newc-formatted cpio archives are supported through cpio.Newc.
//
// Reading from or writing to a file:
//
// f, err := os.Open(...)
// if err ...
// recReader := cpio.Newc.Reader(f)
// err := ForEachRecord(recReader, func(r cpio.Record) error {
//
// })
//
// // Or...
// recWriter := cpio.Newc.Writer(f)
//
// Reading from or writing to an in-memory archive:
//
// a := cpio.InMemArchive()
// err := a.WriteRecord(...)
//
// recReader := a.Reader() // Reads from the "beginning."
//
// if a.Contains("bar/foo") {
//
// }
package cpio
import (
"fmt"
"io"
"os"
"time"
"github.com/u-root/u-root/pkg/ls"
)
var (
formatMap = make(map[string]RecordFormat)
// Debug can be set e.g. to log.Printf to enable debug prints from
// marshaling/unmarshaling cpio archives.
Debug = func(string, ...interface{}) {}
)
// Record represents a CPIO record, which represents a Unix file.
type Record struct {
// ReaderAt contains the content of this CPIO record.
io.ReaderAt
// Info is metadata describing the CPIO record.
Info
// metadata about this item's place in the file
RecPos int64 // Where in the file this record is
RecLen uint64 // How big the record is.
FilePos int64 // Where in the CPIO the file's contents are.
}
// String implements a fmt.Stringer for Record.
//
// String returns a string long-formatted like `ls` would format it.
func (r Record) String() string {
s := ls.LongStringer{
Human: true,
Name: ls.NameStringer{},
}
return s.FileString(LSInfoFromRecord(r))
}
// Info holds metadata about files.
type Info struct {
Ino uint64
Mode uint64
UID uint64
GID uint64
NLink uint64
MTime uint64
FileSize uint64
Dev uint64
Major uint64
Minor uint64
Rmajor uint64
Rminor uint64
Name string
}
func (i Info) String() string {
return fmt.Sprintf("%s: Ino %d Mode %#o UID %d GID %d NLink %d MTime %v FileSize %d Major %d Minor %d Rmajor %d Rminor %d",
i.Name,
i.Ino,
i.Mode,
i.UID,
i.GID,
i.NLink,
time.Unix(int64(i.MTime), 0).UTC(),
i.FileSize,
i.Major,
i.Minor,
i.Rmajor,
i.Rminor)
}
// A RecordReader reads one record from an archive.
type RecordReader interface {
ReadRecord() (Record, error)
}
// A RecordWriter writes one record to an archive.
type RecordWriter interface {
WriteRecord(Record) error
}
// A RecordFormat gives readers and writers for dealing with archives from io
// objects.
//
// CPIO files have a number of records, of which newc is the most widely used
// today.
type RecordFormat interface {
Reader(r io.ReaderAt) RecordReader
NewFileReader(*os.File) (RecordReader, error)
Writer(w io.Writer) RecordWriter
}
// Format returns the RecordFormat with that name, if it exists.
func Format(name string) (RecordFormat, error) {
op, ok := formatMap[name]
if !ok {
return nil, fmt.Errorf("%q is not in cpio format map %v", name, formatMap)
}
return op, nil
}
func modeFromLinux(mode uint64) os.FileMode {
m := os.FileMode(mode & 0o777)
switch mode & S_IFMT {
case S_IFBLK:
m |= os.ModeDevice
case S_IFCHR:
m |= os.ModeDevice | os.ModeCharDevice
case S_IFDIR:
m |= os.ModeDir
case S_IFIFO:
m |= os.ModeNamedPipe
case S_IFLNK:
m |= os.ModeSymlink
case S_IFREG:
// nothing to do
case S_IFSOCK:
m |= os.ModeSocket
}
if mode&S_ISGID != 0 {
m |= os.ModeSetgid
}
if mode&S_ISUID != 0 {
m |= os.ModeSetuid
}
if mode&S_ISVTX != 0 {
m |= os.ModeSticky
}
return m
}
// Copyright 2013-2017 the u-root Authors. All rights reserved
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build !plan9 && !windows
package cpio
import (
"fmt"
"io"
"log"
"os"
"path/filepath"
"syscall"
"time"
"github.com/u-root/u-root/pkg/ls"
"github.com/u-root/u-root/pkg/upath"
"github.com/u-root/uio/uio"
"golang.org/x/sys/unix"
)
var modeMap = map[uint64]os.FileMode{
modeSocket: os.ModeSocket,
modeSymlink: os.ModeSymlink,
modeFile: 0,
modeBlock: os.ModeDevice,
modeDir: os.ModeDir,
modeChar: os.ModeCharDevice,
modeFIFO: os.ModeNamedPipe,
}
// setModes sets the modes, changing the easy ones first and the harder ones last.
// In this way, we set as much as we can before bailing out.
// N.B.: if you set something with S_ISUID, then change the owner,
// the kernel (Linux, OSX, etc.) clears S_ISUID (a good idea). So, the simple thing:
// Do the chmod operations in order of difficulty, and give up as soon as we fail.
// Set the basic permissions -- not including SUID, GUID, etc.
// Set the times
// Set the owner
// Set ALL the mode bits, in case we need to do SUID, etc. If we could not
// set the owner, we won't even try this operation of course, so we won't
// have SUID incorrectly set for the wrong user.
func setModes(r Record) error {
if err := os.Chmod(r.Name, toFileMode(r)&os.ModePerm); err != nil {
return err
}
/*if err := os.Chtimes(r.Name, time.Time{}, time.Unix(int64(r.MTime), 0)); err != nil {
return err
}*/
if err := os.Chown(r.Name, int(r.UID), int(r.GID)); err != nil {
return err
}
if err := os.Chmod(r.Name, toFileMode(r)); err != nil {
return err
}
return nil
}
func toFileMode(r Record) os.FileMode {
m := os.FileMode(perm(r))
if r.Mode&unix.S_ISUID != 0 {
m |= os.ModeSetuid
}
if r.Mode&unix.S_ISGID != 0 {
m |= os.ModeSetgid
}
if r.Mode&unix.S_ISVTX != 0 {
m |= os.ModeSticky
}
return m
}
func perm(r Record) uint32 {
return uint32(r.Mode) & modePermissions
}
func dev(r Record) int {
return int(r.Rmajor<<8 | r.Rminor)
}
func linuxModeToFileType(m uint64) (os.FileMode, error) {
if t, ok := modeMap[m&modeTypeMask]; ok {
return t, nil
}
return 0, fmt.Errorf("invalid file type %#o", m&modeTypeMask)
}
// CreateFile creates a local file for f relative to the current working
// directory.
//
// CreateFile will attempt to set all metadata for the file, including
// ownership, times, and permissions.
func CreateFile(f Record) error {
return CreateFileInRoot(f, ".", true)
}
// CreateFileInRoot creates a local file for f relative to rootDir.
//
// It will attempt to set all metadata for the file, including ownership,
// times, and permissions. If these fail, it only returns an error if
// forcePriv is true.
//
// Block and char device creation will only return error if forcePriv is true.
func CreateFileInRoot(f Record, rootDir string, forcePriv bool) error {
m, err := linuxModeToFileType(f.Mode)
if err != nil {
return err
}
f.Name, err = upath.SafeFilepathJoin(rootDir, f.Name)
if err != nil {
// The behavior is to skip files which are unsafe due to
// zipslip, but continue extracting everything else.
log.Printf("Warning: Skipping file %q due to: %v", f.Name, err)
return nil
}
dir := filepath.Dir(f.Name)
// The problem: many cpio archives do not specify the directories and
// hence the permissions. They just specify the whole path. In order
// to create files in these directories, we have to make them at least
// mode 755.
if _, err := os.Stat(dir); os.IsNotExist(err) && len(dir) > 0 {
if err := os.MkdirAll(dir, 0o755); err != nil {
return fmt.Errorf("CreateFileInRoot %q: %w", f.Name, err)
}
}
switch m {
case os.ModeSocket, os.ModeNamedPipe:
return fmt.Errorf("%q: type %v: cannot create IPC endpoints", f.Name, m)
case os.ModeSymlink:
content, err := io.ReadAll(uio.Reader(f))
if err != nil {
return err
}
return os.Symlink(string(content), f.Name)
case os.FileMode(0):
nf, err := os.Create(f.Name)
if err != nil {
return err
}
defer nf.Close()
if _, err := io.Copy(nf, uio.Reader(f)); err != nil {
return err
}
case os.ModeDir:
if err := os.MkdirAll(f.Name, toFileMode(f)); err != nil {
return err
}
case os.ModeDevice:
if err := mknod(f.Name, perm(f)|syscall.S_IFBLK, dev(f)); err != nil && forcePriv {
return err
}
case os.ModeCharDevice:
if err := mknod(f.Name, perm(f)|syscall.S_IFCHR, dev(f)); err != nil && forcePriv {
return err
}
default:
return fmt.Errorf("%v: Unknown type %#o", f.Name, m)
}
if err := setModes(f); err != nil && forcePriv {
return err
}
return nil
}
// Inumber and devnumbers are unique to Unix-like
// operating systems. You can not uniquely disambiguate a file in a
// Unix system with just an inumber, you need a device number too.
// To handle hard links (unique to Unix) we need to figure out if a
// given file has been seen before. To do this we see if a file has the
// same [dev,ino] tuple as one we have seen. If so, we won't bother
// reading it in.
type devInode struct {
dev uint64
ino uint64
}
// A Recorder is a structure that contains variables used to calculate
// file parameters such as inode numbers for a CPIO file. The life-time
// of a Record structure is meant to be the same as the construction of a
// single CPIO archive. Do not reuse between CPIOs if you don't know what
// you're doing.
type Recorder struct {
inodeMap map[devInode]Info
inumber uint64
}
// Certain elements of the file can not be set by cpio:
// the Inode #
// the Dev
// maintaining these elements leaves us with a non-reproducible
// output stream. In this function, we figure out what inumber
// we need to use, and clear out anything we can.
// We always zero the Dev.
// We try to find the matching inode. If found, we use its inumber.
// If not, we get a new inumber for it and save the inode away.
// This eliminates two of the messier parts of creating reproducible
// output streams.
// The second return value indicates whether it is a hardlink or not.
func (r *Recorder) inode(i Info) (Info, bool) {
d := devInode{dev: i.Dev, ino: i.Ino}
i.Dev = 0
if d, ok := r.inodeMap[d]; ok {
i.Ino = d.Ino
return i, d.Name != i.Name
}
i.Ino = r.inumber
r.inumber++
r.inodeMap[d] = i
return i, false
}
// GetRecord returns a cpio Record for the given path on the local file system.
//
// GetRecord does not follow symlinks. If path is a symlink, the record
// returned will reflect that symlink.
func (r *Recorder) GetRecord(path string) (Record, error) {
fi, err := os.Lstat(path)
if err != nil {
return Record{}, err
}
sys := fi.Sys().(*syscall.Stat_t)
info, done := r.inode(sysInfo(path, sys))
switch fi.Mode() & os.ModeType {
case 0: // Regular file.
if done {
return Record{Info: info}, nil
}
return Record{Info: info, ReaderAt: uio.NewLazyLimitFile(path, int64(info.FileSize))}, nil
case os.ModeSymlink:
linkname, err := os.Readlink(path)
if err != nil {
return Record{}, err
}
return StaticRecord([]byte(linkname), info), nil
default:
return StaticRecord(nil, info), nil
}
}
// NewRecorder creates a new Recorder.
//
// A recorder is a structure that contains variables used to calculate
// file parameters such as inode numbers for a CPIO file. The life-time
// of a Record structure is meant to be the same as the construction of a
// single CPIO archive. Do not reuse between CPIOs if you don't know what
// you're doing.
func NewRecorder() *Recorder {
return &Recorder{make(map[devInode]Info), 2}
}
// LSInfoFromRecord converts a Record to be usable with the ls package for
// listing files.
func LSInfoFromRecord(rec Record) ls.FileInfo {
var target string
mode := modeFromLinux(rec.Mode)
if mode&os.ModeType == os.ModeSymlink {
if l, err := uio.ReadAll(rec); err != nil {
target = err.Error()
} else {
target = string(l)
}
}
return ls.FileInfo{
Name: rec.Name,
Mode: mode,
Rdev: unix.Mkdev(uint32(rec.Rmajor), uint32(rec.Rminor)),
UID: uint32(rec.UID),
GID: uint32(rec.GID),
Size: int64(rec.FileSize),
MTime: time.Unix(int64(rec.MTime), 0).UTC(),
SymlinkTarget: target,
}
}
// Copyright 2013-2019 the u-root Authors. All rights reserved
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build !freebsd && !plan9 && !windows
package cpio
import (
"syscall"
)
func mknod(path string, mode uint32, dev int) (err error) {
return syscall.Mknod(path, mode, dev)
}
// Copyright 2013-2017 the u-root Authors. All rights reserved
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package cpio
import (
"bytes"
"encoding/binary"
"encoding/hex"
"fmt"
"io"
"os"
"github.com/u-root/uio/uio"
)
const (
newcMagic = "070701"
magicLen = 6
)
// Newc is the newc CPIO record format.
var Newc RecordFormat = newc{magic: newcMagic}
type header struct {
Ino uint32
Mode uint32
UID uint32
GID uint32
NLink uint32
MTime uint32
FileSize uint32
Major uint32
Minor uint32
Rmajor uint32
Rminor uint32
NameLength uint32
CRC uint32
}
func headerFromInfo(i Info) header {
var h header
h.Ino = uint32(i.Ino)
h.Mode = uint32(i.Mode)
h.UID = uint32(i.UID)
h.GID = uint32(i.GID)
h.NLink = uint32(i.NLink)
h.MTime = uint32(i.MTime)
h.FileSize = uint32(i.FileSize)
h.Major = uint32(i.Major)
h.Minor = uint32(i.Minor)
h.Rmajor = uint32(i.Rmajor)
h.Rminor = uint32(i.Rminor)
h.NameLength = uint32(len(i.Name)) + 1
return h
}
func (h header) Info() Info {
var i Info
i.Ino = uint64(h.Ino)
i.Mode = uint64(h.Mode)
i.UID = uint64(h.UID)
i.GID = uint64(h.GID)
i.NLink = uint64(h.NLink)
i.MTime = uint64(h.MTime)
i.FileSize = uint64(h.FileSize)
i.Major = uint64(h.Major)
i.Minor = uint64(h.Minor)
i.Rmajor = uint64(h.Rmajor)
i.Rminor = uint64(h.Rminor)
return i
}
// newc implements RecordFormat for the newc format.
type newc struct {
magic string
}
// round4 returns the next multiple of 4 close to n.
func round4(n int64) int64 {
return (n + 3) &^ 0x3
}
type writer struct {
n newc
w io.Writer
pos int64
}
// Writer implements RecordFormat.Writer.
func (n newc) Writer(w io.Writer) RecordWriter {
return NewDedupWriter(&writer{n: n, w: w})
}
func (w *writer) Write(b []byte) (int, error) {
n, err := w.w.Write(b)
if err != nil {
return 0, err
}
w.pos += int64(n)
return n, nil
}
func (w *writer) pad() error {
if o := round4(w.pos); o != w.pos {
var pad [3]byte
if _, err := w.Write(pad[:o-w.pos]); err != nil {
return err
}
}
return nil
}
// WriteRecord writes newc cpio records. It pads the header+name write to 4
// byte alignment and pads the data write as well.
func (w *writer) WriteRecord(f Record) error {
// Write magic.
if _, err := w.Write([]byte(w.n.magic)); err != nil {
return err
}
buf := &bytes.Buffer{}
hdr := headerFromInfo(f.Info)
if f.ReaderAt == nil {
hdr.FileSize = 0
}
hdr.CRC = 0
if err := binary.Write(buf, binary.BigEndian, hdr); err != nil {
return err
}
hexBuf := make([]byte, hex.EncodedLen(buf.Len()))
n := hex.Encode(hexBuf, buf.Bytes())
// It's much easier to debug if we match GNU output format.
hexBuf = bytes.ToUpper(hexBuf)
// Write header.
if _, err := w.Write(hexBuf[:n]); err != nil {
return err
}
// Append NULL char.
cstr := append([]byte(f.Info.Name), 0)
// Write name.
if _, err := w.Write(cstr); err != nil {
return err
}
// Pad to a multiple of 4.
if err := w.pad(); err != nil {
return err
}
// Some files do not have any content.
if f.ReaderAt == nil {
return nil
}
// Write file contents.
m, err := io.Copy(w, uio.Reader(f))
if err != nil {
return err
}
if m != int64(f.Info.FileSize) {
return fmt.Errorf("WriteRecord: %s: wrote %d bytes of file instead of %d bytes; archive is now corrupt", f.Info.Name, m, f.Info.FileSize)
}
if c, ok := f.ReaderAt.(io.Closer); ok {
if err := c.Close(); err != nil {
return err
}
}
if m > 0 {
return w.pad()
}
return nil
}
type reader struct {
n newc
r io.ReaderAt
pos int64
}
// discarder is used to implement ReadAt from a Reader
// by reading, and discarding, data until the offset
// is reached. It can only go forward. It is designed
// for pipe-like files.
type discarder struct {
r io.Reader
pos int64
}
// ReadAt implements ReadAt for a discarder.
// It is an error for the offset to be negative.
func (r *discarder) ReadAt(p []byte, off int64) (int, error) {
if off-r.pos < 0 {
return 0, fmt.Errorf("negative seek on discarder not allowed")
}
if off != r.pos {
i, err := io.Copy(io.Discard, io.LimitReader(r.r, off-r.pos))
if err != nil || i != off-r.pos {
return 0, err
}
r.pos += i
}
n, err := io.ReadFull(r.r, p)
if err != nil {
return n, err
}
r.pos += int64(n)
return n, err
}
var _ io.ReaderAt = &discarder{}
// Reader implements RecordFormat.Reader.
func (n newc) Reader(r io.ReaderAt) RecordReader {
return EOFReader{&reader{n: n, r: r}}
}
// NewFileReader implements RecordFormat.Reader. If the file
// implements ReadAt, then it is used for greater efficiency.
// If it only implements Read, then a discarder will be used
// instead.
// Note a complication:
//
// r, _, _ := os.Pipe()
// var b [2]byte
// _, err := r.ReadAt(b[:], 0)
// fmt.Printf("%v", err)
//
// Pipes claim to implement ReadAt; most Unix kernels
// do not agree. Even a seek to the current position fails.
// This means that
// if rat, ok := r.(io.ReaderAt); ok {
// would seem to work, but would fail when the
// actual ReadAt on the pipe occurs, even for offset 0,
// which does not require a seek! The kernel checks for
// whether the fd is seekable and returns an error,
// even for values of offset which won't require a seek.
// So, the code makes a simple test: can we seek to
// current offset? If not, then the file is wrapped with a
// discardreader. The discard reader is far less efficient
// but allows cpio to read from a pipe.
func (n newc) NewFileReader(f *os.File) (RecordReader, error) {
_, err := f.Seek(0, 0)
if err == nil {
return EOFReader{&reader{n: n, r: f}}, nil
}
return EOFReader{&reader{n: n, r: &discarder{r: f}}}, nil
}
func (r *reader) read(p []byte) error {
n, err := r.r.ReadAt(p, r.pos)
if err == io.EOF {
return io.EOF
}
if err != nil || n != len(p) {
return fmt.Errorf("ReadAt(pos = %d): got %d, want %d bytes; error %w", r.pos, n, len(p), err)
}
r.pos += int64(n)
return nil
}
func (r *reader) readAligned(p []byte) error {
err := r.read(p)
r.pos = round4(r.pos)
return err
}
// ReadRecord implements RecordReader for the newc cpio format.
func (r *reader) ReadRecord() (Record, error) {
hdr := header{}
recPos := r.pos
buf := make([]byte, hex.EncodedLen(binary.Size(hdr))+magicLen)
if err := r.read(buf); err != nil {
return Record{}, err
}
// Check the magic.
if magic := string(buf[:magicLen]); magic != r.n.magic {
return Record{}, fmt.Errorf("reader: magic got %q, want %q", magic, r.n.magic)
}
// Decode hex header fields.
dst := make([]byte, binary.Size(hdr))
if _, err := hex.Decode(dst, buf[magicLen:]); err != nil {
return Record{}, fmt.Errorf("reader: error decoding hex: %w", err)
}
if err := binary.Read(bytes.NewReader(dst), binary.BigEndian, &hdr); err != nil {
return Record{}, err
}
Debug("Decoded header is %v\n", hdr)
// Get the name.
if hdr.NameLength == 0 {
return Record{}, fmt.Errorf("name field of length zero")
}
nameBuf := make([]byte, hdr.NameLength)
if err := r.readAligned(nameBuf); err != nil {
Debug("name read failed")
return Record{}, err
}
info := hdr.Info()
info.Name = Normalize(string(nameBuf[:hdr.NameLength-1]))
recLen := uint64(r.pos - recPos)
filePos := r.pos
//TODO: check if hdr.FileSize is equal to the actual fileSize of the record
content := io.NewSectionReader(r.r, r.pos, int64(hdr.FileSize))
r.pos = round4(r.pos + int64(hdr.FileSize))
return Record{
Info: info,
ReaderAt: content,
RecLen: recLen,
RecPos: recPos,
FilePos: filePos,
}, nil
}
func init() {
formatMap["newc"] = Newc
}
// Copyright 2013-2017 the u-root Authors. All rights reserved
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package cpio
import (
"syscall"
)
func sysInfo(n string, sys *syscall.Stat_t) Info {
return Info{
Ino: sys.Ino,
Mode: uint64(sys.Mode),
UID: uint64(sys.Uid),
GID: uint64(sys.Gid),
NLink: uint64(sys.Nlink),
MTime: uint64(sys.Mtim.Sec),
FileSize: uint64(sys.Size),
Dev: uint64(sys.Dev),
Major: uint64(sys.Dev >> 8),
Minor: uint64(sys.Dev & 0xff),
Rmajor: uint64(sys.Rdev >> 8),
Rminor: uint64(sys.Rdev & 0xff),
Name: n,
}
}
// Copyright 2013-2017 the u-root Authors. All rights reserved
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package cpio
import (
"bytes"
"fmt"
"io"
"math"
"os"
"path"
"strings"
"github.com/u-root/uio/uio"
)
// Trailer is the name of the trailer record.
const Trailer = "TRAILER!!!"
// TrailerRecord is the last record in any CPIO archive.
var TrailerRecord = StaticRecord(nil, Info{Name: Trailer})
// StaticRecord returns a record with the given contents and metadata.
func StaticRecord(contents []byte, info Info) Record {
info.FileSize = uint64(len(contents))
return Record{
ReaderAt: bytes.NewReader(contents),
Info: info,
}
}
// StaticFile returns a normal file record.
func StaticFile(name string, content string, perm uint64) Record {
return StaticRecord([]byte(content), Info{
Name: name,
Mode: S_IFREG | perm,
})
}
// Symlink returns a symlink record at name pointing to target.
func Symlink(name string, target string) Record {
return Record{
ReaderAt: strings.NewReader(target),
Info: Info{
FileSize: uint64(len(target)),
Mode: S_IFLNK | 0o777,
Name: name,
},
}
}
// Directory returns a directory record at name.
func Directory(name string, mode uint64) Record {
return Record{
Info: Info{
Name: name,
Mode: S_IFDIR | mode&^S_IFMT,
},
}
}
// CharDev returns a character device record at name.
func CharDev(name string, perm uint64, rmajor, rminor uint64) Record {
return Record{
Info: Info{
Name: name,
Mode: S_IFCHR | perm,
Rmajor: rmajor,
Rminor: rminor,
},
}
}
// EOFReader is a RecordReader that converts the Trailer record to io.EOF.
type EOFReader struct {
RecordReader
}
// ReadRecord implements RecordReader.
//
// ReadRecord returns io.EOF when the record name is TRAILER!!!.
func (r EOFReader) ReadRecord() (Record, error) {
rec, err := r.RecordReader.ReadRecord()
if err != nil {
return Record{}, err
}
// The end of a CPIO archive is marked by a record whose name is
// "TRAILER!!!".
if rec.Name == Trailer {
return Record{}, io.EOF
}
return rec, nil
}
// DedupWriter is a RecordWriter that does not write more than one record with
// the same path.
//
// There seems to be no harm done in stripping duplicate names when the record
// is written, and lots of harm done if we don't do it.
type DedupWriter struct {
rw RecordWriter
// alreadyWritten keeps track of paths already written to rw.
alreadyWritten map[string]struct{}
}
// NewDedupWriter returns a new deduplicating rw.
func NewDedupWriter(rw RecordWriter) RecordWriter {
return &DedupWriter{
rw: rw,
alreadyWritten: make(map[string]struct{}),
}
}
// WriteRecord implements RecordWriter.
//
// If rec.Name was already seen once before, it will not be written again and
// WriteRecord returns nil.
func (dw *DedupWriter) WriteRecord(rec Record) error {
rec.Name = Normalize(rec.Name)
if _, ok := dw.alreadyWritten[rec.Name]; ok {
return nil
}
dw.alreadyWritten[rec.Name] = struct{}{}
return dw.rw.WriteRecord(rec)
}
// WriteRecords writes multiple records to w.
func WriteRecords(w RecordWriter, files []Record) error {
for _, f := range files {
if err := w.WriteRecord(f); err != nil {
return fmt.Errorf("WriteRecords: writing %q got %w", f.Info.Name, err)
}
}
return nil
}
// WriteRecordsAndDirs writes records to w, with a slight difference from WriteRecords:
// the record path is split and all the
// directories are written first, in order, mimic'ing what happens with
// find . -print
//
// When is this function needed?
// Most cpio programs will create directories as needed for paths such as a/b/c/d
// The cpio creation process for Linux uses find, and will create a
// record for each directory in a/b/c/d
//
// But when code programatically generates a cpio for the Linux kernel,
// the cpio is not generated via find, and Linux will not create
// intermediate directories. The result, seen in practice, is that a path,
// such as a/b/c/d, when unpacked by the linux kernel, will be ignored if
// a/b/c does not exist!
//
// Again, this function is very rarely needed, save when we programatically generate
// an initramfs for Linux.
// This code only works with a deduplicating writer. Further, it will not accept a
// Record if the full pathname of that Record already exists. This is arguably
// overly restrictive but, at the same, avoids some very unpleasant programmer
// errors.
// There is overlap here with DedupWriter but given that this is a Special Snowflake
// function, it seems best to leave the DedupWriter code alone.
func WriteRecordsAndDirs(rw RecordWriter, files []Record) error {
w, ok := rw.(*DedupWriter)
if !ok {
return fmt.Errorf("WriteRecordsAndDirs(%T,...): only DedupWriter allowed:%w", rw, os.ErrInvalid)
}
for _, f := range files {
// This redundant Normalize does no harm, but, yes, it is redundant.
// Signed
// The Department of Redundancy Department.
f.Name = Normalize(f.Name)
if r, ok := w.alreadyWritten[f.Name]; ok {
return fmt.Errorf("WriteRecordsAndDirs: %q already in the archive: %v:%w", f.Name, r, os.ErrExist)
}
var recs []Record
// Paths must be written to the archive in the order in which they
// need to be created, i.e., a/b/c/d must be written as
// a, a/b/, a/b/c, a/b/c/d
// Note: do not use os.Separator here: cpio is a Unix standard, and hence
// / is used.
// do NOT use filepath, use path for the same reason.
// Things you learn the hard way when you run on Windows.
els := strings.Split(path.Dir(f.Name), "/")
for i := range els {
d := path.Join(els[:i+1]...)
recs = append(recs, Directory(d, 0777))
}
recs = append(recs, f)
if err := WriteRecords(rw, recs); err != nil {
return fmt.Errorf("WriteRecords: writing %q got %w", f.Info.Name, err)
}
}
return nil
}
// Passthrough copies from a RecordReader to a RecordWriter.
//
// Passthrough writes a trailer record.
//
// It processes one record at a time to minimize the memory footprint.
func Passthrough(r RecordReader, w RecordWriter) error {
if err := Concat(w, r, nil); err != nil {
return err
}
if err := WriteTrailer(w); err != nil {
return err
}
return nil
}
// WriteTrailer writes the trailer record.
func WriteTrailer(w RecordWriter) error {
return w.WriteRecord(TrailerRecord)
}
// Concat reads files from r one at a time, and writes them to w.
//
// Concat does not write a trailer record and applies transform to every record
// before writing it. transform may be nil.
func Concat(w RecordWriter, r RecordReader, transform func(Record) Record) error {
return ForEachRecord(r, func(f Record) error {
if transform != nil {
f = transform(f)
}
return w.WriteRecord(f)
})
}
// ReadAllRecords returns all records in r in the order in which they were
// read.
func ReadAllRecords(rr RecordReader) ([]Record, error) {
var files []Record
err := ForEachRecord(rr, func(r Record) error {
files = append(files, r)
return nil
})
return files, err
}
// ForEachRecord reads every record from r and applies f.
func ForEachRecord(rr RecordReader, fun func(Record) error) error {
for {
rec, err := rr.ReadRecord()
switch err {
case io.EOF:
return nil
case nil:
if err := fun(rec); err != nil {
return err
}
default:
return err
}
}
}
// Normalize normalizes namepath to be relative to /.
func Normalize(name string) string {
// do not use filepath.IsAbs, it will not work on Windows.
// do not use filepath.Rel, that will not work
// sensibly on windows.
// The only thing one can do is strip all leading
// /
name = strings.TrimLeft(name, "/")
// do not use filepath.Clean here.
// This will result in paths with \\ on windows, and
// / is the cpio standard.
return path.Clean(name)
}
// MakeReproducible changes any fields in a Record such that if we run cpio
// again, with the same files presented to it in the same order, and those
// files have unchanged contents, the cpio file it produces will be bit-for-bit
// identical. This is an essential property for firmware-embedded payloads.
func MakeReproducible(r Record) Record {
// Do NOT zero Ino. The Ino is created in a reproducible manner
// and a non-zero value is critical for creating hard links when
// reading the archive.
// r.Ino = 0
r.Name = Normalize(r.Name)
r.MTime = 0
r.UID = 0
r.GID = 0
r.Dev = 0
r.Major = 0
r.Minor = 0
// Consider that a file may have 10 links,
// but we are only including 1: NLink will be incorrect. In the
// general case, it is almost impossible to set NLink correctly.
if r.NLink > 1 {
r.NLink = math.MaxUint64
}
return r
}
// MakeAllReproducible makes all given records reproducible as in
// MakeReproducible.
func MakeAllReproducible(files []Record) {
for i := range files {
files[i] = MakeReproducible(files[i])
}
}
// AllEqual compares all metadata and contents of r and s.
func AllEqual(r []Record, s []Record) bool {
if len(r) != len(s) {
return false
}
for i := range r {
if !Equal(r[i], s[i]) {
return false
}
}
return true
}
// Equal compares the metadata and contents of r and s.
func Equal(r Record, s Record) bool {
if r.Info != s.Info {
return false
}
return uio.ReaderAtEqual(r.ReaderAt, s.ReaderAt)
}