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 }