Skip to content

Commit 9994195

Browse files
authored
Merge pull request #23 from chrissawer/zip-permissions-fixes
Zip permissions fixes
2 parents 4a98568 + 432fe33 commit 9994195

11 files changed

Lines changed: 180 additions & 3 deletions

extract.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,3 +110,11 @@ func (f fs) OpenFile(name string, flag int, perm os.FileMode) (*os.File, error)
110110
func (f fs) Remove(path string) error {
111111
return os.Remove(path)
112112
}
113+
114+
func (f fs) Stat(name string) (os.FileInfo, error) {
115+
return os.Stat(name)
116+
}
117+
118+
func (f fs) Chmod(name string, mode os.FileMode) error {
119+
return os.Chmod(name, mode)
120+
}

extractor.go

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,13 @@ type Extractor struct {
3838

3939
// Remove removes the named file or (empty) directory.
4040
Remove(path string) error
41+
42+
// Stat returns a FileInfo describing the named file.
43+
Stat(name string) (os.FileInfo, error)
44+
45+
// Chmod changes the mode of the named file to mode.
46+
// If the file is a symbolic link, it changes the mode of the link's target.
47+
Chmod(name string, mode os.FileMode) error
4148
}
4249
}
4350

@@ -346,7 +353,13 @@ func (e *Extractor) Zip(ctx context.Context, body io.Reader, location string, re
346353

347354
switch {
348355
case info.IsDir() || forceDir:
349-
if err := e.FS.MkdirAll(path, info.Mode()|os.ModeDir|100); err != nil {
356+
dirMode := info.Mode() | os.ModeDir | 0100
357+
if _, err := e.FS.Stat(path); err == nil {
358+
// directory already created, update permissions
359+
if err := e.FS.Chmod(path, dirMode); err != nil {
360+
return errors.Annotatef(err, "Set permissions %s", path)
361+
}
362+
} else if err := e.FS.MkdirAll(path, dirMode); err != nil {
350363
return errors.Annotatef(err, "Create directory %s", path)
351364
}
352365
// We only check for symlinks because hard links aren't possible
@@ -379,7 +392,7 @@ func (e *Extractor) Zip(ctx context.Context, body io.Reader, location string, re
379392

380393
func (e *Extractor) copy(ctx context.Context, path string, mode os.FileMode, src io.Reader) error {
381394
// We add the execution permission to be able to create files inside it
382-
err := e.FS.MkdirAll(filepath.Dir(path), mode|os.ModeDir|100)
395+
err := e.FS.MkdirAll(filepath.Dir(path), mode|os.ModeDir|0100)
383396
if err != nil {
384397
return err
385398
}

extractor_test.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import (
99
"os"
1010
"path/filepath"
1111
"runtime"
12+
"strconv"
13+
"strings"
1214
"testing"
1315

1416
"github.com/arduino/go-paths-helper"
@@ -259,6 +261,74 @@ func TestSymLinkMazeHardening(t *testing.T) {
259261
})
260262
}
261263

264+
func TestUnixPermissions(t *testing.T) {
265+
// Disable user's umask to enable creation of files with any permission, restore it after the test
266+
userUmask := UnixUmaskZero()
267+
defer UnixUmask(userUmask)
268+
269+
archiveFilenames := []string{
270+
"testdata/permissions.zip",
271+
"testdata/permissions.tar",
272+
}
273+
for _, archiveFilename := range archiveFilenames {
274+
tmp, err := paths.MkTempDir("", "")
275+
require.NoError(t, err)
276+
defer tmp.RemoveAll()
277+
278+
f, err := paths.New(archiveFilename).Open()
279+
require.NoError(t, err)
280+
err = extract.Archive(context.Background(), f, tmp.String(), nil)
281+
require.NoError(t, err)
282+
283+
filepath.Walk(tmp.String(), func(path string, info os.FileInfo, _ error) error {
284+
filename := filepath.Base(path)
285+
// Desired permissions indicated by part of the filenames inside the zip/tar files
286+
if strings.HasPrefix(filename, "dir") {
287+
desiredPermString := strings.Split(filename, "dir")[1]
288+
desiredPerms, _ := strconv.ParseUint(desiredPermString, 8, 32)
289+
require.Equal(t, os.ModeDir|os.FileMode(OsDirPerms(desiredPerms)), info.Mode())
290+
} else if strings.HasPrefix(filename, "file") {
291+
desiredPermString := strings.Split(filename, "file")[1]
292+
desiredPerms, _ := strconv.ParseUint(desiredPermString, 8, 32)
293+
require.Equal(t, os.FileMode(OsFilePerms(desiredPerms)), info.Mode())
294+
}
295+
return nil
296+
})
297+
}
298+
}
299+
300+
func TestZipDirectoryPermissions(t *testing.T) {
301+
// Disable user's umask to enable creation of files with any permission, restore it after the test
302+
userUmask := UnixUmaskZero()
303+
defer UnixUmask(userUmask)
304+
305+
// This arduino library has files before their containing directories in the zip,
306+
// so a good test case that these directory permissions are created correctly
307+
archive := paths.New("testdata/filesbeforedirectories.zip")
308+
download(t, "https://downloads.arduino.cc/libraries/github.com/arduino-libraries/LiquidCrystal-1.0.7.zip", archive)
309+
310+
tmp, err := paths.MkTempDir("", "")
311+
require.NoError(t, err)
312+
defer tmp.RemoveAll()
313+
314+
f, err := archive.Open()
315+
require.NoError(t, err)
316+
err = extract.Archive(context.Background(), f, tmp.String(), nil)
317+
require.NoError(t, err)
318+
319+
filepath.Walk(tmp.String(), func(path string, info os.FileInfo, _ error) error {
320+
// Test files and directories (excluding the parent) match permissions from the zip file
321+
if path != tmp.String() {
322+
if info.IsDir() {
323+
require.Equal(t, os.ModeDir|os.FileMode(OsDirPerms(0755)), info.Mode())
324+
} else {
325+
require.Equal(t, os.FileMode(OsFilePerms(0644)), info.Mode())
326+
}
327+
}
328+
return nil
329+
})
330+
}
331+
262332
// MockDisk is a disk that chroots to a directory
263333
type MockDisk struct {
264334
Base string
@@ -289,3 +359,13 @@ func (m MockDisk) OpenFile(name string, flag int, perm os.FileMode) (*os.File, e
289359
func (m MockDisk) Remove(path string) error {
290360
return os.Remove(filepath.Join(m.Base, path))
291361
}
362+
363+
func (m MockDisk) Stat(name string) (os.FileInfo, error) {
364+
name = filepath.Join(m.Base, name)
365+
return os.Stat(name)
366+
}
367+
368+
func (m MockDisk) Chmod(name string, mode os.FileMode) error {
369+
name = filepath.Join(m.Base, name)
370+
return os.Chmod(name, mode)
371+
}

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ require (
1111
github.com/klauspost/compress v1.15.13
1212
github.com/stretchr/testify v1.9.0
1313
github.com/ulikunitz/xz v0.5.12
14+
golang.org/x/sys v0.16.0
1415
)
1516

1617
require (

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0o
3131
golang.org/x/crypto v0.0.0-20180214000028-650f4a345ab4/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
3232
golang.org/x/net v0.0.0-20180406214816-61147c48b25b/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
3333
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
34+
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
35+
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
3436
gopkg.in/check.v1 v1.0.0-20160105164936-4f90aeace3a2/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
3537
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
3638
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

loggingfs_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ type LoggedOp struct {
1616
Path string
1717
OldPath string
1818
Mode os.FileMode
19+
Info os.FileInfo
1920
Flags int
2021
Err error
2122
}
@@ -33,6 +34,10 @@ func (op *LoggedOp) String() string {
3334
res += fmt.Sprintf("open %v %s (flags=%04x)", op.Mode, op.Path, op.Flags)
3435
case "remove":
3536
res += fmt.Sprintf("remove %v", op.Path)
37+
case "stat":
38+
res += fmt.Sprintf("stat %v -> %v", op.Path, op.Info)
39+
case "chmod":
40+
res += fmt.Sprintf("chmod %v %s", op.Mode, op.Path)
3641
default:
3742
panic("unknown LoggedOP " + op.Op)
3843
}
@@ -108,6 +113,32 @@ func (m *LoggingFS) Remove(path string) error {
108113
return err
109114
}
110115

116+
func (m *LoggingFS) Stat(path string) (os.FileInfo, error) {
117+
info, err := os.Stat(path)
118+
op := &LoggedOp{
119+
Op: "stat",
120+
Path: path,
121+
Info: info,
122+
Err: err,
123+
}
124+
m.Journal = append(m.Journal, op)
125+
fmt.Println("FS>", op)
126+
return info, err
127+
}
128+
129+
func (m *LoggingFS) Chmod(path string, mode os.FileMode) error {
130+
err := os.Chmod(path, mode)
131+
op := &LoggedOp{
132+
Op: "chmod",
133+
Path: path,
134+
Mode: mode,
135+
Err: err,
136+
}
137+
m.Journal = append(m.Journal, op)
138+
fmt.Println("FS>", op)
139+
return err
140+
}
141+
111142
func (m *LoggingFS) String() string {
112143
res := ""
113144
for _, op := range m.Journal {

testdata/.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
big.tar.gz
22
big.zip
3-
3+
filesbeforedirectories.zip

testdata/permissions.tar

10 KB
Binary file not shown.

testdata/permissions.zip

1.69 KB
Binary file not shown.

umask_unix_test.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
//go:build !windows
2+
3+
package extract_test
4+
5+
import "golang.org/x/sys/unix"
6+
7+
func UnixUmaskZero() int {
8+
return unix.Umask(0)
9+
}
10+
11+
func UnixUmask(userUmask int) {
12+
unix.Umask(userUmask)
13+
}
14+
15+
func OsFilePerms(unixPerms uint64) uint64 {
16+
return unixPerms
17+
}
18+
19+
func OsDirPerms(unixPerms uint64) uint64 {
20+
return unixPerms
21+
}

0 commit comments

Comments
 (0)