github.com/noqcks/syft@v0.0.0-20230920222752-a9e2c4e288e5/internal/file/zip_file_traversal_test.go (about)

     1  //go:build !windows
     2  // +build !windows
     3  
     4  package file
     5  
     6  import (
     7  	"crypto/sha256"
     8  	"encoding/json"
     9  	"errors"
    10  	"fmt"
    11  	"io"
    12  	"os"
    13  	"path"
    14  	"path/filepath"
    15  	"strings"
    16  	"testing"
    17  
    18  	"github.com/go-test/deep"
    19  	"github.com/stretchr/testify/assert"
    20  )
    21  
    22  func equal(r1, r2 io.Reader) (bool, error) {
    23  	w1 := sha256.New()
    24  	w2 := sha256.New()
    25  	n1, err1 := io.Copy(w1, r1)
    26  	if err1 != nil {
    27  		return false, err1
    28  	}
    29  	n2, err2 := io.Copy(w2, r2)
    30  	if err2 != nil {
    31  		return false, err2
    32  	}
    33  
    34  	var b1, b2 [sha256.Size]byte
    35  	copy(b1[:], w1.Sum(nil))
    36  	copy(b2[:], w2.Sum(nil))
    37  
    38  	return n1 != n2 || b1 == b2, nil
    39  }
    40  
    41  func TestUnzipToDir(t *testing.T) {
    42  	cwd, err := os.Getwd()
    43  	if err != nil {
    44  		t.Fatal(err)
    45  	}
    46  
    47  	goldenRootDir := filepath.Join(cwd, "test-fixtures")
    48  	sourceDirPath := path.Join(goldenRootDir, "zip-source")
    49  	archiveFilePath := setupZipFileTest(t, sourceDirPath, false)
    50  
    51  	unzipDestinationDir := t.TempDir()
    52  
    53  	t.Logf("content path: %s", unzipDestinationDir)
    54  
    55  	expectedPaths := len(expectedZipArchiveEntries)
    56  	observedPaths := 0
    57  
    58  	err = UnzipToDir(archiveFilePath, unzipDestinationDir)
    59  	if err != nil {
    60  		t.Fatalf("unable to unzip archive: %+v", err)
    61  	}
    62  
    63  	// compare the source dir tree and the unzipped tree
    64  	err = filepath.Walk(unzipDestinationDir,
    65  		func(path string, info os.FileInfo, err error) error {
    66  			// We don't unzip the root archive dir, since there's no archive entry for it
    67  			if path != unzipDestinationDir {
    68  				t.Logf("unzipped path: %s", path)
    69  				observedPaths++
    70  			}
    71  
    72  			if err != nil {
    73  				t.Fatalf("this should not happen")
    74  				return err
    75  			}
    76  
    77  			goldenPath := filepath.Join(sourceDirPath, strings.TrimPrefix(path, unzipDestinationDir))
    78  
    79  			if info.IsDir() {
    80  				i, err := os.Stat(goldenPath)
    81  				if err != nil {
    82  					t.Fatalf("unable to stat golden path: %+v", err)
    83  				}
    84  				if !i.IsDir() {
    85  					t.Fatalf("mismatched file types: %s", goldenPath)
    86  				}
    87  				return nil
    88  			}
    89  
    90  			// this is a file, not a dir...
    91  
    92  			testFile, err := os.Open(path)
    93  			if err != nil {
    94  				t.Fatalf("unable to open test file=%s :%+v", path, err)
    95  			}
    96  
    97  			goldenFile, err := os.Open(goldenPath)
    98  			if err != nil {
    99  				t.Fatalf("unable to open golden file=%s :%+v", goldenPath, err)
   100  			}
   101  
   102  			same, err := equal(testFile, goldenFile)
   103  			if err != nil {
   104  				t.Fatalf("could not compare files (%s, %s): %+v", goldenPath, path, err)
   105  			}
   106  
   107  			if !same {
   108  				t.Errorf("paths are not the same (%s, %s)", goldenPath, path)
   109  			}
   110  
   111  			return nil
   112  		})
   113  
   114  	if err != nil {
   115  		t.Errorf("failed to walk dir: %+v", err)
   116  	}
   117  
   118  	if observedPaths != expectedPaths {
   119  		t.Errorf("missed test paths: %d != %d", observedPaths, expectedPaths)
   120  	}
   121  }
   122  
   123  func TestContentsFromZip(t *testing.T) {
   124  	tests := []struct {
   125  		name        string
   126  		archivePrep func(tb testing.TB) string
   127  	}{
   128  		{
   129  			name:        "standard, non-nested zip",
   130  			archivePrep: prepZipSourceFixture,
   131  		},
   132  		{
   133  			name:        "zip with prepended bytes",
   134  			archivePrep: prependZipSourceFixtureWithString(t, "junk at the beginning of the file..."),
   135  		},
   136  	}
   137  
   138  	for _, test := range tests {
   139  		t.Run(test.name, func(t *testing.T) {
   140  			archivePath := test.archivePrep(t)
   141  			expected := zipSourceFixtureExpectedContents()
   142  
   143  			var paths []string
   144  			for p := range expected {
   145  				paths = append(paths, p)
   146  			}
   147  
   148  			actual, err := ContentsFromZip(archivePath, paths...)
   149  			if err != nil {
   150  				t.Fatalf("unable to extract from unzip archive: %+v", err)
   151  			}
   152  
   153  			assertZipSourceFixtureContents(t, actual, expected)
   154  		})
   155  	}
   156  }
   157  
   158  func prependZipSourceFixtureWithString(tb testing.TB, value string) func(tb testing.TB) string {
   159  	if len(value) == 0 {
   160  		tb.Fatalf("no bytes given to prefix")
   161  	}
   162  	return func(t testing.TB) string {
   163  		archivePath := prepZipSourceFixture(t)
   164  
   165  		// create a temp file
   166  		tmpFile, err := os.CreateTemp(tb.TempDir(), "syft-ziputil-prependZipSourceFixtureWithString-")
   167  		if err != nil {
   168  			t.Fatalf("unable to create tempfile: %+v", err)
   169  		}
   170  		defer tmpFile.Close()
   171  
   172  		// write value to the temp file
   173  		if _, err := tmpFile.WriteString(value); err != nil {
   174  			t.Fatalf("unable to write to tempfile: %+v", err)
   175  		}
   176  
   177  		// open the original archive
   178  		sourceFile, err := os.Open(archivePath)
   179  		if err != nil {
   180  			t.Fatalf("unable to read source file: %+v", err)
   181  		}
   182  
   183  		// copy all contents from the archive to the temp file
   184  		if _, err := io.Copy(tmpFile, sourceFile); err != nil {
   185  			t.Fatalf("unable to copy source to dest: %+v", err)
   186  		}
   187  
   188  		sourceFile.Close()
   189  
   190  		// remove the original archive and replace it with the temp file
   191  		if err := os.Remove(archivePath); err != nil {
   192  			t.Fatalf("unable to remove original source archive (%q): %+v", archivePath, err)
   193  		}
   194  
   195  		if err := os.Rename(tmpFile.Name(), archivePath); err != nil {
   196  			t.Fatalf("unable to move new archive to old path (%q): %+v", tmpFile.Name(), err)
   197  		}
   198  
   199  		return archivePath
   200  	}
   201  }
   202  
   203  func prepZipSourceFixture(t testing.TB) string {
   204  	t.Helper()
   205  	archivePrefix := path.Join(t.TempDir(), "syft-ziputil-prepZipSourceFixture-")
   206  
   207  	// the zip utility will add ".zip" to the end of the given name
   208  	archivePath := archivePrefix + ".zip"
   209  
   210  	t.Logf("archive path: %s", archivePath)
   211  
   212  	createZipArchive(t, "zip-source", archivePrefix, false)
   213  
   214  	return archivePath
   215  }
   216  
   217  func zipSourceFixtureExpectedContents() map[string]string {
   218  	return map[string]string{
   219  		filepath.Join("some-dir", "a-file.txt"): "A file! nice!",
   220  		filepath.Join("b-file.txt"):             "B file...",
   221  	}
   222  }
   223  
   224  func assertZipSourceFixtureContents(t testing.TB, actual map[string]string, expected map[string]string) {
   225  	t.Helper()
   226  	diffs := deep.Equal(actual, expected)
   227  	if len(diffs) > 0 {
   228  		for _, d := range diffs {
   229  			t.Errorf("diff: %+v", d)
   230  		}
   231  
   232  		b, err := json.MarshalIndent(actual, "", "  ")
   233  		if err != nil {
   234  			t.Fatalf("can't show results: %+v", err)
   235  		}
   236  
   237  		t.Errorf("full result: %s", string(b))
   238  	}
   239  }
   240  
   241  // looks like there isn't a helper for this yet? https://github.com/stretchr/testify/issues/497
   242  func assertErrorAs(expectedErr interface{}) assert.ErrorAssertionFunc {
   243  	return func(t assert.TestingT, actualErr error, i ...interface{}) bool {
   244  		return errors.As(actualErr, &expectedErr)
   245  	}
   246  }
   247  
   248  func TestSafeJoin(t *testing.T) {
   249  	tests := []struct {
   250  		prefix       string
   251  		args         []string
   252  		expected     string
   253  		errAssertion assert.ErrorAssertionFunc
   254  	}{
   255  		// go cases...
   256  		{
   257  			prefix: "/a/place",
   258  			args: []string{
   259  				"somewhere/else",
   260  			},
   261  			expected:     "/a/place/somewhere/else",
   262  			errAssertion: assert.NoError,
   263  		},
   264  		{
   265  			prefix: "/a/place",
   266  			args: []string{
   267  				"somewhere/../else",
   268  			},
   269  			expected:     "/a/place/else",
   270  			errAssertion: assert.NoError,
   271  		},
   272  		{
   273  			prefix: "/a/../place",
   274  			args: []string{
   275  				"somewhere/else",
   276  			},
   277  			expected:     "/place/somewhere/else",
   278  			errAssertion: assert.NoError,
   279  		},
   280  		// zip slip examples....
   281  		{
   282  			prefix: "/a/place",
   283  			args: []string{
   284  				"../../../etc/passwd",
   285  			},
   286  			expected:     "",
   287  			errAssertion: assertErrorAs(&errZipSlipDetected{}),
   288  		},
   289  		{
   290  			prefix: "/a/place",
   291  			args: []string{
   292  				"../",
   293  				"../",
   294  			},
   295  			expected:     "",
   296  			errAssertion: assertErrorAs(&errZipSlipDetected{}),
   297  		},
   298  		{
   299  			prefix: "/a/place",
   300  			args: []string{
   301  				"../",
   302  			},
   303  			expected:     "",
   304  			errAssertion: assertErrorAs(&errZipSlipDetected{}),
   305  		},
   306  	}
   307  
   308  	for _, test := range tests {
   309  		t.Run(fmt.Sprintf("%+v:%+v", test.prefix, test.args), func(t *testing.T) {
   310  			actual, err := safeJoin(test.prefix, test.args...)
   311  			test.errAssertion(t, err)
   312  			assert.Equal(t, test.expected, actual)
   313  		})
   314  	}
   315  }