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