github.com/anchore/syft@v1.38.2/syft/internal/fileresolver/unindexed_directory_test.go (about)

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