From b01831b4c579d1c37ab7c6f408b3e53baa7da444 Mon Sep 17 00:00:00 2001 From: kesonan Date: Sun, 15 Mar 2026 21:55:27 +0800 Subject: [PATCH] (goctl)fix file copy permission missed (#5475) --- tools/goctl/util/zipx/zipx.go | 11 ++- tools/goctl/util/zipx/zipx_test.go | 118 +++++++++++++++++++++++++++++ 2 files changed, 128 insertions(+), 1 deletion(-) create mode 100644 tools/goctl/util/zipx/zipx_test.go diff --git a/tools/goctl/util/zipx/zipx.go b/tools/goctl/util/zipx/zipx.go index e41d42d87..2e72a6d0b 100644 --- a/tools/goctl/util/zipx/zipx.go +++ b/tools/goctl/util/zipx/zipx.go @@ -64,6 +64,15 @@ func fileCopy(file *zip.File, destPath string) error { return err } defer w.Close() + _, err = io.Copy(w, rc) - return err + if err != nil { + return err + } + + if file.Mode().IsRegular() && file.Mode()&0111 != 0 { + return w.Chmod(file.Mode()) + } + + return nil } diff --git a/tools/goctl/util/zipx/zipx_test.go b/tools/goctl/util/zipx/zipx_test.go new file mode 100644 index 000000000..6574c8576 --- /dev/null +++ b/tools/goctl/util/zipx/zipx_test.go @@ -0,0 +1,118 @@ +package zipx + +import ( + "archive/zip" + "os" + "path/filepath" + "runtime" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func createTestZip(t *testing.T, files map[string]struct { + content string + mode os.FileMode +}) string { + t.Helper() + + zipPath := filepath.Join(t.TempDir(), "test.zip") + f, err := os.Create(zipPath) + require.NoError(t, err) + defer f.Close() + + w := zip.NewWriter(f) + defer w.Close() + + for name, file := range files { + header := &zip.FileHeader{ + Name: name, + Method: zip.Deflate, + } + header.SetMode(file.mode) + writer, err := w.CreateHeader(header) + require.NoError(t, err) + _, err = writer.Write([]byte(file.content)) + require.NoError(t, err) + } + + return zipPath +} + +func TestUnpacking(t *testing.T) { + dest := t.TempDir() + zipPath := createTestZip(t, map[string]struct { + content string + mode os.FileMode + }{ + "hello.txt": {content: "hello world", mode: 0644}, + "skip.txt": {content: "should be skipped", mode: 0644}, + }) + + err := Unpacking(zipPath, dest, func(f *zip.File) bool { + return f.Name == "hello.txt" + }) + require.NoError(t, err) + + content, err := os.ReadFile(filepath.Join(dest, "hello.txt")) + assert.NoError(t, err) + assert.Equal(t, "hello world", string(content)) + + _, err = os.Stat(filepath.Join(dest, "skip.txt")) + assert.True(t, os.IsNotExist(err)) +} + +func TestUnpackingPreservesExecutablePermission(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("file permissions not applicable on Windows") + } + + dest := t.TempDir() + zipPath := createTestZip(t, map[string]struct { + content string + mode os.FileMode + }{ + "bin/mybinary": {content: "binary content", mode: 0755}, + "readme.txt": {content: "readme", mode: 0644}, + }) + + err := Unpacking(zipPath, dest, func(f *zip.File) bool { + return true + }) + require.NoError(t, err) + + info, err := os.Stat(filepath.Join(dest, "mybinary")) + require.NoError(t, err) + assert.NotZero(t, info.Mode()&0111, "executable bit should be set") + + info, err = os.Stat(filepath.Join(dest, "readme.txt")) + require.NoError(t, err) + assert.Zero(t, info.Mode()&0111, "executable bit should not be set for regular files") +} + +func TestUnpackingInvalidZip(t *testing.T) { + err := Unpacking("/nonexistent/path.zip", t.TempDir(), func(f *zip.File) bool { + return true + }) + assert.Error(t, err) +} + +func TestUnpackingAllFilesFiltered(t *testing.T) { + dest := t.TempDir() + zipPath := createTestZip(t, map[string]struct { + content string + mode os.FileMode + }{ + "a.txt": {content: "a", mode: 0644}, + }) + + err := Unpacking(zipPath, dest, func(f *zip.File) bool { + return false + }) + require.NoError(t, err) + + entries, err := os.ReadDir(dest) + assert.NoError(t, err) + assert.Empty(t, entries) +}