github.com/hugh712/snapd@v0.0.0-20200910133618-1a99902bd583/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  	"github.com/snapcore/snapd/osutil"
    31  	"github.com/snapcore/snapd/osutil/sys"
    32  	"github.com/snapcore/snapd/randutil"
    33  	"github.com/snapcore/snapd/testutil"
    34  
    35  	. "gopkg.in/check.v1"
    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  	// create a dir without write permission
   334  	err = os.MkdirAll(nested, 0644)
   335  	c.Assert(err, IsNil)
   336  
   337  	// no permission to write in dir
   338  	err = osutil.AtomicSymlink("target", nestedBarSymlink)
   339  	c.Assert(err, ErrorMatches, `symlink target /.*/nested/bar\..*~: permission denied`)
   340  	checkLeftoverFiles(nestedBarSymlink, nil)
   341  
   342  	err = os.Chmod(nested, 0755)
   343  	c.Assert(err, IsNil)
   344  
   345  	err = osutil.AtomicSymlink("target", nestedBarSymlink)
   346  	c.Assert(err, IsNil)
   347  	mustReadSymlink(nestedBarSymlink, "target")
   348  	checkLeftoverFiles(nestedBarSymlink, []string{nestedBarSymlink})
   349  
   350  	// symlink gets replaced
   351  	err = osutil.AtomicSymlink("new-target", nestedBarSymlink)
   352  	c.Assert(err, IsNil)
   353  	mustReadSymlink(nestedBarSymlink, "new-target")
   354  	checkLeftoverFiles(nestedBarSymlink, []string{nestedBarSymlink})
   355  
   356  	// don't care about symlink target
   357  	err = osutil.AtomicSymlink("/this/is/some/funny/path", nestedBarSymlink)
   358  	c.Assert(err, IsNil)
   359  	mustReadSymlink(nestedBarSymlink, "/this/is/some/funny/path")
   360  	checkLeftoverFiles(nestedBarSymlink, []string{nestedBarSymlink})
   361  }
   362  
   363  func (ts *AtomicSymlinkTestSuite) createCollisionSequence(c *C, baseName string, many int) {
   364  	for i := 0; i < many; i++ {
   365  		expectedRandomness := randutil.RandomString(12) + "~"
   366  		// ensure we always get the same result
   367  		err := ioutil.WriteFile(baseName+"."+expectedRandomness, []byte(""), 0644)
   368  		c.Assert(err, IsNil)
   369  	}
   370  }
   371  
   372  func (ts *AtomicSymlinkTestSuite) TestAtomicSymlinkCollisionError(c *C) {
   373  	tmpdir := c.MkDir()
   374  	// ensure we always get the same result
   375  	rand.Seed(1)
   376  	p := filepath.Join(tmpdir, "foo")
   377  	ts.createCollisionSequence(c, p, osutil.MaxSymlinkTries)
   378  	// restart random number sequence
   379  	rand.Seed(1)
   380  
   381  	err := osutil.AtomicSymlink("target", p)
   382  	c.Assert(err, ErrorMatches, "cannot create a temporary symlink")
   383  }
   384  
   385  func (ts *AtomicSymlinkTestSuite) TestAtomicSymlinkCollisionHappy(c *C) {
   386  	tmpdir := c.MkDir()
   387  	// ensure we always get the same result
   388  	rand.Seed(1)
   389  	p := filepath.Join(tmpdir, "foo")
   390  	ts.createCollisionSequence(c, p, osutil.MaxSymlinkTries/2)
   391  	// restart random number sequence
   392  	rand.Seed(1)
   393  
   394  	err := osutil.AtomicSymlink("target", p)
   395  	c.Assert(err, IsNil)
   396  }
   397  
   398  type AtomicRenameTestSuite struct{}
   399  
   400  var _ = Suite(&AtomicRenameTestSuite{})
   401  
   402  func (ts *AtomicRenameTestSuite) TestAtomicRename(c *C) {
   403  	d := c.MkDir()
   404  
   405  	err := ioutil.WriteFile(filepath.Join(d, "foo"), []byte("foobar"), 0644)
   406  	c.Assert(err, IsNil)
   407  
   408  	err = osutil.AtomicRename(filepath.Join(d, "foo"), filepath.Join(d, "bar"))
   409  	c.Assert(err, IsNil)
   410  	c.Check(filepath.Join(d, "bar"), testutil.FileEquals, "foobar")
   411  
   412  	// no nested directory
   413  	nested := filepath.Join(d, "nested")
   414  	err = osutil.AtomicRename(filepath.Join(d, "bar"), filepath.Join(nested, "bar"))
   415  	if !osutil.GetUnsafeIO() {
   416  		// with safe IO first op is to open the source and target directories
   417  		c.Assert(err, ErrorMatches, "open /.*/nested: no such file or directory")
   418  	} else {
   419  		c.Assert(err, ErrorMatches, "rename /.*/bar /.*/nested/bar: no such file or directory")
   420  	}
   421  
   422  	// create a dir without write permission
   423  	err = os.MkdirAll(nested, 0644)
   424  	c.Assert(err, IsNil)
   425  
   426  	// no permission to write in dir
   427  	err = osutil.AtomicRename(filepath.Join(d, "bar"), filepath.Join(nested, "bar"))
   428  	c.Assert(err, ErrorMatches, "rename /.*/bar /.*/nested/bar: permission denied")
   429  
   430  	err = os.Chmod(nested, 0755)
   431  	c.Assert(err, IsNil)
   432  
   433  	// all good now
   434  	err = osutil.AtomicRename(filepath.Join(d, "bar"), filepath.Join(nested, "bar"))
   435  	c.Assert(err, IsNil)
   436  
   437  	err = ioutil.WriteFile(filepath.Join(nested, "new-bar"), []byte("barbar"), 0644)
   438  	c.Assert(err, IsNil)
   439  
   440  	// target is overwritten
   441  	err = osutil.AtomicRename(filepath.Join(nested, "new-bar"), filepath.Join(nested, "bar"))
   442  	c.Assert(err, IsNil)
   443  	c.Check(filepath.Join(nested, "bar"), testutil.FileEquals, "barbar")
   444  
   445  	// no source
   446  	err = osutil.AtomicRename(filepath.Join(d, "does-not-exist"), filepath.Join(nested, "bar"))
   447  	c.Assert(err, ErrorMatches, "rename /.*/does-not-exist /.*/nested/bar: no such file or directory")
   448  }
   449  
   450  // SafeIoAtomicTestSuite runs all Atomic* tests with safe
   451  // io enabled
   452  type SafeIoAtomicTestSuite struct {
   453  	AtomicWriteTestSuite
   454  	AtomicSymlinkTestSuite
   455  	AtomicRenameTestSuite
   456  
   457  	restoreUnsafeIO func()
   458  }
   459  
   460  var _ = Suite(&SafeIoAtomicTestSuite{})
   461  
   462  func (s *SafeIoAtomicTestSuite) SetUpSuite(c *C) {
   463  	s.restoreUnsafeIO = osutil.SetUnsafeIO(false)
   464  }
   465  
   466  func (s *SafeIoAtomicTestSuite) TearDownSuite(c *C) {
   467  	s.restoreUnsafeIO()
   468  }