github.com/meulengracht/snapd@v0.0.0-20210719210640-8bde69bcc84e/bootloader/ubootenv/env.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2017 Canonical Ltd
     5   *
     6   * This program is free software: you can redistribute it and/or modify
     7   * it under the terms of the GNU General Public License version 3 as
     8   * published by the Free Software Foundation.
     9   *
    10   * This program is distributed in the hope that it will be useful,
    11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13   * GNU General Public License for more details.
    14   *
    15   * You should have received a copy of the GNU General Public License
    16   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17   *
    18   */
    19  
    20  package ubootenv
    21  
    22  import (
    23  	"bufio"
    24  	"bytes"
    25  	"encoding/binary"
    26  	"fmt"
    27  	"hash/crc32"
    28  	"io"
    29  	"io/ioutil"
    30  	"os"
    31  	"path/filepath"
    32  	"sort"
    33  	"strings"
    34  )
    35  
    36  // FIXME: add config option for that so that the user can select if
    37  //        he/she wants env with or without flags
    38  var headerSize = 5
    39  
    40  // Env contains the data of the uboot environment
    41  type Env struct {
    42  	fname string
    43  	size  int
    44  	data  map[string]string
    45  }
    46  
    47  // little endian helpers
    48  func readUint32(data []byte) uint32 {
    49  	var ret uint32
    50  	buf := bytes.NewBuffer(data)
    51  	binary.Read(buf, binary.LittleEndian, &ret)
    52  	return ret
    53  }
    54  
    55  func writeUint32(u uint32) []byte {
    56  	buf := bytes.NewBuffer(nil)
    57  	binary.Write(buf, binary.LittleEndian, &u)
    58  	return buf.Bytes()
    59  }
    60  
    61  // Create a new empty uboot env file with the given size
    62  func Create(fname string, size int) (*Env, error) {
    63  	f, err := os.Create(fname)
    64  	if err != nil {
    65  		return nil, err
    66  	}
    67  	defer f.Close()
    68  
    69  	env := &Env{
    70  		fname: fname,
    71  		size:  size,
    72  		data:  make(map[string]string),
    73  	}
    74  
    75  	return env, nil
    76  }
    77  
    78  // OpenFlags instructs open how to alter its behavior.
    79  type OpenFlags int
    80  
    81  const (
    82  	// OpenBestEffort instructs OpenWithFlags to skip malformed data without returning an error.
    83  	OpenBestEffort OpenFlags = 1 << iota
    84  )
    85  
    86  // Open opens a existing uboot env file
    87  func Open(fname string) (*Env, error) {
    88  	return OpenWithFlags(fname, OpenFlags(0))
    89  }
    90  
    91  // OpenWithFlags opens a existing uboot env file, passing additional flags.
    92  func OpenWithFlags(fname string, flags OpenFlags) (*Env, error) {
    93  	f, err := os.Open(fname)
    94  	if err != nil {
    95  		return nil, err
    96  	}
    97  	defer f.Close()
    98  
    99  	contentWithHeader, err := ioutil.ReadAll(f)
   100  	if err != nil {
   101  		return nil, err
   102  	}
   103  
   104  	if len(contentWithHeader) < headerSize {
   105  		return nil, fmt.Errorf("cannot open %q: smaller than expected header", fname)
   106  	}
   107  
   108  	crc := readUint32(contentWithHeader)
   109  
   110  	payload := contentWithHeader[headerSize:]
   111  	actualCRC := crc32.ChecksumIEEE(payload)
   112  	if crc != actualCRC {
   113  		return nil, fmt.Errorf("cannot open %q: bad CRC %v != %v", fname, crc, actualCRC)
   114  	}
   115  
   116  	if eof := bytes.Index(payload, []byte{0, 0}); eof >= 0 {
   117  		payload = payload[:eof]
   118  	}
   119  
   120  	data, err := parseData(payload, flags)
   121  	if err != nil {
   122  		return nil, err
   123  	}
   124  
   125  	env := &Env{
   126  		fname: fname,
   127  		size:  len(contentWithHeader),
   128  		data:  data,
   129  	}
   130  
   131  	return env, nil
   132  }
   133  
   134  func parseData(data []byte, flags OpenFlags) (map[string]string, error) {
   135  	out := make(map[string]string)
   136  
   137  	for _, envStr := range bytes.Split(data, []byte{0}) {
   138  		if len(envStr) == 0 || envStr[0] == 0 || envStr[0] == 255 {
   139  			continue
   140  		}
   141  		l := strings.SplitN(string(envStr), "=", 2)
   142  		if len(l) != 2 || l[0] == "" {
   143  			if flags&OpenBestEffort == OpenBestEffort {
   144  				continue
   145  			}
   146  			return nil, fmt.Errorf("cannot parse line %q as key=value pair", envStr)
   147  		}
   148  		key := l[0]
   149  		value := l[1]
   150  		out[key] = value
   151  	}
   152  
   153  	return out, nil
   154  }
   155  
   156  func (env *Env) String() string {
   157  	out := ""
   158  
   159  	env.iterEnv(func(key, value string) {
   160  		out += fmt.Sprintf("%s=%s\n", key, value)
   161  	})
   162  
   163  	return out
   164  }
   165  
   166  func (env *Env) Size() int {
   167  	return env.size
   168  }
   169  
   170  // Get the value of the environment variable
   171  func (env *Env) Get(name string) string {
   172  	return env.data[name]
   173  }
   174  
   175  // Set an environment name to the given value, if the value is empty
   176  // the variable will be removed from the environment
   177  func (env *Env) Set(name, value string) {
   178  	if name == "" {
   179  		panic(fmt.Sprintf("Set() can not be called with empty key for value: %q", value))
   180  	}
   181  	if value == "" {
   182  		delete(env.data, name)
   183  		return
   184  	}
   185  	env.data[name] = value
   186  }
   187  
   188  // iterEnv calls the passed function f with key, value for environment
   189  // vars. The order is guaranteed (unlike just iterating over the map)
   190  func (env *Env) iterEnv(f func(key, value string)) {
   191  	keys := make([]string, 0, len(env.data))
   192  	for k := range env.data {
   193  		keys = append(keys, k)
   194  	}
   195  	sort.Strings(keys)
   196  
   197  	for _, k := range keys {
   198  		if k == "" {
   199  			panic("iterEnv iterating over a empty key")
   200  		}
   201  
   202  		f(k, env.data[k])
   203  	}
   204  }
   205  
   206  // Save will write out the environment data
   207  func (env *Env) Save() error {
   208  	w := bytes.NewBuffer(nil)
   209  	// will panic if the buffer can't grow, all writes to
   210  	// the buffer will be ok because we sized it correctly
   211  	w.Grow(env.size - headerSize)
   212  
   213  	// write the payload
   214  	env.iterEnv(func(key, value string) {
   215  		w.Write([]byte(fmt.Sprintf("%s=%s", key, value)))
   216  		w.Write([]byte{0})
   217  	})
   218  
   219  	// write double \0 to mark the end of the env
   220  	w.Write([]byte{0})
   221  
   222  	// no keys, so no previous \0 was written so we write one here
   223  	if len(env.data) == 0 {
   224  		w.Write([]byte{0})
   225  	}
   226  
   227  	// write ff into the remaining parts
   228  	writtenSoFar := w.Len()
   229  	for i := 0; i < env.size-headerSize-writtenSoFar; i++ {
   230  		w.Write([]byte{0xff})
   231  	}
   232  
   233  	// checksum
   234  	crc := crc32.ChecksumIEEE(w.Bytes())
   235  
   236  	// ensure dir sync
   237  	dir, err := os.Open(filepath.Dir(env.fname))
   238  	if err != nil {
   239  		return err
   240  	}
   241  	defer dir.Close()
   242  
   243  	// Note that we overwrite the existing file and do not do
   244  	// the usual write-rename. The rationale is that we want to
   245  	// minimize the amount of writes happening on a potential
   246  	// FAT partition where the env is loaded from. The file will
   247  	// always be of a fixed size so we know the writes will not
   248  	// fail because of ENOSPC.
   249  	//
   250  	// The size of the env file never changes so we do not
   251  	// truncate it.
   252  	//
   253  	// We also do not O_TRUNC to avoid reallocations on the FS
   254  	// to minimize risk of fs corruption.
   255  	f, err := os.OpenFile(env.fname, os.O_WRONLY, 0666)
   256  	if err != nil {
   257  		return err
   258  	}
   259  	defer f.Close()
   260  
   261  	if _, err := f.Write(writeUint32(crc)); err != nil {
   262  		return err
   263  	}
   264  	// padding bytes (e.g. for redundant header)
   265  	pad := make([]byte, headerSize-binary.Size(crc))
   266  	if _, err := f.Write(pad); err != nil {
   267  		return err
   268  	}
   269  	if _, err := f.Write(w.Bytes()); err != nil {
   270  		return err
   271  	}
   272  
   273  	if err := f.Sync(); err != nil {
   274  		return err
   275  	}
   276  
   277  	return dir.Sync()
   278  }
   279  
   280  // Import is a helper that imports a given text file that contains
   281  // "key=value" paris into the uboot env. Lines starting with ^# are
   282  // ignored (like the input file on mkenvimage)
   283  func (env *Env) Import(r io.Reader) error {
   284  	scanner := bufio.NewScanner(r)
   285  	for scanner.Scan() {
   286  		line := scanner.Text()
   287  		if strings.HasPrefix(line, "#") || len(line) == 0 {
   288  			continue
   289  		}
   290  		l := strings.SplitN(line, "=", 2)
   291  		if len(l) == 1 {
   292  			return fmt.Errorf("Invalid line: %q", line)
   293  		}
   294  		env.data[l[0]] = l[1]
   295  
   296  	}
   297  
   298  	return scanner.Err()
   299  }