Repository: getporter/porter (go)

Issue:
archive command fails when the bundle name has a slash
## Describe the bug
When I try to archive a bundle that has a slash in the bundle name, archive fails.

## To Reproduce
```console
$ porter archive whalegap.tgz --reference ghcr.io/getporter/examples/whalegap:v0.2.0
Writing logs to /Users/carolynvs/.porter/logs/01G57B1W09P2FB11DQHJYRPBQV.json
Writing logs to /Users/carolynvs/.porter/logs/01G57B1W09P2FB11DQHJYRPBQV.json
Writing logs to /Users/carolynvs/.porter/logs/01G57B1W09P2FB11DQHJYRPBQV.json
Writing logs to /Users/carolynvs/.porter/logs/01G57B1W09P2FB11DQHJYRPBQV.json
mkdir /var/folders/mv/cx7sy9c54wn741p4x6v0_t100000gn/T/examples/whalegap-0.2.0350843934: no such file or directory
```

## Expected behavior
The command should succeed. Seems like the problem is that we are making a directory with the name of the bundle and it's causing a missing parent directory.

Let's fix this by cleaning up the bundle name before using it as a directory name. There may be other allowed characters in a bundle name by the CNAB spec that could also cause trouble when put in a path. Maybe just do a string replace for anything that isn't a-z0-9-_. and turn it into underscores?

## Version
1.0.0-alpha.20


Pull request description:
Sanitize archive folder name
# What does this change

Currently, porter uses bundle name as part of the temporary archive folder for storing all artifacts and metadata that's needed for archiving a bundle.
The bundle name may contain invalid path characters, which causes `porter archive` to fail.

This PR fixes above issue by sanitizing bundle name to remove non-standard path characters before using it as part of the archive folder path.
If a path separator is present in a bundle name, we will replace them with a dash, `-`

# What issue does it fix
Closes #2151 

[1]: https://porter.sh/src/CONTRIBUTING.md#when-to-open-a-pull-request

# Notes for the reviewer

# Checklist
- [ ] Did you write tests?
- [ ] Did you write documentation?
- [ ] Did you change porter.yaml or a storage document record? Update the corresponding schema file.
- [ ] If this is your first pull request, please add your name to the bottom of our [Contributors][contributors] list. Thank you for making Porter better! 🙇‍♀️

# Reviewer Checklist
* Comment with /azp run test-porter-release if a magefile or build script was modified
* Comment with /azp run porter-integration if it's a non-trivial PR

[contributors]: https://porter.sh/src/CONTRIBUTORS.md

Patch:
diff --git a/go.sum b/go.sum
index da0ea075..3103dce3 100644
--- a/go.sum
+++ b/go.sum
@@ -53,8 +53,6 @@ contrib.go.opencensus.io/exporter/stackdriver v0.12.1/go.mod h1:iwB6wGarfphGGe/e
 contrib.go.opencensus.io/integrations/ocsql v0.1.4/go.mod h1:8DsSdjz3F+APR+0z0WkU1aRorQCFfRxvqjUUPMbF3fE=
 contrib.go.opencensus.io/resource v0.1.1/go.mod h1:F361eGI91LCmW1I/Saf+rX0+OFcigGlFvXwEGEnkRLA=
 dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
-get.porter.sh/magefiles v0.1.3 h1:91Y7vFDHGmMBbRfHQqEcIlqpp/RfDCMhyHGVusTYlmE=
-get.porter.sh/magefiles v0.1.3/go.mod h1:Whw/DSX8dcgn7dUPb6csbY6PtuTy1wujwFR2G6ONf20=
 get.porter.sh/magefiles v0.3.0 h1:uwCOTblBx2RFN2IEgIUDP4sNj/C4F26KqAdqSREJL7g=
 get.porter.sh/magefiles v0.3.0/go.mod h1:lwDECEEivbBHACLImnDEhwlr8g3E4xZR1W0DO8FOGa8=
 git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
diff --git a/pkg/porter/archive.go b/pkg/porter/archive.go
index ddaccd12..35d64653 100644
--- a/pkg/porter/archive.go
+++ b/pkg/porter/archive.go
@@ -6,6 +6,7 @@ import (
 	"io"
 	"os"
 	"path/filepath"
+	"strings"
 	"time"
 
 	"get.porter.sh/porter/pkg"
@@ -16,6 +17,7 @@ import (
 	"github.com/cnabio/cnab-go/imagestore/construction"
 	"github.com/docker/docker/pkg/archive"
 	"github.com/pkg/errors"
+	"github.com/spf13/afero"
 )
 
 // ArchiveOptions defines the valid options for performing an archive operation
@@ -88,14 +90,10 @@ type exporter struct {
 }
 
 func (ex *exporter) export() error {
-
 	name := ex.bundle.Name + "-" + ex.bundle.Version
-	archiveDir, err := ex.fs.TempDir("", name)
+	archiveDir, err := ex.createArchiveFolder(name)
 	if err != nil {
-		return err
-	}
-	if err := ex.fs.MkdirAll(archiveDir, 0644); err != nil {
-		return err
+		return fmt.Errorf("can not create archive folder: %w", err)
 	}
 	defer ex.fs.RemoveAll(archiveDir)
 
@@ -186,6 +184,23 @@ func (ex *exporter) addImage(image bundle.BaseImage) error {
 	return checkDigest(image, dig)
 }
 
+// createArchiveFolder set up a temporary directory for storing all data needed to archive a bundle.
+// It sanitizes the name and make sure only the current user has full permission to it.
+// If the name contains a path separator, all path separators will be replaced with "-".
+func (ex *exporter) createArchiveFolder(name string) (string, error) {
+	cleanedPath := strings.ReplaceAll(afero.UnicodeSanitize(name), string(os.PathSeparator), "-")
+	archiveDir, err := ex.fs.TempDir("", cleanedPath)
+	if err != nil {
+		return "", fmt.Errorf("can not create a temporary archive folder: %w", err)
+	}
+
+	err = ex.fs.Chmod(archiveDir, pkg.FileModeDirectory)
+	if err != nil {
+		return "", fmt.Errorf("can not change permission for the temporary archive folder: %w", err)
+	}
+	return archiveDir, nil
+}
+
 // checkDigest compares the content digest of the given image to the given content digest and returns an error if they
 // are both non-empty and do not match
 func checkDigest(image bundle.BaseImage, dig string) error {


Test patch:
diff --git a/pkg/porter/archive_test.go b/pkg/porter/archive_test.go
index 12905cba..7c6d949a 100644
--- a/pkg/porter/archive_test.go
+++ b/pkg/porter/archive_test.go
@@ -4,6 +4,8 @@ import (
 	"context"
 	"testing"
 
+	"get.porter.sh/porter/pkg"
+	"get.porter.sh/porter/tests"
 	"github.com/stretchr/testify/require"
 )
 
@@ -51,3 +53,17 @@ func TestArchive_Validate(t *testing.T) {
 		})
 	}
 }
+
+func TestArchive_ArchiveDirectory(t *testing.T) {
+	p := NewTestPorter(t)
+	defer p.Close()
+	ex := exporter{
+		fs: p.FileSystem,
+	}
+
+	dir, err := ex.createArchiveFolder("examples/test-bundle-0.2.0")
+	require.NoError(t, err)
+	require.Contains(t, dir, "/tmp/examples-test-bundle-0.2.0")
+
+	tests.AssertDirectoryPermissionsEqual(t, dir, pkg.FileModeDirectory)
+}