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