github.com/anchore/syft@v1.4.2-0.20240516191711-1bec1fc5d397/syft/source/directorysource/directory_source_test.go (about)

     1  package directorysource
     2  
     3  import (
     4  	"io/fs"
     5  	"os"
     6  	"path/filepath"
     7  	"testing"
     8  
     9  	"github.com/google/go-cmp/cmp"
    10  	"github.com/stretchr/testify/assert"
    11  	"github.com/stretchr/testify/require"
    12  
    13  	"github.com/anchore/stereoscope/pkg/file"
    14  	"github.com/anchore/syft/syft/artifact"
    15  	"github.com/anchore/syft/syft/internal/fileresolver"
    16  	"github.com/anchore/syft/syft/internal/testutil"
    17  	"github.com/anchore/syft/syft/source"
    18  )
    19  
    20  func TestNewFromDirectory(t *testing.T) {
    21  	testutil.Chdir(t, "..") // run with source/test-fixtures
    22  
    23  	testCases := []struct {
    24  		desc         string
    25  		input        string
    26  		expString    string
    27  		inputPaths   []string
    28  		expectedRefs int
    29  		cxErr        require.ErrorAssertionFunc
    30  	}{
    31  		{
    32  			desc:       "no paths exist",
    33  			input:      "foobar/",
    34  			inputPaths: []string{"/opt/", "/other"},
    35  			cxErr:      require.Error,
    36  		},
    37  		{
    38  			desc:         "path detected",
    39  			input:        "test-fixtures",
    40  			inputPaths:   []string{"path-detected/.vimrc"},
    41  			expectedRefs: 1,
    42  		},
    43  		{
    44  			desc:         "directory ignored",
    45  			input:        "test-fixtures",
    46  			inputPaths:   []string{"path-detected"},
    47  			expectedRefs: 0,
    48  		},
    49  		{
    50  			desc:         "no files-by-path detected",
    51  			input:        "test-fixtures",
    52  			inputPaths:   []string{"no-path-detected"},
    53  			expectedRefs: 0,
    54  		},
    55  	}
    56  	for _, test := range testCases {
    57  		t.Run(test.desc, func(t *testing.T) {
    58  			if test.cxErr == nil {
    59  				test.cxErr = require.NoError
    60  			}
    61  			src, err := New(Config{
    62  				Path: test.input,
    63  			})
    64  			test.cxErr(t, err)
    65  			if err != nil {
    66  				return
    67  			}
    68  			require.NoError(t, err)
    69  			t.Cleanup(func() {
    70  				require.NoError(t, src.Close())
    71  			})
    72  			assert.Equal(t, test.input, src.Describe().Metadata.(source.DirectoryMetadata).Path)
    73  
    74  			res, err := src.FileResolver(source.SquashedScope)
    75  			require.NoError(t, err)
    76  
    77  			refs, err := res.FilesByPath(test.inputPaths...)
    78  			require.NoError(t, err)
    79  
    80  			if len(refs) != test.expectedRefs {
    81  				t.Errorf("unexpected number of refs returned: %d != %d", len(refs), test.expectedRefs)
    82  			}
    83  
    84  		})
    85  	}
    86  }
    87  
    88  func Test_DirectorySource_FilesByGlob(t *testing.T) {
    89  	testutil.Chdir(t, "..") // run with source/test-fixtures
    90  
    91  	testCases := []struct {
    92  		desc     string
    93  		input    string
    94  		glob     string
    95  		expected int
    96  	}{
    97  		{
    98  			input:    "test-fixtures",
    99  			desc:     "no matches",
   100  			glob:     "bar/foo",
   101  			expected: 0,
   102  		},
   103  		{
   104  			input:    "test-fixtures/path-detected",
   105  			desc:     "a single match",
   106  			glob:     "**/*vimrc",
   107  			expected: 1,
   108  		},
   109  		{
   110  			input:    "test-fixtures/path-detected",
   111  			desc:     "multiple matches",
   112  			glob:     "**",
   113  			expected: 2,
   114  		},
   115  	}
   116  	for _, test := range testCases {
   117  		t.Run(test.desc, func(t *testing.T) {
   118  			src, err := New(Config{Path: test.input})
   119  			require.NoError(t, err)
   120  
   121  			res, err := src.FileResolver(source.SquashedScope)
   122  			require.NoError(t, err)
   123  			t.Cleanup(func() {
   124  				require.NoError(t, src.Close())
   125  			})
   126  
   127  			contents, err := res.FilesByGlob(test.glob)
   128  			require.NoError(t, err)
   129  			if len(contents) != test.expected {
   130  				t.Errorf("unexpected number of files found by glob (%s): %d != %d", test.glob, len(contents), test.expected)
   131  			}
   132  
   133  		})
   134  	}
   135  }
   136  
   137  func Test_DirectorySource_Exclusions(t *testing.T) {
   138  	testutil.Chdir(t, "..") // run with source/test-fixtures
   139  
   140  	testCases := []struct {
   141  		desc       string
   142  		input      string
   143  		glob       string
   144  		expected   []string
   145  		exclusions []string
   146  		err        bool
   147  	}{
   148  		{
   149  			input:      "test-fixtures/system_paths",
   150  			desc:       "exclude everything",
   151  			glob:       "**",
   152  			expected:   nil,
   153  			exclusions: []string{"**/*"},
   154  		},
   155  		{
   156  			input: "test-fixtures/image-simple",
   157  			desc:  "a single path excluded",
   158  			glob:  "**",
   159  			expected: []string{
   160  				"Dockerfile",
   161  				"file-1.txt",
   162  				"file-2.txt",
   163  			},
   164  			exclusions: []string{"**/target/**"},
   165  		},
   166  		{
   167  			input: "test-fixtures/image-simple",
   168  			desc:  "exclude explicit directory relative to the root",
   169  			glob:  "**",
   170  			expected: []string{
   171  				"Dockerfile",
   172  				"file-1.txt",
   173  				"file-2.txt",
   174  				//"target/really/nested/file-3.txt", // explicitly skipped
   175  			},
   176  			exclusions: []string{"./target"},
   177  		},
   178  		{
   179  			input: "test-fixtures/image-simple",
   180  			desc:  "exclude explicit file relative to the root",
   181  			glob:  "**",
   182  			expected: []string{
   183  				"Dockerfile",
   184  				//"file-1.txt",  // explicitly skipped
   185  				"file-2.txt",
   186  				"target/really/nested/file-3.txt",
   187  			},
   188  			exclusions: []string{"./file-1.txt"},
   189  		},
   190  		{
   191  			input: "test-fixtures/image-simple",
   192  			desc:  "exclude wildcard relative to the root",
   193  			glob:  "**",
   194  			expected: []string{
   195  				"Dockerfile",
   196  				//"file-1.txt",  // explicitly skipped
   197  				//"file-2.txt", // explicitly skipped
   198  				"target/really/nested/file-3.txt",
   199  			},
   200  			exclusions: []string{"./*.txt"},
   201  		},
   202  		{
   203  			input: "test-fixtures/image-simple",
   204  			desc:  "exclude files deeper",
   205  			glob:  "**",
   206  			expected: []string{
   207  				"Dockerfile",
   208  				"file-1.txt",
   209  				"file-2.txt",
   210  				//"target/really/nested/file-3.txt", // explicitly skipped
   211  			},
   212  			exclusions: []string{"**/really/**"},
   213  		},
   214  		{
   215  			input: "test-fixtures/image-simple",
   216  			desc:  "files excluded with extension",
   217  			glob:  "**",
   218  			expected: []string{
   219  				"Dockerfile",
   220  				//"file-1.txt",  // explicitly skipped
   221  				//"file-2.txt", // explicitly skipped
   222  				//"target/really/nested/file-3.txt", // explicitly skipped
   223  			},
   224  			exclusions: []string{"**/*.txt"},
   225  		},
   226  		{
   227  			input: "test-fixtures/image-simple",
   228  			desc:  "keep files with different extensions",
   229  			glob:  "**",
   230  			expected: []string{
   231  				"Dockerfile",
   232  				"file-1.txt",
   233  				"file-2.txt",
   234  				"target/really/nested/file-3.txt",
   235  			},
   236  			exclusions: []string{"**/target/**/*.jar"},
   237  		},
   238  		{
   239  			input: "test-fixtures/path-detected",
   240  			desc:  "file directly excluded",
   241  			glob:  "**",
   242  			expected: []string{
   243  				".vimrc",
   244  			},
   245  			exclusions: []string{"**/empty"},
   246  		},
   247  		{
   248  			input: "test-fixtures/path-detected",
   249  			desc:  "pattern error containing **/",
   250  			glob:  "**",
   251  			expected: []string{
   252  				".vimrc",
   253  			},
   254  			exclusions: []string{"/**/empty"},
   255  			err:        true,
   256  		},
   257  		{
   258  			input: "test-fixtures/path-detected",
   259  			desc:  "pattern error incorrect start",
   260  			glob:  "**",
   261  			expected: []string{
   262  				".vimrc",
   263  			},
   264  			exclusions: []string{"empty"},
   265  			err:        true,
   266  		},
   267  		{
   268  			input: "test-fixtures/path-detected",
   269  			desc:  "pattern error starting with /",
   270  			glob:  "**",
   271  			expected: []string{
   272  				".vimrc",
   273  			},
   274  			exclusions: []string{"/empty"},
   275  			err:        true,
   276  		},
   277  	}
   278  
   279  	for _, test := range testCases {
   280  		t.Run(test.desc, func(t *testing.T) {
   281  			src, err := New(Config{
   282  				Path: test.input,
   283  				Exclude: source.ExcludeConfig{
   284  					Paths: test.exclusions,
   285  				},
   286  			})
   287  			require.NoError(t, err)
   288  			t.Cleanup(func() {
   289  				require.NoError(t, src.Close())
   290  			})
   291  
   292  			if test.err {
   293  				_, err = src.FileResolver(source.SquashedScope)
   294  				require.Error(t, err)
   295  				return
   296  			}
   297  			require.NoError(t, err)
   298  
   299  			res, err := src.FileResolver(source.SquashedScope)
   300  			require.NoError(t, err)
   301  
   302  			locations, err := res.FilesByGlob(test.glob)
   303  			require.NoError(t, err)
   304  
   305  			var actual []string
   306  			for _, l := range locations {
   307  				actual = append(actual, l.RealPath)
   308  			}
   309  
   310  			assert.ElementsMatchf(t, test.expected, actual, "diff \n"+cmp.Diff(test.expected, actual))
   311  		})
   312  	}
   313  }
   314  
   315  func Test_getDirectoryExclusionFunctions_crossPlatform(t *testing.T) {
   316  	testCases := []struct {
   317  		desc     string
   318  		root     string
   319  		path     string
   320  		finfo    os.FileInfo
   321  		exclude  string
   322  		walkHint error
   323  	}{
   324  		{
   325  			desc:     "directory exclusion",
   326  			root:     "/",
   327  			path:     "/usr/var/lib",
   328  			exclude:  "**/var/lib",
   329  			finfo:    file.ManualInfo{ModeValue: os.ModeDir},
   330  			walkHint: fs.SkipDir,
   331  		},
   332  		{
   333  			desc:     "no file info",
   334  			root:     "/",
   335  			path:     "/usr/var/lib",
   336  			exclude:  "**/var/lib",
   337  			walkHint: fileresolver.ErrSkipPath,
   338  		},
   339  		// linux specific tests...
   340  		{
   341  			desc:     "linux doublestar",
   342  			root:     "/usr",
   343  			path:     "/usr/var/lib/etc.txt",
   344  			exclude:  "**/*.txt",
   345  			finfo:    file.ManualInfo{},
   346  			walkHint: fileresolver.ErrSkipPath,
   347  		},
   348  		{
   349  			desc:    "linux relative",
   350  			root:    "/usr/var/lib",
   351  			path:    "/usr/var/lib/etc.txt",
   352  			exclude: "./*.txt",
   353  			finfo:   file.ManualInfo{},
   354  
   355  			walkHint: fileresolver.ErrSkipPath,
   356  		},
   357  		{
   358  			desc:     "linux one level",
   359  			root:     "/usr",
   360  			path:     "/usr/var/lib/etc.txt",
   361  			exclude:  "*/*.txt",
   362  			finfo:    file.ManualInfo{},
   363  			walkHint: nil,
   364  		},
   365  		// NOTE: since these tests will run in linux and macOS, the windows paths will be
   366  		// considered relative if they do not start with a forward slash and paths with backslashes
   367  		// won't be modified by the filepath.ToSlash call, so these are emulating the result of
   368  		// filepath.ToSlash usage
   369  
   370  		// windows specific tests...
   371  		{
   372  			desc:     "windows doublestar",
   373  			root:     "/C:/User/stuff",
   374  			path:     "/C:/User/stuff/thing.txt",
   375  			exclude:  "**/*.txt",
   376  			finfo:    file.ManualInfo{},
   377  			walkHint: fileresolver.ErrSkipPath,
   378  		},
   379  		{
   380  			desc:     "windows relative",
   381  			root:     "/C:/User/stuff",
   382  			path:     "/C:/User/stuff/thing.txt",
   383  			exclude:  "./*.txt",
   384  			finfo:    file.ManualInfo{},
   385  			walkHint: fileresolver.ErrSkipPath,
   386  		},
   387  		{
   388  			desc:     "windows one level",
   389  			root:     "/C:/User/stuff",
   390  			path:     "/C:/User/stuff/thing.txt",
   391  			exclude:  "*/*.txt",
   392  			finfo:    file.ManualInfo{},
   393  			walkHint: nil,
   394  		},
   395  	}
   396  
   397  	for _, test := range testCases {
   398  		t.Run(test.desc, func(t *testing.T) {
   399  			fns, err := GetDirectoryExclusionFunctions(test.root, []string{test.exclude})
   400  			require.NoError(t, err)
   401  
   402  			for _, f := range fns {
   403  				result := f("", test.path, test.finfo, nil)
   404  				require.Equal(t, test.walkHint, result)
   405  			}
   406  		})
   407  	}
   408  }
   409  
   410  func Test_DirectorySource_FilesByPathDoesNotExist(t *testing.T) {
   411  	testutil.Chdir(t, "..") // run with source/test-fixtures
   412  
   413  	testCases := []struct {
   414  		desc     string
   415  		input    string
   416  		path     string
   417  		expected string
   418  	}{
   419  		{
   420  			input: "test-fixtures/path-detected",
   421  			desc:  "path does not exist",
   422  			path:  "foo",
   423  		},
   424  	}
   425  	for _, test := range testCases {
   426  		t.Run(test.desc, func(t *testing.T) {
   427  			src, err := New(Config{Path: test.input})
   428  			require.NoError(t, err)
   429  			t.Cleanup(func() {
   430  				require.NoError(t, src.Close())
   431  			})
   432  
   433  			res, err := src.FileResolver(source.SquashedScope)
   434  			require.NoError(t, err)
   435  
   436  			refs, err := res.FilesByPath(test.path)
   437  			require.NoError(t, err)
   438  
   439  			assert.Len(t, refs, 0)
   440  		})
   441  	}
   442  }
   443  
   444  func Test_DirectorySource_ID(t *testing.T) {
   445  	testutil.Chdir(t, "..") // run with source/test-fixtures
   446  
   447  	tests := []struct {
   448  		name    string
   449  		cfg     Config
   450  		want    artifact.ID
   451  		wantErr require.ErrorAssertionFunc
   452  	}{
   453  		{
   454  			name:    "empty",
   455  			cfg:     Config{},
   456  			wantErr: require.Error,
   457  		},
   458  		{
   459  			name: "to a non-existent directory",
   460  			cfg: Config{
   461  				Path: "./test-fixtures/does-not-exist",
   462  			},
   463  			wantErr: require.Error,
   464  		},
   465  		{
   466  			name:    "with odd unclean path through non-existent directory",
   467  			cfg:     Config{Path: "test-fixtures/does-not-exist/../"},
   468  			wantErr: require.Error,
   469  		},
   470  		{
   471  			name: "to a file (not a directory)",
   472  			cfg: Config{
   473  				Path: "./test-fixtures/image-simple/Dockerfile",
   474  			},
   475  			wantErr: require.Error,
   476  		},
   477  		{
   478  			name: "to dir with name and version",
   479  			cfg: Config{
   480  				Path: "./test-fixtures",
   481  				Alias: source.Alias{
   482  					Name:    "name-me-that!",
   483  					Version: "version-me-this!",
   484  				},
   485  			},
   486  			want: artifact.ID("51a5f2a1536cf4b5220d4247814b07eec5862ab0547050f90e9ae216548ded7e"),
   487  		},
   488  		{
   489  			name: "to different dir with name and version",
   490  			cfg: Config{
   491  				Path: "./test-fixtures/image-simple",
   492  				Alias: source.Alias{
   493  					Name:    "name-me-that!",
   494  					Version: "version-me-this!",
   495  				},
   496  			},
   497  			// note: this must match the previous value because the alias should trump the path info
   498  			want: artifact.ID("51a5f2a1536cf4b5220d4247814b07eec5862ab0547050f90e9ae216548ded7e"),
   499  		},
   500  		{
   501  			name: "with path",
   502  			cfg:  Config{Path: "./test-fixtures"},
   503  			want: artifact.ID("c2f936b0054dc6114fc02a3446bf8916bde8fdf87166a23aee22ea011b443522"),
   504  		},
   505  		{
   506  			name: "with unclean path",
   507  			cfg:  Config{Path: "test-fixtures/image-simple/../"},
   508  			want: artifact.ID("c2f936b0054dc6114fc02a3446bf8916bde8fdf87166a23aee22ea011b443522"),
   509  		},
   510  		{
   511  			name: "other fields do not affect ID",
   512  			cfg: Config{
   513  				Path: "test-fixtures",
   514  				Base: "a-base!",
   515  				Exclude: source.ExcludeConfig{
   516  					Paths: []string{"a", "b"},
   517  				},
   518  			},
   519  			want: artifact.ID("c2f936b0054dc6114fc02a3446bf8916bde8fdf87166a23aee22ea011b443522"),
   520  		},
   521  	}
   522  	for _, tt := range tests {
   523  		t.Run(tt.name, func(t *testing.T) {
   524  			if tt.wantErr == nil {
   525  				tt.wantErr = require.NoError
   526  			}
   527  			s, err := New(tt.cfg)
   528  			tt.wantErr(t, err)
   529  			if err != nil {
   530  				return
   531  			}
   532  			assert.Equalf(t, tt.want, s.ID(), "ID()")
   533  		})
   534  	}
   535  }
   536  
   537  func Test_cleanDirPath(t *testing.T) {
   538  	testutil.Chdir(t, "..") // run with source/test-fixtures
   539  
   540  	abs, err := filepath.Abs("test-fixtures")
   541  	require.NoError(t, err)
   542  
   543  	tests := []struct {
   544  		name string
   545  		path string
   546  		base string
   547  		want string
   548  	}{
   549  		{
   550  			name: "abs path, abs base, base contained in path",
   551  			path: filepath.Join(abs, "system_paths/outside_root"),
   552  			base: abs,
   553  			want: "system_paths/outside_root",
   554  		},
   555  		{
   556  			name: "abs path, abs base, base not contained in path",
   557  			path: "/var/folders/8x/gw98pp6535s4r8drc374tb1r0000gn/T/001/some/path",
   558  			base: "/var/folders/8x/gw98pp6535s4r8drc374tb1r0000gn/T/002",
   559  			want: "/var/folders/8x/gw98pp6535s4r8drc374tb1r0000gn/T/001/some/path",
   560  		},
   561  		{
   562  			name: "path and base match",
   563  			path: "/var/folders/8x/gw98pp6535s4r8drc374tb1r0000gn/T/001/some/path",
   564  			base: "/var/folders/8x/gw98pp6535s4r8drc374tb1r0000gn/T/001/some/path",
   565  			want: "/var/folders/8x/gw98pp6535s4r8drc374tb1r0000gn/T/001/some/path",
   566  		},
   567  	}
   568  	for _, tt := range tests {
   569  		t.Run(tt.name, func(t *testing.T) {
   570  			assert.Equal(t, tt.want, cleanDirPath(tt.path, tt.base))
   571  		})
   572  	}
   573  }