github.com/stolowski/snapd@v0.0.0-20210407085831-115137ce5a22/boot/modeenv_test.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2019-2020 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 boot_test
    21  
    22  import (
    23  	"bytes"
    24  	"encoding/json"
    25  	"fmt"
    26  	"io/ioutil"
    27  	"os"
    28  	"path/filepath"
    29  	"strings"
    30  
    31  	"github.com/mvo5/goconfigparser"
    32  	. "gopkg.in/check.v1"
    33  
    34  	"github.com/snapcore/snapd/boot"
    35  	"github.com/snapcore/snapd/dirs"
    36  	"github.com/snapcore/snapd/testutil"
    37  )
    38  
    39  // baseBootSuite is used to setup the common test environment
    40  type modeenvSuite struct {
    41  	testutil.BaseTest
    42  
    43  	tmpdir          string
    44  	mockModeenvPath string
    45  }
    46  
    47  var _ = Suite(&modeenvSuite{})
    48  
    49  func (s *modeenvSuite) SetUpTest(c *C) {
    50  	s.tmpdir = c.MkDir()
    51  	s.mockModeenvPath = filepath.Join(s.tmpdir, dirs.SnapModeenvFile)
    52  }
    53  
    54  func (s *modeenvSuite) TestKnownKnown(c *C) {
    55  	// double check keys as found with reflect
    56  	c.Check(boot.ModeenvKnownKeys, DeepEquals, map[string]bool{
    57  		"mode":                     true,
    58  		"recovery_system":          true,
    59  		"current_recovery_systems": true,
    60  		"good_recovery_systems":    true,
    61  		// keep this comment to make old go fmt happy
    62  		"base":            true,
    63  		"try_base":        true,
    64  		"base_status":     true,
    65  		"current_kernels": true,
    66  		"model":           true,
    67  		"grade":           true,
    68  		// keep this comment to make old go fmt happy
    69  		"current_kernel_command_lines":         true,
    70  		"current_trusted_boot_assets":          true,
    71  		"current_trusted_recovery_boot_assets": true,
    72  	})
    73  }
    74  
    75  func (s *modeenvSuite) TestReadEmptyErrors(c *C) {
    76  	modeenv, err := boot.ReadModeenv("/no/such/file")
    77  	c.Assert(os.IsNotExist(err), Equals, true)
    78  	c.Assert(modeenv, IsNil)
    79  }
    80  
    81  func (s *modeenvSuite) makeMockModeenvFile(c *C, content string) {
    82  	err := os.MkdirAll(filepath.Dir(s.mockModeenvPath), 0755)
    83  	c.Assert(err, IsNil)
    84  	err = ioutil.WriteFile(s.mockModeenvPath, []byte(content), 0644)
    85  	c.Assert(err, IsNil)
    86  }
    87  
    88  func (s *modeenvSuite) TestWasReadSanity(c *C) {
    89  	modeenv := &boot.Modeenv{}
    90  	c.Check(modeenv.WasRead(), Equals, false)
    91  }
    92  
    93  func (s *modeenvSuite) TestReadEmpty(c *C) {
    94  	s.makeMockModeenvFile(c, "")
    95  
    96  	modeenv, err := boot.ReadModeenv(s.tmpdir)
    97  	c.Assert(err, ErrorMatches, "internal error: mode is unset")
    98  	c.Assert(modeenv, IsNil)
    99  }
   100  
   101  func (s *modeenvSuite) TestReadMode(c *C) {
   102  	s.makeMockModeenvFile(c, "mode=run")
   103  
   104  	modeenv, err := boot.ReadModeenv(s.tmpdir)
   105  	c.Assert(err, IsNil)
   106  	c.Check(modeenv.Mode, Equals, "run")
   107  	c.Check(modeenv.RecoverySystem, Equals, "")
   108  	c.Check(modeenv.Base, Equals, "")
   109  }
   110  
   111  func (s *modeenvSuite) TestDeepEqualDiskVsMemoryInvariant(c *C) {
   112  	s.makeMockModeenvFile(c, `mode=recovery
   113  recovery_system=20191126
   114  base=core20_123.snap
   115  try_base=core20_124.snap
   116  base_status=try
   117  `)
   118  
   119  	diskModeenv, err := boot.ReadModeenv(s.tmpdir)
   120  	c.Assert(err, IsNil)
   121  	inMemoryModeenv := &boot.Modeenv{
   122  		Mode:           "recovery",
   123  		RecoverySystem: "20191126",
   124  		Base:           "core20_123.snap",
   125  		TryBase:        "core20_124.snap",
   126  		BaseStatus:     "try",
   127  	}
   128  	c.Assert(inMemoryModeenv.DeepEqual(diskModeenv), Equals, true)
   129  	c.Assert(diskModeenv.DeepEqual(inMemoryModeenv), Equals, true)
   130  }
   131  
   132  func (s *modeenvSuite) TestCopyDeepEquals(c *C) {
   133  	s.makeMockModeenvFile(c, `mode=recovery
   134  recovery_system=20191126
   135  base=core20_123.snap
   136  try_base=core20_124.snap
   137  base_status=try
   138  current_trusted_boot_assets={"thing1":["hash1","hash2"],"thing2":["hash3"]}
   139  current_kernel_command_lines=["foo", "bar"]
   140  `)
   141  
   142  	diskModeenv, err := boot.ReadModeenv(s.tmpdir)
   143  	c.Assert(err, IsNil)
   144  	inMemoryModeenv := &boot.Modeenv{
   145  		Mode:           "recovery",
   146  		RecoverySystem: "20191126",
   147  		Base:           "core20_123.snap",
   148  		TryBase:        "core20_124.snap",
   149  		BaseStatus:     "try",
   150  		CurrentTrustedBootAssets: boot.BootAssetsMap{
   151  			"thing1": {"hash1", "hash2"},
   152  			"thing2": {"hash3"},
   153  		},
   154  		CurrentKernelCommandLines: boot.BootCommandLines{
   155  			"foo", "bar",
   156  		},
   157  	}
   158  
   159  	c.Assert(inMemoryModeenv.DeepEqual(diskModeenv), Equals, true)
   160  	c.Assert(diskModeenv.DeepEqual(inMemoryModeenv), Equals, true)
   161  
   162  	diskModeenv2, err := diskModeenv.Copy()
   163  	c.Assert(err, IsNil)
   164  	c.Assert(diskModeenv.DeepEqual(diskModeenv2), Equals, true)
   165  	c.Assert(diskModeenv2.DeepEqual(diskModeenv), Equals, true)
   166  	c.Assert(inMemoryModeenv.DeepEqual(diskModeenv2), Equals, true)
   167  	c.Assert(diskModeenv2.DeepEqual(inMemoryModeenv), Equals, true)
   168  
   169  	inMemoryModeenv2, err := inMemoryModeenv.Copy()
   170  	c.Assert(err, IsNil)
   171  	c.Assert(inMemoryModeenv.DeepEqual(inMemoryModeenv2), Equals, true)
   172  	c.Assert(inMemoryModeenv2.DeepEqual(inMemoryModeenv), Equals, true)
   173  	c.Assert(inMemoryModeenv2.DeepEqual(diskModeenv), Equals, true)
   174  	c.Assert(diskModeenv.DeepEqual(inMemoryModeenv2), Equals, true)
   175  }
   176  
   177  func (s *modeenvSuite) TestCopyDiskWriteWorks(c *C) {
   178  	s.makeMockModeenvFile(c, `mode=recovery
   179  recovery_system=20191126
   180  base=core20_123.snap
   181  try_base=core20_124.snap
   182  base_status=try
   183  `)
   184  
   185  	diskModeenv, err := boot.ReadModeenv(s.tmpdir)
   186  	c.Assert(err, IsNil)
   187  	dupDiskModeenv, err := diskModeenv.Copy()
   188  	c.Assert(err, IsNil)
   189  
   190  	// move the original file out of the way
   191  	err = os.Rename(dirs.SnapModeenvFileUnder(s.tmpdir), dirs.SnapModeenvFileUnder(s.tmpdir)+".orig")
   192  	c.Assert(err, IsNil)
   193  	c.Assert(dirs.SnapModeenvFileUnder(s.tmpdir), testutil.FileAbsent)
   194  
   195  	// write the duplicate, it should write to the same original location and it
   196  	// should be the same content
   197  	err = dupDiskModeenv.Write()
   198  	c.Assert(err, IsNil)
   199  	c.Assert(dirs.SnapModeenvFileUnder(s.tmpdir), testutil.FilePresent)
   200  	origBytes, err := ioutil.ReadFile(dirs.SnapModeenvFileUnder(s.tmpdir) + ".orig")
   201  	c.Assert(err, IsNil)
   202  	// the files should be the same
   203  	c.Assert(dirs.SnapModeenvFileUnder(s.tmpdir), testutil.FileEquals, string(origBytes))
   204  }
   205  
   206  func (s *modeenvSuite) TestCopyMemoryWriteFails(c *C) {
   207  	inMemoryModeenv := &boot.Modeenv{
   208  		Mode:           "recovery",
   209  		RecoverySystem: "20191126",
   210  		Base:           "core20_123.snap",
   211  		TryBase:        "core20_124.snap",
   212  		BaseStatus:     "try",
   213  	}
   214  	dupInMemoryModeenv, err := inMemoryModeenv.Copy()
   215  	c.Assert(err, IsNil)
   216  
   217  	// write the duplicate, it should fail
   218  	err = dupInMemoryModeenv.Write()
   219  	c.Assert(err, ErrorMatches, "internal error: must use WriteTo with modeenv not read from disk")
   220  }
   221  
   222  func (s *modeenvSuite) TestDeepEquals(c *C) {
   223  	// start with two identical modeenvs
   224  	modeenv1 := &boot.Modeenv{
   225  		Mode:                   "recovery",
   226  		RecoverySystem:         "20191126",
   227  		CurrentRecoverySystems: []string{"1", "2"},
   228  		GoodRecoverySystems:    []string{"3"},
   229  
   230  		Base:           "core20_123.snap",
   231  		TryBase:        "core20_124.snap",
   232  		BaseStatus:     "try",
   233  		CurrentKernels: []string{"k1", "k2"},
   234  
   235  		Model:   "model",
   236  		BrandID: "brand",
   237  		Grade:   "secured",
   238  
   239  		CurrentTrustedBootAssets: boot.BootAssetsMap{
   240  			"thing1": {"hash1", "hash2"},
   241  			"thing2": {"hash3"},
   242  		},
   243  
   244  		CurrentKernelCommandLines: boot.BootCommandLines{
   245  			"foo",
   246  			"foo bar",
   247  		},
   248  	}
   249  
   250  	modeenv2 := &boot.Modeenv{
   251  		Mode:                   "recovery",
   252  		RecoverySystem:         "20191126",
   253  		CurrentRecoverySystems: []string{"1", "2"},
   254  		GoodRecoverySystems:    []string{"3"},
   255  
   256  		Base:           "core20_123.snap",
   257  		TryBase:        "core20_124.snap",
   258  		BaseStatus:     "try",
   259  		CurrentKernels: []string{"k1", "k2"},
   260  
   261  		Model:   "model",
   262  		BrandID: "brand",
   263  		Grade:   "secured",
   264  
   265  		CurrentTrustedBootAssets: boot.BootAssetsMap{
   266  			"thing1": {"hash1", "hash2"},
   267  			"thing2": {"hash3"},
   268  		},
   269  
   270  		CurrentKernelCommandLines: boot.BootCommandLines{
   271  			"foo",
   272  			"foo bar",
   273  		},
   274  	}
   275  
   276  	// same object should be the same
   277  	c.Assert(modeenv1.DeepEqual(modeenv1), Equals, true)
   278  
   279  	// no difference should be the same at the start
   280  	c.Assert(modeenv1.DeepEqual(modeenv2), Equals, true)
   281  	c.Assert(modeenv2.DeepEqual(modeenv1), Equals, true)
   282  
   283  	// invert CurrentKernels
   284  	modeenv2.CurrentKernels = []string{"k2", "k1"}
   285  	c.Assert(modeenv1.DeepEqual(modeenv2), Equals, false)
   286  	c.Assert(modeenv2.DeepEqual(modeenv1), Equals, false)
   287  
   288  	// make CurrentKernels capitalized
   289  	modeenv2.CurrentKernels = []string{"K1", "k2"}
   290  	c.Assert(modeenv1.DeepEqual(modeenv2), Equals, false)
   291  	c.Assert(modeenv2.DeepEqual(modeenv1), Equals, false)
   292  
   293  	// make CurrentKernels disappear
   294  	modeenv2.CurrentKernels = nil
   295  	c.Assert(modeenv1.DeepEqual(modeenv2), Equals, false)
   296  	c.Assert(modeenv2.DeepEqual(modeenv1), Equals, false)
   297  
   298  	// make it identical again
   299  	modeenv2.CurrentKernels = []string{"k1", "k2"}
   300  	c.Assert(modeenv1.DeepEqual(modeenv2), Equals, true)
   301  	// change kernel command lines
   302  	modeenv2.CurrentKernelCommandLines = boot.BootCommandLines{
   303  		// reversed order
   304  		"foo bar",
   305  		"foo",
   306  	}
   307  	c.Assert(modeenv1.DeepEqual(modeenv2), Equals, false)
   308  	// clear kernel command lines list
   309  	modeenv2.CurrentKernelCommandLines = nil
   310  	c.Assert(modeenv1.DeepEqual(modeenv2), Equals, false)
   311  
   312  	// make it identical again
   313  	modeenv2.CurrentKernelCommandLines = boot.BootCommandLines{
   314  		"foo",
   315  		"foo bar",
   316  	}
   317  	c.Assert(modeenv1.DeepEqual(modeenv2), Equals, true)
   318  
   319  	// change the list of current recovery systems
   320  	modeenv2.CurrentRecoverySystems = append(modeenv2.CurrentRecoverySystems, "1234")
   321  	c.Assert(modeenv1.DeepEqual(modeenv2), Equals, false)
   322  	// make it identical again
   323  	modeenv2.CurrentRecoverySystems = []string{"1", "2"}
   324  	c.Assert(modeenv1.DeepEqual(modeenv2), Equals, true)
   325  
   326  	// change the list of good recovery systems
   327  	modeenv2.GoodRecoverySystems = append(modeenv2.GoodRecoverySystems, "999")
   328  	c.Assert(modeenv1.DeepEqual(modeenv2), Equals, false)
   329  }
   330  
   331  func (s *modeenvSuite) TestReadModeWithRecoverySystem(c *C) {
   332  	s.makeMockModeenvFile(c, `mode=recovery
   333  recovery_system=20191126
   334  `)
   335  
   336  	modeenv, err := boot.ReadModeenv(s.tmpdir)
   337  	c.Assert(err, IsNil)
   338  	c.Check(modeenv.Mode, Equals, "recovery")
   339  	c.Check(modeenv.RecoverySystem, Equals, "20191126")
   340  }
   341  
   342  func (s *modeenvSuite) TestReadModeenvWithUnknownKeysKeepsWrites(c *C) {
   343  	s.makeMockModeenvFile(c, `first_unknown=thing
   344  mode=recovery
   345  recovery_system=20191126
   346  unknown_key=some unknown value
   347  a_key=other
   348  `)
   349  
   350  	modeenv, err := boot.ReadModeenv(s.tmpdir)
   351  	c.Assert(err, IsNil)
   352  	c.Check(modeenv.Mode, Equals, "recovery")
   353  	c.Check(modeenv.RecoverySystem, Equals, "20191126")
   354  
   355  	c.Assert(modeenv.Write(), IsNil)
   356  
   357  	c.Assert(s.mockModeenvPath, testutil.FileEquals, `mode=recovery
   358  recovery_system=20191126
   359  a_key=other
   360  first_unknown=thing
   361  unknown_key=some unknown value
   362  `)
   363  }
   364  
   365  func (s *modeenvSuite) TestReadModeenvWithUnknownKeysDeepEqualsSameWithoutUnknownKeys(c *C) {
   366  	s.makeMockModeenvFile(c, `first_unknown=thing
   367  mode=recovery
   368  recovery_system=20191126
   369  try_base=core20_124.snap
   370  base_status=try
   371  unknown_key=some unknown value
   372  current_trusted_boot_assets={"grubx64.efi":["hash1","hash2"]}
   373  current_trusted_recovery_boot_assets={"bootx64.efi":["shimhash1","shimhash2"],"grubx64.efi":["recovery-hash1"]}
   374  a_key=other
   375  `)
   376  
   377  	modeenvWithExtraKeys, err := boot.ReadModeenv(s.tmpdir)
   378  	c.Assert(err, IsNil)
   379  	c.Check(modeenvWithExtraKeys.Mode, Equals, "recovery")
   380  	c.Check(modeenvWithExtraKeys.RecoverySystem, Equals, "20191126")
   381  
   382  	// should be the same as one that with just those keys in memory
   383  	c.Assert(modeenvWithExtraKeys.DeepEqual(&boot.Modeenv{
   384  		Mode:           "recovery",
   385  		RecoverySystem: "20191126",
   386  		TryBase:        "core20_124.snap",
   387  		BaseStatus:     boot.TryStatus,
   388  		CurrentTrustedBootAssets: boot.BootAssetsMap{
   389  			"grubx64.efi": []string{"hash1", "hash2"},
   390  		},
   391  		CurrentTrustedRecoveryBootAssets: boot.BootAssetsMap{
   392  			"bootx64.efi": []string{"shimhash1", "shimhash2"},
   393  			"grubx64.efi": []string{"recovery-hash1"},
   394  		},
   395  	}), Equals, true)
   396  }
   397  
   398  func (s *modeenvSuite) TestReadModeWithBase(c *C) {
   399  	s.makeMockModeenvFile(c, `mode=recovery
   400  recovery_system=20191126
   401  base=core20_123.snap
   402  try_base=core20_124.snap
   403  base_status=try
   404  `)
   405  
   406  	modeenv, err := boot.ReadModeenv(s.tmpdir)
   407  	c.Assert(err, IsNil)
   408  	c.Check(modeenv.Mode, Equals, "recovery")
   409  	c.Check(modeenv.RecoverySystem, Equals, "20191126")
   410  	c.Check(modeenv.Base, Equals, "core20_123.snap")
   411  	c.Check(modeenv.TryBase, Equals, "core20_124.snap")
   412  	c.Check(modeenv.BaseStatus, Equals, boot.TryStatus)
   413  }
   414  
   415  func (s *modeenvSuite) TestReadModeWithGrade(c *C) {
   416  	s.makeMockModeenvFile(c, `mode=run
   417  grade=dangerous
   418  `)
   419  	modeenv, err := boot.ReadModeenv(s.tmpdir)
   420  	c.Assert(err, IsNil)
   421  	c.Check(modeenv.Mode, Equals, "run")
   422  	c.Check(modeenv.Grade, Equals, "dangerous")
   423  
   424  	s.makeMockModeenvFile(c, `mode=run
   425  grade=some-random-grade-string
   426  `)
   427  	modeenv, err = boot.ReadModeenv(s.tmpdir)
   428  	c.Assert(err, IsNil)
   429  	c.Check(modeenv.Mode, Equals, "run")
   430  	c.Check(modeenv.Grade, Equals, "some-random-grade-string")
   431  }
   432  
   433  func (s *modeenvSuite) TestReadModeWithModel(c *C) {
   434  	tt := []struct {
   435  		entry        string
   436  		model, brand string
   437  	}{
   438  		{
   439  			entry: "my-brand/my-model",
   440  			brand: "my-brand",
   441  			model: "my-model",
   442  		}, {
   443  			entry: "my-brand/",
   444  		}, {
   445  			entry: "my-model/",
   446  		}, {
   447  			entry: "foobar",
   448  		}, {
   449  			entry: "/",
   450  		}, {
   451  			entry: ",",
   452  		}, {
   453  			entry: "",
   454  		},
   455  	}
   456  
   457  	for _, t := range tt {
   458  		s.makeMockModeenvFile(c, `mode=run
   459  model=`+t.entry+"\n")
   460  		modeenv, err := boot.ReadModeenv(s.tmpdir)
   461  		c.Assert(err, IsNil)
   462  		c.Check(modeenv.Mode, Equals, "run")
   463  		c.Check(modeenv.Model, Equals, t.model)
   464  		c.Check(modeenv.BrandID, Equals, t.brand)
   465  	}
   466  }
   467  
   468  func (s *modeenvSuite) TestReadModeWithCurrentKernels(c *C) {
   469  
   470  	tt := []struct {
   471  		kernelString    string
   472  		expectedKernels []string
   473  	}{
   474  		{
   475  			"pc-kernel_1.snap",
   476  			[]string{"pc-kernel_1.snap"},
   477  		},
   478  		{
   479  			"pc-kernel_1.snap,pc-kernel_2.snap",
   480  			[]string{"pc-kernel_1.snap", "pc-kernel_2.snap"},
   481  		},
   482  		{
   483  			"pc-kernel_1.snap,,,,,pc-kernel_2.snap",
   484  			[]string{"pc-kernel_1.snap", "pc-kernel_2.snap"},
   485  		},
   486  		// we should be robust in parsing the modeenv against garbage
   487  		{
   488  			`pc-kernel_1.snap,this-is-not-a-real-snap$%^&^%$#@#$%^%"$,pc-kernel_2.snap`,
   489  			[]string{"pc-kernel_1.snap", `this-is-not-a-real-snap$%^&^%$#@#$%^%"$`, "pc-kernel_2.snap"},
   490  		},
   491  		{",,,", nil},
   492  		{"", nil},
   493  	}
   494  
   495  	for _, t := range tt {
   496  		s.makeMockModeenvFile(c, `mode=recovery
   497  recovery_system=20191126
   498  current_kernels=`+t.kernelString+"\n")
   499  
   500  		modeenv, err := boot.ReadModeenv(s.tmpdir)
   501  		c.Assert(err, IsNil)
   502  		c.Check(modeenv.Mode, Equals, "recovery")
   503  		c.Check(modeenv.RecoverySystem, Equals, "20191126")
   504  		c.Check(len(modeenv.CurrentKernels), Equals, len(t.expectedKernels))
   505  		if len(t.expectedKernels) != 0 {
   506  			c.Check(modeenv.CurrentKernels, DeepEquals, t.expectedKernels)
   507  		}
   508  	}
   509  }
   510  
   511  func (s *modeenvSuite) TestWriteToNonExisting(c *C) {
   512  	c.Assert(s.mockModeenvPath, testutil.FileAbsent)
   513  
   514  	modeenv := &boot.Modeenv{Mode: "run"}
   515  	err := modeenv.WriteTo(s.tmpdir)
   516  	c.Assert(err, IsNil)
   517  
   518  	c.Assert(s.mockModeenvPath, testutil.FileEquals, "mode=run\n")
   519  }
   520  
   521  func (s *modeenvSuite) TestWriteToExisting(c *C) {
   522  	s.makeMockModeenvFile(c, "mode=run")
   523  
   524  	modeenv, err := boot.ReadModeenv(s.tmpdir)
   525  	c.Assert(err, IsNil)
   526  	modeenv.Mode = "recovery"
   527  	err = modeenv.WriteTo(s.tmpdir)
   528  	c.Assert(err, IsNil)
   529  
   530  	c.Assert(s.mockModeenvPath, testutil.FileEquals, "mode=recovery\n")
   531  }
   532  
   533  func (s *modeenvSuite) TestWriteExisting(c *C) {
   534  	s.makeMockModeenvFile(c, "mode=run")
   535  
   536  	modeenv, err := boot.ReadModeenv(s.tmpdir)
   537  	c.Assert(err, IsNil)
   538  	modeenv.Mode = "recovery"
   539  	err = modeenv.Write()
   540  	c.Assert(err, IsNil)
   541  
   542  	c.Assert(s.mockModeenvPath, testutil.FileEquals, "mode=recovery\n")
   543  }
   544  
   545  func (s *modeenvSuite) TestWriteFreshError(c *C) {
   546  	modeenv := &boot.Modeenv{Mode: "recovery"}
   547  
   548  	err := modeenv.Write()
   549  	c.Assert(err, ErrorMatches, `internal error: must use WriteTo with modeenv not read from disk`)
   550  }
   551  
   552  func (s *modeenvSuite) TestWriteToNonExistingFull(c *C) {
   553  	c.Assert(s.mockModeenvPath, testutil.FileAbsent)
   554  
   555  	modeenv := &boot.Modeenv{
   556  		Mode:                   "run",
   557  		RecoverySystem:         "20191128",
   558  		CurrentRecoverySystems: []string{"20191128", "2020-02-03", "20240101-FOO"},
   559  		// keep this comment to make gofmt 1.9 happy
   560  		Base:           "core20_321.snap",
   561  		TryBase:        "core20_322.snap",
   562  		BaseStatus:     boot.TryStatus,
   563  		CurrentKernels: []string{"pc-kernel_1.snap", "pc-kernel_2.snap"},
   564  	}
   565  	err := modeenv.WriteTo(s.tmpdir)
   566  	c.Assert(err, IsNil)
   567  
   568  	c.Assert(s.mockModeenvPath, testutil.FileEquals, `mode=run
   569  recovery_system=20191128
   570  current_recovery_systems=20191128,2020-02-03,20240101-FOO
   571  base=core20_321.snap
   572  try_base=core20_322.snap
   573  base_status=try
   574  current_kernels=pc-kernel_1.snap,pc-kernel_2.snap
   575  `)
   576  }
   577  
   578  func (s *modeenvSuite) TestReadRecoverySystems(c *C) {
   579  	tt := []struct {
   580  		systemsString   string
   581  		expectedSystems []string
   582  	}{
   583  		{
   584  			"20191126",
   585  			[]string{"20191126"},
   586  		}, {
   587  			"20191128,2020-02-03,20240101-FOO",
   588  			[]string{"20191128", "2020-02-03", "20240101-FOO"},
   589  		},
   590  		{",,,", nil},
   591  		{"", nil},
   592  	}
   593  
   594  	for _, t := range tt {
   595  		c.Logf("tc: %q", t.systemsString)
   596  		s.makeMockModeenvFile(c, fmt.Sprintf(`mode=recovery
   597  recovery_system=20191126
   598  current_recovery_systems=%[1]s
   599  good_recovery_systems=%[1]s
   600  `, t.systemsString))
   601  
   602  		modeenv, err := boot.ReadModeenv(s.tmpdir)
   603  		c.Assert(err, IsNil)
   604  		c.Check(modeenv.Mode, Equals, "recovery")
   605  		c.Check(modeenv.RecoverySystem, Equals, "20191126")
   606  		c.Check(modeenv.CurrentRecoverySystems, DeepEquals, t.expectedSystems)
   607  		c.Check(modeenv.GoodRecoverySystems, DeepEquals, t.expectedSystems)
   608  	}
   609  }
   610  
   611  type fancyDataBothMarshallers struct {
   612  	Foo []string
   613  }
   614  
   615  func (f *fancyDataBothMarshallers) MarshalModeenvValue() (string, error) {
   616  	return strings.Join(f.Foo, "#"), nil
   617  }
   618  
   619  func (f *fancyDataBothMarshallers) UnmarshalModeenvValue(v string) error {
   620  	f.Foo = strings.Split(v, "#")
   621  	return nil
   622  }
   623  
   624  func (f *fancyDataBothMarshallers) MarshalJSON() ([]byte, error) {
   625  	return nil, fmt.Errorf("unexpected call to JSON marshaller")
   626  }
   627  
   628  func (f *fancyDataBothMarshallers) UnmarshalJSON(data []byte) error {
   629  	return fmt.Errorf("unexpected call to JSON unmarshaller")
   630  }
   631  
   632  type fancyDataJSONOnly struct {
   633  	Foo []string
   634  }
   635  
   636  func (f *fancyDataJSONOnly) MarshalJSON() ([]byte, error) {
   637  	return json.Marshal(f.Foo)
   638  }
   639  
   640  func (f *fancyDataJSONOnly) UnmarshalJSON(data []byte) error {
   641  	return json.Unmarshal(data, &f.Foo)
   642  }
   643  
   644  func (s *modeenvSuite) TestFancyMarshalUnmarshal(c *C) {
   645  	var buf bytes.Buffer
   646  
   647  	dboth := fancyDataBothMarshallers{Foo: []string{"1", "two"}}
   648  	err := boot.MarshalModeenvEntryTo(&buf, "fancy", &dboth)
   649  	c.Assert(err, IsNil)
   650  	c.Check(buf.String(), Equals, `fancy=1#two
   651  `)
   652  
   653  	djson := fancyDataJSONOnly{Foo: []string{"1", "two", "with\nnewline"}}
   654  	err = boot.MarshalModeenvEntryTo(&buf, "fancy_json", &djson)
   655  	c.Assert(err, IsNil)
   656  	c.Check(buf.String(), Equals, `fancy=1#two
   657  fancy_json=["1","two","with\nnewline"]
   658  `)
   659  
   660  	cfg := goconfigparser.New()
   661  	cfg.AllowNoSectionHeader = true
   662  	err = cfg.Read(&buf)
   663  	c.Assert(err, IsNil)
   664  
   665  	var dbothRev fancyDataBothMarshallers
   666  	err = boot.UnmarshalModeenvValueFromCfg(cfg, "fancy", &dbothRev)
   667  	c.Assert(err, IsNil)
   668  	c.Check(dbothRev, DeepEquals, dboth)
   669  
   670  	var djsonRev fancyDataJSONOnly
   671  	err = boot.UnmarshalModeenvValueFromCfg(cfg, "fancy_json", &djsonRev)
   672  	c.Assert(err, IsNil)
   673  	c.Check(djsonRev, DeepEquals, djson)
   674  }
   675  
   676  func (s *modeenvSuite) TestFancyUnmarshalJSONEmpty(c *C) {
   677  	var buf bytes.Buffer
   678  
   679  	cfg := goconfigparser.New()
   680  	cfg.AllowNoSectionHeader = true
   681  	err := cfg.Read(&buf)
   682  	c.Assert(err, IsNil)
   683  
   684  	var djsonRev fancyDataJSONOnly
   685  	err = boot.UnmarshalModeenvValueFromCfg(cfg, "fancy_json", &djsonRev)
   686  	c.Assert(err, IsNil)
   687  	c.Check(djsonRev.Foo, IsNil)
   688  }
   689  
   690  func (s *modeenvSuite) TestMarshalCurrentTrustedBootAssets(c *C) {
   691  	c.Assert(s.mockModeenvPath, testutil.FileAbsent)
   692  
   693  	modeenv := &boot.Modeenv{
   694  		Mode:           "run",
   695  		RecoverySystem: "20191128",
   696  		CurrentTrustedBootAssets: boot.BootAssetsMap{
   697  			"grubx64.efi": []string{"hash1", "hash2"},
   698  		},
   699  		CurrentTrustedRecoveryBootAssets: boot.BootAssetsMap{
   700  			"grubx64.efi": []string{"recovery-hash1"},
   701  			"bootx64.efi": []string{"shimhash1", "shimhash2"},
   702  		},
   703  	}
   704  	err := modeenv.WriteTo(s.tmpdir)
   705  	c.Assert(err, IsNil)
   706  
   707  	c.Assert(s.mockModeenvPath, testutil.FileEquals, `mode=run
   708  recovery_system=20191128
   709  current_trusted_boot_assets={"grubx64.efi":["hash1","hash2"]}
   710  current_trusted_recovery_boot_assets={"bootx64.efi":["shimhash1","shimhash2"],"grubx64.efi":["recovery-hash1"]}
   711  `)
   712  
   713  	modeenvRead, err := boot.ReadModeenv(s.tmpdir)
   714  	c.Assert(err, IsNil)
   715  	c.Assert(modeenvRead.CurrentTrustedBootAssets, DeepEquals, boot.BootAssetsMap{
   716  		"grubx64.efi": []string{"hash1", "hash2"},
   717  	})
   718  	c.Assert(modeenvRead.CurrentTrustedRecoveryBootAssets, DeepEquals, boot.BootAssetsMap{
   719  		"grubx64.efi": []string{"recovery-hash1"},
   720  		"bootx64.efi": []string{"shimhash1", "shimhash2"},
   721  	})
   722  }
   723  
   724  func (s *modeenvSuite) TestMarshalKernelCommandLines(c *C) {
   725  	c.Assert(s.mockModeenvPath, testutil.FileAbsent)
   726  
   727  	modeenv := &boot.Modeenv{
   728  		Mode:           "run",
   729  		RecoverySystem: "20191128",
   730  		CurrentKernelCommandLines: boot.BootCommandLines{
   731  			`snapd_recovery_mode=run panic=-1 console=ttyS0,io,9600n8`,
   732  			`snapd_recovery_mode=run candidate panic=-1 console=ttyS0,io,9600n8`,
   733  		},
   734  	}
   735  	err := modeenv.WriteTo(s.tmpdir)
   736  	c.Assert(err, IsNil)
   737  
   738  	c.Assert(s.mockModeenvPath, testutil.FileEquals, `mode=run
   739  recovery_system=20191128
   740  current_kernel_command_lines=["snapd_recovery_mode=run panic=-1 console=ttyS0,io,9600n8","snapd_recovery_mode=run candidate panic=-1 console=ttyS0,io,9600n8"]
   741  `)
   742  
   743  	modeenvRead, err := boot.ReadModeenv(s.tmpdir)
   744  	c.Assert(err, IsNil)
   745  	c.Assert(modeenvRead.CurrentKernelCommandLines, DeepEquals, boot.BootCommandLines{
   746  		`snapd_recovery_mode=run panic=-1 console=ttyS0,io,9600n8`,
   747  		`snapd_recovery_mode=run candidate panic=-1 console=ttyS0,io,9600n8`,
   748  	})
   749  }