From 3a8c87e7db2fdc5d82035f1fc50e6af4e5fb705a Mon Sep 17 00:00:00 2001 From: Tonis Tiigi Date: Fri, 27 Sep 2024 17:57:05 -0700 Subject: [PATCH] add support for non-octal mode setting Signed-off-by: Tonis Tiigi --- bench/go.mod | 2 +- bench/go.sum | 2 ++ copy/copy.go | 26 ++++++++++++---- copy/copy_linux.go | 4 ++- copy/copy_unix.go | 4 ++- copy/copy_unix_test.go | 68 ++++++++++++++++++++++++++++++++++++++++++ copy/copy_windows.go | 4 +++ go.mod | 3 +- go.sum | 2 ++ 9 files changed, 105 insertions(+), 10 deletions(-) diff --git a/bench/go.mod b/bench/go.mod index d6eebca..0b0fbc0 100644 --- a/bench/go.mod +++ b/bench/go.mod @@ -1,6 +1,6 @@ module github.com/tonistiigi/fsutil/bench -go 1.20 +go 1.21 require ( github.com/containerd/continuity v0.4.1 diff --git a/bench/go.sum b/bench/go.sum index b94c40c..a26a4fb 100644 --- a/bench/go.sum +++ b/bench/go.sum @@ -544,6 +544,7 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= @@ -888,6 +889,7 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= diff --git a/copy/copy.go b/copy/copy.go index f8cc0a4..c2b1ab9 100644 --- a/copy/copy.go +++ b/copy/copy.go @@ -13,6 +13,7 @@ import ( "github.com/containerd/continuity/fs" "github.com/moby/patternmatcher" "github.com/pkg/errors" + mode "github.com/tonistiigi/dchapes-mode" "github.com/tonistiigi/fsutil" ) @@ -83,12 +84,21 @@ func Copy(ctx context.Context, srcRoot, src, dstRoot, dst string, opts ...Opt) e } } + var modeSet *mode.Set + if ci.ModeStr != "" { + ms, err := mode.ParseWithUmask(ci.ModeStr, 0) + if err != nil { + return err + } + modeSet = &ms + } + dst, err := fs.RootPath(dstRoot, filepath.Clean(dst)) if err != nil { return err } - c, err := newCopier(dstRoot, ci.Chown, ci.Utime, ci.Mode, ci.XAttrErrorHandler, ci.IncludePatterns, ci.ExcludePatterns, ci.AlwaysReplaceExistingDestPaths, ci.ChangeFunc) + c, err := newCopier(dstRoot, ci.Chown, ci.Utime, ci.Mode, modeSet, ci.XAttrErrorHandler, ci.IncludePatterns, ci.ExcludePatterns, ci.AlwaysReplaceExistingDestPaths, ci.ChangeFunc) if err != nil { return err } @@ -161,10 +171,12 @@ type Chowner func(*User) (*User, error) type XAttrErrorHandler func(dst, src, xattrKey string, err error) error type CopyInfo struct { - Chown Chowner - Utime *time.Time - AllowWildcards bool - Mode *int + Chown Chowner + Utime *time.Time + AllowWildcards bool + Mode *int + // ModeStr is mode in non-octal format. Overrides Mode if non-empty. + ModeStr string XAttrErrorHandler XAttrErrorHandler CopyDirContents bool FollowLinks bool @@ -234,6 +246,7 @@ type copier struct { chown Chowner utime *time.Time mode *int + modeSet *mode.Set inodes map[uint64]string xattrErrorHandler XAttrErrorHandler includePatternMatcher *patternmatcher.PatternMatcher @@ -250,7 +263,7 @@ type parentDir struct { copied bool } -func newCopier(root string, chown Chowner, tm *time.Time, mode *int, xeh XAttrErrorHandler, includePatterns, excludePatterns []string, alwaysReplaceExistingDestPaths bool, changeFunc fsutil.ChangeFunc) (*copier, error) { +func newCopier(root string, chown Chowner, tm *time.Time, mode *int, modeSet *mode.Set, xeh XAttrErrorHandler, includePatterns, excludePatterns []string, alwaysReplaceExistingDestPaths bool, changeFunc fsutil.ChangeFunc) (*copier, error) { if xeh == nil { xeh = func(dst, src, key string, err error) error { return err @@ -282,6 +295,7 @@ func newCopier(root string, chown Chowner, tm *time.Time, mode *int, xeh XAttrEr utime: tm, xattrErrorHandler: xeh, mode: mode, + modeSet: modeSet, includePatternMatcher: includePatternMatcher, excludePatternMatcher: excludePatternMatcher, changefn: changeFunc, diff --git a/copy/copy_linux.go b/copy/copy_linux.go index 6d9b490..9b046c5 100644 --- a/copy/copy_linux.go +++ b/copy/copy_linux.go @@ -29,7 +29,9 @@ func (c *copier) copyFileInfo(fi os.FileInfo, src, name string) error { } m := fi.Mode() - if c.mode != nil { + if c.modeSet != nil { + m = c.modeSet.Apply(m) + } else if c.mode != nil { m = os.FileMode(*c.mode).Perm() if *c.mode&syscall.S_ISGID != 0 { m |= os.ModeSetgid diff --git a/copy/copy_unix.go b/copy/copy_unix.go index 4a7d0c8..e90a41d 100644 --- a/copy/copy_unix.go +++ b/copy/copy_unix.go @@ -30,7 +30,9 @@ func (c *copier) copyFileInfo(fi os.FileInfo, src, name string) error { } m := fi.Mode() - if c.mode != nil { + if c.modeSet != nil { + m = c.modeSet.Apply(m) + } else if c.mode != nil { m = os.FileMode(*c.mode).Perm() if *c.mode&syscall.S_ISGID != 0 { m |= os.ModeSetgid diff --git a/copy/copy_unix_test.go b/copy/copy_unix_test.go index 405f757..55f4081 100644 --- a/copy/copy_unix_test.go +++ b/copy/copy_unix_test.go @@ -93,3 +93,71 @@ func TestCopySetuid(t *testing.T) { assert.Equal(t, os.FileMode(0), fi.Mode()&os.ModeSetgid) assert.Equal(t, os.FileMode(0), fi.Mode()&os.ModeSticky) } + +func TestCopyModeTextFormat(t *testing.T) { + t1 := t.TempDir() + + err := os.WriteFile(filepath.Join(t1, "file"), []byte("hello"), 0644) + require.NoError(t, err) + + err = os.WriteFile(filepath.Join(t1, "executable_file"), []byte("world"), 0755) + require.NoError(t, err) + + err = os.Mkdir(filepath.Join(t1, "dir"), 0750) + require.NoError(t, err) + + err = os.Mkdir(filepath.Join(t1, "restricted_dir"), 0700) + require.NoError(t, err) + + testCases := []struct { + name string + modeStr string + expectedFilePerm os.FileMode + expectedExecPerm os.FileMode + expectedDirPerm os.FileMode + expectedRestrictDirPerm os.FileMode + }{ + {"remove write for others", "go-w", 0644, 0755, 0750, 0700}, + {"add execute for user", "u+x", 0744, 0755, 0750, 0700}, + {"remove all permissions for group", "g-rwx", 0604, 0705, 0700, 0700}, + {"add read for others", "o+r", 0644, 0755, 0754, 0704}, + {"remove execute for all", "a-x", 0644, 0644, 0640, 0600}, + {"remove others and add execute for group", "o-rwx,g+x", 0650, 0750, 0750, 0710}, + {"capital X (apply execute only if directory)", "a+X", 0644, 0755, 0751, 0711}, + {"remove execute and add write for user", "u-x,u+w", 0644, 0655, 0650, 0600}, + {"add execute for user and others", "u+x,o+x", 0745, 0755, 0751, 0701}, + {"add write and read for group and others", "g+rw,o+rw", 0666, 0777, 0776, 0766}, + {"set read-only for all", "a=r", 0444, 0444, 0444, 0444}, + {"set full permissions for user only", "u=rwx,g=,o=", 0700, 0700, 0700, 0700}, + {"remove all permissions for others", "o-rwx", 0640, 0750, 0750, 0700}, + {"remove read for group, add execute for all", "g-r,a+x", 0715, 0715, 0711, 0711}, + {"complex permissions change", "u+rw,g+r,o-x,o+w", 0646, 0756, 0752, 0742}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t2 := t.TempDir() + + err := Copy(context.TODO(), t1, ".", t2, ".", WithCopyInfo(CopyInfo{ + ModeStr: tc.modeStr, + })) + require.NoError(t, err) + + fi, err := os.Lstat(filepath.Join(t2, "file")) + require.NoError(t, err) + assert.Equal(t, tc.expectedFilePerm, fi.Mode().Perm(), "file %04o, got %04o", tc.expectedFilePerm, fi.Mode().Perm()) + + execFileInfo, err := os.Lstat(filepath.Join(t2, "executable_file")) + require.NoError(t, err) + assert.Equal(t, tc.expectedExecPerm, execFileInfo.Mode().Perm(), "executable file %04o, got %04o", tc.expectedExecPerm, execFileInfo.Mode().Perm()) + + dirInfo, err := os.Lstat(filepath.Join(t2, "dir")) + require.NoError(t, err) + assert.Equal(t, tc.expectedDirPerm, dirInfo.Mode().Perm(), "dir %04o, got %04o", tc.expectedDirPerm, dirInfo.Mode().Perm()) + + restrictDirInfo, err := os.Lstat(filepath.Join(t2, "restricted_dir")) + require.NoError(t, err) + assert.Equal(t, tc.expectedRestrictDirPerm, restrictDirInfo.Mode().Perm(), "restricted dir %04o, got %04o", tc.expectedRestrictDirPerm, restrictDirInfo.Mode().Perm()) + }) + } +} diff --git a/copy/copy_windows.go b/copy/copy_windows.go index 58f822d..a049565 100644 --- a/copy/copy_windows.go +++ b/copy/copy_windows.go @@ -37,6 +37,10 @@ func getFileSecurityInfo(name string) (*windows.SID, *windows.ACL, error) { } func (c *copier) copyFileInfo(fi os.FileInfo, src, name string) error { + if c.modeSet != nil { + return errors.Errorf("non-octal mode not supported on windows") + } + if err := os.Chmod(name, fi.Mode()); err != nil { return errors.Wrapf(err, "failed to chmod %s", name) } diff --git a/go.mod b/go.mod index 4aeae36..c84c4aa 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/tonistiigi/fsutil -go 1.20 +go 1.21 require ( github.com/Microsoft/go-winio v0.5.2 @@ -9,6 +9,7 @@ require ( github.com/opencontainers/go-digest v1.0.0 github.com/pkg/errors v0.9.1 github.com/stretchr/testify v1.8.4 + github.com/tonistiigi/dchapes-mode v0.0.0-20241001053921-ca0759fec205 golang.org/x/sync v0.1.0 golang.org/x/sys v0.11.0 google.golang.org/protobuf v1.31.0 diff --git a/go.sum b/go.sum index 488fbea..dac6bf1 100644 --- a/go.sum +++ b/go.sum @@ -26,6 +26,8 @@ github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/tonistiigi/dchapes-mode v0.0.0-20241001053921-ca0759fec205 h1:eUk79E1w8yMtXeHSzjKorxuC8qJOnyXQnLaJehxpJaI= +github.com/tonistiigi/dchapes-mode v0.0.0-20241001053921-ca0759fec205/go.mod h1:3Iuxbr0P7D3zUzBMAZB+ois3h/et0shEz0qApgHYGpY= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=