github.com/anchore/syft@v1.38.2/syft/source/filesource/file_source_test.go (about)

     1  package filesource
     2  
     3  import (
     4  	"io"
     5  	"os"
     6  	"os/exec"
     7  	"path"
     8  	"path/filepath"
     9  	"syscall"
    10  	"testing"
    11  
    12  	"github.com/stretchr/testify/assert"
    13  	"github.com/stretchr/testify/require"
    14  
    15  	"github.com/anchore/syft/syft/artifact"
    16  	"github.com/anchore/syft/syft/file"
    17  	"github.com/anchore/syft/syft/internal/testutil"
    18  	"github.com/anchore/syft/syft/source"
    19  )
    20  
    21  func TestNewFromFile(t *testing.T) {
    22  	testutil.Chdir(t, "..") // run with source/test-fixtures
    23  
    24  	testCases := []struct {
    25  		desc       string
    26  		input      string
    27  		expString  string
    28  		testPathFn func(file.Resolver) ([]file.Location, error)
    29  		expRefs    int
    30  	}{
    31  		{
    32  			desc:  "path detected by glob",
    33  			input: "test-fixtures/file-index-filter/.vimrc",
    34  			testPathFn: func(resolver file.Resolver) ([]file.Location, error) {
    35  				return resolver.FilesByGlob("**/.vimrc", "**/.2", "**/.1/*", "**/empty")
    36  			},
    37  			expRefs: 1,
    38  		},
    39  		{
    40  			desc:  "path detected by abs path",
    41  			input: "test-fixtures/file-index-filter/.vimrc",
    42  			testPathFn: func(resolver file.Resolver) ([]file.Location, error) {
    43  				return resolver.FilesByPath("/.vimrc", "/.2", "/.1/something", "/empty")
    44  			},
    45  			expRefs: 1,
    46  		},
    47  		{
    48  			desc:  "path detected by relative path",
    49  			input: "test-fixtures/file-index-filter/.vimrc",
    50  			testPathFn: func(resolver file.Resolver) ([]file.Location, error) {
    51  				return resolver.FilesByPath(".vimrc", "/.2", "/.1/something", "empty")
    52  			},
    53  			expRefs: 1,
    54  		},
    55  		{
    56  			desc:  "normal path",
    57  			input: "test-fixtures/actual-path/empty",
    58  			testPathFn: func(resolver file.Resolver) ([]file.Location, error) {
    59  				return resolver.FilesByPath("empty")
    60  			},
    61  			expRefs: 1,
    62  		},
    63  		{
    64  			desc:  "path containing symlink",
    65  			input: "test-fixtures/symlink/empty",
    66  			testPathFn: func(resolver file.Resolver) ([]file.Location, error) {
    67  				return resolver.FilesByPath("empty")
    68  			},
    69  			expRefs: 1,
    70  		},
    71  	}
    72  	for _, test := range testCases {
    73  		t.Run(test.desc, func(t *testing.T) {
    74  			src, err := New(Config{
    75  				Path: test.input,
    76  			})
    77  			require.NoError(t, err)
    78  			t.Cleanup(func() {
    79  				require.NoError(t, src.Close())
    80  			})
    81  
    82  			assert.Equal(t, test.input, src.Describe().Metadata.(source.FileMetadata).Path)
    83  
    84  			res, err := src.FileResolver(source.SquashedScope)
    85  			require.NoError(t, err)
    86  
    87  			refs, err := test.testPathFn(res)
    88  			require.NoError(t, err)
    89  			require.Len(t, refs, test.expRefs)
    90  			if test.expRefs == 1 {
    91  				assert.Equal(t, path.Base(test.input), path.Base(refs[0].RealPath))
    92  			}
    93  
    94  		})
    95  	}
    96  }
    97  
    98  func TestNewFromFile_WithArchive(t *testing.T) {
    99  	testutil.Chdir(t, "..") // run with source/test-fixtures
   100  
   101  	testCases := []struct {
   102  		desc               string
   103  		input              string
   104  		expString          string
   105  		inputPaths         []string
   106  		expRefs            int
   107  		layer2             bool
   108  		contents           string
   109  		skipExtractArchive bool
   110  	}{
   111  		{
   112  			desc:       "path detected",
   113  			input:      "test-fixtures/path-detected",
   114  			inputPaths: []string{"/.vimrc"},
   115  			expRefs:    1,
   116  		},
   117  		{
   118  			desc:       "use first entry for duplicate paths",
   119  			input:      "test-fixtures/path-detected",
   120  			inputPaths: []string{"/.vimrc"},
   121  			expRefs:    1,
   122  			layer2:     true,
   123  			contents:   "Another .vimrc file",
   124  		},
   125  		{
   126  			desc:               "skip extract archive",
   127  			input:              "test-fixtures/path-detected",
   128  			inputPaths:         []string{"/.vimrc"},
   129  			expRefs:            0,
   130  			layer2:             false,
   131  			skipExtractArchive: true,
   132  		},
   133  	}
   134  	for _, test := range testCases {
   135  		t.Run(test.desc, func(t *testing.T) {
   136  			archivePath := setupArchiveTest(t, test.input, test.layer2)
   137  
   138  			cfg := Config{
   139  				Path:               archivePath,
   140  				SkipExtractArchive: test.skipExtractArchive,
   141  			}
   142  
   143  			src, err := New(cfg)
   144  			require.NoError(t, err)
   145  			t.Cleanup(func() {
   146  				require.NoError(t, src.Close())
   147  			})
   148  
   149  			assert.Equal(t, archivePath, src.Describe().Metadata.(source.FileMetadata).Path)
   150  
   151  			res, err := src.FileResolver(source.SquashedScope)
   152  			require.NoError(t, err)
   153  
   154  			refs, err := res.FilesByPath(test.inputPaths...)
   155  			require.NoError(t, err)
   156  			assert.Len(t, refs, test.expRefs)
   157  
   158  			if test.contents != "" {
   159  				reader, err := res.FileContentsByLocation(refs[0])
   160  				require.NoError(t, err)
   161  
   162  				data, err := io.ReadAll(reader)
   163  				require.NoError(t, err)
   164  
   165  				assert.Equal(t, test.contents, string(data))
   166  			}
   167  
   168  		})
   169  	}
   170  }
   171  
   172  // setupArchiveTest encapsulates common test setup work for tar file tests. It returns a cleanup function,
   173  // which should be called (typically deferred) by the caller, the path of the created tar archive, and an error,
   174  // which should trigger a fatal test failure in the consuming test. The returned cleanup function will never be nil
   175  // (even if there's an error), and it should always be called.
   176  func setupArchiveTest(t testing.TB, sourceDirPath string, layer2 bool) string {
   177  	t.Helper()
   178  
   179  	archivePrefix, err := os.CreateTemp("", "syft-archive-TEST-")
   180  	require.NoError(t, err)
   181  
   182  	t.Cleanup(func() {
   183  		assert.NoError(t, os.Remove(archivePrefix.Name()))
   184  	})
   185  
   186  	destinationArchiveFilePath := archivePrefix.Name() + ".tar"
   187  	t.Logf("archive path: %s", destinationArchiveFilePath)
   188  	createArchive(t, sourceDirPath, destinationArchiveFilePath, layer2)
   189  
   190  	t.Cleanup(func() {
   191  		assert.NoError(t, os.Remove(destinationArchiveFilePath))
   192  	})
   193  
   194  	cwd, err := os.Getwd()
   195  	require.NoError(t, err)
   196  
   197  	t.Logf("running from: %s", cwd)
   198  
   199  	return destinationArchiveFilePath
   200  }
   201  
   202  // createArchive creates a new archive file at destinationArchivePath based on the directory found at sourceDirPath.
   203  func createArchive(t testing.TB, sourceDirPath, destinationArchivePath string, layer2 bool) {
   204  	t.Helper()
   205  
   206  	cwd, err := os.Getwd()
   207  	if err != nil {
   208  		t.Fatalf("unable to get cwd: %+v", err)
   209  	}
   210  
   211  	cmd := exec.Command("./generate-tar-fixture-from-source-dir.sh", destinationArchivePath, path.Base(sourceDirPath))
   212  	cmd.Dir = filepath.Join(cwd, "test-fixtures")
   213  
   214  	if err := cmd.Start(); err != nil {
   215  		t.Fatalf("unable to start generate zip fixture script: %+v", err)
   216  	}
   217  
   218  	if err := cmd.Wait(); err != nil {
   219  		if exiterr, ok := err.(*exec.ExitError); ok {
   220  			// The program has exited with an exit code != 0
   221  
   222  			// This works on both Unix and Windows. Although package
   223  			// syscall is generally platform dependent, WaitStatus is
   224  			// defined for both Unix and Windows and in both cases has
   225  			// an ExitStatus() method with the same signature.
   226  			if status, ok := exiterr.Sys().(syscall.WaitStatus); ok {
   227  				if status.ExitStatus() != 0 {
   228  					t.Fatalf("failed to generate fixture: rc=%d", status.ExitStatus())
   229  				}
   230  			}
   231  		} else {
   232  			t.Fatalf("unable to get generate fixture script result: %+v", err)
   233  		}
   234  	}
   235  
   236  	if layer2 {
   237  		cmd = exec.Command("tar", "-rvf", destinationArchivePath, ".")
   238  		cmd.Dir = filepath.Join(cwd, "test-fixtures", path.Base(sourceDirPath+"-2"))
   239  		if err := cmd.Start(); err != nil {
   240  			t.Fatalf("unable to start tar appending fixture script: %+v", err)
   241  		}
   242  		_ = cmd.Wait()
   243  	}
   244  }
   245  
   246  func Test_FileSource_ID(t *testing.T) {
   247  	testutil.Chdir(t, "..") // run with source/test-fixtures
   248  
   249  	tests := []struct {
   250  		name       string
   251  		cfg        Config
   252  		want       artifact.ID
   253  		wantDigest string
   254  		wantErr    require.ErrorAssertionFunc
   255  	}{
   256  		{
   257  			name:    "empty",
   258  			cfg:     Config{},
   259  			wantErr: require.Error,
   260  		},
   261  		{
   262  			name: "does not exist",
   263  			cfg: Config{
   264  				Path: "./test-fixtures/does-not-exist",
   265  			},
   266  			wantErr: require.Error,
   267  		},
   268  		{
   269  			name: "to dir",
   270  			cfg: Config{
   271  				Path: "./test-fixtures/image-simple",
   272  			},
   273  			wantErr: require.Error,
   274  		},
   275  		{
   276  			name:       "with path",
   277  			cfg:        Config{Path: "./test-fixtures/image-simple/Dockerfile"},
   278  			want:       artifact.ID("db7146472cf6d49b3ac01b42812fb60020b0b4898b97491b21bb690c808d5159"),
   279  			wantDigest: "sha256:38601c0bb4269a10ce1d00590ea7689c1117dd9274c758653934ab4f2016f80f",
   280  		},
   281  		{
   282  			name: "with path and alias",
   283  			cfg: Config{
   284  				Path: "./test-fixtures/image-simple/Dockerfile",
   285  				Alias: source.Alias{
   286  					Name:    "name-me-that!",
   287  					Version: "version-me-this!",
   288  				},
   289  			},
   290  			want:       artifact.ID("3c713003305ac6605255cec8bf4ea649aa44b2b9a9f3a07bd683869d1363438a"),
   291  			wantDigest: "sha256:38601c0bb4269a10ce1d00590ea7689c1117dd9274c758653934ab4f2016f80f",
   292  		},
   293  		{
   294  			name: "other fields do not affect ID",
   295  			cfg: Config{
   296  				Path: "test-fixtures/image-simple/Dockerfile",
   297  				Exclude: source.ExcludeConfig{
   298  					Paths: []string{"a", "b"},
   299  				},
   300  			},
   301  			want:       artifact.ID("db7146472cf6d49b3ac01b42812fb60020b0b4898b97491b21bb690c808d5159"),
   302  			wantDigest: "sha256:38601c0bb4269a10ce1d00590ea7689c1117dd9274c758653934ab4f2016f80f",
   303  		},
   304  	}
   305  	for _, tt := range tests {
   306  		t.Run(tt.name, func(t *testing.T) {
   307  			if tt.wantErr == nil {
   308  				tt.wantErr = require.NoError
   309  			}
   310  			newSource, err := New(tt.cfg)
   311  			tt.wantErr(t, err)
   312  			if err != nil {
   313  				return
   314  			}
   315  			s := newSource.(*fileSource)
   316  			assert.Equalf(t, tt.want, s.ID(), "ID() mismatch")
   317  			assert.Equalf(t, tt.wantDigest, s.digestForVersion, "digestForVersion mismatch")
   318  		})
   319  	}
   320  }