Repository: cerbos/cerbos
Pull request #1473 (2023-02-22 09:47:15)

enhancement: Add support for reading archive files for the disk driver
Adds function to attempt retrieval of an `fs.FS` instance from `tar`, `zip` and `gzip` files if they have the matching extensions, otherwise defaults to `os.DirFS`.

TODO
- [x] Swop out for existing os.DirFS() calls where appropriate
- [x] Tidy file extension parsing
- [x] Fix [dirwatch](https://github.com/Sambigeara/cerbos/blob/e20b91705e28d41bb120b10102ef6b28d2779d94/internal/storage/disk/dirwatch.go#L51-L53)
- [x] Docs

codecov[bot]: # [Codecov](https://codecov.io/gh/cerbos/cerbos/pull/1473?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cerbos) Report
> Merging [#1473](https://codecov.io/gh/cerbos/cerbos/pull/1473?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cerbos) (19c8ded) into [main](https://codecov.io/gh/cerbos/cerbos/commit/62244d2ef73a828b3028762e37fefa917f656500?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cerbos) (62244d2) will **increase** coverage by `13.93%`.
> The diff coverage is `n/a`.

<details><summary>Additional details and impacted files</summary>


[![Impacted file tree graph](https://codecov.io/gh/cerbos/cerbos/pull/1473/graphs/tree.svg?width=650&height=150&src=pr&token=tGaxiUZUzL&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cerbos)](https://codecov.io/gh/cerbos/cerbos/pull/1473?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cerbos)

```diff
@@             Coverage Diff             @@
##             main    #1473       +/-   ##
===========================================
+ Coverage   54.09%   68.02%   +13.93%     
===========================================
  Files         117        7      -110     
  Lines       13150     1057    -12093     
===========================================
- Hits         7113      719     -6394     
+ Misses       5435      282     -5153     
+ Partials      602       56      -546     
```


| [Impacted Files](https://codecov.io/gh/cerbos/cerbos/pull/1473?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cerbos) | Coverage Δ | |
|---|---|---|
| [internal/server/tests.go](https://codecov.io/gh/cerbos/cerbos/pull/1473?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cerbos#diff-aW50ZXJuYWwvc2VydmVyL3Rlc3RzLmdv) | `96.96% <ø> (-0.01%)` | :arrow_down: |
| [internal/policy/policy.go](https://codecov.io/gh/cerbos/cerbos/pull/1473?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cerbos#diff-aW50ZXJuYWwvcG9saWN5L3BvbGljeS5nbw==) | | |
| [internal/util/filesystem.go](https://codecov.io/gh/cerbos/cerbos/pull/1473?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cerbos#diff-aW50ZXJuYWwvdXRpbC9maWxlc3lzdGVtLmdv) | | |
| [internal/storage/db/internal/model.go](https://codecov.io/gh/cerbos/cerbos/pull/1473?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cerbos#diff-aW50ZXJuYWwvc3RvcmFnZS9kYi9pbnRlcm5hbC9tb2RlbC5nbw==) | | |
| [internal/auxdata/auxdata.go](https://codecov.io/gh/cerbos/cerbos/pull/1473?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cerbos#diff-aW50ZXJuYWwvYXV4ZGF0YS9hdXhkYXRhLmdv) | | |
| [internal/storage/db/postgres/conf.go](https://codecov.io/gh/cerbos/cerbos/pull/1473?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cerbos#diff-aW50ZXJuYWwvc3RvcmFnZS9kYi9wb3N0Z3Jlcy9jb25mLmdv) | | |
| [internal/telemetry/telemetry.go](https://codecov.io/gh/cerbos/cerbos/pull/1473?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cerbos#diff-aW50ZXJuYWwvdGVsZW1ldHJ5L3RlbGVtZXRyeS5nbw==) | | |
| [internal/audit/file/log.go](https://codecov.io/gh/cerbos/cerbos/pull/1473?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cerbos#diff-aW50ZXJuYWwvYXVkaXQvZmlsZS9sb2cuZ28=) | | |
| [internal/audit/local/conf.go](https://codecov.io/gh/cerbos/cerbos/pull/1473?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cerbos#diff-aW50ZXJuYWwvYXVkaXQvbG9jYWwvY29uZi5nbw==) | | |
| [internal/engine/metrics.go](https://codecov.io/gh/cerbos/cerbos/pull/1473?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cerbos#diff-aW50ZXJuYWwvZW5naW5lL21ldHJpY3MuZ28=) | | |
| ... and [101 more](https://codecov.io/gh/cerbos/cerbos/pull/1473?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cerbos) | |

</details>
Sambigeara: Was hoping to wrap this up today, but manual testing just highlighted an issue with the `watchDir` function [here](https://github.com/Sambigeara/cerbos/blob/e20b91705e28d41bb120b10102ef6b28d2779d94/internal/storage/disk/dirwatch.go#L51-L53). Have to step away now, but will fix this (and do more thorough testing) when I pick this up again on Friday.
charithe: We should probably disable directory watch if the path is not a directory because I don't think it makes sense in the context of an archive file.
Sambigeara: The code coverage is confusing me a little, as my tests are covering the code paths that codecov seems to think I'm not 🤷‍♂️ 
Sambigeara: The force push to [4f9853b](https://github.com/cerbos/cerbos/commit/4f9853b2319d7d546816826ba8b4297e7d914b14) was just a rebase on `main`

Patch:
diff --git a/.gitattributes b/.gitattributes
index eaf9bd6c..e455317c 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -2,4 +2,6 @@
 assets/ filter=lfs diff=lfs merge=lfs -text
 **/*.svg filter=lfs diff=lfs merge=lfs -text
 **/*.gif filter=lfs diff=lfs merge=lfs -text
-**/*.zip filter=lfs diff=lfs merge=lfs -text
\ No newline at end of file
+**/*.zip filter=lfs diff=lfs merge=lfs -text
+**/*.tar filter=lfs diff=lfs merge=lfs -text
+**/*.tgz filter=lfs diff=lfs merge=lfs -text
diff --git a/cmd/cerbos/compile/compile.go b/cmd/cerbos/compile/compile.go
index 2b336142..f725b468 100644
--- a/cmd/cerbos/compile/compile.go
+++ b/cmd/cerbos/compile/compile.go
@@ -29,6 +29,7 @@ import (
 	"github.com/cerbos/cerbos/internal/schema"
 	"github.com/cerbos/cerbos/internal/storage/disk"
 	"github.com/cerbos/cerbos/internal/storage/index"
+	"github.com/cerbos/cerbos/internal/util"
 	"github.com/cerbos/cerbos/internal/verify"
 )
 
@@ -76,7 +77,12 @@ func (c *Cmd) Run(k *kong.Kong) error {
 
 	p := printer.New(k.Stdout, k.Stderr)
 
-	idx, err := index.Build(ctx, os.DirFS(c.Dir), index.WithBuildFailureLogLevel(zap.DebugLevel))
+	fsys, err := util.OpenDirectoryFS(c.Dir)
+	if err != nil {
+		return err
+	}
+
+	idx, err := index.Build(ctx, fsys, index.WithBuildFailureLogLevel(zap.DebugLevel))
 	if err != nil {
 		idxErr := new(index.BuildError)
 		if errors.As(err, &idxErr) {
@@ -115,7 +121,11 @@ func (c *Cmd) Run(k *kong.Kong) error {
 			return fmt.Errorf("failed to create engine: %w", err)
 		}
 
-		results, err := verify.Verify(ctx, c.testsDir(), eng, verifyConf)
+		testFsys, err := c.testsDir()
+		if err != nil {
+			return err
+		}
+		results, err := verify.Verify(ctx, testFsys, eng, verifyConf)
 		if err != nil {
 			return fmt.Errorf("failed to run tests: %w", err)
 		}
@@ -135,12 +145,12 @@ func (c *Cmd) Run(k *kong.Kong) error {
 	return nil
 }
 
-func (c *Cmd) testsDir() fs.FS {
+func (c *Cmd) testsDir() (fs.FS, error) {
 	if c.Tests == "" {
-		return os.DirFS(c.Dir)
+		return util.OpenDirectoryFS(c.Dir)
 	}
 
-	return os.DirFS(c.Tests)
+	return util.OpenDirectoryFS(c.Tests)
 }
 
 func (c *Cmd) Help() string {
diff --git a/cmd/cerbosctl/put/internal/files/find.go b/cmd/cerbosctl/put/internal/files/find.go
index f9e5ed70..944db27a 100644
--- a/cmd/cerbosctl/put/internal/files/find.go
+++ b/cmd/cerbosctl/put/internal/files/find.go
@@ -4,7 +4,6 @@
 package files
 
 import (
-	"archive/zip"
 	"fmt"
 	"io/fs"
 	"os"
@@ -32,18 +31,12 @@ func find(path string, recursive bool, fileType util.IndexedFileType, callback c
 	}
 
 	switch {
-	case util.IsZip(path):
-		zipFile, err := zip.OpenReader(path)
+	case fileInfo.IsDir() || util.IsArchiveFile(path):
+		fsys, err := util.OpenDirectoryFS(path)
 		if err != nil {
-			return fmt.Errorf("failed to open zip file %s: %w", path, err)
-		}
-		defer zipFile.Close()
-
-		if err := doFind(zipFile, fileType, recursive, callback); err != nil {
-			return fmt.Errorf("failed to find files %s: %w", path, err)
+			return err
 		}
-	case fileInfo.IsDir():
-		if err := doFind(os.DirFS(path), fileType, recursive, callback); err != nil {
+		if err := doFind(fsys, fileType, recursive, callback); err != nil {
 			return fmt.Errorf("failed to find files %s: %w", path, err)
 		}
 	default:
diff --git a/docs/modules/configuration/pages/storage.adoc b/docs/modules/configuration/pages/storage.adoc
index 0cfa8af6..da9f45e1 100644
--- a/docs/modules/configuration/pages/storage.adoc
+++ b/docs/modules/configuration/pages/storage.adoc
@@ -32,6 +32,23 @@ storage:
 
 CAUTION: On some platforms the automatic change detection feature can be inefficient and resource-intensive if the watched directory contains many files or gets updated frequently.
 
+=== Archive Files
+
+Alternatively, you can opt to archive and/or compress your policies directory into a Zip (`.zip`), Tar (`.tar`) or Gzip file (`.tgz` or `.tar.gz`). The archive is assumed to be laid out like a standard policy directory. It must contain no non-policy YAML files.
+
+You specify the file in your config like so:
+
+.Archived fileset using a Zip file
+[source,yaml,linenums]
+----
+storage:
+  driver: disk
+  disk:
+    directory: /etc/cerbos/policies.zip
+----
+
+NOTE: Change detection will be disabled when using archive files.
+
 [id="blob-driver"]
 == Blob driver
 
diff --git a/go.mod b/go.mod
index 30ce747a..8d98258b 100644
--- a/go.mod
+++ b/go.mod
@@ -47,6 +47,7 @@ require (
 	github.com/lestrrat-go/jwx/v2 v2.0.8
 	github.com/mattn/go-isatty v0.0.17
 	github.com/minio/minio-go/v7 v7.0.49
+	github.com/nlepage/go-tarfs v1.1.0
 	github.com/oklog/ulid/v2 v2.1.0
 	github.com/olekukonko/tablewriter v0.0.5
 	github.com/ory/dockertest/v3 v3.9.1
diff --git a/go.sum b/go.sum
index 8325c7ac..f35a80f0 100644
--- a/go.sum
+++ b/go.sum
@@ -1785,6 +1785,8 @@ github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OS
 github.com/ncw/swift v1.0.47/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM=
 github.com/neo4j/neo4j-go-driver v1.8.1-0.20200803113522-b626aa943eba/go.mod h1:ncO5VaFWh0Nrt+4KT4mOZboaczBZcLuHrG+/sUeP8gI=
 github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
+github.com/nlepage/go-tarfs v1.1.0 h1:bsACOiZMB/zFjYG/sE01070i9Fl26MnRpw0L6WuyfVs=
+github.com/nlepage/go-tarfs v1.1.0/go.mod h1:IhxRcLhLkawBetnwu/JNuoPkq/6cclAllhgEa6SmzS8=
 github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
 github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
 github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs=
diff --git a/internal/storage/disk/disk.go b/internal/storage/disk/disk.go
index f28f068a..ae386e70 100644
--- a/internal/storage/disk/disk.go
+++ b/internal/storage/disk/disk.go
@@ -7,7 +7,6 @@ import (
 	"context"
 	"fmt"
 	"io"
-	"os"
 	"path/filepath"
 
 	"github.com/cerbos/cerbos/internal/config"
@@ -15,6 +14,7 @@ import (
 	"github.com/cerbos/cerbos/internal/policy"
 	"github.com/cerbos/cerbos/internal/storage"
 	"github.com/cerbos/cerbos/internal/storage/index"
+	"github.com/cerbos/cerbos/internal/util"
 	"go.uber.org/zap"
 )
 
@@ -49,7 +49,13 @@ func NewStore(ctx context.Context, conf *Conf) (*Store, error) {
 	}
 
 	zap.S().Named("disk.store").Infof("Initializing disk store from %s", dir)
-	idx, err := index.Build(ctx, os.DirFS(dir))
+
+	fsys, err := util.OpenDirectoryFS(dir)
+	if err != nil {
+		return nil, err
+	}
+
+	idx, err := index.Build(ctx, fsys)
 	if err != nil {
 		return nil, err
 	}
@@ -59,7 +65,7 @@ func NewStore(ctx context.Context, conf *Conf) (*Store, error) {
 		idx:                 idx,
 		SubscriptionManager: storage.NewSubscriptionManager(ctx),
 	}
-	if conf.WatchForChanges {
+	if conf.WatchForChanges && !util.IsArchiveFile(dir) {
 		if err := watchDir(ctx, dir, s.idx, s.SubscriptionManager, defaultCooldownPeriod); err != nil {
 			return nil, err
 		}
diff --git a/internal/util/filesystem.go b/internal/util/filesystem.go
index 800bb4a2..7d2286ca 100644
--- a/internal/util/filesystem.go
+++ b/internal/util/filesystem.go
@@ -4,12 +4,17 @@
 package util
 
 import (
+	"archive/zip"
+	"compress/gzip"
 	"errors"
 	"fmt"
+	"io"
 	"io/fs"
+	"os"
 	"path/filepath"
 	"strings"
 
+	"github.com/nlepage/go-tarfs"
 	"google.golang.org/protobuf/proto"
 )
 
@@ -65,6 +70,65 @@ func IsZip(fileName string) bool {
 	return strings.HasSuffix(fileName, ".zip")
 }
 
+func IsTar(fileName string) bool {
+	return strings.HasSuffix(fileName, ".tar")
+}
+
+func IsGzip(fileName string) bool {
+	return strings.HasSuffix(fileName, ".tar.gz") || strings.HasSuffix(fileName, ".tgz")
+}
+
+func IsArchiveFile(fileName string) bool {
+	return IsZip(fileName) || IsTar(fileName) || IsGzip(fileName)
+}
+
+func getFsFromTar(r io.Reader) (fs.FS, error) {
+	tfs, err := tarfs.New(r)
+	if err != nil {
+		return nil, fmt.Errorf("failed to open tar file: %w", err)
+	}
+	return tfs, nil
+}
+
+// OpenDirectoryFS attempts to open a directory FS at the given location. It'll initially check if the target file is an archive,
+// and if so, will return the appropriate type which implements the fs.FS interface.
+func OpenDirectoryFS(path string) (fs.FS, error) {
+	// We don't use `switch filepath.Ext(path)` here because it only suffixes from the final `.`, so `.tar.gz` won't be
+	// correctly handled
+	switch {
+	case IsZip(path):
+		zr, err := zip.OpenReader(path)
+		if err != nil {
+			return nil, fmt.Errorf("failed to open zip file: %w", err)
+		}
+		return zr, nil
+	case IsTar(path):
+		f, err := os.Open(path)
+		if err != nil {
+			return nil, fmt.Errorf("failed to open tar file: %w", err)
+		}
+		defer f.Close()
+
+		return getFsFromTar(f)
+	case IsGzip(path):
+		f, err := os.Open(path)
+		if err != nil {
+			return nil, fmt.Errorf("failed to open gzip file: %w", err)
+		}
+		defer f.Close()
+
+		gzr, err := gzip.NewReader(f)
+		if err != nil {
+			return nil, fmt.Errorf("failed to open gzip file: %w", err)
+		}
+		defer gzr.Close()
+
+		return getFsFromTar(gzr)
+	}
+
+	return os.DirFS(path), nil
+}
+
 // LoadFromJSONOrYAML reads a JSON or YAML encoded protobuf from the given path.
 func LoadFromJSONOrYAML(fsys fs.FS, path string, dest proto.Message) error {
 	f, err := fsys.Open(path)


Test patch:
diff --git a/internal/storage/index/builder_test.go b/internal/storage/index/builder_test.go
index 9e0e6dfe..95c0db65 100644
--- a/internal/storage/index/builder_test.go
+++ b/internal/storage/index/builder_test.go
@@ -9,7 +9,6 @@ import (
 	"errors"
 	"io"
 	"io/fs"
-	"os"
 	"path/filepath"
 	"strings"
 	"testing"
@@ -31,7 +30,10 @@ import (
 func TestBuildIndexWithDisk(t *testing.T) {
 	dir := test.PathToDir(t, "store")
 
-	idx, err := Build(context.Background(), os.DirFS(dir))
+	fsys, err := util.OpenDirectoryFS(dir)
+	require.NoError(t, err)
+
+	idx, err := Build(context.Background(), fsys)
 	require.NoError(t, err)
 	require.NotNil(t, idx)
 
diff --git a/internal/storage/index/index_test.go b/internal/storage/index/index_test.go
index 4eb4f828..1f5c0dda 100644
--- a/internal/storage/index/index_test.go
+++ b/internal/storage/index/index_test.go
@@ -21,11 +21,6 @@ import (
 )
 
 func TestIndexLoadPolicy(t *testing.T) {
-	base := test.PathToDir(t, "store")
-	fsys := os.DirFS(base)
-	idx, err := index.Build(context.Background(), fsys)
-	require.NoError(t, err)
-
 	policyFiles := []string{
 		"derived_roles/common_roles.yaml",
 		"derived_roles/derived_roles_01.yaml",
@@ -46,7 +41,15 @@ func TestIndexLoadPolicy(t *testing.T) {
 		"resource_policies/policy_06.yaml",
 	}
 
-	t.Run("load policy", func(t *testing.T) {
+	testLoadPolicy := func(t *testing.T, path string) {
+		t.Helper()
+
+		base := test.PathToDir(t, path)
+		fsys, err := util.OpenDirectoryFS(base)
+		require.NoError(t, err)
+		idx, err := index.Build(context.Background(), fsys)
+		require.NoError(t, err)
+
 		t.Run("should load the policies", func(t *testing.T) {
 			policies, err := idx.LoadPolicy(context.Background(), policyFiles...)
 			require.NoError(t, err)
@@ -79,6 +82,19 @@ func TestIndexLoadPolicy(t *testing.T) {
 				require.Equal(t, wrapperspb.UInt64(util.HashPB(p, policy.IgnoreHashFields)), p.Metadata.Hash)
 			}
 		})
+	}
+
+	t.Run("load policy", func(t *testing.T) {
+		testLoadPolicy(t, "store")
+	})
+	t.Run("load policy zip", func(t *testing.T) {
+		testLoadPolicy(t, "store_archive/policies.zip")
+	})
+	t.Run("load policy tar", func(t *testing.T) {
+		testLoadPolicy(t, "store_archive/policies.tar")
+	})
+	t.Run("load policy gzip", func(t *testing.T) {
+		testLoadPolicy(t, "store_archive/policies.tgz")
 	})
 }
 
diff --git a/internal/test/testdata/store_archive/policies.tar b/internal/test/testdata/store_archive/policies.tar
new file mode 100644
index 00000000..a889ef17
--- /dev/null
+++ b/internal/test/testdata/store_archive/policies.tar
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:c06cbca64541c5df699390546c3ed0991a4daf0858e040f0487c8b6831443cea
+size 41472
diff --git a/internal/test/testdata/store_archive/policies.tgz b/internal/test/testdata/store_archive/policies.tgz
new file mode 100644
index 00000000..20a1af9b
--- /dev/null
+++ b/internal/test/testdata/store_archive/policies.tgz
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:c8da8c0bc2d1901fd9c4f2203113790565a7c3f9e33ee1847e673e0698d40e73
+size 2956
diff --git a/internal/test/testdata/store_archive/policies.zip b/internal/test/testdata/store_archive/policies.zip
new file mode 100644
index 00000000..d8fcb18a
--- /dev/null
+++ b/internal/test/testdata/store_archive/policies.zip
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:381c0da22603d01f43567b9a36a832ff00c80ff0719c3f99e4e0b634513ecdcc
+size 13428
diff --git a/internal/util/filesystem_test.go b/internal/util/filesystem_test.go
index 6367a274..07776327 100644
--- a/internal/util/filesystem_test.go
+++ b/internal/util/filesystem_test.go
@@ -147,3 +147,25 @@ func TestRelativeSchemaPath(t *testing.T) {
 		})
 	}
 }
+
+func TestIsArchiveFile(t *testing.T) {
+	tests := []struct {
+		fileName string
+		want     bool
+	}{
+		{"foo.zip", true},
+		{"foo.tar", true},
+		{"foo.tgz", true},
+		{"foo.tar.gz", true},
+		// Unsupported files
+		{"foo/", false},
+		{"foo.yaml", false},
+	}
+	for _, tt := range tests {
+		t.Run(tt.fileName, func(t *testing.T) {
+			if got := util.IsArchiveFile(tt.fileName); got != tt.want {
+				t.Errorf("IsArchiveFile() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}