github.com/meulengracht/snapd@v0.0.0-20210719210640-8bde69bcc84e/osutil/io_test.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2014-2015 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 osutil_test
    21  
    22  import (
    23  	"errors"
    24  	"io/ioutil"
    25  	"math/rand"
    26  	"os"
    27  	"path/filepath"
    28  	"strings"
    29  
    30  	. "gopkg.in/check.v1"
    31  
    32  	"github.com/snapcore/snapd/osutil"
    33  	"github.com/snapcore/snapd/osutil/sys"
    34  	"github.com/snapcore/snapd/randutil"
    35  	"github.com/snapcore/snapd/testutil"
    36  )
    37  
    38  type AtomicWriteTestSuite struct{}
    39  
    40  var _ = Suite(&AtomicWriteTestSuite{})
    41  
    42  func (ts *AtomicWriteTestSuite) TestAtomicWriteFile(c *C) {
    43  	tmpdir := c.MkDir()
    44  
    45  	p := filepath.Join(tmpdir, "foo")
    46  	err := osutil.AtomicWriteFile(p, []byte("canary"), 0644, 0)
    47  	c.Assert(err, IsNil)
    48  
    49  	c.Check(p, testutil.FileEquals, "canary")
    50  
    51  	// no files left behind!
    52  	d, err := ioutil.ReadDir(tmpdir)
    53  	c.Assert(err, IsNil)
    54  	c.Assert(len(d), Equals, 1)
    55  }
    56  
    57  func (ts *AtomicWriteTestSuite) TestAtomicWriteFilePermissions(c *C) {
    58  	tmpdir := c.MkDir()
    59  
    60  	p := filepath.Join(tmpdir, "foo")
    61  	err := osutil.AtomicWriteFile(p, []byte(""), 0600, 0)
    62  	c.Assert(err, IsNil)
    63  
    64  	st, err := os.Stat(p)
    65  	c.Assert(err, IsNil)
    66  	c.Assert(st.Mode()&os.ModePerm, Equals, os.FileMode(0600))
    67  }
    68  
    69  func (ts *AtomicWriteTestSuite) TestAtomicWriteFileOverwrite(c *C) {
    70  	tmpdir := c.MkDir()
    71  	p := filepath.Join(tmpdir, "foo")
    72  	c.Assert(ioutil.WriteFile(p, []byte("hello"), 0644), IsNil)
    73  	c.Assert(osutil.AtomicWriteFile(p, []byte("hi"), 0600, 0), IsNil)
    74  
    75  	c.Assert(p, testutil.FileEquals, "hi")
    76  }
    77  
    78  func (ts *AtomicWriteTestSuite) TestAtomicWriteFileSymlinkNoFollow(c *C) {
    79  	tmpdir := c.MkDir()
    80  	rodir := filepath.Join(tmpdir, "ro")
    81  	p := filepath.Join(rodir, "foo")
    82  	s := filepath.Join(tmpdir, "foo")
    83  	c.Assert(os.MkdirAll(rodir, 0755), IsNil)
    84  	c.Assert(os.Symlink(s, p), IsNil)
    85  	c.Assert(os.Chmod(rodir, 0500), IsNil)
    86  	defer os.Chmod(rodir, 0700)
    87  
    88  	err := osutil.AtomicWriteFile(p, []byte("hi"), 0600, 0)
    89  	c.Assert(err, NotNil)
    90  }
    91  
    92  func (ts *AtomicWriteTestSuite) TestAtomicWriteFileAbsoluteSymlinks(c *C) {
    93  	tmpdir := c.MkDir()
    94  	rodir := filepath.Join(tmpdir, "ro")
    95  	p := filepath.Join(rodir, "foo")
    96  	s := filepath.Join(tmpdir, "foo")
    97  	c.Assert(os.MkdirAll(rodir, 0755), IsNil)
    98  	c.Assert(os.Symlink(s, p), IsNil)
    99  	c.Assert(os.Chmod(rodir, 0500), IsNil)
   100  	defer os.Chmod(rodir, 0700)
   101  
   102  	err := osutil.AtomicWriteFile(p, []byte("hi"), 0600, osutil.AtomicWriteFollow)
   103  	c.Assert(err, IsNil)
   104  
   105  	c.Assert(p, testutil.FileEquals, "hi")
   106  }
   107  
   108  func (ts *AtomicWriteTestSuite) TestAtomicWriteFileOverwriteAbsoluteSymlink(c *C) {
   109  	tmpdir := c.MkDir()
   110  	rodir := filepath.Join(tmpdir, "ro")
   111  	p := filepath.Join(rodir, "foo")
   112  	s := filepath.Join(tmpdir, "foo")
   113  	c.Assert(os.MkdirAll(rodir, 0755), IsNil)
   114  	c.Assert(os.Symlink(s, p), IsNil)
   115  	c.Assert(os.Chmod(rodir, 0500), IsNil)
   116  	defer os.Chmod(rodir, 0700)
   117  
   118  	c.Assert(ioutil.WriteFile(s, []byte("hello"), 0644), IsNil)
   119  	c.Assert(osutil.AtomicWriteFile(p, []byte("hi"), 0600, osutil.AtomicWriteFollow), IsNil)
   120  
   121  	c.Assert(p, testutil.FileEquals, "hi")
   122  }
   123  
   124  func (ts *AtomicWriteTestSuite) TestAtomicWriteFileRelativeSymlinks(c *C) {
   125  	tmpdir := c.MkDir()
   126  	rodir := filepath.Join(tmpdir, "ro")
   127  	p := filepath.Join(rodir, "foo")
   128  	c.Assert(os.MkdirAll(rodir, 0755), IsNil)
   129  	c.Assert(os.Symlink("../foo", p), IsNil)
   130  	c.Assert(os.Chmod(rodir, 0500), IsNil)
   131  	defer os.Chmod(rodir, 0700)
   132  
   133  	err := osutil.AtomicWriteFile(p, []byte("hi"), 0600, osutil.AtomicWriteFollow)
   134  	c.Assert(err, IsNil)
   135  
   136  	c.Assert(p, testutil.FileEquals, "hi")
   137  }
   138  
   139  func (ts *AtomicWriteTestSuite) TestAtomicWriteFileOverwriteRelativeSymlink(c *C) {
   140  	tmpdir := c.MkDir()
   141  	rodir := filepath.Join(tmpdir, "ro")
   142  	p := filepath.Join(rodir, "foo")
   143  	s := filepath.Join(tmpdir, "foo")
   144  	c.Assert(os.MkdirAll(rodir, 0755), IsNil)
   145  	c.Assert(os.Symlink("../foo", p), IsNil)
   146  	c.Assert(os.Chmod(rodir, 0500), IsNil)
   147  	defer os.Chmod(rodir, 0700)
   148  
   149  	c.Assert(ioutil.WriteFile(s, []byte("hello"), 0644), IsNil)
   150  	c.Assert(osutil.AtomicWriteFile(p, []byte("hi"), 0600, osutil.AtomicWriteFollow), IsNil)
   151  
   152  	c.Assert(p, testutil.FileEquals, "hi")
   153  }
   154  
   155  func (ts *AtomicWriteTestSuite) TestAtomicWriteFileNoOverwriteTmpExisting(c *C) {
   156  	tmpdir := c.MkDir()
   157  	// ensure we always get the same result
   158  	rand.Seed(1)
   159  	expectedRandomness := randutil.RandomString(12) + "~"
   160  	// ensure we always get the same result
   161  	rand.Seed(1)
   162  
   163  	p := filepath.Join(tmpdir, "foo")
   164  	err := ioutil.WriteFile(p+"."+expectedRandomness, []byte(""), 0644)
   165  	c.Assert(err, IsNil)
   166  
   167  	err = osutil.AtomicWriteFile(p, []byte(""), 0600, 0)
   168  	c.Assert(err, ErrorMatches, "open .*: file exists")
   169  }
   170  
   171  func (ts *AtomicWriteTestSuite) TestAtomicFileChownError(c *C) {
   172  	eUid := sys.UserID(42)
   173  	eGid := sys.GroupID(74)
   174  	eErr := errors.New("this didn't work")
   175  	defer osutil.MockChown(func(fd *os.File, uid sys.UserID, gid sys.GroupID) error {
   176  		c.Check(uid, Equals, eUid)
   177  		c.Check(gid, Equals, eGid)
   178  		return eErr
   179  	})()
   180  
   181  	d := c.MkDir()
   182  	p := filepath.Join(d, "foo")
   183  
   184  	aw, err := osutil.NewAtomicFile(p, 0644, 0, eUid, eGid)
   185  	c.Assert(err, IsNil)
   186  	defer aw.Cancel()
   187  
   188  	_, err = aw.Write([]byte("hello"))
   189  	c.Assert(err, IsNil)
   190  
   191  	c.Check(aw.Commit(), Equals, eErr)
   192  }
   193  
   194  func (ts *AtomicWriteTestSuite) TestAtomicFileCancelError(c *C) {
   195  	d := c.MkDir()
   196  	p := filepath.Join(d, "foo")
   197  	aw, err := osutil.NewAtomicFile(p, 0644, 0, osutil.NoChown, osutil.NoChown)
   198  	c.Assert(err, IsNil)
   199  
   200  	c.Assert(aw.File.Close(), IsNil)
   201  	// Depending on golang version the error is one of the two.
   202  	c.Check(aw.Cancel(), ErrorMatches, "invalid argument|file already closed")
   203  }
   204  
   205  func (ts *AtomicWriteTestSuite) TestAtomicFileCancelBadError(c *C) {
   206  	d := c.MkDir()
   207  	p := filepath.Join(d, "foo")
   208  	aw, err := osutil.NewAtomicFile(p, 0644, 0, osutil.NoChown, osutil.NoChown)
   209  	c.Assert(err, IsNil)
   210  	defer aw.Close()
   211  
   212  	osutil.SetAtomicFileRenamed(aw, true)
   213  
   214  	c.Check(aw.Cancel(), Equals, osutil.ErrCannotCancel)
   215  }
   216  
   217  func (ts *AtomicWriteTestSuite) TestAtomicFileCancelNoClose(c *C) {
   218  	d := c.MkDir()
   219  	p := filepath.Join(d, "foo")
   220  	aw, err := osutil.NewAtomicFile(p, 0644, 0, osutil.NoChown, osutil.NoChown)
   221  	c.Assert(err, IsNil)
   222  	c.Assert(aw.Close(), IsNil)
   223  
   224  	c.Check(aw.Cancel(), IsNil)
   225  }
   226  
   227  func (ts *AtomicWriteTestSuite) TestAtomicFileCancel(c *C) {
   228  	d := c.MkDir()
   229  	p := filepath.Join(d, "foo")
   230  
   231  	aw, err := osutil.NewAtomicFile(p, 0644, 0, osutil.NoChown, osutil.NoChown)
   232  	c.Assert(err, IsNil)
   233  	fn := aw.File.Name()
   234  	c.Check(osutil.FileExists(fn), Equals, true)
   235  	c.Check(aw.Cancel(), IsNil)
   236  	c.Check(osutil.FileExists(fn), Equals, false)
   237  }
   238  
   239  func (ts *AtomicWriteTestSuite) TestAtomicFileCommitAs(c *C) {
   240  	d := c.MkDir()
   241  	initialTarget := filepath.Join(d, "foo")
   242  	actualTarget := filepath.Join(d, "bar")
   243  
   244  	aw, err := osutil.NewAtomicFile(initialTarget, 0644, 0, osutil.NoChown, osutil.NoChown)
   245  	c.Assert(err, IsNil)
   246  	defer aw.Cancel()
   247  	fn := aw.File.Name()
   248  	c.Check(osutil.FileExists(fn), Equals, true)
   249  	c.Check(strings.HasPrefix(fn, initialTarget), Equals, true, Commentf("unexpected temporary file name prefix: %q", fn))
   250  	_, err = aw.WriteString("this is test data")
   251  	c.Assert(err, IsNil)
   252  
   253  	err = aw.CommitAs(actualTarget)
   254  	c.Assert(err, IsNil)
   255  	c.Check(fn, testutil.FileAbsent)
   256  	c.Check(actualTarget, testutil.FileEquals, "this is test data")
   257  	c.Check(initialTarget, testutil.FileAbsent)
   258  
   259  	// not confused when CommitAs uses the same name as initially
   260  	sameNameTarget := filepath.Join(d, "baz")
   261  	aw, err = osutil.NewAtomicFile(sameNameTarget, 0644, 0, osutil.NoChown, osutil.NoChown)
   262  	c.Assert(err, IsNil)
   263  	defer aw.Cancel()
   264  	_, err = aw.WriteString("this is baz")
   265  	c.Assert(err, IsNil)
   266  	err = aw.CommitAs(sameNameTarget)
   267  	c.Assert(err, IsNil)
   268  	c.Check(sameNameTarget, testutil.FileEquals, "this is baz")
   269  
   270  	// overwrites any existing file on CommitAs (same as Commit)
   271  	overwrittenTarget := filepath.Join(d, "will-overwrite")
   272  	err = ioutil.WriteFile(overwrittenTarget, []byte("overwritten"), 0644)
   273  	c.Assert(err, IsNil)
   274  	aw, err = osutil.NewAtomicFile(filepath.Join(d, "temp-name"), 0644, 0, osutil.NoChown, osutil.NoChown)
   275  	c.Assert(err, IsNil)
   276  	defer aw.Cancel()
   277  	_, err = aw.WriteString("this will overwrite existing file")
   278  	c.Assert(err, IsNil)
   279  	err = aw.CommitAs(overwrittenTarget)
   280  	c.Assert(err, IsNil)
   281  	c.Check(overwrittenTarget, testutil.FileEquals, "this will overwrite existing file")
   282  }
   283  
   284  func (ts *AtomicWriteTestSuite) TestAtomicFileCommitAsDifferentDirErr(c *C) {
   285  	d := c.MkDir()
   286  	initialTarget := filepath.Join(d, "foo")
   287  	differentDirTarget := filepath.Join(c.MkDir(), "bar")
   288  
   289  	aw, err := osutil.NewAtomicFile(initialTarget, 0644, 0, osutil.NoChown, osutil.NoChown)
   290  	c.Assert(err, IsNil)
   291  	_, err = aw.WriteString("this is test data")
   292  	c.Assert(err, IsNil)
   293  
   294  	err = aw.CommitAs(differentDirTarget)
   295  	c.Assert(err, ErrorMatches, `cannot commit as "bar" to a different directory .*`)
   296  }
   297  
   298  type AtomicSymlinkTestSuite struct{}
   299  
   300  var _ = Suite(&AtomicSymlinkTestSuite{})
   301  
   302  func (ts *AtomicSymlinkTestSuite) TestAtomicSymlink(c *C) {
   303  	mustReadSymlink := func(p, exp string) {
   304  		target, err := os.Readlink(p)
   305  		c.Assert(err, IsNil)
   306  		c.Check(exp, Equals, target)
   307  	}
   308  
   309  	checkLeftoverFiles := func(sym string, exp []string) {
   310  		res, err := filepath.Glob(sym + "*")
   311  		c.Assert(err, IsNil)
   312  		if len(exp) != 0 {
   313  			c.Assert(res, DeepEquals, exp)
   314  		} else {
   315  			c.Assert(res, HasLen, 0)
   316  		}
   317  	}
   318  
   319  	d := c.MkDir()
   320  	barSymlink := filepath.Join(d, "bar")
   321  	err := osutil.AtomicSymlink("target", barSymlink)
   322  	c.Assert(err, IsNil)
   323  	mustReadSymlink(barSymlink, "target")
   324  	checkLeftoverFiles(barSymlink, []string{barSymlink})
   325  
   326  	// no nested directory
   327  	nested := filepath.Join(d, "nested")
   328  	nestedBarSymlink := filepath.Join(nested, "bar")
   329  	err = osutil.AtomicSymlink("target", nestedBarSymlink)
   330  	c.Assert(err, ErrorMatches, `symlink target /.*/nested/bar\..*~: no such file or directory`)
   331  	checkLeftoverFiles(nestedBarSymlink, nil)
   332  
   333  	if os.Geteuid() != 0 {
   334  		// create a dir without write permission
   335  		err = os.MkdirAll(nested, 0644)
   336  		c.Assert(err, IsNil)
   337  
   338  		// no permission to write in dir
   339  		err = osutil.AtomicSymlink("target", nestedBarSymlink)
   340  		c.Assert(err, ErrorMatches, `symlink target /.*/nested/bar\..*~: permission denied`)
   341  		checkLeftoverFiles(nestedBarSymlink, nil)
   342  
   343  		err = os.Chmod(nested, 0755)
   344  		c.Assert(err, IsNil)
   345  	}
   346  
   347  	err = osutil.AtomicSymlink("target", nestedBarSymlink)
   348  	c.Assert(err, IsNil)
   349  	mustReadSymlink(nestedBarSymlink, "target")
   350  	checkLeftoverFiles(nestedBarSymlink, []string{nestedBarSymlink})
   351  
   352  	// symlink gets replaced
   353  	err = osutil.AtomicSymlink("new-target", nestedBarSymlink)
   354  	c.Assert(err, IsNil)
   355  	mustReadSymlink(nestedBarSymlink, "new-target")
   356  	checkLeftoverFiles(nestedBarSymlink, []string{nestedBarSymlink})
   357  
   358  	// don't care about symlink target
   359  	err = osutil.AtomicSymlink("/this/is/some/funny/path", nestedBarSymlink)
   360  	c.Assert(err, IsNil)
   361  	mustReadSymlink(nestedBarSymlink, "/this/is/some/funny/path")
   362  	checkLeftoverFiles(nestedBarSymlink, []string{nestedBarSymlink})
   363  }
   364  
   365  func (ts *AtomicSymlinkTestSuite) createCollisionSequence(c *C, baseName string, many int) {
   366  	for i := 0; i < many; i++ {
   367  		expectedRandomness := randutil.RandomString(12) + "~"
   368  		// ensure we always get the same result
   369  		err := ioutil.WriteFile(baseName+"."+expectedRandomness, []byte(""), 0644)
   370  		c.Assert(err, IsNil)
   371  	}
   372  }
   373  
   374  func (ts *AtomicSymlinkTestSuite) TestAtomicSymlinkCollisionError(c *C) {
   375  	tmpdir := c.MkDir()
   376  	// ensure we always get the same result
   377  	rand.Seed(1)
   378  	p := filepath.Join(tmpdir, "foo")
   379  	ts.createCollisionSequence(c, p, osutil.MaxSymlinkTries)
   380  	// restart random number sequence
   381  	rand.Seed(1)
   382  
   383  	err := osutil.AtomicSymlink("target", p)
   384  	c.Assert(err, ErrorMatches, "cannot create a temporary symlink")
   385  }
   386  
   387  func (ts *AtomicSymlinkTestSuite) TestAtomicSymlinkCollisionHappy(c *C) {
   388  	tmpdir := c.MkDir()
   389  	// ensure we always get the same result
   390  	rand.Seed(1)
   391  	p := filepath.Join(tmpdir, "foo")
   392  	ts.createCollisionSequence(c, p, osutil.MaxSymlinkTries/2)
   393  	// restart random number sequence
   394  	rand.Seed(1)
   395  
   396  	err := osutil.AtomicSymlink("target", p)
   397  	c.Assert(err, IsNil)
   398  }
   399  
   400  type AtomicRenameTestSuite struct{}
   401  
   402  var _ = Suite(&AtomicRenameTestSuite{})
   403  
   404  func (ts *AtomicRenameTestSuite) TestAtomicRename(c *C) {
   405  	d := c.MkDir()
   406  
   407  	err := ioutil.WriteFile(filepath.Join(d, "foo"), []byte("foobar"), 0644)
   408  	c.Assert(err, IsNil)
   409  
   410  	err = osutil.AtomicRename(filepath.Join(d, "foo"), filepath.Join(d, "bar"))
   411  	c.Assert(err, IsNil)
   412  	c.Check(filepath.Join(d, "bar"), testutil.FileEquals, "foobar")
   413  
   414  	// no nested directory
   415  	nested := filepath.Join(d, "nested")
   416  	err = osutil.AtomicRename(filepath.Join(d, "bar"), filepath.Join(nested, "bar"))
   417  	if !osutil.GetUnsafeIO() {
   418  		// with safe IO first op is to open the source and target directories
   419  		c.Assert(err, ErrorMatches, "open /.*/nested: no such file or directory")
   420  	} else {
   421  		c.Assert(err, ErrorMatches, "rename /.*/bar /.*/nested/bar: no such file or directory")
   422  	}
   423  
   424  	if os.Geteuid() != 0 {
   425  		// create a dir without write permission
   426  		err = os.MkdirAll(nested, 0644)
   427  		c.Assert(err, IsNil)
   428  
   429  		// no permission to write in dir
   430  		err = osutil.AtomicRename(filepath.Join(d, "bar"), filepath.Join(nested, "bar"))
   431  		c.Assert(err, ErrorMatches, "rename /.*/bar /.*/nested/bar: permission denied")
   432  
   433  		err = os.Chmod(nested, 0755)
   434  		c.Assert(err, IsNil)
   435  	}
   436  
   437  	// all good now
   438  	err = osutil.AtomicRename(filepath.Join(d, "bar"), filepath.Join(nested, "bar"))
   439  	c.Assert(err, IsNil)
   440  
   441  	err = ioutil.WriteFile(filepath.Join(nested, "new-bar"), []byte("barbar"), 0644)
   442  	c.Assert(err, IsNil)
   443  
   444  	// target is overwritten
   445  	err = osutil.AtomicRename(filepath.Join(nested, "new-bar"), filepath.Join(nested, "bar"))
   446  	c.Assert(err, IsNil)
   447  	c.Check(filepath.Join(nested, "bar"), testutil.FileEquals, "barbar")
   448  
   449  	// no source
   450  	err = osutil.AtomicRename(filepath.Join(d, "does-not-exist"), filepath.Join(nested, "bar"))
   451  	c.Assert(err, ErrorMatches, "rename /.*/does-not-exist /.*/nested/bar: no such file or directory")
   452  }
   453  
   454  // SafeIoAtomicTestSuite runs all Atomic* tests with safe
   455  // io enabled
   456  type SafeIoAtomicTestSuite struct {
   457  	AtomicWriteTestSuite
   458  	AtomicSymlinkTestSuite
   459  	AtomicRenameTestSuite
   460  
   461  	restoreUnsafeIO func()
   462  }
   463  
   464  var _ = Suite(&SafeIoAtomicTestSuite{})
   465  
   466  func (s *SafeIoAtomicTestSuite) SetUpSuite(c *C) {
   467  	s.restoreUnsafeIO = osutil.SetUnsafeIO(false)
   468  }
   469  
   470  func (s *SafeIoAtomicTestSuite) TearDownSuite(c *C) {
   471  	s.restoreUnsafeIO()
   472  }