github.com/nextlinux/gosbom@v0.81.1-0.20230627115839-1ff50c281391/gosbom/internal/fileresolver/unindexed_directory_test.go (about)

     1  //go:build !windows
     2  // +build !windows
     3  
     4  package fileresolver
     5  
     6  import (
     7  	"io"
     8  	"os"
     9  	"path"
    10  	"path/filepath"
    11  	"sort"
    12  	"strings"
    13  	"testing"
    14  	"time"
    15  
    16  	"github.com/google/go-cmp/cmp"
    17  	"github.com/google/go-cmp/cmp/cmpopts"
    18  	"github.com/nextlinux/gosbom/gosbom/file"
    19  	"github.com/scylladb/go-set/strset"
    20  	"github.com/stretchr/testify/assert"
    21  	"github.com/stretchr/testify/require"
    22  
    23  	stereoscopeFile "github.com/anchore/stereoscope/pkg/file"
    24  )
    25  
    26  func Test_UnindexDirectoryResolver_RequestRelativePathWithinSymlink(t *testing.T) {
    27  	pwd, err := os.Getwd()
    28  
    29  	// we need to mimic a shell, otherwise we won't get a path within a symlink
    30  	targetPath := filepath.Join(pwd, "./test-fixtures/symlinked-root/nested/link-root/nested")
    31  	t.Setenv("PWD", targetPath)
    32  
    33  	require.NoError(t, err)
    34  	require.NoError(t, os.Chdir(targetPath))
    35  	t.Cleanup(func() {
    36  		require.NoError(t, os.Chdir(pwd))
    37  	})
    38  
    39  	resolver := NewFromUnindexedDirectory("./")
    40  	require.NoError(t, err)
    41  
    42  	locations, err := resolver.FilesByPath("file2.txt")
    43  	require.NoError(t, err)
    44  	require.Len(t, locations, 1)
    45  
    46  	// TODO: this is technically not correct behavior since this is reporting the symlink path (virtual path) and
    47  	// not the real path.
    48  	require.False(t, filepath.IsAbs(locations[0].RealPath), "should be relative path")
    49  }
    50  
    51  func Test_UnindexDirectoryResolver_FilesByPath_request_response(t *testing.T) {
    52  	// /
    53  	//   somewhere/
    54  	//     outside.txt
    55  	//   root-link -> ./
    56  	//   path/
    57  	//     to/
    58  	//       abs-inside.txt -> /path/to/the/file.txt               # absolute link to somewhere inside of the root
    59  	//       rel-inside.txt -> ./the/file.txt                      # relative link to somewhere inside of the root
    60  	//       the/
    61  	//		   file.txt
    62  	//         abs-outside.txt -> /somewhere/outside.txt           # absolute link to outside of the root
    63  	//         rel-outside -> ../../../somewhere/outside.txt       # relative link to outside of the root
    64  	//
    65  
    66  	testDir, err := os.Getwd()
    67  	require.NoError(t, err)
    68  	relative := filepath.Join("test-fixtures", "req-resp")
    69  	absolute := filepath.Join(testDir, relative)
    70  
    71  	absInsidePath := filepath.Join(absolute, "path", "to", "abs-inside.txt")
    72  	absOutsidePath := filepath.Join(absolute, "path", "to", "the", "abs-outside.txt")
    73  
    74  	relativeViaLink := filepath.Join(relative, "root-link")
    75  	absoluteViaLink := filepath.Join(absolute, "root-link")
    76  
    77  	relativeViaDoubleLink := filepath.Join(relative, "root-link", "root-link")
    78  	absoluteViaDoubleLink := filepath.Join(absolute, "root-link", "root-link")
    79  
    80  	cleanup := func() {
    81  		_ = os.Remove(absInsidePath)
    82  		_ = os.Remove(absOutsidePath)
    83  	}
    84  
    85  	// ensure the absolute symlinks are cleaned up from any previous runs
    86  	cleanup()
    87  
    88  	require.NoError(t, os.Symlink(filepath.Join(absolute, "path", "to", "the", "file.txt"), absInsidePath))
    89  	require.NoError(t, os.Symlink(filepath.Join(absolute, "somewhere", "outside.txt"), absOutsidePath))
    90  
    91  	t.Cleanup(cleanup)
    92  
    93  	cases := []struct {
    94  		name                string
    95  		cwd                 string
    96  		root                string
    97  		base                string
    98  		input               string
    99  		expectedRealPath    string
   100  		expectedVirtualPath string
   101  	}{
   102  		{
   103  			name:             "relative root, relative request, direct",
   104  			root:             relative,
   105  			input:            "path/to/the/file.txt",
   106  			expectedRealPath: "path/to/the/file.txt",
   107  		},
   108  		{
   109  			name:             "abs root, relative request, direct",
   110  			root:             absolute,
   111  			input:            "path/to/the/file.txt",
   112  			expectedRealPath: "path/to/the/file.txt",
   113  		},
   114  		{
   115  			name:             "relative root, abs request, direct",
   116  			root:             relative,
   117  			input:            "/path/to/the/file.txt",
   118  			expectedRealPath: "path/to/the/file.txt",
   119  		},
   120  		{
   121  			name:             "abs root, abs request, direct",
   122  			root:             absolute,
   123  			input:            "/path/to/the/file.txt",
   124  			expectedRealPath: "path/to/the/file.txt",
   125  		},
   126  		// cwd within root...
   127  		{
   128  			name:             "relative root, relative request, direct, cwd within root",
   129  			cwd:              filepath.Join(relative, "path/to"),
   130  			root:             "../../",
   131  			input:            "path/to/the/file.txt",
   132  			expectedRealPath: "path/to/the/file.txt",
   133  		},
   134  		{
   135  			name:             "abs root, relative request, direct, cwd within root",
   136  			cwd:              filepath.Join(relative, "path/to"),
   137  			root:             absolute,
   138  			input:            "path/to/the/file.txt",
   139  			expectedRealPath: "path/to/the/file.txt",
   140  		},
   141  		{
   142  			name:             "relative root, abs request, direct, cwd within root",
   143  			cwd:              filepath.Join(relative, "path/to"),
   144  			root:             "../../",
   145  			input:            "/path/to/the/file.txt",
   146  			expectedRealPath: "path/to/the/file.txt",
   147  		},
   148  		{
   149  			name: "abs root, abs request, direct, cwd within root",
   150  			cwd:  filepath.Join(relative, "path/to"),
   151  
   152  			root:             absolute,
   153  			input:            "/path/to/the/file.txt",
   154  			expectedRealPath: "path/to/the/file.txt",
   155  		},
   156  		// cwd within symlink root...
   157  		{
   158  			name:  "relative root, relative request, direct, cwd within symlink root",
   159  			cwd:   relativeViaLink,
   160  			root:  "./",
   161  			input: "path/to/the/file.txt",
   162  			// note: this is inconsistent with the directory resolver. The real path is essentially the virtual path
   163  			// in this case for the unindexed resolver, which is not correct.
   164  			expectedRealPath: "path/to/the/file.txt",
   165  		},
   166  		{
   167  			name:             "abs root, relative request, direct, cwd within symlink root",
   168  			cwd:              relativeViaLink,
   169  			root:             absoluteViaLink,
   170  			input:            "path/to/the/file.txt",
   171  			expectedRealPath: "path/to/the/file.txt",
   172  		},
   173  		{
   174  			name:  "relative root, abs request, direct, cwd within symlink root",
   175  			cwd:   relativeViaLink,
   176  			root:  "./",
   177  			input: "/path/to/the/file.txt",
   178  			// note: this is inconsistent with the directory resolver. The real path is essentially the virtual path
   179  			// in this case for the unindexed resolver, which is not correct.
   180  			expectedRealPath: "path/to/the/file.txt",
   181  		},
   182  		{
   183  			name:             "abs root, abs request, direct, cwd within symlink root",
   184  			cwd:              relativeViaLink,
   185  			root:             absoluteViaLink,
   186  			input:            "/path/to/the/file.txt",
   187  			expectedRealPath: "path/to/the/file.txt",
   188  		},
   189  		// cwd within symlink root, request nested within...
   190  		{
   191  			name:  "relative root, relative nested request, direct, cwd within symlink root",
   192  			cwd:   relativeViaLink,
   193  			root:  "./path",
   194  			input: "to/the/file.txt",
   195  			// note: this is inconsistent with the directory resolver. The real path is essentially the virtual path
   196  			// in this case for the unindexed resolver, which is not correct.
   197  			expectedRealPath: "to/the/file.txt",
   198  		},
   199  		{
   200  			name:             "abs root, relative nested request, direct, cwd within symlink root",
   201  			cwd:              relativeViaLink,
   202  			root:             filepath.Join(absoluteViaLink, "path"),
   203  			input:            "to/the/file.txt",
   204  			expectedRealPath: "to/the/file.txt",
   205  		},
   206  		{
   207  			name:  "relative root, abs nested request, direct, cwd within symlink root",
   208  			cwd:   relativeViaLink,
   209  			root:  "./path",
   210  			input: "/to/the/file.txt",
   211  			// note: this is inconsistent with the directory resolver. The real path is essentially the virtual path
   212  			// in this case for the unindexed resolver, which is not correct.
   213  			expectedRealPath: "to/the/file.txt",
   214  		},
   215  		{
   216  			name:             "abs root, abs nested request, direct, cwd within symlink root",
   217  			cwd:              relativeViaLink,
   218  			root:             filepath.Join(absoluteViaLink, "path"),
   219  			input:            "/to/the/file.txt",
   220  			expectedRealPath: "to/the/file.txt",
   221  		},
   222  		// cwd within DOUBLE symlink root...
   223  		{
   224  			name:  "relative root, relative request, direct, cwd within (double) symlink root",
   225  			cwd:   relativeViaDoubleLink,
   226  			root:  "./",
   227  			input: "path/to/the/file.txt",
   228  			// note: this is inconsistent with the directory resolver. The real path is essentially the virtual path
   229  			// in this case for the unindexed resolver, which is not correct.
   230  			expectedRealPath: "path/to/the/file.txt",
   231  		},
   232  		{
   233  			name:             "abs root, relative request, direct, cwd within (double) symlink root",
   234  			cwd:              relativeViaDoubleLink,
   235  			root:             absoluteViaDoubleLink,
   236  			input:            "path/to/the/file.txt",
   237  			expectedRealPath: "path/to/the/file.txt",
   238  		},
   239  		{
   240  			name:  "relative root, abs request, direct, cwd within (double) symlink root",
   241  			cwd:   relativeViaDoubleLink,
   242  			root:  "./",
   243  			input: "/path/to/the/file.txt",
   244  			// note: this is inconsistent with the directory resolver. The real path is essentially the virtual path
   245  			// in this case for the unindexed resolver, which is not correct.
   246  			expectedRealPath: "path/to/the/file.txt",
   247  		},
   248  		{
   249  			name:             "abs root, abs request, direct, cwd within (double) symlink root",
   250  			cwd:              relativeViaDoubleLink,
   251  			root:             absoluteViaDoubleLink,
   252  			input:            "/path/to/the/file.txt",
   253  			expectedRealPath: "path/to/the/file.txt",
   254  		},
   255  		// cwd within DOUBLE symlink root, request nested within...
   256  		{
   257  			name:  "relative root, relative nested request, direct, cwd within (double) symlink root",
   258  			cwd:   relativeViaDoubleLink,
   259  			root:  "./path",
   260  			input: "to/the/file.txt",
   261  			// note: this is inconsistent with the directory resolver. The real path is essentially the virtual path
   262  			// in this case for the unindexed resolver, which is not correct.
   263  			expectedRealPath: "to/the/file.txt",
   264  		},
   265  		{
   266  			name:             "abs root, relative nested request, direct, cwd within (double) symlink root",
   267  			cwd:              relativeViaDoubleLink,
   268  			root:             filepath.Join(absoluteViaDoubleLink, "path"),
   269  			input:            "to/the/file.txt",
   270  			expectedRealPath: "to/the/file.txt",
   271  		},
   272  		{
   273  			name:  "relative root, abs nested request, direct, cwd within (double) symlink root",
   274  			cwd:   relativeViaDoubleLink,
   275  			root:  "./path",
   276  			input: "/to/the/file.txt",
   277  			// note: this is inconsistent with the directory resolver. The real path is essentially the virtual path
   278  			// in this case for the unindexed resolver, which is not correct.
   279  			expectedRealPath: "to/the/file.txt",
   280  		},
   281  		{
   282  			name:             "abs root, abs nested request, direct, cwd within (double) symlink root",
   283  			cwd:              relativeViaDoubleLink,
   284  			root:             filepath.Join(absoluteViaDoubleLink, "path"),
   285  			input:            "/to/the/file.txt",
   286  			expectedRealPath: "to/the/file.txt",
   287  		},
   288  		// cwd within DOUBLE symlink root, request nested DEEP within...
   289  		{
   290  			name:  "relative root, relative nested request, direct, cwd deep within (double) symlink root",
   291  			cwd:   filepath.Join(relativeViaDoubleLink, "path", "to"),
   292  			root:  "../",
   293  			input: "to/the/file.txt",
   294  			// note: this is inconsistent with the directory resolver. The real path is essentially the virtual path
   295  			// in this case for the unindexed resolver, which is not correct.
   296  			expectedRealPath: "to/the/file.txt",
   297  		},
   298  		{
   299  			name:             "abs root, relative nested request, direct, cwd deep within (double) symlink root",
   300  			cwd:              filepath.Join(relativeViaDoubleLink, "path", "to"),
   301  			root:             filepath.Join(absoluteViaDoubleLink, "path"),
   302  			input:            "to/the/file.txt",
   303  			expectedRealPath: "to/the/file.txt",
   304  		},
   305  		{
   306  			name:  "relative root, abs nested request, direct, cwd deep within (double) symlink root",
   307  			cwd:   filepath.Join(relativeViaDoubleLink, "path", "to"),
   308  			root:  "../",
   309  			input: "/to/the/file.txt",
   310  			// note: this is inconsistent with the directory resolver. The real path is essentially the virtual path
   311  			// in this case for the unindexed resolver, which is not correct.
   312  			expectedRealPath: "to/the/file.txt",
   313  		},
   314  		{
   315  			name:             "abs root, abs nested request, direct, cwd deep within (double) symlink root",
   316  			cwd:              filepath.Join(relativeViaDoubleLink, "path", "to"),
   317  			root:             filepath.Join(absoluteViaDoubleLink, "path"),
   318  			input:            "/to/the/file.txt",
   319  			expectedRealPath: "to/the/file.txt",
   320  		},
   321  		// link to outside of root cases...
   322  		{
   323  			name:                "relative root, relative request, abs indirect (outside of root)",
   324  			root:                filepath.Join(relative, "path"),
   325  			input:               "to/the/abs-outside.txt",
   326  			expectedRealPath:    filepath.Join(absolute, "/somewhere/outside.txt"),
   327  			expectedVirtualPath: "to/the/abs-outside.txt",
   328  		},
   329  		{
   330  			name:                "abs root, relative request, abs indirect (outside of root)",
   331  			root:                filepath.Join(absolute, "path"),
   332  			input:               "to/the/abs-outside.txt",
   333  			expectedRealPath:    filepath.Join(absolute, "/somewhere/outside.txt"),
   334  			expectedVirtualPath: "to/the/abs-outside.txt",
   335  		},
   336  		{
   337  			name:                "relative root, abs request, abs indirect (outside of root)",
   338  			root:                filepath.Join(relative, "path"),
   339  			input:               "/to/the/abs-outside.txt",
   340  			expectedRealPath:    filepath.Join(absolute, "/somewhere/outside.txt"),
   341  			expectedVirtualPath: "to/the/abs-outside.txt",
   342  		},
   343  		{
   344  			name:                "abs root, abs request, abs indirect (outside of root)",
   345  			root:                filepath.Join(absolute, "path"),
   346  			input:               "/to/the/abs-outside.txt",
   347  			expectedRealPath:    filepath.Join(absolute, "/somewhere/outside.txt"),
   348  			expectedVirtualPath: "to/the/abs-outside.txt",
   349  		},
   350  		{
   351  			name:  "relative root, relative request, relative indirect (outside of root)",
   352  			root:  filepath.Join(relative, "path"),
   353  			input: "to/the/rel-outside.txt",
   354  			//expectedRealPath:    filepath.Join(absolute, "/somewhere/outside.txt"),
   355  			// TODO: the real path is not correct
   356  			expectedRealPath:    "../somewhere/outside.txt",
   357  			expectedVirtualPath: "to/the/rel-outside.txt",
   358  		},
   359  		{
   360  			name:  "abs root, relative request, relative indirect (outside of root)",
   361  			root:  filepath.Join(absolute, "path"),
   362  			input: "to/the/rel-outside.txt",
   363  			//expectedRealPath:    filepath.Join(absolute, "/somewhere/outside.txt"),
   364  			// TODO: the real path is not correct
   365  			expectedRealPath:    "../somewhere/outside.txt",
   366  			expectedVirtualPath: "to/the/rel-outside.txt",
   367  		},
   368  		{
   369  			name:  "relative root, abs request, relative indirect (outside of root)",
   370  			root:  filepath.Join(relative, "path"),
   371  			input: "/to/the/rel-outside.txt",
   372  			//expectedRealPath:    filepath.Join(absolute, "/somewhere/outside.txt"),
   373  			// TODO: the real path is not correct
   374  			expectedRealPath:    "../somewhere/outside.txt",
   375  			expectedVirtualPath: "to/the/rel-outside.txt",
   376  		},
   377  		{
   378  			name:  "abs root, abs request, relative indirect (outside of root)",
   379  			root:  filepath.Join(absolute, "path"),
   380  			input: "/to/the/rel-outside.txt",
   381  			//expectedRealPath:    filepath.Join(absolute, "/somewhere/outside.txt"),
   382  			// TODO: the real path is not correct
   383  			expectedRealPath:    "../somewhere/outside.txt",
   384  			expectedVirtualPath: "to/the/rel-outside.txt",
   385  		},
   386  		// link to outside of root cases... cwd within symlink root
   387  		{
   388  			name:                "relative root, relative request, abs indirect (outside of root), cwd within symlink root",
   389  			cwd:                 relativeViaLink,
   390  			root:                "path",
   391  			input:               "to/the/abs-outside.txt",
   392  			expectedRealPath:    filepath.Join(absolute, "/somewhere/outside.txt"),
   393  			expectedVirtualPath: "to/the/abs-outside.txt",
   394  		},
   395  		{
   396  			name:                "abs root, relative request, abs indirect (outside of root), cwd within symlink root",
   397  			cwd:                 relativeViaLink,
   398  			root:                filepath.Join(absolute, "path"),
   399  			input:               "to/the/abs-outside.txt",
   400  			expectedRealPath:    filepath.Join(absolute, "/somewhere/outside.txt"),
   401  			expectedVirtualPath: "to/the/abs-outside.txt",
   402  		},
   403  		{
   404  			name:                "relative root, abs request, abs indirect (outside of root), cwd within symlink root",
   405  			cwd:                 relativeViaLink,
   406  			root:                "path",
   407  			input:               "/to/the/abs-outside.txt",
   408  			expectedRealPath:    filepath.Join(absolute, "/somewhere/outside.txt"),
   409  			expectedVirtualPath: "to/the/abs-outside.txt",
   410  		},
   411  		{
   412  			name:                "abs root, abs request, abs indirect (outside of root), cwd within symlink root",
   413  			cwd:                 relativeViaLink,
   414  			root:                filepath.Join(absolute, "path"),
   415  			input:               "/to/the/abs-outside.txt",
   416  			expectedRealPath:    filepath.Join(absolute, "/somewhere/outside.txt"),
   417  			expectedVirtualPath: "to/the/abs-outside.txt",
   418  		},
   419  		{
   420  			name:  "relative root, relative request, relative indirect (outside of root), cwd within symlink root",
   421  			cwd:   relativeViaLink,
   422  			root:  "path",
   423  			input: "to/the/rel-outside.txt",
   424  			//expectedRealPath:    filepath.Join(absolute, "/somewhere/outside.txt"),
   425  			// TODO: the real path is not correct
   426  			expectedRealPath:    "../somewhere/outside.txt",
   427  			expectedVirtualPath: "to/the/rel-outside.txt",
   428  		},
   429  		{
   430  			name:  "abs root, relative request, relative indirect (outside of root), cwd within symlink root",
   431  			cwd:   relativeViaLink,
   432  			root:  filepath.Join(absolute, "path"),
   433  			input: "to/the/rel-outside.txt",
   434  			//expectedRealPath:    filepath.Join(absolute, "/somewhere/outside.txt"),
   435  			// TODO: the real path is not correct
   436  			expectedRealPath:    "../somewhere/outside.txt",
   437  			expectedVirtualPath: "to/the/rel-outside.txt",
   438  		},
   439  		{
   440  			name:  "relative root, abs request, relative indirect (outside of root), cwd within symlink root",
   441  			cwd:   relativeViaLink,
   442  			root:  "path",
   443  			input: "/to/the/rel-outside.txt",
   444  			//expectedRealPath:    filepath.Join(absolute, "/somewhere/outside.txt"),
   445  			// TODO: the real path is not correct
   446  			expectedRealPath:    "../somewhere/outside.txt",
   447  			expectedVirtualPath: "to/the/rel-outside.txt",
   448  		},
   449  		{
   450  			name:  "abs root, abs request, relative indirect (outside of root), cwd within symlink root",
   451  			cwd:   relativeViaLink,
   452  			root:  filepath.Join(absolute, "path"),
   453  			input: "/to/the/rel-outside.txt",
   454  			//expectedRealPath:    filepath.Join(absolute, "/somewhere/outside.txt"),
   455  			// TODO: the real path is not correct
   456  			expectedRealPath:    "../somewhere/outside.txt",
   457  			expectedVirtualPath: "to/the/rel-outside.txt",
   458  		},
   459  		{
   460  			name:  "relative root, relative request, relative indirect (outside of root), cwd within DOUBLE symlink root",
   461  			cwd:   relativeViaDoubleLink,
   462  			root:  "path",
   463  			input: "to/the/rel-outside.txt",
   464  			//expectedRealPath:    filepath.Join(absolute, "/somewhere/outside.txt"),
   465  			// TODO: the real path is not correct
   466  			expectedRealPath:    "../somewhere/outside.txt",
   467  			expectedVirtualPath: "to/the/rel-outside.txt",
   468  		},
   469  		{
   470  			name:  "abs root, relative request, relative indirect (outside of root), cwd within DOUBLE symlink root",
   471  			cwd:   relativeViaDoubleLink,
   472  			root:  filepath.Join(absolute, "path"),
   473  			input: "to/the/rel-outside.txt",
   474  			//expectedRealPath:    filepath.Join(absolute, "/somewhere/outside.txt"),
   475  			// TODO: the real path is not correct
   476  			expectedRealPath:    "../somewhere/outside.txt",
   477  			expectedVirtualPath: "to/the/rel-outside.txt",
   478  		},
   479  		{
   480  			name:  "relative root, abs request, relative indirect (outside of root), cwd within DOUBLE symlink root",
   481  			cwd:   relativeViaDoubleLink,
   482  			root:  "path",
   483  			input: "/to/the/rel-outside.txt",
   484  			//expectedRealPath:    filepath.Join(absolute, "/somewhere/outside.txt"),
   485  			// TODO: the real path is not correct
   486  			expectedRealPath:    "../somewhere/outside.txt",
   487  			expectedVirtualPath: "to/the/rel-outside.txt",
   488  		},
   489  		{
   490  			name:  "abs root, abs request, relative indirect (outside of root), cwd within DOUBLE symlink root",
   491  			cwd:   relativeViaDoubleLink,
   492  			root:  filepath.Join(absolute, "path"),
   493  			input: "/to/the/rel-outside.txt",
   494  			//expectedRealPath:    filepath.Join(absolute, "/somewhere/outside.txt"),
   495  			// TODO: the real path is not correct
   496  			expectedRealPath:    "../somewhere/outside.txt",
   497  			expectedVirtualPath: "to/the/rel-outside.txt",
   498  		},
   499  	}
   500  	for _, c := range cases {
   501  		t.Run(c.name, func(t *testing.T) {
   502  
   503  			// we need to mimic a shell, otherwise we won't get a path within a symlink
   504  			targetPath := filepath.Join(testDir, c.cwd)
   505  			t.Setenv("PWD", filepath.Clean(targetPath))
   506  
   507  			require.NoError(t, err)
   508  			require.NoError(t, os.Chdir(targetPath))
   509  			t.Cleanup(func() {
   510  				require.NoError(t, os.Chdir(testDir))
   511  			})
   512  
   513  			resolver := NewFromUnindexedDirectory(c.root)
   514  			require.NotNil(t, resolver)
   515  
   516  			refs, err := resolver.FilesByPath(c.input)
   517  			require.NoError(t, err)
   518  			if c.expectedRealPath == "" {
   519  				require.Empty(t, refs)
   520  				return
   521  			}
   522  			require.Len(t, refs, 1)
   523  			assert.Equal(t, c.expectedRealPath, refs[0].RealPath, "real path different")
   524  			assert.Equal(t, c.expectedVirtualPath, refs[0].VirtualPath, "virtual path different")
   525  		})
   526  	}
   527  }
   528  
   529  func Test_UnindexedDirectoryResolver_Basic(t *testing.T) {
   530  	wd, err := os.Getwd()
   531  	require.NoError(t, err)
   532  
   533  	r := NewFromUnindexedDirectory(path.Join(wd, "test-fixtures"))
   534  	locations, err := r.FilesByGlob("image-symlinks/*")
   535  	require.NoError(t, err)
   536  	require.Len(t, locations, 5)
   537  }
   538  
   539  func Test_UnindexedDirectoryResolver_FilesByPath_relativeRoot(t *testing.T) {
   540  	cases := []struct {
   541  		name         string
   542  		relativeRoot string
   543  		input        string
   544  		expected     []string
   545  	}{
   546  		{
   547  			name:         "should find a file from an absolute input",
   548  			relativeRoot: "./test-fixtures/",
   549  			input:        "/image-symlinks/file-1.txt",
   550  			expected: []string{
   551  				"image-symlinks/file-1.txt",
   552  			},
   553  		},
   554  		{
   555  			name:         "should find a file from a relative path",
   556  			relativeRoot: "./test-fixtures/",
   557  			input:        "image-symlinks/file-1.txt",
   558  			expected: []string{
   559  				"image-symlinks/file-1.txt",
   560  			},
   561  		},
   562  		{
   563  			name: "should find a file from a relative path (root above cwd)",
   564  			// TODO: refactor me! this test depends on the structure of the source dir not changing, which isn't great
   565  			relativeRoot: "../",
   566  			input:        "fileresolver/deferred.go",
   567  			expected: []string{
   568  				"fileresolver/deferred.go",
   569  			},
   570  		},
   571  	}
   572  	for _, c := range cases {
   573  		t.Run(c.name, func(t *testing.T) {
   574  			resolver := NewFromUnindexedDirectory(c.relativeRoot)
   575  
   576  			refs, err := resolver.FilesByPath(c.input)
   577  			require.NoError(t, err)
   578  			assert.Len(t, refs, len(c.expected))
   579  			s := strset.New()
   580  			for _, actual := range refs {
   581  				s.Add(actual.RealPath)
   582  			}
   583  			assert.ElementsMatch(t, c.expected, s.List())
   584  		})
   585  	}
   586  }
   587  
   588  func Test_UnindexedDirectoryResolver_FilesByPath_absoluteRoot(t *testing.T) {
   589  	cases := []struct {
   590  		name         string
   591  		relativeRoot string
   592  		input        string
   593  		expected     []string
   594  	}{
   595  		{
   596  			name:         "should find a file from an absolute input",
   597  			relativeRoot: "./test-fixtures/",
   598  			input:        "/image-symlinks/file-1.txt",
   599  			expected: []string{
   600  				"image-symlinks/file-1.txt",
   601  			},
   602  		},
   603  		{
   604  			name:         "should find a file from a relative path",
   605  			relativeRoot: "./test-fixtures/",
   606  			input:        "image-symlinks/file-1.txt",
   607  			expected: []string{
   608  				"image-symlinks/file-1.txt",
   609  			},
   610  		},
   611  		{
   612  			name: "should find a file from a relative path (root above cwd)",
   613  			// TODO: refactor me! this test depends on the structure of the source dir not changing, which isn't great
   614  			relativeRoot: "../",
   615  			input:        "fileresolver/directory.go",
   616  			expected: []string{
   617  				"fileresolver/directory.go",
   618  			},
   619  		},
   620  	}
   621  	for _, c := range cases {
   622  		t.Run(c.name, func(t *testing.T) {
   623  			// note: this test is all about asserting correct functionality when the given analysis path
   624  			// is an absolute path
   625  			absRoot, err := filepath.Abs(c.relativeRoot)
   626  			require.NoError(t, err)
   627  
   628  			resolver := NewFromUnindexedDirectory(absRoot)
   629  			assert.NoError(t, err)
   630  
   631  			refs, err := resolver.FilesByPath(c.input)
   632  			require.NoError(t, err)
   633  			assert.Len(t, refs, len(c.expected))
   634  			s := strset.New()
   635  			for _, actual := range refs {
   636  				s.Add(actual.RealPath)
   637  			}
   638  			assert.ElementsMatch(t, c.expected, s.List())
   639  		})
   640  	}
   641  }
   642  
   643  func Test_UnindexedDirectoryResolver_FilesByPath(t *testing.T) {
   644  	cases := []struct {
   645  		name                 string
   646  		root                 string
   647  		input                string
   648  		expected             string
   649  		refCount             int
   650  		forcePositiveHasPath bool
   651  	}{
   652  		{
   653  			name:     "finds a file (relative)",
   654  			root:     "./test-fixtures/",
   655  			input:    "image-symlinks/file-1.txt",
   656  			expected: "image-symlinks/file-1.txt",
   657  			refCount: 1,
   658  		},
   659  		{
   660  			name:     "finds a file with relative indirection",
   661  			root:     "./test-fixtures/../test-fixtures",
   662  			input:    "image-symlinks/file-1.txt",
   663  			expected: "image-symlinks/file-1.txt",
   664  			refCount: 1,
   665  		},
   666  		{
   667  			name:     "managed non-existing files (relative)",
   668  			root:     "./test-fixtures/",
   669  			input:    "test-fixtures/image-symlinks/bogus.txt",
   670  			refCount: 0,
   671  		},
   672  		{
   673  			name:     "finds a file (absolute)",
   674  			root:     "./test-fixtures/",
   675  			input:    "/image-symlinks/file-1.txt",
   676  			expected: "image-symlinks/file-1.txt",
   677  			refCount: 1,
   678  		},
   679  		{
   680  			name:                 "directories ignored",
   681  			root:                 "./test-fixtures/",
   682  			input:                "/image-symlinks",
   683  			refCount:             0,
   684  			forcePositiveHasPath: true,
   685  		},
   686  	}
   687  	for _, c := range cases {
   688  		t.Run(c.name, func(t *testing.T) {
   689  			resolver := NewFromUnindexedDirectory(c.root)
   690  
   691  			hasPath := resolver.HasPath(c.input)
   692  			if !c.forcePositiveHasPath {
   693  				if c.refCount != 0 && !hasPath {
   694  					t.Errorf("expected HasPath() to indicate existence, but did not")
   695  				} else if c.refCount == 0 && hasPath {
   696  					t.Errorf("expected HasPath() to NOT indicate existence, but does")
   697  				}
   698  			} else if !hasPath {
   699  				t.Errorf("expected HasPath() to indicate existence, but did not (force path)")
   700  			}
   701  
   702  			refs, err := resolver.FilesByPath(c.input)
   703  			require.NoError(t, err)
   704  			assert.Len(t, refs, c.refCount)
   705  			for _, actual := range refs {
   706  				assert.Equal(t, c.expected, actual.RealPath)
   707  			}
   708  		})
   709  	}
   710  }
   711  
   712  func Test_UnindexedDirectoryResolver_MultipleFilesByPath(t *testing.T) {
   713  	cases := []struct {
   714  		name     string
   715  		input    []string
   716  		refCount int
   717  	}{
   718  		{
   719  			name:     "finds multiple files",
   720  			input:    []string{"image-symlinks/file-1.txt", "image-symlinks/file-2.txt"},
   721  			refCount: 2,
   722  		},
   723  		{
   724  			name:     "skips non-existing files",
   725  			input:    []string{"image-symlinks/bogus.txt", "image-symlinks/file-1.txt"},
   726  			refCount: 1,
   727  		},
   728  		{
   729  			name:     "does not return anything for non-existing directories",
   730  			input:    []string{"non-existing/bogus.txt", "non-existing/file-1.txt"},
   731  			refCount: 0,
   732  		},
   733  	}
   734  	for _, c := range cases {
   735  		t.Run(c.name, func(t *testing.T) {
   736  			resolver := NewFromUnindexedDirectory("./test-fixtures")
   737  			refs, err := resolver.FilesByPath(c.input...)
   738  			assert.NoError(t, err)
   739  
   740  			if len(refs) != c.refCount {
   741  				t.Errorf("unexpected number of refs: %d != %d", len(refs), c.refCount)
   742  			}
   743  		})
   744  	}
   745  }
   746  
   747  func Test_UnindexedDirectoryResolver_FilesByGlobMultiple(t *testing.T) {
   748  	resolver := NewFromUnindexedDirectory("./test-fixtures")
   749  	refs, err := resolver.FilesByGlob("**/image-symlinks/file*")
   750  	assert.NoError(t, err)
   751  
   752  	assert.Len(t, refs, 2)
   753  }
   754  
   755  func Test_UnindexedDirectoryResolver_FilesByGlobRecursive(t *testing.T) {
   756  	resolver := NewFromUnindexedDirectory("./test-fixtures/image-symlinks")
   757  	refs, err := resolver.FilesByGlob("**/*.txt")
   758  	assert.NoError(t, err)
   759  	assert.Len(t, refs, 6)
   760  }
   761  
   762  func Test_UnindexedDirectoryResolver_FilesByGlobSingle(t *testing.T) {
   763  	resolver := NewFromUnindexedDirectory("./test-fixtures")
   764  	refs, err := resolver.FilesByGlob("**/image-symlinks/*1.txt")
   765  	assert.NoError(t, err)
   766  
   767  	assert.Len(t, refs, 1)
   768  	assert.Equal(t, "image-symlinks/file-1.txt", refs[0].RealPath)
   769  }
   770  
   771  func Test_UnindexedDirectoryResolver_FilesByPath_ResolvesSymlinks(t *testing.T) {
   772  
   773  	tests := []struct {
   774  		name    string
   775  		fixture string
   776  	}{
   777  		{
   778  			name:    "one degree",
   779  			fixture: "link_to_new_readme",
   780  		},
   781  		{
   782  			name:    "two degrees",
   783  			fixture: "link_to_link_to_new_readme",
   784  		},
   785  	}
   786  
   787  	for _, test := range tests {
   788  		t.Run(test.name, func(t *testing.T) {
   789  			resolver := NewFromUnindexedDirectory("./test-fixtures/symlinks-simple")
   790  
   791  			refs, err := resolver.FilesByPath(test.fixture)
   792  			require.NoError(t, err)
   793  			require.Len(t, refs, 1)
   794  
   795  			reader, err := resolver.FileContentsByLocation(refs[0])
   796  			require.NoError(t, err)
   797  
   798  			actual, err := io.ReadAll(reader)
   799  			require.NoError(t, err)
   800  
   801  			expected, err := os.ReadFile("test-fixtures/symlinks-simple/readme")
   802  			require.NoError(t, err)
   803  
   804  			require.Equal(t, string(expected), string(actual))
   805  		})
   806  	}
   807  }
   808  
   809  func Test_UnindexedDirectoryResolverDoesNotIgnoreRelativeSystemPaths(t *testing.T) {
   810  	// let's make certain that "dev/place" is not ignored, since it is not "/dev/place"
   811  	resolver := NewFromUnindexedDirectory("test-fixtures/system_paths/target")
   812  
   813  	// all paths should be found (non filtering matches a path)
   814  	locations, err := resolver.FilesByGlob("**/place")
   815  	assert.NoError(t, err)
   816  	// 4: within target/
   817  	// 1: target/link --> relative path to "place" // NOTE: this is filtered out since it not unique relative to outside_root/link_target/place
   818  	// 1: outside_root/link_target/place
   819  	assert.Len(t, locations, 5)
   820  
   821  	// ensure that symlink indexing outside of root worked
   822  	testLocation := "../outside_root/link_target/place"
   823  	ok := false
   824  	for _, location := range locations {
   825  		if strings.HasSuffix(location.RealPath, testLocation) {
   826  			ok = true
   827  		}
   828  	}
   829  
   830  	if !ok {
   831  		t.Fatalf("could not find test location=%q", testLocation)
   832  	}
   833  }
   834  
   835  func Test_UnindexedDirectoryResover_IndexingNestedSymLinks(t *testing.T) {
   836  	resolver := NewFromUnindexedDirectory("./test-fixtures/symlinks-simple")
   837  
   838  	// check that we can get the real path
   839  	locations, err := resolver.FilesByPath("./readme")
   840  	require.NoError(t, err)
   841  	assert.Len(t, locations, 1)
   842  
   843  	// check that we can access the same file via 1 symlink
   844  	locations, err = resolver.FilesByPath("./link_to_new_readme")
   845  	require.NoError(t, err)
   846  	require.Len(t, locations, 1)
   847  	assert.Equal(t, "readme", locations[0].RealPath)
   848  	assert.Equal(t, "link_to_new_readme", locations[0].VirtualPath)
   849  
   850  	// check that we can access the same file via 2 symlinks
   851  	locations, err = resolver.FilesByPath("./link_to_link_to_new_readme")
   852  	require.NoError(t, err)
   853  	require.Len(t, locations, 1)
   854  	assert.Equal(t, "readme", locations[0].RealPath)
   855  	assert.Equal(t, "link_to_link_to_new_readme", locations[0].VirtualPath)
   856  
   857  	// check that we can access the same file via 2 symlinks
   858  	locations, err = resolver.FilesByGlob("**/link_*")
   859  	require.NoError(t, err)
   860  	require.Len(t, locations, 1) // you would think this is 2, however, they point to the same file, and glob only returns unique files
   861  
   862  	// returned locations can be in any order
   863  	expectedVirtualPaths := []string{
   864  		"link_to_link_to_new_readme",
   865  		//"link_to_new_readme", // we filter out this one because the first symlink resolves to the same file
   866  	}
   867  
   868  	expectedRealPaths := []string{
   869  		"readme",
   870  	}
   871  
   872  	actualRealPaths := strset.New()
   873  	actualVirtualPaths := strset.New()
   874  	for _, a := range locations {
   875  		actualVirtualPaths.Add(a.VirtualPath)
   876  		actualRealPaths.Add(a.RealPath)
   877  	}
   878  
   879  	assert.ElementsMatch(t, expectedVirtualPaths, actualVirtualPaths.List())
   880  	assert.ElementsMatch(t, expectedRealPaths, actualRealPaths.List())
   881  }
   882  
   883  func Test_UnindexedDirectoryResover_IndexingNestedSymLinksOutsideOfRoot(t *testing.T) {
   884  	resolver := NewFromUnindexedDirectory("./test-fixtures/symlinks-multiple-roots/root")
   885  
   886  	// check that we can get the real path
   887  	locations, err := resolver.FilesByPath("./readme")
   888  	require.NoError(t, err)
   889  	assert.Len(t, locations, 1)
   890  
   891  	// check that we can access the same file via 2 symlinks (link_to_link_to_readme -> link_to_readme -> readme)
   892  	locations, err = resolver.FilesByPath("./link_to_link_to_readme")
   893  	require.NoError(t, err)
   894  	assert.Len(t, locations, 1)
   895  
   896  	// something looks wrong here
   897  	t.Failed()
   898  }
   899  
   900  func Test_UnindexedDirectoryResover_RootViaSymlink(t *testing.T) {
   901  	resolver := NewFromUnindexedDirectory("./test-fixtures/symlinked-root/nested/link-root")
   902  
   903  	locations, err := resolver.FilesByPath("./file1.txt")
   904  	require.NoError(t, err)
   905  	assert.Len(t, locations, 1)
   906  
   907  	locations, err = resolver.FilesByPath("./nested/file2.txt")
   908  	require.NoError(t, err)
   909  	assert.Len(t, locations, 1)
   910  
   911  	locations, err = resolver.FilesByPath("./nested/linked-file1.txt")
   912  	require.NoError(t, err)
   913  	assert.Len(t, locations, 1)
   914  }
   915  
   916  func Test_UnindexedDirectoryResolver_FileContentsByLocation(t *testing.T) {
   917  	cwd, err := os.Getwd()
   918  	require.NoError(t, err)
   919  
   920  	r := NewFromUnindexedDirectory(path.Join(cwd, "test-fixtures/image-simple"))
   921  	require.NoError(t, err)
   922  
   923  	tests := []struct {
   924  		name     string
   925  		location file.Location
   926  		expects  string
   927  		err      bool
   928  	}{
   929  		{
   930  			name:     "use file reference for content requests",
   931  			location: file.NewLocation("file-1.txt"),
   932  			expects:  "this file has contents",
   933  		},
   934  		{
   935  			name:     "error on empty file reference",
   936  			location: file.NewLocationFromDirectory("doesn't matter", stereoscopeFile.Reference{}),
   937  			err:      true,
   938  		},
   939  	}
   940  	for _, test := range tests {
   941  		t.Run(test.name, func(t *testing.T) {
   942  
   943  			actual, err := r.FileContentsByLocation(test.location)
   944  			if test.err {
   945  				require.Error(t, err)
   946  				return
   947  			}
   948  
   949  			require.NoError(t, err)
   950  			if test.expects != "" {
   951  				b, err := io.ReadAll(actual)
   952  				require.NoError(t, err)
   953  				assert.Equal(t, test.expects, string(b))
   954  			}
   955  		})
   956  	}
   957  }
   958  
   959  func Test_UnindexedDirectoryResover_SymlinkLoopWithGlobsShouldResolve(t *testing.T) {
   960  	test := func(t *testing.T) {
   961  		resolver := NewFromUnindexedDirectory("./test-fixtures/symlinks-loop")
   962  
   963  		locations, err := resolver.FilesByGlob("**/file.target")
   964  		require.NoError(t, err)
   965  
   966  		require.Len(t, locations, 1)
   967  		assert.Equal(t, "devices/loop0/file.target", locations[0].RealPath)
   968  	}
   969  
   970  	testWithTimeout(t, 5*time.Second, test)
   971  }
   972  
   973  func Test_UnindexedDirectoryResolver_FilesByPath_baseRoot(t *testing.T) {
   974  	cases := []struct {
   975  		name     string
   976  		root     string
   977  		input    string
   978  		expected []string
   979  	}{
   980  		{
   981  			name:  "should find the base file",
   982  			root:  "./test-fixtures/symlinks-base/",
   983  			input: "./base",
   984  			expected: []string{
   985  				"base",
   986  			},
   987  		},
   988  		{
   989  			name:  "should follow a link with a pivoted root",
   990  			root:  "./test-fixtures/symlinks-base/",
   991  			input: "./foo",
   992  			expected: []string{
   993  				"base",
   994  			},
   995  		},
   996  		{
   997  			name:  "should follow a relative link with extra parents",
   998  			root:  "./test-fixtures/symlinks-base/",
   999  			input: "./bar",
  1000  			expected: []string{
  1001  				"base",
  1002  			},
  1003  		},
  1004  		{
  1005  			name:  "should follow an absolute link with extra parents",
  1006  			root:  "./test-fixtures/symlinks-base/",
  1007  			input: "./baz",
  1008  			expected: []string{
  1009  				"base",
  1010  			},
  1011  		},
  1012  		{
  1013  			name:  "should follow an absolute link with extra parents",
  1014  			root:  "./test-fixtures/symlinks-base/",
  1015  			input: "./sub/link",
  1016  			expected: []string{
  1017  				"sub/item",
  1018  			},
  1019  		},
  1020  		{
  1021  			name:  "should follow chained pivoted link",
  1022  			root:  "./test-fixtures/symlinks-base/",
  1023  			input: "./chain",
  1024  			expected: []string{
  1025  				"base",
  1026  			},
  1027  		},
  1028  	}
  1029  	for _, c := range cases {
  1030  		t.Run(c.name, func(t *testing.T) {
  1031  			resolver := NewFromRootedUnindexedDirectory(c.root, c.root)
  1032  
  1033  			refs, err := resolver.FilesByPath(c.input)
  1034  			require.NoError(t, err)
  1035  			assert.Len(t, refs, len(c.expected))
  1036  			s := strset.New()
  1037  			for _, actual := range refs {
  1038  				s.Add(actual.RealPath)
  1039  			}
  1040  			assert.ElementsMatch(t, c.expected, s.List())
  1041  		})
  1042  	}
  1043  
  1044  }
  1045  
  1046  func Test_UnindexedDirectoryResolver_resolvesLinks(t *testing.T) {
  1047  	tests := []struct {
  1048  		name     string
  1049  		runner   func(file.Resolver) []file.Location
  1050  		expected []file.Location
  1051  	}{
  1052  		{
  1053  			name: "by glob to links",
  1054  			runner: func(resolver file.Resolver) []file.Location {
  1055  				// links are searched, but resolve to the real files
  1056  				// for that reason we need to place **/ in front (which is not the same for other resolvers)
  1057  				actualLocations, err := resolver.FilesByGlob("**/*ink-*")
  1058  				assert.NoError(t, err)
  1059  				return actualLocations
  1060  			},
  1061  			expected: []file.Location{
  1062  				file.NewVirtualLocation("file-1.txt", "link-1"),
  1063  				file.NewVirtualLocation("file-2.txt", "link-2"),
  1064  				// we already have this real file path via another link, so only one is returned
  1065  				// file.NewVirtualLocation("file-2.txt", "link-indirect"),
  1066  				file.NewVirtualLocation("file-3.txt", "link-within"),
  1067  			},
  1068  		},
  1069  		{
  1070  			name: "by basename",
  1071  			runner: func(resolver file.Resolver) []file.Location {
  1072  				// links are searched, but resolve to the real files
  1073  				actualLocations, err := resolver.FilesByGlob("**/file-2.txt")
  1074  				assert.NoError(t, err)
  1075  				return actualLocations
  1076  			},
  1077  			expected: []file.Location{
  1078  				// this has two copies in the base image, which overwrites the same location
  1079  				file.NewLocation("file-2.txt"),
  1080  			},
  1081  		},
  1082  		{
  1083  			name: "by basename glob",
  1084  			runner: func(resolver file.Resolver) []file.Location {
  1085  				// links are searched, but resolve to the real files
  1086  				actualLocations, err := resolver.FilesByGlob("**/file-?.txt")
  1087  				assert.NoError(t, err)
  1088  				return actualLocations
  1089  			},
  1090  			expected: []file.Location{
  1091  				file.NewLocation("file-1.txt"),
  1092  				file.NewLocation("file-2.txt"),
  1093  				file.NewLocation("file-3.txt"),
  1094  				file.NewLocation("parent/file-4.txt"),
  1095  			},
  1096  		},
  1097  		{
  1098  			name: "by basename glob to links",
  1099  			runner: func(resolver file.Resolver) []file.Location {
  1100  				actualLocations, err := resolver.FilesByGlob("**/link-*")
  1101  				assert.NoError(t, err)
  1102  				return actualLocations
  1103  			},
  1104  			expected: []file.Location{
  1105  				file.NewVirtualLocationFromDirectory("file-1.txt", "link-1", stereoscopeFile.Reference{RealPath: "file-1.txt"}),
  1106  				file.NewVirtualLocationFromDirectory("file-2.txt", "link-2", stereoscopeFile.Reference{RealPath: "file-2.txt"}),
  1107  				// we already have this real file path via another link, so only one is returned
  1108  				//file.NewVirtualLocationFromDirectory("file-2.txt", "link-indirect", stereoscopeFile.Reference{RealPath: "file-2.txt"}),
  1109  				file.NewVirtualLocationFromDirectory("file-3.txt", "link-within", stereoscopeFile.Reference{RealPath: "file-3.txt"}),
  1110  			},
  1111  		},
  1112  		{
  1113  			name: "by extension",
  1114  			runner: func(resolver file.Resolver) []file.Location {
  1115  				// links are searched, but resolve to the real files
  1116  				actualLocations, err := resolver.FilesByGlob("**/*.txt")
  1117  				assert.NoError(t, err)
  1118  				return actualLocations
  1119  			},
  1120  			expected: []file.Location{
  1121  				file.NewLocation("file-1.txt"),
  1122  				file.NewLocation("file-2.txt"),
  1123  				file.NewLocation("file-3.txt"),
  1124  				file.NewLocation("parent/file-4.txt"),
  1125  			},
  1126  		},
  1127  		{
  1128  			name: "by path to degree 1 link",
  1129  			runner: func(resolver file.Resolver) []file.Location {
  1130  				// links resolve to the final file
  1131  				actualLocations, err := resolver.FilesByPath("/link-2")
  1132  				assert.NoError(t, err)
  1133  				return actualLocations
  1134  			},
  1135  			expected: []file.Location{
  1136  				// we have multiple copies across layers
  1137  				file.NewVirtualLocation("file-2.txt", "link-2"),
  1138  			},
  1139  		},
  1140  		{
  1141  			name: "by path to degree 2 link",
  1142  			runner: func(resolver file.Resolver) []file.Location {
  1143  				// multiple links resolves to the final file
  1144  				actualLocations, err := resolver.FilesByPath("/link-indirect")
  1145  				assert.NoError(t, err)
  1146  				return actualLocations
  1147  			},
  1148  			expected: []file.Location{
  1149  				// we have multiple copies across layers
  1150  				file.NewVirtualLocation("file-2.txt", "link-indirect"),
  1151  			},
  1152  		},
  1153  	}
  1154  
  1155  	for _, test := range tests {
  1156  		t.Run(test.name, func(t *testing.T) {
  1157  			resolver := NewFromUnindexedDirectory("./test-fixtures/symlinks-from-image-symlinks-fixture")
  1158  
  1159  			actual := test.runner(resolver)
  1160  
  1161  			compareLocations(t, test.expected, actual)
  1162  		})
  1163  	}
  1164  }
  1165  
  1166  func Test_UnindexedDirectoryResolver_DoNotAddVirtualPathsToTree(t *testing.T) {
  1167  	resolver := NewFromUnindexedDirectory("./test-fixtures/symlinks-prune-indexing")
  1168  
  1169  	allLocations := resolver.AllLocations()
  1170  	var allRealPaths []stereoscopeFile.Path
  1171  	for l := range allLocations {
  1172  		allRealPaths = append(allRealPaths, stereoscopeFile.Path(l.RealPath))
  1173  	}
  1174  	pathSet := stereoscopeFile.NewPathSet(allRealPaths...)
  1175  
  1176  	assert.False(t,
  1177  		pathSet.Contains("before-path/file.txt"),
  1178  		"symlink destinations should only be indexed at their real path, not through their virtual (symlinked) path",
  1179  	)
  1180  
  1181  	assert.False(t,
  1182  		pathSet.Contains("a-path/file.txt"),
  1183  		"symlink destinations should only be indexed at their real path, not through their virtual (symlinked) path",
  1184  	)
  1185  }
  1186  
  1187  func Test_UnindexedDirectoryResolver_FilesContents_errorOnDirRequest(t *testing.T) {
  1188  	resolver := NewFromUnindexedDirectory("./test-fixtures/system_paths")
  1189  
  1190  	dirLoc := file.NewLocation("arg/foo")
  1191  
  1192  	reader, err := resolver.FileContentsByLocation(dirLoc)
  1193  	require.Error(t, err)
  1194  	require.Nil(t, reader)
  1195  }
  1196  
  1197  func Test_UnindexedDirectoryResolver_AllLocations(t *testing.T) {
  1198  	resolver := NewFromUnindexedDirectory("./test-fixtures/symlinks-from-image-symlinks-fixture")
  1199  
  1200  	paths := strset.New()
  1201  	for loc := range resolver.AllLocations() {
  1202  		if strings.HasPrefix(loc.RealPath, "/") {
  1203  			// ignore outside of the fixture root for now
  1204  			continue
  1205  		}
  1206  		paths.Add(loc.RealPath)
  1207  	}
  1208  	expected := []string{
  1209  		"file-1.txt",
  1210  		"file-2.txt",
  1211  		"file-3.txt",
  1212  		"link-1",
  1213  		"link-2",
  1214  		"link-dead",
  1215  		"link-indirect",
  1216  		"link-within",
  1217  		"parent",
  1218  		"parent-link",
  1219  		"parent/file-4.txt",
  1220  	}
  1221  
  1222  	pathsList := paths.List()
  1223  	sort.Strings(pathsList)
  1224  
  1225  	assert.ElementsMatchf(t, expected, pathsList, "expected all paths to be indexed, but found different paths: \n%s", cmp.Diff(expected, paths.List()))
  1226  }
  1227  
  1228  func Test_WritableUnindexedDirectoryResolver(t *testing.T) {
  1229  	tmpdir := t.TempDir()
  1230  
  1231  	p := "some/path/file"
  1232  	c := "some contents"
  1233  
  1234  	dr := NewFromUnindexedDirectory(tmpdir)
  1235  
  1236  	locations, err := dr.FilesByPath(p)
  1237  	require.NoError(t, err)
  1238  	require.Len(t, locations, 0)
  1239  
  1240  	err = dr.Write(file.NewLocation(p), strings.NewReader(c))
  1241  	require.NoError(t, err)
  1242  
  1243  	locations, err = dr.FilesByPath(p)
  1244  	require.NoError(t, err)
  1245  	require.Len(t, locations, 1)
  1246  
  1247  	reader, err := dr.FileContentsByLocation(locations[0])
  1248  	require.NoError(t, err)
  1249  	bytes, err := io.ReadAll(reader)
  1250  	require.Equal(t, c, string(bytes))
  1251  }
  1252  
  1253  func testWithTimeout(t *testing.T, timeout time.Duration, test func(*testing.T)) {
  1254  	done := make(chan bool)
  1255  	go func() {
  1256  		test(t)
  1257  		done <- true
  1258  	}()
  1259  
  1260  	select {
  1261  	case <-time.After(timeout):
  1262  		t.Fatal("test timed out")
  1263  	case <-done:
  1264  	}
  1265  }
  1266  
  1267  func compareLocations(t *testing.T, expected, actual []file.Location) {
  1268  	t.Helper()
  1269  	ignoreUnexported := cmpopts.IgnoreFields(file.LocationData{}, "ref")
  1270  	ignoreMetadata := cmpopts.IgnoreFields(file.LocationMetadata{}, "Annotations")
  1271  	ignoreFS := cmpopts.IgnoreFields(file.Coordinates{}, "FileSystemID")
  1272  
  1273  	sort.Sort(file.Locations(expected))
  1274  	sort.Sort(file.Locations(actual))
  1275  
  1276  	if d := cmp.Diff(expected, actual,
  1277  		ignoreUnexported,
  1278  		ignoreFS,
  1279  		ignoreMetadata,
  1280  	); d != "" {
  1281  
  1282  		t.Errorf("unexpected locations (-want +got):\n%s", d)
  1283  	}
  1284  
  1285  }