github.com/anchore/syft@v1.4.2-0.20240516191711-1bec1fc5d397/syft/internal/fileresolver/directory_test.go (about)

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