github.com/argoproj/argo-cd/v3@v3.2.1/util/io/files/tar_test.go (about)

     1  package files_test
     2  
     3  import (
     4  	"archive/tar"
     5  	"compress/gzip"
     6  	"fmt"
     7  	"io"
     8  	"math"
     9  	"os"
    10  	"path"
    11  	"path/filepath"
    12  	"testing"
    13  
    14  	"github.com/stretchr/testify/assert"
    15  	"github.com/stretchr/testify/require"
    16  
    17  	"github.com/argoproj/argo-cd/v3/test"
    18  	"github.com/argoproj/argo-cd/v3/util/io/files"
    19  )
    20  
    21  func TestTgz(t *testing.T) {
    22  	t.Parallel()
    23  
    24  	type fixture struct {
    25  		file *os.File
    26  	}
    27  	setup := func(t *testing.T) *fixture {
    28  		t.Helper()
    29  		testDir := getTestDataDir(t)
    30  		f, err := os.CreateTemp(testDir, "")
    31  		require.NoError(t, err)
    32  		return &fixture{
    33  			file: f,
    34  		}
    35  	}
    36  	teardown := func(f *fixture) {
    37  		f.file.Close()
    38  		os.Remove(f.file.Name())
    39  	}
    40  	prepareRead := func(f *fixture) {
    41  		_, err := f.file.Seek(0, io.SeekStart)
    42  		require.NoError(t, err)
    43  	}
    44  
    45  	t.Run("will tgz folder successfully", func(t *testing.T) {
    46  		// given
    47  		t.Parallel()
    48  		exclusions := []string{}
    49  		f := setup(t)
    50  		defer teardown(f)
    51  
    52  		// when
    53  		filesWritten, err := files.Tgz(getTestAppDir(t), nil, exclusions, f.file)
    54  
    55  		// then
    56  		assert.Equal(t, 3, filesWritten)
    57  		require.NoError(t, err)
    58  		prepareRead(f)
    59  		files, err := read(f.file)
    60  		require.NoError(t, err)
    61  		assert.Len(t, files, 8)
    62  		assert.Contains(t, files, "README.md")
    63  		assert.Contains(t, files, "applicationset/latest/kustomization.yaml")
    64  		assert.Contains(t, files, "applicationset/stable/kustomization.yaml")
    65  		assert.Contains(t, files, "applicationset/readme-symlink")
    66  		assert.Equal(t, "../README.md", files["applicationset/readme-symlink"])
    67  	})
    68  	t.Run("will exclude files from the exclusion list", func(t *testing.T) {
    69  		// given
    70  		t.Parallel()
    71  		exclusions := []string{"README.md"}
    72  		f := setup(t)
    73  		defer teardown(f)
    74  
    75  		// when
    76  		filesWritten, err := files.Tgz(getTestAppDir(t), nil, exclusions, f.file)
    77  
    78  		// then
    79  		assert.Equal(t, 2, filesWritten)
    80  		require.NoError(t, err)
    81  		prepareRead(f)
    82  		files, err := read(f.file)
    83  		require.NoError(t, err)
    84  		assert.Len(t, files, 7)
    85  		assert.Contains(t, files, "applicationset/latest/kustomization.yaml")
    86  		assert.Contains(t, files, "applicationset/stable/kustomization.yaml")
    87  	})
    88  	t.Run("will exclude directories from the exclusion list", func(t *testing.T) {
    89  		// given
    90  		t.Parallel()
    91  		exclusions := []string{"README.md", "applicationset/latest"}
    92  		f := setup(t)
    93  		defer teardown(f)
    94  
    95  		// when
    96  		filesWritten, err := files.Tgz(getTestAppDir(t), nil, exclusions, f.file)
    97  
    98  		// then
    99  		assert.Equal(t, 1, filesWritten)
   100  		require.NoError(t, err)
   101  		prepareRead(f)
   102  		files, err := read(f.file)
   103  		require.NoError(t, err)
   104  		assert.Len(t, files, 5)
   105  		assert.Contains(t, files, "applicationset/stable/kustomization.yaml")
   106  	})
   107  }
   108  
   109  func TestUntgz(t *testing.T) {
   110  	createTmpDir := func(t *testing.T) string {
   111  		t.Helper()
   112  		tmpDir, err := os.MkdirTemp(getTestDataDir(t), "")
   113  		require.NoErrorf(t, err, "error creating tmpDir: %s", err)
   114  		return tmpDir
   115  	}
   116  	deleteTmpDir := func(t *testing.T, dirname string) {
   117  		t.Helper()
   118  		assert.NoError(t, os.RemoveAll(dirname), "error removing tmpDir")
   119  	}
   120  	createTgz := func(t *testing.T, fromDir, destDir string) *os.File {
   121  		t.Helper()
   122  		f, err := os.CreateTemp(destDir, "")
   123  		require.NoErrorf(t, err, "error creating tmpFile in %q: %s", destDir, err)
   124  		_, err = files.Tgz(fromDir, nil, nil, f)
   125  		require.NoErrorf(t, err, "error during Tgz: %s", err)
   126  		_, err = f.Seek(0, io.SeekStart)
   127  		require.NoErrorf(t, err, "seek error: %s", err)
   128  		return f
   129  	}
   130  	readFiles := func(t *testing.T, basedir string) map[string]string {
   131  		t.Helper()
   132  		names := make(map[string]string)
   133  		err := filepath.Walk(basedir, func(path string, info os.FileInfo, err error) error {
   134  			if err != nil {
   135  				return err
   136  			}
   137  			link := ""
   138  			if files.IsSymlink(info) {
   139  				link, err = os.Readlink(path)
   140  				if err != nil {
   141  					return err
   142  				}
   143  			}
   144  			relativePath, err := files.RelativePath(path, basedir)
   145  			require.NoError(t, err)
   146  			names[relativePath] = link
   147  			return nil
   148  		})
   149  		require.NoErrorf(t, err, "error reading files: %s", err)
   150  		return names
   151  	}
   152  	t.Run("will untgz successfully", func(t *testing.T) {
   153  		// given
   154  		tmpDir := createTmpDir(t)
   155  		defer deleteTmpDir(t, tmpDir)
   156  		tgzFile := createTgz(t, getTestAppDir(t), tmpDir)
   157  		defer tgzFile.Close()
   158  
   159  		destDir := filepath.Join(tmpDir, "untgz1")
   160  
   161  		// when
   162  		err := files.Untgz(destDir, tgzFile, math.MaxInt64, false)
   163  
   164  		// then
   165  		require.NoError(t, err)
   166  		names := readFiles(t, destDir)
   167  		assert.Len(t, names, 8)
   168  		assert.Contains(t, names, "README.md")
   169  		assert.Contains(t, names, "applicationset/latest/kustomization.yaml")
   170  		assert.Contains(t, names, "applicationset/stable/kustomization.yaml")
   171  		assert.Contains(t, names, "applicationset/readme-symlink")
   172  		assert.Equal(t, "../README.md", names["applicationset/readme-symlink"])
   173  	})
   174  	t.Run("will protect against symlink exploit", func(t *testing.T) {
   175  		// given
   176  		tmpDir := createTmpDir(t)
   177  		defer deleteTmpDir(t, tmpDir)
   178  		tgzFile := createTgz(t, filepath.Join(getTestDataDir(t), "symlink-exploit"), tmpDir)
   179  
   180  		defer tgzFile.Close()
   181  
   182  		destDir := filepath.Join(tmpDir, "untgz2")
   183  
   184  		// when
   185  		err := files.Untgz(destDir, tgzFile, math.MaxInt64, false)
   186  
   187  		// then
   188  		assert.ErrorContains(t, err, "illegal filepath in symlink")
   189  	})
   190  	t.Run("will protect against symlink exploit when relativizing symlinks", func(t *testing.T) {
   191  		// given
   192  		tmpDir := createTmpDir(t)
   193  		defer deleteTmpDir(t, tmpDir)
   194  		tgzFile := createTgz(t, filepath.Join(getTestDataDir(t), "symlink-exploit"), tmpDir)
   195  
   196  		defer tgzFile.Close()
   197  
   198  		destDir := filepath.Join(tmpDir, "untgz2")
   199  
   200  		// when
   201  		err := files.Untgz(destDir, tgzFile, math.MaxInt64, false)
   202  
   203  		// then
   204  		assert.ErrorContains(t, err, "illegal filepath in symlink")
   205  	})
   206  
   207  	t.Run("preserves file mode", func(t *testing.T) {
   208  		// given
   209  		tmpDir := createTmpDir(t)
   210  		defer deleteTmpDir(t, tmpDir)
   211  		tgzFile := createTgz(t, filepath.Join(getTestDataDir(t), "executable"), tmpDir)
   212  		defer tgzFile.Close()
   213  
   214  		destDir := filepath.Join(tmpDir, "untgz1")
   215  
   216  		// when
   217  		err := files.Untgz(destDir, tgzFile, math.MaxInt64, true)
   218  		require.NoError(t, err)
   219  
   220  		// then
   221  
   222  		scriptFileInfo, err := os.Stat(path.Join(destDir, "script.sh"))
   223  		require.NoError(t, err)
   224  		assert.Equal(t, os.FileMode(0o755), scriptFileInfo.Mode())
   225  	})
   226  	t.Run("relativizes symlinks", func(t *testing.T) {
   227  		// given
   228  		tmpDir := createTmpDir(t)
   229  		defer deleteTmpDir(t, tmpDir)
   230  		tgzFile := createTgz(t, getTestAppDir(t), tmpDir)
   231  		defer tgzFile.Close()
   232  
   233  		destDir := filepath.Join(tmpDir, "symlink-relativize")
   234  
   235  		// when
   236  		err := files.Untgz(destDir, tgzFile, math.MaxInt64, false)
   237  
   238  		// then
   239  		require.NoError(t, err)
   240  		names := readFiles(t, destDir)
   241  		assert.Equal(t, "../README.md", names["applicationset/readme-symlink"])
   242  	})
   243  }
   244  
   245  // read returns a map with the filename as key. In case
   246  // the file is a symlink, the value will be populated with
   247  // the target file pointed by the symlink.
   248  func read(tgz *os.File) (map[string]string, error) {
   249  	files := make(map[string]string)
   250  	gzr, err := gzip.NewReader(tgz)
   251  	if err != nil {
   252  		return nil, fmt.Errorf("error reading file: %w", err)
   253  	}
   254  	defer gzr.Close()
   255  
   256  	tr := tar.NewReader(gzr)
   257  
   258  	for {
   259  		header, err := tr.Next()
   260  		if err != nil {
   261  			if err == io.EOF {
   262  				break
   263  			}
   264  			return nil, fmt.Errorf("error while iterating on tar reader: %w", err)
   265  		}
   266  		if header == nil {
   267  			continue
   268  		}
   269  		files[header.Name] = header.Linkname
   270  	}
   271  	return files, nil
   272  }
   273  
   274  // getTestAppDir will return the full path of the app dir under
   275  // the 'testdata' folder.
   276  func getTestAppDir(t *testing.T) string {
   277  	t.Helper()
   278  	return filepath.Join(getTestDataDir(t), "app")
   279  }
   280  
   281  // getTestDataDir will return the full path of the testdata dir
   282  // under the running test folder.
   283  func getTestDataDir(t *testing.T) string {
   284  	t.Helper()
   285  	return filepath.Join(test.GetTestDir(t), "testdata")
   286  }