Tizen_4.0 base
[platform/upstream/docker-engine.git] / pkg / archive / archive_test.go
1 package archive
2
3 import (
4         "archive/tar"
5         "bytes"
6         "fmt"
7         "io"
8         "io/ioutil"
9         "os"
10         "os/exec"
11         "path/filepath"
12         "runtime"
13         "strings"
14         "testing"
15         "time"
16
17         "github.com/stretchr/testify/assert"
18         "github.com/stretchr/testify/require"
19 )
20
21 var tmp string
22
23 func init() {
24         tmp = "/tmp/"
25         if runtime.GOOS == "windows" {
26                 tmp = os.Getenv("TEMP") + `\`
27         }
28 }
29
30 var defaultArchiver = NewDefaultArchiver()
31
32 func defaultTarUntar(src, dst string) error {
33         return defaultArchiver.TarUntar(src, dst)
34 }
35
36 func defaultUntarPath(src, dst string) error {
37         return defaultArchiver.UntarPath(src, dst)
38 }
39
40 func defaultCopyFileWithTar(src, dst string) (err error) {
41         return defaultArchiver.CopyFileWithTar(src, dst)
42 }
43
44 func defaultCopyWithTar(src, dst string) error {
45         return defaultArchiver.CopyWithTar(src, dst)
46 }
47
48 func TestIsArchivePathDir(t *testing.T) {
49         cmd := exec.Command("sh", "-c", "mkdir -p /tmp/archivedir")
50         output, err := cmd.CombinedOutput()
51         if err != nil {
52                 t.Fatalf("Fail to create an archive file for test : %s.", output)
53         }
54         if IsArchivePath(tmp + "archivedir") {
55                 t.Fatalf("Incorrectly recognised directory as an archive")
56         }
57 }
58
59 func TestIsArchivePathInvalidFile(t *testing.T) {
60         cmd := exec.Command("sh", "-c", "dd if=/dev/zero bs=1024 count=1 of=/tmp/archive && gzip --stdout /tmp/archive > /tmp/archive.gz")
61         output, err := cmd.CombinedOutput()
62         if err != nil {
63                 t.Fatalf("Fail to create an archive file for test : %s.", output)
64         }
65         if IsArchivePath(tmp + "archive") {
66                 t.Fatalf("Incorrectly recognised invalid tar path as archive")
67         }
68         if IsArchivePath(tmp + "archive.gz") {
69                 t.Fatalf("Incorrectly recognised invalid compressed tar path as archive")
70         }
71 }
72
73 func TestIsArchivePathTar(t *testing.T) {
74         var whichTar string
75         if runtime.GOOS == "solaris" {
76                 whichTar = "gtar"
77         } else {
78                 whichTar = "tar"
79         }
80         cmdStr := fmt.Sprintf("touch /tmp/archivedata && %s -cf /tmp/archive /tmp/archivedata && gzip --stdout /tmp/archive > /tmp/archive.gz", whichTar)
81         cmd := exec.Command("sh", "-c", cmdStr)
82         output, err := cmd.CombinedOutput()
83         if err != nil {
84                 t.Fatalf("Fail to create an archive file for test : %s.", output)
85         }
86         if !IsArchivePath(tmp + "/archive") {
87                 t.Fatalf("Did not recognise valid tar path as archive")
88         }
89         if !IsArchivePath(tmp + "archive.gz") {
90                 t.Fatalf("Did not recognise valid compressed tar path as archive")
91         }
92 }
93
94 func testDecompressStream(t *testing.T, ext, compressCommand string) {
95         cmd := exec.Command("sh", "-c",
96                 fmt.Sprintf("touch /tmp/archive && %s /tmp/archive", compressCommand))
97         output, err := cmd.CombinedOutput()
98         if err != nil {
99                 t.Fatalf("Failed to create an archive file for test : %s.", output)
100         }
101         filename := "archive." + ext
102         archive, err := os.Open(tmp + filename)
103         if err != nil {
104                 t.Fatalf("Failed to open file %s: %v", filename, err)
105         }
106         defer archive.Close()
107
108         r, err := DecompressStream(archive)
109         if err != nil {
110                 t.Fatalf("Failed to decompress %s: %v", filename, err)
111         }
112         if _, err = ioutil.ReadAll(r); err != nil {
113                 t.Fatalf("Failed to read the decompressed stream: %v ", err)
114         }
115         if err = r.Close(); err != nil {
116                 t.Fatalf("Failed to close the decompressed stream: %v ", err)
117         }
118 }
119
120 func TestDecompressStreamGzip(t *testing.T) {
121         testDecompressStream(t, "gz", "gzip -f")
122 }
123
124 func TestDecompressStreamBzip2(t *testing.T) {
125         testDecompressStream(t, "bz2", "bzip2 -f")
126 }
127
128 func TestDecompressStreamXz(t *testing.T) {
129         if runtime.GOOS == "windows" {
130                 t.Skip("Xz not present in msys2")
131         }
132         testDecompressStream(t, "xz", "xz -f")
133 }
134
135 func TestCompressStreamXzUnsupported(t *testing.T) {
136         dest, err := os.Create(tmp + "dest")
137         if err != nil {
138                 t.Fatalf("Fail to create the destination file")
139         }
140         defer dest.Close()
141
142         _, err = CompressStream(dest, Xz)
143         if err == nil {
144                 t.Fatalf("Should fail as xz is unsupported for compression format.")
145         }
146 }
147
148 func TestCompressStreamBzip2Unsupported(t *testing.T) {
149         dest, err := os.Create(tmp + "dest")
150         if err != nil {
151                 t.Fatalf("Fail to create the destination file")
152         }
153         defer dest.Close()
154
155         _, err = CompressStream(dest, Xz)
156         if err == nil {
157                 t.Fatalf("Should fail as xz is unsupported for compression format.")
158         }
159 }
160
161 func TestCompressStreamInvalid(t *testing.T) {
162         dest, err := os.Create(tmp + "dest")
163         if err != nil {
164                 t.Fatalf("Fail to create the destination file")
165         }
166         defer dest.Close()
167
168         _, err = CompressStream(dest, -1)
169         if err == nil {
170                 t.Fatalf("Should fail as xz is unsupported for compression format.")
171         }
172 }
173
174 func TestExtensionInvalid(t *testing.T) {
175         compression := Compression(-1)
176         output := compression.Extension()
177         if output != "" {
178                 t.Fatalf("The extension of an invalid compression should be an empty string.")
179         }
180 }
181
182 func TestExtensionUncompressed(t *testing.T) {
183         compression := Uncompressed
184         output := compression.Extension()
185         if output != "tar" {
186                 t.Fatalf("The extension of an uncompressed archive should be 'tar'.")
187         }
188 }
189 func TestExtensionBzip2(t *testing.T) {
190         compression := Bzip2
191         output := compression.Extension()
192         if output != "tar.bz2" {
193                 t.Fatalf("The extension of a bzip2 archive should be 'tar.bz2'")
194         }
195 }
196 func TestExtensionGzip(t *testing.T) {
197         compression := Gzip
198         output := compression.Extension()
199         if output != "tar.gz" {
200                 t.Fatalf("The extension of a bzip2 archive should be 'tar.gz'")
201         }
202 }
203 func TestExtensionXz(t *testing.T) {
204         compression := Xz
205         output := compression.Extension()
206         if output != "tar.xz" {
207                 t.Fatalf("The extension of a bzip2 archive should be 'tar.xz'")
208         }
209 }
210
211 func TestCmdStreamLargeStderr(t *testing.T) {
212         cmd := exec.Command("sh", "-c", "dd if=/dev/zero bs=1k count=1000 of=/dev/stderr; echo hello")
213         out, _, err := cmdStream(cmd, nil)
214         if err != nil {
215                 t.Fatalf("Failed to start command: %s", err)
216         }
217         errCh := make(chan error)
218         go func() {
219                 _, err := io.Copy(ioutil.Discard, out)
220                 errCh <- err
221         }()
222         select {
223         case err := <-errCh:
224                 if err != nil {
225                         t.Fatalf("Command should not have failed (err=%.100s...)", err)
226                 }
227         case <-time.After(5 * time.Second):
228                 t.Fatalf("Command did not complete in 5 seconds; probable deadlock")
229         }
230 }
231
232 func TestCmdStreamBad(t *testing.T) {
233         // TODO Windows: Figure out why this is failing in CI but not locally
234         if runtime.GOOS == "windows" {
235                 t.Skip("Failing on Windows CI machines")
236         }
237         badCmd := exec.Command("sh", "-c", "echo hello; echo >&2 error couldn\\'t reverse the phase pulser; exit 1")
238         out, _, err := cmdStream(badCmd, nil)
239         if err != nil {
240                 t.Fatalf("Failed to start command: %s", err)
241         }
242         if output, err := ioutil.ReadAll(out); err == nil {
243                 t.Fatalf("Command should have failed")
244         } else if err.Error() != "exit status 1: error couldn't reverse the phase pulser\n" {
245                 t.Fatalf("Wrong error value (%s)", err)
246         } else if s := string(output); s != "hello\n" {
247                 t.Fatalf("Command output should be '%s', not '%s'", "hello\\n", output)
248         }
249 }
250
251 func TestCmdStreamGood(t *testing.T) {
252         cmd := exec.Command("sh", "-c", "echo hello; exit 0")
253         out, _, err := cmdStream(cmd, nil)
254         if err != nil {
255                 t.Fatal(err)
256         }
257         if output, err := ioutil.ReadAll(out); err != nil {
258                 t.Fatalf("Command should not have failed (err=%s)", err)
259         } else if s := string(output); s != "hello\n" {
260                 t.Fatalf("Command output should be '%s', not '%s'", "hello\\n", output)
261         }
262 }
263
264 func TestUntarPathWithInvalidDest(t *testing.T) {
265         tempFolder, err := ioutil.TempDir("", "docker-archive-test")
266         if err != nil {
267                 t.Fatal(err)
268         }
269         defer os.RemoveAll(tempFolder)
270         invalidDestFolder := filepath.Join(tempFolder, "invalidDest")
271         // Create a src file
272         srcFile := filepath.Join(tempFolder, "src")
273         tarFile := filepath.Join(tempFolder, "src.tar")
274         os.Create(srcFile)
275         os.Create(invalidDestFolder) // being a file (not dir) should cause an error
276
277         // Translate back to Unix semantics as next exec.Command is run under sh
278         srcFileU := srcFile
279         tarFileU := tarFile
280         if runtime.GOOS == "windows" {
281                 tarFileU = "/tmp/" + filepath.Base(filepath.Dir(tarFile)) + "/src.tar"
282                 srcFileU = "/tmp/" + filepath.Base(filepath.Dir(srcFile)) + "/src"
283         }
284
285         cmd := exec.Command("sh", "-c", "tar cf "+tarFileU+" "+srcFileU)
286         _, err = cmd.CombinedOutput()
287         if err != nil {
288                 t.Fatal(err)
289         }
290
291         err = defaultUntarPath(tarFile, invalidDestFolder)
292         if err == nil {
293                 t.Fatalf("UntarPath with invalid destination path should throw an error.")
294         }
295 }
296
297 func TestUntarPathWithInvalidSrc(t *testing.T) {
298         dest, err := ioutil.TempDir("", "docker-archive-test")
299         if err != nil {
300                 t.Fatalf("Fail to create the destination file")
301         }
302         defer os.RemoveAll(dest)
303         err = defaultUntarPath("/invalid/path", dest)
304         if err == nil {
305                 t.Fatalf("UntarPath with invalid src path should throw an error.")
306         }
307 }
308
309 func TestUntarPath(t *testing.T) {
310         tmpFolder, err := ioutil.TempDir("", "docker-archive-test")
311         if err != nil {
312                 t.Fatal(err)
313         }
314         defer os.RemoveAll(tmpFolder)
315         srcFile := filepath.Join(tmpFolder, "src")
316         tarFile := filepath.Join(tmpFolder, "src.tar")
317         os.Create(filepath.Join(tmpFolder, "src"))
318
319         destFolder := filepath.Join(tmpFolder, "dest")
320         err = os.MkdirAll(destFolder, 0740)
321         if err != nil {
322                 t.Fatalf("Fail to create the destination file")
323         }
324
325         // Translate back to Unix semantics as next exec.Command is run under sh
326         srcFileU := srcFile
327         tarFileU := tarFile
328         if runtime.GOOS == "windows" {
329                 tarFileU = "/tmp/" + filepath.Base(filepath.Dir(tarFile)) + "/src.tar"
330                 srcFileU = "/tmp/" + filepath.Base(filepath.Dir(srcFile)) + "/src"
331         }
332         cmd := exec.Command("sh", "-c", "tar cf "+tarFileU+" "+srcFileU)
333         _, err = cmd.CombinedOutput()
334         if err != nil {
335                 t.Fatal(err)
336         }
337
338         err = defaultUntarPath(tarFile, destFolder)
339         if err != nil {
340                 t.Fatalf("UntarPath shouldn't throw an error, %s.", err)
341         }
342         expectedFile := filepath.Join(destFolder, srcFileU)
343         _, err = os.Stat(expectedFile)
344         if err != nil {
345                 t.Fatalf("Destination folder should contain the source file but did not.")
346         }
347 }
348
349 // Do the same test as above but with the destination as file, it should fail
350 func TestUntarPathWithDestinationFile(t *testing.T) {
351         tmpFolder, err := ioutil.TempDir("", "docker-archive-test")
352         if err != nil {
353                 t.Fatal(err)
354         }
355         defer os.RemoveAll(tmpFolder)
356         srcFile := filepath.Join(tmpFolder, "src")
357         tarFile := filepath.Join(tmpFolder, "src.tar")
358         os.Create(filepath.Join(tmpFolder, "src"))
359
360         // Translate back to Unix semantics as next exec.Command is run under sh
361         srcFileU := srcFile
362         tarFileU := tarFile
363         if runtime.GOOS == "windows" {
364                 tarFileU = "/tmp/" + filepath.Base(filepath.Dir(tarFile)) + "/src.tar"
365                 srcFileU = "/tmp/" + filepath.Base(filepath.Dir(srcFile)) + "/src"
366         }
367         cmd := exec.Command("sh", "-c", "tar cf "+tarFileU+" "+srcFileU)
368         _, err = cmd.CombinedOutput()
369         if err != nil {
370                 t.Fatal(err)
371         }
372         destFile := filepath.Join(tmpFolder, "dest")
373         _, err = os.Create(destFile)
374         if err != nil {
375                 t.Fatalf("Fail to create the destination file")
376         }
377         err = defaultUntarPath(tarFile, destFile)
378         if err == nil {
379                 t.Fatalf("UntarPath should throw an error if the destination if a file")
380         }
381 }
382
383 // Do the same test as above but with the destination folder already exists
384 // and the destination file is a directory
385 // It's working, see https://github.com/docker/docker/issues/10040
386 func TestUntarPathWithDestinationSrcFileAsFolder(t *testing.T) {
387         tmpFolder, err := ioutil.TempDir("", "docker-archive-test")
388         if err != nil {
389                 t.Fatal(err)
390         }
391         defer os.RemoveAll(tmpFolder)
392         srcFile := filepath.Join(tmpFolder, "src")
393         tarFile := filepath.Join(tmpFolder, "src.tar")
394         os.Create(srcFile)
395
396         // Translate back to Unix semantics as next exec.Command is run under sh
397         srcFileU := srcFile
398         tarFileU := tarFile
399         if runtime.GOOS == "windows" {
400                 tarFileU = "/tmp/" + filepath.Base(filepath.Dir(tarFile)) + "/src.tar"
401                 srcFileU = "/tmp/" + filepath.Base(filepath.Dir(srcFile)) + "/src"
402         }
403
404         cmd := exec.Command("sh", "-c", "tar cf "+tarFileU+" "+srcFileU)
405         _, err = cmd.CombinedOutput()
406         if err != nil {
407                 t.Fatal(err)
408         }
409         destFolder := filepath.Join(tmpFolder, "dest")
410         err = os.MkdirAll(destFolder, 0740)
411         if err != nil {
412                 t.Fatalf("Fail to create the destination folder")
413         }
414         // Let's create a folder that will has the same path as the extracted file (from tar)
415         destSrcFileAsFolder := filepath.Join(destFolder, srcFileU)
416         err = os.MkdirAll(destSrcFileAsFolder, 0740)
417         if err != nil {
418                 t.Fatal(err)
419         }
420         err = defaultUntarPath(tarFile, destFolder)
421         if err != nil {
422                 t.Fatalf("UntarPath should throw not throw an error if the extracted file already exists and is a folder")
423         }
424 }
425
426 func TestCopyWithTarInvalidSrc(t *testing.T) {
427         tempFolder, err := ioutil.TempDir("", "docker-archive-test")
428         if err != nil {
429                 t.Fatal(nil)
430         }
431         destFolder := filepath.Join(tempFolder, "dest")
432         invalidSrc := filepath.Join(tempFolder, "doesnotexists")
433         err = os.MkdirAll(destFolder, 0740)
434         if err != nil {
435                 t.Fatal(err)
436         }
437         err = defaultCopyWithTar(invalidSrc, destFolder)
438         if err == nil {
439                 t.Fatalf("archiver.CopyWithTar with invalid src path should throw an error.")
440         }
441 }
442
443 func TestCopyWithTarInexistentDestWillCreateIt(t *testing.T) {
444         tempFolder, err := ioutil.TempDir("", "docker-archive-test")
445         if err != nil {
446                 t.Fatal(nil)
447         }
448         srcFolder := filepath.Join(tempFolder, "src")
449         inexistentDestFolder := filepath.Join(tempFolder, "doesnotexists")
450         err = os.MkdirAll(srcFolder, 0740)
451         if err != nil {
452                 t.Fatal(err)
453         }
454         err = defaultCopyWithTar(srcFolder, inexistentDestFolder)
455         if err != nil {
456                 t.Fatalf("CopyWithTar with an inexistent folder shouldn't fail.")
457         }
458         _, err = os.Stat(inexistentDestFolder)
459         if err != nil {
460                 t.Fatalf("CopyWithTar with an inexistent folder should create it.")
461         }
462 }
463
464 // Test CopyWithTar with a file as src
465 func TestCopyWithTarSrcFile(t *testing.T) {
466         folder, err := ioutil.TempDir("", "docker-archive-test")
467         if err != nil {
468                 t.Fatal(err)
469         }
470         defer os.RemoveAll(folder)
471         dest := filepath.Join(folder, "dest")
472         srcFolder := filepath.Join(folder, "src")
473         src := filepath.Join(folder, filepath.Join("src", "src"))
474         err = os.MkdirAll(srcFolder, 0740)
475         if err != nil {
476                 t.Fatal(err)
477         }
478         err = os.MkdirAll(dest, 0740)
479         if err != nil {
480                 t.Fatal(err)
481         }
482         ioutil.WriteFile(src, []byte("content"), 0777)
483         err = defaultCopyWithTar(src, dest)
484         if err != nil {
485                 t.Fatalf("archiver.CopyWithTar shouldn't throw an error, %s.", err)
486         }
487         _, err = os.Stat(dest)
488         // FIXME Check the content
489         if err != nil {
490                 t.Fatalf("Destination file should be the same as the source.")
491         }
492 }
493
494 // Test CopyWithTar with a folder as src
495 func TestCopyWithTarSrcFolder(t *testing.T) {
496         folder, err := ioutil.TempDir("", "docker-archive-test")
497         if err != nil {
498                 t.Fatal(err)
499         }
500         defer os.RemoveAll(folder)
501         dest := filepath.Join(folder, "dest")
502         src := filepath.Join(folder, filepath.Join("src", "folder"))
503         err = os.MkdirAll(src, 0740)
504         if err != nil {
505                 t.Fatal(err)
506         }
507         err = os.MkdirAll(dest, 0740)
508         if err != nil {
509                 t.Fatal(err)
510         }
511         ioutil.WriteFile(filepath.Join(src, "file"), []byte("content"), 0777)
512         err = defaultCopyWithTar(src, dest)
513         if err != nil {
514                 t.Fatalf("archiver.CopyWithTar shouldn't throw an error, %s.", err)
515         }
516         _, err = os.Stat(dest)
517         // FIXME Check the content (the file inside)
518         if err != nil {
519                 t.Fatalf("Destination folder should contain the source file but did not.")
520         }
521 }
522
523 func TestCopyFileWithTarInvalidSrc(t *testing.T) {
524         tempFolder, err := ioutil.TempDir("", "docker-archive-test")
525         if err != nil {
526                 t.Fatal(err)
527         }
528         defer os.RemoveAll(tempFolder)
529         destFolder := filepath.Join(tempFolder, "dest")
530         err = os.MkdirAll(destFolder, 0740)
531         if err != nil {
532                 t.Fatal(err)
533         }
534         invalidFile := filepath.Join(tempFolder, "doesnotexists")
535         err = defaultCopyFileWithTar(invalidFile, destFolder)
536         if err == nil {
537                 t.Fatalf("archiver.CopyWithTar with invalid src path should throw an error.")
538         }
539 }
540
541 func TestCopyFileWithTarInexistentDestWillCreateIt(t *testing.T) {
542         tempFolder, err := ioutil.TempDir("", "docker-archive-test")
543         if err != nil {
544                 t.Fatal(nil)
545         }
546         defer os.RemoveAll(tempFolder)
547         srcFile := filepath.Join(tempFolder, "src")
548         inexistentDestFolder := filepath.Join(tempFolder, "doesnotexists")
549         _, err = os.Create(srcFile)
550         if err != nil {
551                 t.Fatal(err)
552         }
553         err = defaultCopyFileWithTar(srcFile, inexistentDestFolder)
554         if err != nil {
555                 t.Fatalf("CopyWithTar with an inexistent folder shouldn't fail.")
556         }
557         _, err = os.Stat(inexistentDestFolder)
558         if err != nil {
559                 t.Fatalf("CopyWithTar with an inexistent folder should create it.")
560         }
561         // FIXME Test the src file and content
562 }
563
564 func TestCopyFileWithTarSrcFolder(t *testing.T) {
565         folder, err := ioutil.TempDir("", "docker-archive-copyfilewithtar-test")
566         if err != nil {
567                 t.Fatal(err)
568         }
569         defer os.RemoveAll(folder)
570         dest := filepath.Join(folder, "dest")
571         src := filepath.Join(folder, "srcfolder")
572         err = os.MkdirAll(src, 0740)
573         if err != nil {
574                 t.Fatal(err)
575         }
576         err = os.MkdirAll(dest, 0740)
577         if err != nil {
578                 t.Fatal(err)
579         }
580         err = defaultCopyFileWithTar(src, dest)
581         if err == nil {
582                 t.Fatalf("CopyFileWithTar should throw an error with a folder.")
583         }
584 }
585
586 func TestCopyFileWithTarSrcFile(t *testing.T) {
587         folder, err := ioutil.TempDir("", "docker-archive-test")
588         if err != nil {
589                 t.Fatal(err)
590         }
591         defer os.RemoveAll(folder)
592         dest := filepath.Join(folder, "dest")
593         srcFolder := filepath.Join(folder, "src")
594         src := filepath.Join(folder, filepath.Join("src", "src"))
595         err = os.MkdirAll(srcFolder, 0740)
596         if err != nil {
597                 t.Fatal(err)
598         }
599         err = os.MkdirAll(dest, 0740)
600         if err != nil {
601                 t.Fatal(err)
602         }
603         ioutil.WriteFile(src, []byte("content"), 0777)
604         err = defaultCopyWithTar(src, dest+"/")
605         if err != nil {
606                 t.Fatalf("archiver.CopyFileWithTar shouldn't throw an error, %s.", err)
607         }
608         _, err = os.Stat(dest)
609         if err != nil {
610                 t.Fatalf("Destination folder should contain the source file but did not.")
611         }
612 }
613
614 func TestTarFiles(t *testing.T) {
615         // TODO Windows: Figure out how to port this test.
616         if runtime.GOOS == "windows" {
617                 t.Skip("Failing on Windows")
618         }
619         // try without hardlinks
620         if err := checkNoChanges(1000, false); err != nil {
621                 t.Fatal(err)
622         }
623         // try with hardlinks
624         if err := checkNoChanges(1000, true); err != nil {
625                 t.Fatal(err)
626         }
627 }
628
629 func checkNoChanges(fileNum int, hardlinks bool) error {
630         srcDir, err := ioutil.TempDir("", "docker-test-srcDir")
631         if err != nil {
632                 return err
633         }
634         defer os.RemoveAll(srcDir)
635
636         destDir, err := ioutil.TempDir("", "docker-test-destDir")
637         if err != nil {
638                 return err
639         }
640         defer os.RemoveAll(destDir)
641
642         _, err = prepareUntarSourceDirectory(fileNum, srcDir, hardlinks)
643         if err != nil {
644                 return err
645         }
646
647         err = defaultTarUntar(srcDir, destDir)
648         if err != nil {
649                 return err
650         }
651
652         changes, err := ChangesDirs(destDir, srcDir)
653         if err != nil {
654                 return err
655         }
656         if len(changes) > 0 {
657                 return fmt.Errorf("with %d files and %v hardlinks: expected 0 changes, got %d", fileNum, hardlinks, len(changes))
658         }
659         return nil
660 }
661
662 func tarUntar(t *testing.T, origin string, options *TarOptions) ([]Change, error) {
663         archive, err := TarWithOptions(origin, options)
664         if err != nil {
665                 t.Fatal(err)
666         }
667         defer archive.Close()
668
669         buf := make([]byte, 10)
670         if _, err := archive.Read(buf); err != nil {
671                 return nil, err
672         }
673         wrap := io.MultiReader(bytes.NewReader(buf), archive)
674
675         detectedCompression := DetectCompression(buf)
676         compression := options.Compression
677         if detectedCompression.Extension() != compression.Extension() {
678                 return nil, fmt.Errorf("Wrong compression detected. Actual compression: %s, found %s", compression.Extension(), detectedCompression.Extension())
679         }
680
681         tmp, err := ioutil.TempDir("", "docker-test-untar")
682         if err != nil {
683                 return nil, err
684         }
685         defer os.RemoveAll(tmp)
686         if err := Untar(wrap, tmp, nil); err != nil {
687                 return nil, err
688         }
689         if _, err := os.Stat(tmp); err != nil {
690                 return nil, err
691         }
692
693         return ChangesDirs(origin, tmp)
694 }
695
696 func TestTarUntar(t *testing.T) {
697         // TODO Windows: Figure out how to fix this test.
698         if runtime.GOOS == "windows" {
699                 t.Skip("Failing on Windows")
700         }
701         origin, err := ioutil.TempDir("", "docker-test-untar-origin")
702         if err != nil {
703                 t.Fatal(err)
704         }
705         defer os.RemoveAll(origin)
706         if err := ioutil.WriteFile(filepath.Join(origin, "1"), []byte("hello world"), 0700); err != nil {
707                 t.Fatal(err)
708         }
709         if err := ioutil.WriteFile(filepath.Join(origin, "2"), []byte("welcome!"), 0700); err != nil {
710                 t.Fatal(err)
711         }
712         if err := ioutil.WriteFile(filepath.Join(origin, "3"), []byte("will be ignored"), 0700); err != nil {
713                 t.Fatal(err)
714         }
715
716         for _, c := range []Compression{
717                 Uncompressed,
718                 Gzip,
719         } {
720                 changes, err := tarUntar(t, origin, &TarOptions{
721                         Compression:     c,
722                         ExcludePatterns: []string{"3"},
723                 })
724
725                 if err != nil {
726                         t.Fatalf("Error tar/untar for compression %s: %s", c.Extension(), err)
727                 }
728
729                 if len(changes) != 1 || changes[0].Path != "/3" {
730                         t.Fatalf("Unexpected differences after tarUntar: %v", changes)
731                 }
732         }
733 }
734
735 func TestTarWithOptions(t *testing.T) {
736         // TODO Windows: Figure out how to fix this test.
737         if runtime.GOOS == "windows" {
738                 t.Skip("Failing on Windows")
739         }
740         origin, err := ioutil.TempDir("", "docker-test-untar-origin")
741         if err != nil {
742                 t.Fatal(err)
743         }
744         if _, err := ioutil.TempDir(origin, "folder"); err != nil {
745                 t.Fatal(err)
746         }
747         defer os.RemoveAll(origin)
748         if err := ioutil.WriteFile(filepath.Join(origin, "1"), []byte("hello world"), 0700); err != nil {
749                 t.Fatal(err)
750         }
751         if err := ioutil.WriteFile(filepath.Join(origin, "2"), []byte("welcome!"), 0700); err != nil {
752                 t.Fatal(err)
753         }
754
755         cases := []struct {
756                 opts       *TarOptions
757                 numChanges int
758         }{
759                 {&TarOptions{IncludeFiles: []string{"1"}}, 2},
760                 {&TarOptions{ExcludePatterns: []string{"2"}}, 1},
761                 {&TarOptions{ExcludePatterns: []string{"1", "folder*"}}, 2},
762                 {&TarOptions{IncludeFiles: []string{"1", "1"}}, 2},
763                 {&TarOptions{IncludeFiles: []string{"1"}, RebaseNames: map[string]string{"1": "test"}}, 4},
764         }
765         for _, testCase := range cases {
766                 changes, err := tarUntar(t, origin, testCase.opts)
767                 if err != nil {
768                         t.Fatalf("Error tar/untar when testing inclusion/exclusion: %s", err)
769                 }
770                 if len(changes) != testCase.numChanges {
771                         t.Errorf("Expected %d changes, got %d for %+v:",
772                                 testCase.numChanges, len(changes), testCase.opts)
773                 }
774         }
775 }
776
777 // Some tar archives such as http://haproxy.1wt.eu/download/1.5/src/devel/haproxy-1.5-dev21.tar.gz
778 // use PAX Global Extended Headers.
779 // Failing prevents the archives from being uncompressed during ADD
780 func TestTypeXGlobalHeaderDoesNotFail(t *testing.T) {
781         hdr := tar.Header{Typeflag: tar.TypeXGlobalHeader}
782         tmpDir, err := ioutil.TempDir("", "docker-test-archive-pax-test")
783         if err != nil {
784                 t.Fatal(err)
785         }
786         defer os.RemoveAll(tmpDir)
787         err = createTarFile(filepath.Join(tmpDir, "pax_global_header"), tmpDir, &hdr, nil, true, nil, false)
788         if err != nil {
789                 t.Fatal(err)
790         }
791 }
792
793 // Some tar have both GNU specific (huge uid) and Ustar specific (long name) things.
794 // Not supposed to happen (should use PAX instead of Ustar for long name) but it does and it should still work.
795 func TestUntarUstarGnuConflict(t *testing.T) {
796         f, err := os.Open("testdata/broken.tar")
797         if err != nil {
798                 t.Fatal(err)
799         }
800         defer f.Close()
801
802         found := false
803         tr := tar.NewReader(f)
804         // Iterate through the files in the archive.
805         for {
806                 hdr, err := tr.Next()
807                 if err == io.EOF {
808                         // end of tar archive
809                         break
810                 }
811                 if err != nil {
812                         t.Fatal(err)
813                 }
814                 if hdr.Name == "root/.cpanm/work/1395823785.24209/Plack-1.0030/blib/man3/Plack::Middleware::LighttpdScriptNameFix.3pm" {
815                         found = true
816                         break
817                 }
818         }
819         if !found {
820                 t.Fatalf("%s not found in the archive", "root/.cpanm/work/1395823785.24209/Plack-1.0030/blib/man3/Plack::Middleware::LighttpdScriptNameFix.3pm")
821         }
822 }
823
824 func prepareUntarSourceDirectory(numberOfFiles int, targetPath string, makeLinks bool) (int, error) {
825         fileData := []byte("fooo")
826         for n := 0; n < numberOfFiles; n++ {
827                 fileName := fmt.Sprintf("file-%d", n)
828                 if err := ioutil.WriteFile(filepath.Join(targetPath, fileName), fileData, 0700); err != nil {
829                         return 0, err
830                 }
831                 if makeLinks {
832                         if err := os.Link(filepath.Join(targetPath, fileName), filepath.Join(targetPath, fileName+"-link")); err != nil {
833                                 return 0, err
834                         }
835                 }
836         }
837         totalSize := numberOfFiles * len(fileData)
838         return totalSize, nil
839 }
840
841 func BenchmarkTarUntar(b *testing.B) {
842         origin, err := ioutil.TempDir("", "docker-test-untar-origin")
843         if err != nil {
844                 b.Fatal(err)
845         }
846         tempDir, err := ioutil.TempDir("", "docker-test-untar-destination")
847         if err != nil {
848                 b.Fatal(err)
849         }
850         target := filepath.Join(tempDir, "dest")
851         n, err := prepareUntarSourceDirectory(100, origin, false)
852         if err != nil {
853                 b.Fatal(err)
854         }
855         defer os.RemoveAll(origin)
856         defer os.RemoveAll(tempDir)
857
858         b.ResetTimer()
859         b.SetBytes(int64(n))
860         for n := 0; n < b.N; n++ {
861                 err := defaultTarUntar(origin, target)
862                 if err != nil {
863                         b.Fatal(err)
864                 }
865                 os.RemoveAll(target)
866         }
867 }
868
869 func BenchmarkTarUntarWithLinks(b *testing.B) {
870         origin, err := ioutil.TempDir("", "docker-test-untar-origin")
871         if err != nil {
872                 b.Fatal(err)
873         }
874         tempDir, err := ioutil.TempDir("", "docker-test-untar-destination")
875         if err != nil {
876                 b.Fatal(err)
877         }
878         target := filepath.Join(tempDir, "dest")
879         n, err := prepareUntarSourceDirectory(100, origin, true)
880         if err != nil {
881                 b.Fatal(err)
882         }
883         defer os.RemoveAll(origin)
884         defer os.RemoveAll(tempDir)
885
886         b.ResetTimer()
887         b.SetBytes(int64(n))
888         for n := 0; n < b.N; n++ {
889                 err := defaultTarUntar(origin, target)
890                 if err != nil {
891                         b.Fatal(err)
892                 }
893                 os.RemoveAll(target)
894         }
895 }
896
897 func TestUntarInvalidFilenames(t *testing.T) {
898         // TODO Windows: Figure out how to fix this test.
899         if runtime.GOOS == "windows" {
900                 t.Skip("Passes but hits breakoutError: platform and architecture is not supported")
901         }
902         for i, headers := range [][]*tar.Header{
903                 {
904                         {
905                                 Name:     "../victim/dotdot",
906                                 Typeflag: tar.TypeReg,
907                                 Mode:     0644,
908                         },
909                 },
910                 {
911                         {
912                                 // Note the leading slash
913                                 Name:     "/../victim/slash-dotdot",
914                                 Typeflag: tar.TypeReg,
915                                 Mode:     0644,
916                         },
917                 },
918         } {
919                 if err := testBreakout("untar", "docker-TestUntarInvalidFilenames", headers); err != nil {
920                         t.Fatalf("i=%d. %v", i, err)
921                 }
922         }
923 }
924
925 func TestUntarHardlinkToSymlink(t *testing.T) {
926         // TODO Windows. There may be a way of running this, but turning off for now
927         if runtime.GOOS == "windows" {
928                 t.Skip("hardlinks on Windows")
929         }
930         for i, headers := range [][]*tar.Header{
931                 {
932                         {
933                                 Name:     "symlink1",
934                                 Typeflag: tar.TypeSymlink,
935                                 Linkname: "regfile",
936                                 Mode:     0644,
937                         },
938                         {
939                                 Name:     "symlink2",
940                                 Typeflag: tar.TypeLink,
941                                 Linkname: "symlink1",
942                                 Mode:     0644,
943                         },
944                         {
945                                 Name:     "regfile",
946                                 Typeflag: tar.TypeReg,
947                                 Mode:     0644,
948                         },
949                 },
950         } {
951                 if err := testBreakout("untar", "docker-TestUntarHardlinkToSymlink", headers); err != nil {
952                         t.Fatalf("i=%d. %v", i, err)
953                 }
954         }
955 }
956
957 func TestUntarInvalidHardlink(t *testing.T) {
958         // TODO Windows. There may be a way of running this, but turning off for now
959         if runtime.GOOS == "windows" {
960                 t.Skip("hardlinks on Windows")
961         }
962         for i, headers := range [][]*tar.Header{
963                 { // try reading victim/hello (../)
964                         {
965                                 Name:     "dotdot",
966                                 Typeflag: tar.TypeLink,
967                                 Linkname: "../victim/hello",
968                                 Mode:     0644,
969                         },
970                 },
971                 { // try reading victim/hello (/../)
972                         {
973                                 Name:     "slash-dotdot",
974                                 Typeflag: tar.TypeLink,
975                                 // Note the leading slash
976                                 Linkname: "/../victim/hello",
977                                 Mode:     0644,
978                         },
979                 },
980                 { // try writing victim/file
981                         {
982                                 Name:     "loophole-victim",
983                                 Typeflag: tar.TypeLink,
984                                 Linkname: "../victim",
985                                 Mode:     0755,
986                         },
987                         {
988                                 Name:     "loophole-victim/file",
989                                 Typeflag: tar.TypeReg,
990                                 Mode:     0644,
991                         },
992                 },
993                 { // try reading victim/hello (hardlink, symlink)
994                         {
995                                 Name:     "loophole-victim",
996                                 Typeflag: tar.TypeLink,
997                                 Linkname: "../victim",
998                                 Mode:     0755,
999                         },
1000                         {
1001                                 Name:     "symlink",
1002                                 Typeflag: tar.TypeSymlink,
1003                                 Linkname: "loophole-victim/hello",
1004                                 Mode:     0644,
1005                         },
1006                 },
1007                 { // Try reading victim/hello (hardlink, hardlink)
1008                         {
1009                                 Name:     "loophole-victim",
1010                                 Typeflag: tar.TypeLink,
1011                                 Linkname: "../victim",
1012                                 Mode:     0755,
1013                         },
1014                         {
1015                                 Name:     "hardlink",
1016                                 Typeflag: tar.TypeLink,
1017                                 Linkname: "loophole-victim/hello",
1018                                 Mode:     0644,
1019                         },
1020                 },
1021                 { // Try removing victim directory (hardlink)
1022                         {
1023                                 Name:     "loophole-victim",
1024                                 Typeflag: tar.TypeLink,
1025                                 Linkname: "../victim",
1026                                 Mode:     0755,
1027                         },
1028                         {
1029                                 Name:     "loophole-victim",
1030                                 Typeflag: tar.TypeReg,
1031                                 Mode:     0644,
1032                         },
1033                 },
1034         } {
1035                 if err := testBreakout("untar", "docker-TestUntarInvalidHardlink", headers); err != nil {
1036                         t.Fatalf("i=%d. %v", i, err)
1037                 }
1038         }
1039 }
1040
1041 func TestUntarInvalidSymlink(t *testing.T) {
1042         // TODO Windows. There may be a way of running this, but turning off for now
1043         if runtime.GOOS == "windows" {
1044                 t.Skip("hardlinks on Windows")
1045         }
1046         for i, headers := range [][]*tar.Header{
1047                 { // try reading victim/hello (../)
1048                         {
1049                                 Name:     "dotdot",
1050                                 Typeflag: tar.TypeSymlink,
1051                                 Linkname: "../victim/hello",
1052                                 Mode:     0644,
1053                         },
1054                 },
1055                 { // try reading victim/hello (/../)
1056                         {
1057                                 Name:     "slash-dotdot",
1058                                 Typeflag: tar.TypeSymlink,
1059                                 // Note the leading slash
1060                                 Linkname: "/../victim/hello",
1061                                 Mode:     0644,
1062                         },
1063                 },
1064                 { // try writing victim/file
1065                         {
1066                                 Name:     "loophole-victim",
1067                                 Typeflag: tar.TypeSymlink,
1068                                 Linkname: "../victim",
1069                                 Mode:     0755,
1070                         },
1071                         {
1072                                 Name:     "loophole-victim/file",
1073                                 Typeflag: tar.TypeReg,
1074                                 Mode:     0644,
1075                         },
1076                 },
1077                 { // try reading victim/hello (symlink, symlink)
1078                         {
1079                                 Name:     "loophole-victim",
1080                                 Typeflag: tar.TypeSymlink,
1081                                 Linkname: "../victim",
1082                                 Mode:     0755,
1083                         },
1084                         {
1085                                 Name:     "symlink",
1086                                 Typeflag: tar.TypeSymlink,
1087                                 Linkname: "loophole-victim/hello",
1088                                 Mode:     0644,
1089                         },
1090                 },
1091                 { // try reading victim/hello (symlink, hardlink)
1092                         {
1093                                 Name:     "loophole-victim",
1094                                 Typeflag: tar.TypeSymlink,
1095                                 Linkname: "../victim",
1096                                 Mode:     0755,
1097                         },
1098                         {
1099                                 Name:     "hardlink",
1100                                 Typeflag: tar.TypeLink,
1101                                 Linkname: "loophole-victim/hello",
1102                                 Mode:     0644,
1103                         },
1104                 },
1105                 { // try removing victim directory (symlink)
1106                         {
1107                                 Name:     "loophole-victim",
1108                                 Typeflag: tar.TypeSymlink,
1109                                 Linkname: "../victim",
1110                                 Mode:     0755,
1111                         },
1112                         {
1113                                 Name:     "loophole-victim",
1114                                 Typeflag: tar.TypeReg,
1115                                 Mode:     0644,
1116                         },
1117                 },
1118                 { // try writing to victim/newdir/newfile with a symlink in the path
1119                         {
1120                                 // this header needs to be before the next one, or else there is an error
1121                                 Name:     "dir/loophole",
1122                                 Typeflag: tar.TypeSymlink,
1123                                 Linkname: "../../victim",
1124                                 Mode:     0755,
1125                         },
1126                         {
1127                                 Name:     "dir/loophole/newdir/newfile",
1128                                 Typeflag: tar.TypeReg,
1129                                 Mode:     0644,
1130                         },
1131                 },
1132         } {
1133                 if err := testBreakout("untar", "docker-TestUntarInvalidSymlink", headers); err != nil {
1134                         t.Fatalf("i=%d. %v", i, err)
1135                 }
1136         }
1137 }
1138
1139 func TestTempArchiveCloseMultipleTimes(t *testing.T) {
1140         reader := ioutil.NopCloser(strings.NewReader("hello"))
1141         tempArchive, err := NewTempArchive(reader, "")
1142         buf := make([]byte, 10)
1143         n, err := tempArchive.Read(buf)
1144         if n != 5 {
1145                 t.Fatalf("Expected to read 5 bytes. Read %d instead", n)
1146         }
1147         for i := 0; i < 3; i++ {
1148                 if err = tempArchive.Close(); err != nil {
1149                         t.Fatalf("i=%d. Unexpected error closing temp archive: %v", i, err)
1150                 }
1151         }
1152 }
1153
1154 func TestReplaceFileTarWrapper(t *testing.T) {
1155         filesInArchive := 20
1156         testcases := []struct {
1157                 doc       string
1158                 filename  string
1159                 modifier  TarModifierFunc
1160                 expected  string
1161                 fileCount int
1162         }{
1163                 {
1164                         doc:       "Modifier creates a new file",
1165                         filename:  "newfile",
1166                         modifier:  createModifier(t),
1167                         expected:  "the new content",
1168                         fileCount: filesInArchive + 1,
1169                 },
1170                 {
1171                         doc:       "Modifier replaces a file",
1172                         filename:  "file-2",
1173                         modifier:  createOrReplaceModifier,
1174                         expected:  "the new content",
1175                         fileCount: filesInArchive,
1176                 },
1177                 {
1178                         doc:       "Modifier replaces the last file",
1179                         filename:  fmt.Sprintf("file-%d", filesInArchive-1),
1180                         modifier:  createOrReplaceModifier,
1181                         expected:  "the new content",
1182                         fileCount: filesInArchive,
1183                 },
1184                 {
1185                         doc:       "Modifier appends to a file",
1186                         filename:  "file-3",
1187                         modifier:  appendModifier,
1188                         expected:  "fooo\nnext line",
1189                         fileCount: filesInArchive,
1190                 },
1191         }
1192
1193         for _, testcase := range testcases {
1194                 sourceArchive, cleanup := buildSourceArchive(t, filesInArchive)
1195                 defer cleanup()
1196
1197                 resultArchive := ReplaceFileTarWrapper(
1198                         sourceArchive,
1199                         map[string]TarModifierFunc{testcase.filename: testcase.modifier})
1200
1201                 actual := readFileFromArchive(t, resultArchive, testcase.filename, testcase.fileCount, testcase.doc)
1202                 assert.Equal(t, testcase.expected, actual, testcase.doc)
1203         }
1204 }
1205
1206 func buildSourceArchive(t *testing.T, numberOfFiles int) (io.ReadCloser, func()) {
1207         srcDir, err := ioutil.TempDir("", "docker-test-srcDir")
1208         require.NoError(t, err)
1209
1210         _, err = prepareUntarSourceDirectory(numberOfFiles, srcDir, false)
1211         require.NoError(t, err)
1212
1213         sourceArchive, err := TarWithOptions(srcDir, &TarOptions{})
1214         require.NoError(t, err)
1215         return sourceArchive, func() {
1216                 os.RemoveAll(srcDir)
1217                 sourceArchive.Close()
1218         }
1219 }
1220
1221 func createOrReplaceModifier(path string, header *tar.Header, content io.Reader) (*tar.Header, []byte, error) {
1222         return &tar.Header{
1223                 Mode:     0600,
1224                 Typeflag: tar.TypeReg,
1225         }, []byte("the new content"), nil
1226 }
1227
1228 func createModifier(t *testing.T) TarModifierFunc {
1229         return func(path string, header *tar.Header, content io.Reader) (*tar.Header, []byte, error) {
1230                 assert.Nil(t, content)
1231                 return createOrReplaceModifier(path, header, content)
1232         }
1233 }
1234
1235 func appendModifier(path string, header *tar.Header, content io.Reader) (*tar.Header, []byte, error) {
1236         buffer := bytes.Buffer{}
1237         if content != nil {
1238                 if _, err := buffer.ReadFrom(content); err != nil {
1239                         return nil, nil, err
1240                 }
1241         }
1242         buffer.WriteString("\nnext line")
1243         return &tar.Header{Mode: 0600, Typeflag: tar.TypeReg}, buffer.Bytes(), nil
1244 }
1245
1246 func readFileFromArchive(t *testing.T, archive io.ReadCloser, name string, expectedCount int, doc string) string {
1247         destDir, err := ioutil.TempDir("", "docker-test-destDir")
1248         require.NoError(t, err)
1249         defer os.RemoveAll(destDir)
1250
1251         err = Untar(archive, destDir, nil)
1252         require.NoError(t, err)
1253
1254         files, _ := ioutil.ReadDir(destDir)
1255         assert.Len(t, files, expectedCount, doc)
1256
1257         content, err := ioutil.ReadFile(filepath.Join(destDir, name))
1258         assert.NoError(t, err)
1259         return string(content)
1260 }