go.mway.dev/x@v0.0.0-20240520034138-950aede9a3fb/archive/extract/extract_test.go (about)

     1  // Copyright (c) 2024 Matt Way
     2  //
     3  // Permission is hereby granted, free of charge, to any person obtaining a copy
     4  // of this software and associated documentation files (the "Software"), to
     5  // deal in the Software without restriction, including without limitation the
     6  // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
     7  // sell copies of the Software, and to permit persons to whom the Software is
     8  // furnished to do so, subject to the following conditions:
     9  //
    10  // The above copyright notice and this permission notice shall be included in
    11  // all copies or substantial portions of the Software.
    12  //
    13  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    14  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    15  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    16  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    17  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
    18  // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
    19  // IN THE THE SOFTWARE.
    20  
    21  package extract
    22  
    23  import (
    24  	"bytes"
    25  	"context"
    26  	"errors"
    27  	"io/fs"
    28  	"os"
    29  	"path/filepath"
    30  	"strings"
    31  	"testing"
    32  
    33  	"github.com/stretchr/testify/require"
    34  )
    35  
    36  var _fullTree = map[string]string{
    37  	"foo/bar/bat/testfile": "foo/bar/bat/",
    38  	"foo/bar/qux/testfile": "foo/bar/qux/",
    39  	"foo/bar/testfile":     "foo/bar/",
    40  	"foo/baz/bat/testfile": "foo/baz/bat/",
    41  	"foo/baz/qux/testfile": "foo/baz/qux/",
    42  	"foo/baz/testfile":     "foo/baz/",
    43  	"foo/testfile":         "foo/",
    44  }
    45  
    46  func TestExtract(t *testing.T) {
    47  	cases := map[string]struct { //nolint:govet
    48  		giveArchives []string
    49  		giveOptions  []Option
    50  		wantTree     map[string]string
    51  		wantMissing  []string
    52  		wantErr      error
    53  	}{
    54  		"gzip": {
    55  			giveArchives: []string{
    56  				"testdata/foo.tar.gz",
    57  				"testdata/foo.tgz",
    58  			},
    59  			giveOptions: nil,
    60  			wantTree:    _fullTree,
    61  			wantMissing: nil,
    62  			wantErr:     nil,
    63  		},
    64  		"bzip2": {
    65  			giveArchives: []string{
    66  				"testdata/foo.tar.bz",
    67  				"testdata/foo.tar.bz2",
    68  				"testdata/foo.tbz",
    69  				"testdata/foo.tbz2",
    70  			},
    71  			giveOptions: nil,
    72  			wantTree:    _fullTree,
    73  			wantMissing: nil,
    74  			wantErr:     nil,
    75  		},
    76  		"xz": {
    77  			giveArchives: []string{
    78  				"testdata/foo.tar.xz",
    79  			},
    80  			giveOptions: nil,
    81  			wantTree:    _fullTree,
    82  			wantMissing: nil,
    83  			wantErr:     nil,
    84  		},
    85  		"zip": {
    86  			giveArchives: []string{
    87  				"testdata/foo.zip",
    88  			},
    89  			giveOptions: nil,
    90  			wantTree:    _fullTree,
    91  			wantMissing: nil,
    92  			wantErr:     nil,
    93  		},
    94  		"include paths": {
    95  			giveArchives: []string{
    96  				"testdata/foo.tar.bz",
    97  				"testdata/foo.tar.bz2",
    98  				"testdata/foo.tar.gz",
    99  				"testdata/foo.tar.xz",
   100  				"testdata/foo.tbz",
   101  				"testdata/foo.tbz2",
   102  				"testdata/foo.tgz",
   103  				"testdata/foo.zip",
   104  			},
   105  			giveOptions: []Option{
   106  				IncludePaths(map[string]string{
   107  					"foo/*ar/bat/testfile": "overridden-bat-dst",
   108  					"foo/baz/*/testfile":   "",
   109  					"foo/testfile":         "",
   110  				}),
   111  			},
   112  			wantTree: map[string]string{
   113  				"overridden-bat-dst":   _fullTree["foo/bar/bat/testfile"],
   114  				"foo/baz/bat/testfile": _fullTree["foo/baz/bat/testfile"],
   115  				"foo/baz/qux/testfile": _fullTree["foo/baz/qux/testfile"],
   116  				"foo/testfile":         _fullTree["foo/testfile"],
   117  			},
   118  			wantMissing: []string{
   119  				"foo/bar/bat/testfile",
   120  				"foo/bar/qux/testfile",
   121  				"foo/bar/testfile",
   122  				"foo/baz/testfile",
   123  			},
   124  			wantErr: nil,
   125  		},
   126  		"exclude paths": {
   127  			giveArchives: []string{
   128  				"testdata/foo.tar.bz",
   129  				"testdata/foo.tar.bz2",
   130  				"testdata/foo.tar.gz",
   131  				"testdata/foo.tar.xz",
   132  				"testdata/foo.tbz",
   133  				"testdata/foo.tbz2",
   134  				"testdata/foo.tgz",
   135  				"testdata/foo.zip",
   136  			},
   137  			giveOptions: []Option{
   138  				ExcludePaths([]string{
   139  					"foo/bar/*",
   140  					"foo/bar/*/*",
   141  					"foo/baz/*",
   142  				}),
   143  			},
   144  			wantTree: map[string]string{
   145  				"foo/testfile":         _fullTree["foo/testfile"],
   146  				"foo/baz/bat/testfile": _fullTree["foo/baz/bat/testfile"],
   147  				"foo/baz/qux/testfile": _fullTree["foo/baz/qux/testfile"],
   148  			},
   149  			wantMissing: []string{
   150  				"foo/bar/bat/testfile",
   151  				"foo/bar/qux/testfile",
   152  				"foo/bar/testfile",
   153  				"foo/baz/testfile",
   154  			},
   155  			wantErr: nil,
   156  		},
   157  		"strip prefix": {
   158  			giveArchives: []string{
   159  				"testdata/foo.tar.bz",
   160  				"testdata/foo.tar.bz2",
   161  				"testdata/foo.tar.gz",
   162  				"testdata/foo.tar.xz",
   163  				"testdata/foo.tbz",
   164  				"testdata/foo.tbz2",
   165  				"testdata/foo.tgz",
   166  				"testdata/foo.zip",
   167  			},
   168  			giveOptions: []Option{
   169  				StripPrefix("foo"),
   170  				IncludePaths(map[string]string{
   171  					"testfile": "",
   172  				}),
   173  			},
   174  			wantTree: map[string]string{
   175  				"testfile": _fullTree["foo/testfile"],
   176  			},
   177  			wantMissing: []string{
   178  				"foo/bar/bat/testfile",
   179  				"foo/bar/bat/testfile",
   180  				"foo/bar/qux/testfile",
   181  				"foo/bar/testfile",
   182  				"foo/baz/bat/testfile",
   183  				"foo/baz/qux/testfile",
   184  				"foo/baz/testfile",
   185  			},
   186  			wantErr: nil,
   187  		},
   188  	}
   189  
   190  	for name, tt := range cases {
   191  		t.Run(name, func(t *testing.T) {
   192  			for _, archive := range tt.giveArchives {
   193  				t.Run(filepath.Base(archive), func(t *testing.T) {
   194  					var (
   195  						dst  = t.TempDir()
   196  						opts = Options{}.With(tt.giveOptions...)
   197  					)
   198  
   199  					err := Extract(context.Background(), dst, archive, opts)
   200  					require.ErrorIs(t, err, tt.wantErr)
   201  
   202  					for relpath, contents := range tt.wantTree {
   203  						path := filepath.Join(dst, relpath)
   204  
   205  						stat, statErr := os.Stat(path)
   206  						require.NoError(t, statErr)
   207  						require.False(t, stat.IsDir())
   208  
   209  						raw, readErr := os.ReadFile(path)
   210  						require.NoError(t, readErr)
   211  						require.Equal(t, contents, string(bytes.TrimSpace(raw)))
   212  					}
   213  
   214  					for _, relpath := range tt.wantMissing {
   215  						_, statErr := os.Stat(filepath.Join(dst, relpath))
   216  						require.ErrorIs(t, statErr, os.ErrNotExist, relpath)
   217  					}
   218  				})
   219  			}
   220  		})
   221  	}
   222  }
   223  
   224  func TestExtractOutput(t *testing.T) {
   225  	archives := []string{
   226  		"testdata/foo.tar.bz",
   227  		"testdata/foo.tar.bz2",
   228  		"testdata/foo.tar.gz",
   229  		"testdata/foo.tar.xz",
   230  		"testdata/foo.tbz",
   231  		"testdata/foo.tbz2",
   232  		"testdata/foo.tgz",
   233  		"testdata/foo.zip",
   234  	}
   235  
   236  	for _, archive := range archives {
   237  		t.Run(filepath.Base(archive), func(t *testing.T) {
   238  			buf := bytes.NewBuffer(nil)
   239  			require.NoError(
   240  				t,
   241  				Extract(
   242  					context.Background(),
   243  					ExtractToTempDir,
   244  					archive,
   245  					Output(buf),
   246  				),
   247  			)
   248  
   249  			var (
   250  				lines = strings.Split(strings.TrimSpace(buf.String()), "\n")
   251  				count int
   252  			)
   253  
   254  			// Ignore ._* files.
   255  			for _, line := range lines {
   256  				if !strings.Contains(line, "/._") {
   257  					count++
   258  				}
   259  			}
   260  
   261  			require.Equal(t, len(_fullTree), count)
   262  		})
   263  	}
   264  }
   265  
   266  func TestExtractTempDirWithCallback(t *testing.T) {
   267  	archives := []string{
   268  		"testdata/foo.tar.bz",
   269  		"testdata/foo.tar.bz2",
   270  		"testdata/foo.tar.gz",
   271  		"testdata/foo.tar.xz",
   272  		"testdata/foo.tbz",
   273  		"testdata/foo.tbz2",
   274  		"testdata/foo.tgz",
   275  		"testdata/foo.zip",
   276  	}
   277  
   278  	for _, archive := range archives {
   279  		t.Run(filepath.Base(archive), func(t *testing.T) {
   280  			var (
   281  				wantErr = errors.New("done")
   282  				err     = Extract(
   283  					context.Background(),
   284  					ExtractToTempDir,
   285  					archive,
   286  					Callback(func(_ context.Context, dir string) error {
   287  						err := filepath.WalkDir(
   288  							dir,
   289  							func(path string, _ fs.DirEntry, err error) error {
   290  								require.NoError(t, err)
   291  
   292  								contents, exists := _fullTree[path]
   293  								if !exists {
   294  									return nil
   295  								}
   296  
   297  								raw, err := os.ReadFile(path)
   298  								require.NoError(t, err)
   299  								require.Equal(t, contents, string(raw))
   300  								return nil
   301  							},
   302  						)
   303  						require.NoError(t, err)
   304  						return wantErr
   305  					}),
   306  				)
   307  			)
   308  			require.ErrorIs(t, err, wantErr)
   309  		})
   310  	}
   311  }
   312  
   313  func TestStripPrefix(t *testing.T) {
   314  	cases := map[string]struct { //nolint:govet
   315  		givePath     string
   316  		givePrefix   string
   317  		wantPath     string
   318  		wantStripped bool
   319  		wantErr      error
   320  	}{
   321  		"matched exact": {
   322  			givePath:     "foo/bar/baz/bat",
   323  			givePrefix:   "foo/bar",
   324  			wantPath:     "baz/bat",
   325  			wantStripped: true,
   326  			wantErr:      nil,
   327  		},
   328  		"matched exact with trailing slash": {
   329  			givePath:     "foo/bar/baz/bat",
   330  			givePrefix:   "foo/bar/",
   331  			wantPath:     "baz/bat",
   332  			wantStripped: true,
   333  			wantErr:      nil,
   334  		},
   335  		"matched glob": {
   336  			givePath:     "foo/bar/baz/bat",
   337  			givePrefix:   "f*/*",
   338  			wantPath:     "baz/bat",
   339  			wantStripped: true,
   340  			wantErr:      nil,
   341  		},
   342  		"not matched exact": {
   343  			givePath:     "foo/bar",
   344  			givePrefix:   "bar",
   345  			wantPath:     "foo/bar",
   346  			wantStripped: false,
   347  			wantErr:      nil,
   348  		},
   349  		"not matched glob": {
   350  			givePath:     "foo/bar",
   351  			givePrefix:   "o*",
   352  			wantPath:     "foo/bar",
   353  			wantStripped: false,
   354  			wantErr:      nil,
   355  		},
   356  		"empty prefix": {
   357  			givePath:     "foo/bar",
   358  			givePrefix:   "",
   359  			wantPath:     "foo/bar",
   360  			wantStripped: true,
   361  			wantErr:      nil,
   362  		},
   363  		"empty strings": {
   364  			givePath:     "",
   365  			givePrefix:   "",
   366  			wantPath:     "",
   367  			wantStripped: true,
   368  			wantErr:      nil,
   369  		},
   370  	}
   371  
   372  	for name, tt := range cases {
   373  		t.Run(name, func(t *testing.T) {
   374  			path, stripped, err := stripPrefix(tt.givePath, tt.givePrefix)
   375  			require.Equal(t, tt.wantPath, path)
   376  			require.Equal(t, tt.wantStripped, stripped)
   377  			require.ErrorIs(t, err, tt.wantErr)
   378  		})
   379  	}
   380  }