github.com/kastenhq/syft@v0.0.0-20230821225854-0710af25cdbe/syft/internal/fileresolver/directory_test.go (about)

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