github.com/anchore/syft@v1.4.2-0.20240516191711-1bec1fc5d397/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  	}{
   110  		{
   111  			desc:       "path detected",
   112  			input:      "test-fixtures/path-detected",
   113  			inputPaths: []string{"/.vimrc"},
   114  			expRefs:    1,
   115  		},
   116  		{
   117  			desc:       "use first entry for duplicate paths",
   118  			input:      "test-fixtures/path-detected",
   119  			inputPaths: []string{"/.vimrc"},
   120  			expRefs:    1,
   121  			layer2:     true,
   122  			contents:   "Another .vimrc file",
   123  		},
   124  	}
   125  	for _, test := range testCases {
   126  		t.Run(test.desc, func(t *testing.T) {
   127  			archivePath := setupArchiveTest(t, test.input, test.layer2)
   128  
   129  			src, err := New(Config{
   130  				Path: archivePath,
   131  			})
   132  			require.NoError(t, err)
   133  			t.Cleanup(func() {
   134  				require.NoError(t, src.Close())
   135  			})
   136  
   137  			assert.Equal(t, archivePath, src.Describe().Metadata.(source.FileMetadata).Path)
   138  
   139  			res, err := src.FileResolver(source.SquashedScope)
   140  			require.NoError(t, err)
   141  
   142  			refs, err := res.FilesByPath(test.inputPaths...)
   143  			require.NoError(t, err)
   144  			assert.Len(t, refs, test.expRefs)
   145  
   146  			if test.contents != "" {
   147  				reader, err := res.FileContentsByLocation(refs[0])
   148  				require.NoError(t, err)
   149  
   150  				data, err := io.ReadAll(reader)
   151  				require.NoError(t, err)
   152  
   153  				assert.Equal(t, test.contents, string(data))
   154  			}
   155  
   156  		})
   157  	}
   158  }
   159  
   160  // setupArchiveTest encapsulates common test setup work for tar file tests. It returns a cleanup function,
   161  // which should be called (typically deferred) by the caller, the path of the created tar archive, and an error,
   162  // which should trigger a fatal test failure in the consuming test. The returned cleanup function will never be nil
   163  // (even if there's an error), and it should always be called.
   164  func setupArchiveTest(t testing.TB, sourceDirPath string, layer2 bool) string {
   165  	t.Helper()
   166  
   167  	archivePrefix, err := os.CreateTemp("", "syft-archive-TEST-")
   168  	require.NoError(t, err)
   169  
   170  	t.Cleanup(func() {
   171  		assert.NoError(t, os.Remove(archivePrefix.Name()))
   172  	})
   173  
   174  	destinationArchiveFilePath := archivePrefix.Name() + ".tar"
   175  	t.Logf("archive path: %s", destinationArchiveFilePath)
   176  	createArchive(t, sourceDirPath, destinationArchiveFilePath, layer2)
   177  
   178  	t.Cleanup(func() {
   179  		assert.NoError(t, os.Remove(destinationArchiveFilePath))
   180  	})
   181  
   182  	cwd, err := os.Getwd()
   183  	require.NoError(t, err)
   184  
   185  	t.Logf("running from: %s", cwd)
   186  
   187  	return destinationArchiveFilePath
   188  }
   189  
   190  // createArchive creates a new archive file at destinationArchivePath based on the directory found at sourceDirPath.
   191  func createArchive(t testing.TB, sourceDirPath, destinationArchivePath string, layer2 bool) {
   192  	t.Helper()
   193  
   194  	cwd, err := os.Getwd()
   195  	if err != nil {
   196  		t.Fatalf("unable to get cwd: %+v", err)
   197  	}
   198  
   199  	cmd := exec.Command("./generate-tar-fixture-from-source-dir.sh", destinationArchivePath, path.Base(sourceDirPath))
   200  	cmd.Dir = filepath.Join(cwd, "test-fixtures")
   201  
   202  	if err := cmd.Start(); err != nil {
   203  		t.Fatalf("unable to start generate zip fixture script: %+v", err)
   204  	}
   205  
   206  	if err := cmd.Wait(); err != nil {
   207  		if exiterr, ok := err.(*exec.ExitError); ok {
   208  			// The program has exited with an exit code != 0
   209  
   210  			// This works on both Unix and Windows. Although package
   211  			// syscall is generally platform dependent, WaitStatus is
   212  			// defined for both Unix and Windows and in both cases has
   213  			// an ExitStatus() method with the same signature.
   214  			if status, ok := exiterr.Sys().(syscall.WaitStatus); ok {
   215  				if status.ExitStatus() != 0 {
   216  					t.Fatalf("failed to generate fixture: rc=%d", status.ExitStatus())
   217  				}
   218  			}
   219  		} else {
   220  			t.Fatalf("unable to get generate fixture script result: %+v", err)
   221  		}
   222  	}
   223  
   224  	if layer2 {
   225  		cmd = exec.Command("tar", "-rvf", destinationArchivePath, ".")
   226  		cmd.Dir = filepath.Join(cwd, "test-fixtures", path.Base(sourceDirPath+"-2"))
   227  		if err := cmd.Start(); err != nil {
   228  			t.Fatalf("unable to start tar appending fixture script: %+v", err)
   229  		}
   230  		_ = cmd.Wait()
   231  	}
   232  }
   233  
   234  func Test_FileSource_ID(t *testing.T) {
   235  	testutil.Chdir(t, "..") // run with source/test-fixtures
   236  
   237  	tests := []struct {
   238  		name       string
   239  		cfg        Config
   240  		want       artifact.ID
   241  		wantDigest string
   242  		wantErr    require.ErrorAssertionFunc
   243  	}{
   244  		{
   245  			name:    "empty",
   246  			cfg:     Config{},
   247  			wantErr: require.Error,
   248  		},
   249  		{
   250  			name: "does not exist",
   251  			cfg: Config{
   252  				Path: "./test-fixtures/does-not-exist",
   253  			},
   254  			wantErr: require.Error,
   255  		},
   256  		{
   257  			name: "to dir",
   258  			cfg: Config{
   259  				Path: "./test-fixtures/image-simple",
   260  			},
   261  			wantErr: require.Error,
   262  		},
   263  		{
   264  			name:       "with path",
   265  			cfg:        Config{Path: "./test-fixtures/image-simple/Dockerfile"},
   266  			want:       artifact.ID("db7146472cf6d49b3ac01b42812fb60020b0b4898b97491b21bb690c808d5159"),
   267  			wantDigest: "sha256:38601c0bb4269a10ce1d00590ea7689c1117dd9274c758653934ab4f2016f80f",
   268  		},
   269  		{
   270  			name: "with path and alias",
   271  			cfg: Config{
   272  				Path: "./test-fixtures/image-simple/Dockerfile",
   273  				Alias: source.Alias{
   274  					Name:    "name-me-that!",
   275  					Version: "version-me-this!",
   276  				},
   277  			},
   278  			want:       artifact.ID("3c713003305ac6605255cec8bf4ea649aa44b2b9a9f3a07bd683869d1363438a"),
   279  			wantDigest: "sha256:38601c0bb4269a10ce1d00590ea7689c1117dd9274c758653934ab4f2016f80f",
   280  		},
   281  		{
   282  			name: "other fields do not affect ID",
   283  			cfg: Config{
   284  				Path: "test-fixtures/image-simple/Dockerfile",
   285  				Exclude: source.ExcludeConfig{
   286  					Paths: []string{"a", "b"},
   287  				},
   288  			},
   289  			want:       artifact.ID("db7146472cf6d49b3ac01b42812fb60020b0b4898b97491b21bb690c808d5159"),
   290  			wantDigest: "sha256:38601c0bb4269a10ce1d00590ea7689c1117dd9274c758653934ab4f2016f80f",
   291  		},
   292  	}
   293  	for _, tt := range tests {
   294  		t.Run(tt.name, func(t *testing.T) {
   295  			if tt.wantErr == nil {
   296  				tt.wantErr = require.NoError
   297  			}
   298  			newSource, err := New(tt.cfg)
   299  			tt.wantErr(t, err)
   300  			if err != nil {
   301  				return
   302  			}
   303  			s := newSource.(*fileSource)
   304  			assert.Equalf(t, tt.want, s.ID(), "ID() mismatch")
   305  			assert.Equalf(t, tt.wantDigest, s.digestForVersion, "digestForVersion mismatch")
   306  		})
   307  	}
   308  }