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

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2014-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 osutil_test
    21  
    22  import (
    23  	"errors"
    24  	"io/ioutil"
    25  	"os"
    26  	"os/exec"
    27  	"path/filepath"
    28  	"strings"
    29  	"syscall"
    30  	"time"
    31  
    32  	. "gopkg.in/check.v1"
    33  
    34  	"github.com/snapcore/snapd/osutil"
    35  	"github.com/snapcore/snapd/testutil"
    36  )
    37  
    38  type cpSuite struct {
    39  	testutil.BaseTest
    40  
    41  	dir  string
    42  	f1   string
    43  	f2   string
    44  	data []byte
    45  	log  []string
    46  	errs []error
    47  	idx  int
    48  }
    49  
    50  var _ = Suite(&cpSuite{})
    51  
    52  func (s *cpSuite) mockCopyFile(fin, fout osutil.Fileish, fi os.FileInfo) error {
    53  	return s.µ("copyfile")
    54  }
    55  
    56  func (s *cpSuite) mockOpenFile(name string, flag int, perm os.FileMode) (osutil.Fileish, error) {
    57  	return &mockfile{s}, s.µ("open")
    58  }
    59  
    60  func (s *cpSuite) µ(msg string) (err error) {
    61  	s.log = append(s.log, msg)
    62  	if len(s.errs) > 0 {
    63  		err = s.errs[0]
    64  		if len(s.errs) > 1 {
    65  			s.errs = s.errs[1:]
    66  		}
    67  	}
    68  
    69  	return err
    70  }
    71  
    72  func (s *cpSuite) SetUpTest(c *C) {
    73  	s.errs = nil
    74  	s.log = nil
    75  	s.dir = c.MkDir()
    76  	s.f1 = filepath.Join(s.dir, "f1")
    77  	s.f2 = filepath.Join(s.dir, "f2")
    78  	s.data = []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    79  	c.Assert(ioutil.WriteFile(s.f1, s.data, 0644), IsNil)
    80  }
    81  
    82  func (s *cpSuite) mock() {
    83  	s.AddCleanup(osutil.MockCopyFile(s.mockCopyFile))
    84  	s.AddCleanup(osutil.MockOpenFile(s.mockOpenFile))
    85  }
    86  
    87  func (s *cpSuite) TestCp(c *C) {
    88  	c.Check(osutil.CopyFile(s.f1, s.f2, osutil.CopyFlagDefault), IsNil)
    89  	c.Check(s.f2, testutil.FileEquals, s.data)
    90  }
    91  
    92  func (s *cpSuite) TestCpNoOverwrite(c *C) {
    93  	_, err := os.Create(s.f2)
    94  	c.Assert(err, IsNil)
    95  	c.Check(osutil.CopyFile(s.f1, s.f2, osutil.CopyFlagDefault), NotNil)
    96  }
    97  
    98  func (s *cpSuite) TestCpOverwrite(c *C) {
    99  	_, err := os.Create(s.f2)
   100  	c.Assert(err, IsNil)
   101  	c.Check(osutil.CopyFile(s.f1, s.f2, osutil.CopyFlagOverwrite), IsNil)
   102  	c.Check(s.f2, testutil.FileEquals, s.data)
   103  }
   104  
   105  func (s *cpSuite) TestCpOverwriteTruncates(c *C) {
   106  	c.Assert(ioutil.WriteFile(s.f2, []byte("xxxxxxxxxxxxxxxx"), 0644), IsNil)
   107  	c.Check(osutil.CopyFile(s.f1, s.f2, osutil.CopyFlagOverwrite), IsNil)
   108  	c.Check(s.f2, testutil.FileEquals, s.data)
   109  }
   110  
   111  func (s *cpSuite) TestCpSync(c *C) {
   112  	s.mock()
   113  	c.Check(osutil.CopyFile(s.f1, s.f2, osutil.CopyFlagDefault), IsNil)
   114  	c.Check(strings.Join(s.log, ":"), Not(Matches), `.*:sync(:.*)?`)
   115  
   116  	s.log = nil
   117  	c.Check(osutil.CopyFile(s.f1, s.f2, osutil.CopyFlagSync), IsNil)
   118  	c.Check(strings.Join(s.log, ":"), Matches, `(.*:)?sync(:.*)?`)
   119  }
   120  
   121  func (s *cpSuite) TestCpCantOpen(c *C) {
   122  	s.mock()
   123  	s.errs = []error{errors.New("xyzzy"), nil}
   124  
   125  	c.Check(osutil.CopyFile(s.f1, s.f2, osutil.CopyFlagSync), ErrorMatches, `unable to open \S+/f1: xyzzy`)
   126  }
   127  
   128  func (s *cpSuite) TestCpCantStat(c *C) {
   129  	s.mock()
   130  	s.errs = []error{nil, errors.New("xyzzy"), nil}
   131  
   132  	c.Check(osutil.CopyFile(s.f1, s.f2, osutil.CopyFlagSync), ErrorMatches, `unable to stat \S+/f1: xyzzy`)
   133  }
   134  
   135  func (s *cpSuite) TestCpCantCreate(c *C) {
   136  	s.mock()
   137  	s.errs = []error{nil, nil, errors.New("xyzzy"), nil}
   138  
   139  	c.Check(osutil.CopyFile(s.f1, s.f2, osutil.CopyFlagSync), ErrorMatches, `unable to create \S+/f2: xyzzy`)
   140  }
   141  
   142  func (s *cpSuite) TestCpCantCopy(c *C) {
   143  	s.mock()
   144  	s.errs = []error{nil, nil, nil, errors.New("xyzzy"), nil}
   145  
   146  	c.Check(osutil.CopyFile(s.f1, s.f2, osutil.CopyFlagSync), ErrorMatches, `unable to copy \S+/f1 to \S+/f2: xyzzy`)
   147  }
   148  
   149  func (s *cpSuite) TestCpCantSync(c *C) {
   150  	s.mock()
   151  	s.errs = []error{nil, nil, nil, nil, errors.New("xyzzy"), nil}
   152  
   153  	c.Check(osutil.CopyFile(s.f1, s.f2, osutil.CopyFlagSync), ErrorMatches, `unable to sync \S+/f2: xyzzy`)
   154  }
   155  
   156  func (s *cpSuite) TestCpCantStop2(c *C) {
   157  	s.mock()
   158  	s.errs = []error{nil, nil, nil, nil, nil, errors.New("xyzzy"), nil}
   159  
   160  	c.Check(osutil.CopyFile(s.f1, s.f2, osutil.CopyFlagSync), ErrorMatches, `when closing \S+/f2: xyzzy`)
   161  }
   162  
   163  func (s *cpSuite) TestCpCantStop1(c *C) {
   164  	s.mock()
   165  	s.errs = []error{nil, nil, nil, nil, nil, nil, errors.New("xyzzy"), nil}
   166  
   167  	c.Check(osutil.CopyFile(s.f1, s.f2, osutil.CopyFlagSync), ErrorMatches, `when closing \S+/f1: xyzzy`)
   168  }
   169  
   170  type mockfile struct {
   171  	s *cpSuite
   172  }
   173  
   174  var mockst = mockstat{}
   175  
   176  func (f *mockfile) Close() error               { return f.s.µ("close") }
   177  func (f *mockfile) Sync() error                { return f.s.µ("sync") }
   178  func (f *mockfile) Fd() uintptr                { f.s.µ("fd"); return 42 }
   179  func (f *mockfile) Read([]byte) (int, error)   { return 0, f.s.µ("read") }
   180  func (f *mockfile) Write([]byte) (int, error)  { return 0, f.s.µ("write") }
   181  func (f *mockfile) Stat() (os.FileInfo, error) { return mockst, f.s.µ("stat") }
   182  
   183  type mockstat struct{}
   184  
   185  func (mockstat) Name() string       { return "mockstat" }
   186  func (mockstat) Size() int64        { return 42 }
   187  func (mockstat) Mode() os.FileMode  { return 0644 }
   188  func (mockstat) ModTime() time.Time { return time.Now() }
   189  func (mockstat) IsDir() bool        { return false }
   190  func (mockstat) Sys() interface{}   { return nil }
   191  
   192  func (s *cpSuite) TestCopySpecialFileSimple(c *C) {
   193  	sync := testutil.MockCommand(c, "sync", "")
   194  	defer sync.Restore()
   195  
   196  	src := filepath.Join(c.MkDir(), "fifo")
   197  	err := syscall.Mkfifo(src, 0644)
   198  	c.Assert(err, IsNil)
   199  	dir := c.MkDir()
   200  	dst := filepath.Join(dir, "copied-fifo")
   201  
   202  	err = osutil.CopySpecialFile(src, dst)
   203  	c.Assert(err, IsNil)
   204  
   205  	st, err := os.Stat(dst)
   206  	c.Assert(err, IsNil)
   207  	c.Check((st.Mode() & os.ModeNamedPipe), Equals, os.ModeNamedPipe)
   208  	c.Check(sync.Calls(), DeepEquals, [][]string{{"sync", dir}})
   209  }
   210  
   211  func (s *cpSuite) TestCopySpecialFileErrors(c *C) {
   212  	err := osutil.CopySpecialFile("no-such-file", "no-such-target")
   213  	c.Assert(err, ErrorMatches, "failed to copy device node:.*cp:.*stat.*no-such-file.*")
   214  }
   215  
   216  func (s *cpSuite) TestCopyPreserveAll(c *C) {
   217  	src := filepath.Join(c.MkDir(), "meep")
   218  	dst := filepath.Join(c.MkDir(), "copied-meep")
   219  
   220  	err := ioutil.WriteFile(src, []byte(nil), 0644)
   221  	c.Assert(err, IsNil)
   222  
   223  	// Give the file a different mtime to ensure CopyFlagPreserveAll
   224  	// really works.
   225  	//
   226  	// You wonder why "touch" is used? And want to me about
   227  	// syscall.Utime()? Well, syscall not implemented on armhf
   228  	// Aha, syscall.Utimes() then? No, not implemented on arm64
   229  	// Really, this is a just a test, touch is good enough!
   230  	err = exec.Command("touch", src, "-d", "2007-08-23 08:21:42").Run()
   231  	c.Assert(err, IsNil)
   232  
   233  	err = osutil.CopyFile(src, dst, osutil.CopyFlagPreserveAll)
   234  	c.Assert(err, IsNil)
   235  
   236  	// ensure that the mtime got preserved
   237  	st1, err := os.Stat(src)
   238  	c.Assert(err, IsNil)
   239  	st2, err := os.Stat(dst)
   240  	c.Assert(err, IsNil)
   241  	c.Assert(st1.ModTime(), Equals, st2.ModTime())
   242  }
   243  
   244  func (s *cpSuite) TestCopyPreserveAllSync(c *C) {
   245  	dir := c.MkDir()
   246  	mocked := testutil.MockCommand(c, "cp", "").Also("sync", "")
   247  	defer mocked.Restore()
   248  
   249  	src := filepath.Join(dir, "meep")
   250  	dst := filepath.Join(dir, "copied-meep")
   251  
   252  	err := ioutil.WriteFile(src, []byte(nil), 0644)
   253  	c.Assert(err, IsNil)
   254  
   255  	err = osutil.CopyFile(src, dst, osutil.CopyFlagPreserveAll|osutil.CopyFlagSync)
   256  	c.Assert(err, IsNil)
   257  
   258  	c.Check(mocked.Calls(), DeepEquals, [][]string{
   259  		{"cp", "-av", src, dst},
   260  		{"sync"},
   261  	})
   262  }
   263  
   264  func (s *cpSuite) TestCopyPreserveAllSyncCpFailure(c *C) {
   265  	dir := c.MkDir()
   266  	mocked := testutil.MockCommand(c, "cp", "echo OUCH: cp failed.;exit 42").Also("sync", "")
   267  	defer mocked.Restore()
   268  
   269  	src := filepath.Join(dir, "meep")
   270  	dst := filepath.Join(dir, "copied-meep")
   271  
   272  	err := ioutil.WriteFile(src, []byte(nil), 0644)
   273  	c.Assert(err, IsNil)
   274  
   275  	err = osutil.CopyFile(src, dst, osutil.CopyFlagPreserveAll|osutil.CopyFlagSync)
   276  	c.Assert(err, ErrorMatches, `failed to copy all: "OUCH: cp failed." \(42\)`)
   277  	c.Check(mocked.Calls(), DeepEquals, [][]string{
   278  		{"cp", "-av", src, dst},
   279  	})
   280  }
   281  
   282  func (s *cpSuite) TestCopyPreserveAllSyncSyncFailure(c *C) {
   283  	dir := c.MkDir()
   284  	mocked := testutil.MockCommand(c, "cp", "").Also("sync", "echo OUCH: sync failed.;exit 42")
   285  	defer mocked.Restore()
   286  
   287  	src := filepath.Join(dir, "meep")
   288  	dst := filepath.Join(dir, "copied-meep")
   289  
   290  	err := ioutil.WriteFile(src, []byte(nil), 0644)
   291  	c.Assert(err, IsNil)
   292  
   293  	err = osutil.CopyFile(src, dst, osutil.CopyFlagPreserveAll|osutil.CopyFlagSync)
   294  	c.Assert(err, ErrorMatches, `failed to sync: "OUCH: sync failed." \(42\)`)
   295  
   296  	c.Check(mocked.Calls(), DeepEquals, [][]string{
   297  		{"cp", "-av", src, dst},
   298  		{"sync"},
   299  	})
   300  }
   301  
   302  func (s *cpSuite) TestAtomicWriteFileCopySimple(c *C) {
   303  	err := osutil.AtomicWriteFileCopy(s.f2, s.f1, 0)
   304  	c.Assert(err, IsNil)
   305  	c.Assert(s.f2, testutil.FileEquals, s.data)
   306  
   307  }
   308  
   309  func (s *cpSuite) TestAtomicWriteFileCopyOverwrites(c *C) {
   310  	err := ioutil.WriteFile(s.f2, []byte("this is f2 content"), 0644)
   311  	c.Assert(err, IsNil)
   312  
   313  	err = osutil.AtomicWriteFileCopy(s.f2, s.f1, 0)
   314  	c.Assert(err, IsNil)
   315  	c.Assert(s.f2, testutil.FileEquals, s.data)
   316  }
   317  
   318  func (s *cpSuite) TestAtomicWriteFileCopySymlinks(c *C) {
   319  	f2Symlink := filepath.Join(s.dir, "f2-symlink")
   320  	err := os.Symlink(s.f2, f2Symlink)
   321  	c.Assert(err, IsNil)
   322  
   323  	f2SymlinkNoFollow := filepath.Join(s.dir, "f2-symlink-no-follow")
   324  	err = os.Symlink(s.f2, f2SymlinkNoFollow)
   325  	c.Assert(err, IsNil)
   326  
   327  	// follows symlink, dst is f2
   328  	err = osutil.AtomicWriteFileCopy(f2Symlink, s.f1, osutil.AtomicWriteFollow)
   329  	c.Assert(err, IsNil)
   330  	c.Check(osutil.IsSymlink(f2Symlink), Equals, true, Commentf("%q is not a symlink", f2Symlink))
   331  	c.Check(s.f2, testutil.FileEquals, s.data)
   332  	c.Check(f2SymlinkNoFollow, testutil.FileEquals, s.data)
   333  
   334  	// when not following, copy overwrites the symlink
   335  	err = osutil.AtomicWriteFileCopy(f2SymlinkNoFollow, s.f1, 0)
   336  	c.Assert(err, IsNil)
   337  	c.Check(osutil.IsSymlink(f2SymlinkNoFollow), Equals, false, Commentf("%q is not a file", f2SymlinkNoFollow))
   338  	c.Check(f2SymlinkNoFollow, testutil.FileEquals, s.data)
   339  }
   340  
   341  func (s *cpSuite) TestAtomicWriteFileCopyErrReal(c *C) {
   342  	err := osutil.AtomicWriteFileCopy(s.f2, filepath.Join(s.dir, "random-file"), 0)
   343  	c.Assert(err, ErrorMatches, "unable to open source file .*/random-file: open .* no such file or directory")
   344  
   345  	dir := c.MkDir()
   346  
   347  	err = osutil.AtomicWriteFileCopy(filepath.Join(dir, "random-dir", "f3"), s.f1, 0)
   348  	c.Assert(err, ErrorMatches, `cannot create atomic file: open .*/random-dir/f3\.[a-zA-Z0-9]+~: no such file or directory`)
   349  
   350  	err = os.MkdirAll(filepath.Join(dir, "read-only"), 0000)
   351  	c.Assert(err, IsNil)
   352  	err = osutil.AtomicWriteFileCopy(filepath.Join(dir, "read-only", "f3"), s.f1, 0)
   353  	c.Assert(err, ErrorMatches, `cannot create atomic file: open .*/read-only/f3\.[a-zA-Z0-9]+~: permission denied`)
   354  }
   355  
   356  func (s *cpSuite) TestAtomicWriteFileCopyErrMockedCopy(c *C) {
   357  	s.mock()
   358  	s.errs = []error{
   359  		nil, // openFile
   360  		nil, // src.Stat()
   361  		errors.New("copy fail"),
   362  	}
   363  
   364  	err := osutil.AtomicWriteFileCopy(s.f2, s.f1, 0)
   365  	c.Assert(err, ErrorMatches, `unable to copy .*/f1 to .*/f2\.[a-zA-Z0-9]+~: copy fail`)
   366  	entries, err := filepath.Glob(filepath.Join(s.dir, "*"))
   367  	c.Assert(err, IsNil)
   368  	c.Assert(entries, DeepEquals, []string{
   369  		filepath.Join(s.dir, "f1"),
   370  	})
   371  }