diff --git a/add.go b/add.go index 89e9b7c23fa..6045a4710b4 100644 --- a/add.go +++ b/add.go @@ -26,6 +26,7 @@ import ( v1 "github.com/opencontainers/image-spec/specs-go/v1" "github.com/opencontainers/runtime-spec/specs-go" "github.com/sirupsen/logrus" + "github.com/tonistiigi/dchapes-mode" "go.podman.io/buildah/copier" "go.podman.io/buildah/define" "go.podman.io/buildah/internal/tmpdir" @@ -125,7 +126,7 @@ type AddAndCopyOptions struct { } // getURL writes a tar archive containing the named content -func getURL(src string, chown *idtools.IDPair, mountpoint, renameTarget string, writer io.Writer, chmod *os.FileMode, srcDigest digest.Digest, certPath string, insecureSkipTLSVerify types.OptionalBool, timestamp *time.Time) error { +func getURL(src string, chown *idtools.IDPair, mountpoint, renameTarget string, writer io.Writer, chmod string, srcDigest digest.Digest, certPath string, insecureSkipTLSVerify types.OptionalBool, timestamp *time.Time) error { url, err := url.Parse(src) if err != nil { return err @@ -214,9 +215,13 @@ func getURL(src string, chown *idtools.IDPair, mountpoint, renameTarget string, uid = chown.UID gid = chown.GID } - var mode int64 = 0o600 - if chmod != nil { - mode = int64(*chmod) + var mod int64 = 0o600 + if chmod != "" { + p, err := mode.Parse(chmod) + if err != nil { + return fmt.Errorf("parsing chmod %q: %w", chmod, err) + } + mod = int64(p.Apply(os.FileMode(mod))) } hdr := tar.Header{ Typeflag: tar.TypeReg, @@ -224,7 +229,7 @@ func getURL(src string, chown *idtools.IDPair, mountpoint, renameTarget string, Size: size, Uid: uid, Gid: gid, - Mode: mode, + Mode: mod, ModTime: date, } err = tw.WriteHeader(&hdr) @@ -408,15 +413,6 @@ func (b *Builder) Add(destination string, extract bool, options AddAndCopyOption return fmt.Errorf("looking up UID/GID for %q: %w", options.Chown, err) } } - var chmodDirsFiles *os.FileMode - if options.Chmod != "" { - p, err := strconv.ParseUint(options.Chmod, 8, 32) - if err != nil { - return fmt.Errorf("parsing chmod %q: %w", options.Chmod, err) - } - perm := os.FileMode(p) - chmodDirsFiles = &perm - } chownDirs = &idtools.IDPair{UID: int(userUID), GID: int(userGID)} chownFiles = &idtools.IDPair{UID: int(userUID), GID: int(userGID)} @@ -609,10 +605,9 @@ func (b *Builder) Add(destination string, extract bool, options AddAndCopyOption GIDMap: srcGIDMap, Excludes: options.Excludes, ExpandArchives: extract, + Chmod: options.Chmod, ChownDirs: chownDirs, - ChmodDirs: chmodDirsFiles, ChownFiles: chownFiles, - ChmodFiles: chmodDirsFiles, KeepDirectoryNames: options.DirCopyContents == types.OptionalBoolFalse, StripSetuidBit: options.StripSetuidBit, StripSetgidBit: options.StripSetgidBit, @@ -626,7 +621,7 @@ func (b *Builder) Add(destination string, extract bool, options AddAndCopyOption } else { go func() { getErr = retry.IfNecessary(context.TODO(), func() error { - return getURL(src, chownFiles, mountPoint, renameTarget, pipeWriter, chmodDirsFiles, srcDigest, options.CertPath, options.InsecureSkipTLSVerify, options.Timestamp) + return getURL(src, chownFiles, mountPoint, renameTarget, pipeWriter, options.Chmod, srcDigest, options.CertPath, options.InsecureSkipTLSVerify, options.Timestamp) }, &retry.Options{ MaxRetry: options.MaxRetries, Delay: options.RetryDelay, @@ -777,10 +772,9 @@ func (b *Builder) Add(destination string, extract bool, options AddAndCopyOption GIDMap: srcGIDMap, Excludes: options.Excludes, ExpandArchives: extract, + Chmod: options.Chmod, ChownDirs: chownDirs, - ChmodDirs: chmodDirsFiles, ChownFiles: chownFiles, - ChmodFiles: chmodDirsFiles, KeepDirectoryNames: options.DirCopyContents == types.OptionalBoolFalse, StripSetuidBit: options.StripSetuidBit, StripSetgidBit: options.StripSetgidBit, diff --git a/commit_test.go b/commit_test.go index 6d5767b3dc4..b7e6795f465 100644 --- a/commit_test.go +++ b/commit_test.go @@ -575,3 +575,88 @@ func TestCommitEmpty(t *testing.T) { require.Equalf(t, layerDigest.Digest(), image.RootFS.DiffIDs[len(image.RootFS.DiffIDs)-1], "expected new diff ID to match the randomly-generated layer") }) } + +func TestCommitChmod(t *testing.T) { + ctx := context.TODO() + graphDriverName := os.Getenv("STORAGE_DRIVER") + if graphDriverName == "" { + graphDriverName = "vfs" + } + t.Logf("using storage driver %q", graphDriverName) + store, err := storage.GetStore(storageTypes.StoreOptions{ + RunRoot: t.TempDir(), + GraphRoot: t.TempDir(), + GraphDriverName: graphDriverName, + }) + require.NoError(t, err, "initializing storage") + t.Cleanup(func() { _, err := store.Shutdown(true); assert.NoError(t, err) }) + + // Build a from-scratch image with one layer. + builderOptions := BuilderOptions{ + FromImage: "scratch", + NamespaceOptions: []NamespaceOption{{ + Name: string(rspec.NetworkNamespace), + Host: true, + }}, + SystemContext: &testSystemContext, + } + b, err := NewBuilder(ctx, store, builderOptions) + require.NoError(t, err, "creating builder") + imgName := "image0" + b.SetCreatedBy(imgName) + + type perms struct { + chmod string + perm int64 + startMode os.FileMode + } + + filePerms := map[string]perms{ + "symbolic": {chmod: "u=rwX,go=rX", perm: 0o644}, + "symbolic adding mode bits": {chmod: "u=rwX,go=rX", perm: 0o755, startMode: 0o111}, + "octal": {chmod: "750", perm: 0o750}, + "octal overwrite start mode": {chmod: "644", perm: 0o644, startMode: 0o753}, + "clear group and other mode bits": {chmod: "go=", perm: 0o700, startMode: 0o777}, + } + for name, v := range filePerms { + f := makeFile(t, name, 0) + if v.startMode != 0 { + err = os.Chmod(f, v.startMode) + require.NoError(t, err, "chmod", f) + } + err = b.Add("/", false, AddAndCopyOptions{Chmod: v.chmod}, f) + require.NoError(t, err, "adding", f) + } + + commitOptions := CommitOptions{ + SystemContext: &testSystemContext, + } + ref, err := imageStorage.Transport.ParseStoreReference(store, imgName) + require.NoError(t, err, "parsing reference for to-be-committed image", imgName) + _, _, _, err = b.Commit(ctx, ref, commitOptions) + require.NoError(t, err, "committing", imgName) + + src, err := ref.NewImageSource(ctx, &testSystemContext) + require.NoError(t, err, "opening image source") + defer src.Close() + img, err := ref.NewImage(ctx, &testSystemContext) + require.NoError(t, err, "opening image") + defer img.Close() + + infos, err := img.LayerInfosForCopy(ctx) + require.NoError(t, err, "getting layer infos") + + for i, blobInfo := range infos { + rc, _, err := src.GetBlob(ctx, blobInfo, nil) + require.NoError(t, err, "getting blob", i) + defer rc.Close() + tr := tar.NewReader(rc) + entry, err := tr.Next() + for entry != nil { + expected := filePerms[entry.Name] + require.Equal(t, expected.perm, entry.Mode) + entry, err = tr.Next() + } + require.ErrorIs(t, err, io.EOF) + } +} diff --git a/copier/copier.go b/copier/copier.go index 5f7bc0d0e43..a1f56335ff4 100644 --- a/copier/copier.go +++ b/copier/copier.go @@ -23,6 +23,7 @@ import ( "unicode" "github.com/sirupsen/logrus" + "github.com/tonistiigi/dchapes-mode" "go.podman.io/image/v5/pkg/compression" "go.podman.io/image/v5/types" "go.podman.io/storage/pkg/archive" @@ -386,6 +387,7 @@ type GetOptions struct { UIDMap, GIDMap []idtools.IDMap // map from hostIDs to containerIDs in the output archive Excludes []string // contents to pretend don't exist, using the OS-specific path separator ExpandArchives bool // extract the contents of named items that are archives + Chmod string // set permissions in octal or symbolic notation. overrides ChmodDirs and ChmodFiles if set. no effect on archives being extracted ChownDirs *idtools.IDPair // set ownership on directories. no effect on archives being extracted ChmodDirs *os.FileMode // set permissions on directories. no effect on archives being extracted ChownFiles *idtools.IDPair // set ownership of files. no effect on archives being extracted @@ -444,7 +446,8 @@ func Get(root string, directory string, options GetOptions, globs []string, bulk type PutOptions struct { UIDMap, GIDMap []idtools.IDMap // map from containerIDs to hostIDs when writing contents to disk DefaultDirOwner *idtools.IDPair // set ownership of implicitly-created directories, default is ChownDirs, or 0:0 if ChownDirs not set - DefaultDirMode *os.FileMode // set permissions on implicitly-created directories, default is ChmodDirs, or 0755 if ChmodDirs not set + DefaultDirMode *os.FileMode // set permissions on implicitly-created directories, default is Chmod or ChmodDirs, or 0755 if neither is set + Chmod string // set permissions in octal or symbolic notation. overrides ChmodDirs and ChmodFiles if set ChownDirs *idtools.IDPair // set ownership of newly-created directories ChmodDirs *os.FileMode // set permissions on newly-created directories ChownFiles *idtools.IDPair // set ownership of newly-created files @@ -1408,6 +1411,14 @@ func copierHandlerGet(bulkWriter io.Writer, req request, pm *fileutils.PatternMa if err != nil { return errorResponse("copier: get: error reading info about directory %q: %v", req.Directory, err) } + var chmod *mode.Set + if req.GetOptions.Chmod != "" { + p, err := mode.Parse(req.GetOptions.Chmod) + if err != nil { + return errorResponse("copier: get: parsing chmod %q: %v", req.GetOptions.Chmod, err) + } + chmod = &p + } cb := func() error { tw := tar.NewWriter(bulkWriter) defer tw.Close() @@ -1461,7 +1472,7 @@ func copierHandlerGet(bulkWriter io.Writer, req request, pm *fileutils.PatternMa return fmt.Errorf("copier: get: %w", err) } - if err := copierHandlerGetOne(parentInfo, parentSymlinkTarget, parentName, parent, req.GetOptions, tw, hardlinkChecker, idMappings); err != nil { + if err := copierHandlerGetOne(parentInfo, parentSymlinkTarget, parentName, parent, req.GetOptions, tw, hardlinkChecker, idMappings, chmod); err != nil { if req.GetOptions.IgnoreUnreadable && errorIsPermission(err) { continue } else if errors.Is(err, os.ErrNotExist) { @@ -1584,7 +1595,7 @@ func copierHandlerGet(bulkWriter io.Writer, req request, pm *fileutils.PatternMa } } // add the item to the outgoing tar stream - if err := copierHandlerGetOne(info, symlinkTarget, rel, path, options, tw, hardlinkChecker, idMappings); err != nil { + if err := copierHandlerGetOne(info, symlinkTarget, rel, path, options, tw, hardlinkChecker, idMappings, chmod); err != nil { if req.GetOptions.IgnoreUnreadable && errorIsPermission(err) { return ok } else if errors.Is(err, os.ErrNotExist) { @@ -1626,7 +1637,7 @@ func copierHandlerGet(bulkWriter io.Writer, req request, pm *fileutils.PatternMa return fmt.Errorf("copier: get: %w", err) } - if err := copierHandlerGetOne(info, symlinkTarget, name, item, req.GetOptions, tw, hardlinkChecker, idMappings); err != nil { + if err := copierHandlerGetOne(info, symlinkTarget, name, item, req.GetOptions, tw, hardlinkChecker, idMappings, chmod); err != nil { if req.GetOptions.IgnoreUnreadable && errorIsPermission(err) { continue } @@ -1697,7 +1708,7 @@ func getTargetIfSymlink(path string, info os.FileInfo) (string, error) { return "", nil } -func copierHandlerGetOne(srcfi os.FileInfo, symlinkTarget, name, contentPath string, options GetOptions, tw *tar.Writer, hardlinkChecker *hardlinkChecker, idMappings *idtools.IDMappings) error { +func copierHandlerGetOne(srcfi os.FileInfo, symlinkTarget, name, contentPath string, options GetOptions, tw *tar.Writer, hardlinkChecker *hardlinkChecker, idMappings *idtools.IDMappings, chmod *mode.Set) error { // build the header using the name provided hdr, err := tar.FileInfoHeader(srcfi, symlinkTarget) if err != nil { @@ -1807,11 +1818,14 @@ func copierHandlerGetOne(srcfi os.FileInfo, symlinkTarget, name, contentPath str } } // force ownership and/or permissions, if requested + if chmod != nil { + hdr.Mode = int64(chmod.Apply(srcfi.Mode())) + } if hdr.Typeflag == tar.TypeDir { if options.ChownDirs != nil { hdr.Uid, hdr.Gid = options.ChownDirs.UID, options.ChownDirs.GID } - if options.ChmodDirs != nil { + if options.ChmodDirs != nil && chmod == nil { hdr.Mode = int64(*options.ChmodDirs) } if !strings.HasSuffix(hdr.Name, "/") { @@ -1821,7 +1835,7 @@ func copierHandlerGetOne(srcfi os.FileInfo, symlinkTarget, name, contentPath str if options.ChownFiles != nil { hdr.Uid, hdr.Gid = options.ChownFiles.UID, options.ChownFiles.GID } - if options.ChmodFiles != nil { + if options.ChmodFiles != nil && chmod == nil { hdr.Mode = int64(*options.ChmodFiles) } } @@ -1887,6 +1901,15 @@ func copierHandlerPut(bulkReader io.Reader, req request, idMappings *idtools.IDM if req.PutOptions.ChmodDirs != nil { defaultDirMode = *req.PutOptions.ChmodDirs } + var chmod *mode.Set + if req.PutOptions.Chmod != "" { + p, err := mode.Parse(req.PutOptions.Chmod) + if err != nil { + return errorResponse("parsing chmod %q: %v", req.PutOptions.Chmod, err) + } + chmod = &p + defaultDirMode = chmod.Apply(defaultDirMode) + } if req.PutOptions.DefaultDirOwner != nil { defaultDirUID, defaultDirGID = req.PutOptions.DefaultDirOwner.UID, req.PutOptions.DefaultDirOwner.GID } @@ -2078,13 +2101,17 @@ func copierHandlerPut(bulkReader io.Reader, req request, idMappings *idtools.IDM if req.PutOptions.StripStickyBit && hdr.Mode&cISVTX == cISVTX { hdr.Mode &^= cISVTX } - if hdr.Typeflag == tar.TypeDir { - if req.PutOptions.ChmodDirs != nil { - hdr.Mode = int64(*req.PutOptions.ChmodDirs) - } + if chmod != nil { + hdr.Mode = int64(chmod.Apply(os.FileMode(hdr.Mode))) } else { - if req.PutOptions.ChmodFiles != nil { - hdr.Mode = int64(*req.PutOptions.ChmodFiles) + if hdr.Typeflag == tar.TypeDir { + if req.PutOptions.ChmodDirs != nil { + hdr.Mode = int64(*req.PutOptions.ChmodDirs) + } + } else { + if req.PutOptions.ChmodFiles != nil { + hdr.Mode = int64(*req.PutOptions.ChmodFiles) + } } } // create the new item diff --git a/copier/copier_test.go b/copier/copier_test.go index e1123d10d2b..b7b85cdef42 100644 --- a/copier/copier_test.go +++ b/copier/copier_test.go @@ -26,6 +26,7 @@ import ( "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/tonistiigi/dchapes-mode" "go.podman.io/image/v5/types" "go.podman.io/storage/pkg/idtools" "go.podman.io/storage/pkg/reexec" @@ -809,7 +810,7 @@ func testGetSingle(t *testing.T) { assert.Equal(t, filepath.Base(name), filepath.FromSlash(hdr.Name), "expected item named %q, got %q", filepath.Base(name), filepath.FromSlash(hdr.Name)) hdr, err = tr.Next() } - assert.Equal(t, io.EOF.Error(), err.Error(), "expected EOF at end of archive, got %q", err.Error()) + assert.ErrorIs(t, err, io.EOF, "expected EOF at end of archive") if !t.Failed() && testItem.Typeflag == tar.TypeReg && testItem.Mode&(cISUID|cISGID|cISVTX) != 0 { for _, stripSetuidBit := range []bool{false, true} { for _, stripSetgidBit := range []bool{false, true} { @@ -852,7 +853,7 @@ func testGetSingle(t *testing.T) { } hdr, err = tr.Next() } - assert.Equal(t, io.EOF.Error(), err.Error(), "expected EOF at end of archive, got %q", err.Error()) + assert.ErrorIs(t, err, io.EOF, "expected EOF at end of archive") wg.Wait() assert.NoErrorf(t, getErr, "unexpected error from Get(%q): %v", name, getErr) pipeReader.Close() @@ -1640,7 +1641,7 @@ func testGetMultiple(t *testing.T) { expectedContents = append(expectedContents, filepath.FromSlash(item)) } sort.Strings(expectedContents) - assert.Equal(t, io.EOF.Error(), err.Error(), "expected EOF at end of archive, got %q", err.Error()) + assert.ErrorIs(t, err, io.EOF, "expected EOF at end of archive") wg.Wait() assert.NoErrorf(t, getErr, "unexpected error from Get(%q)", testCase.pattern) assert.Equal(t, expectedContents, actualContents, "Get(%q,excludes=%v) didn't produce the right set of items", testCase.pattern, excludes) @@ -3049,3 +3050,112 @@ func TestCannotChangeMultipleRequestsWithDifferentChroot(t *testing.T) { require.NoError(t, encoder.Encode(&request{Request: requestQuit})) require.NoError(t, cmd.Wait()) } + +func TestChmodNoChroot(t *testing.T) { + couldChroot := canChroot + canChroot = false + testChmod(t) + canChroot = couldChroot +} + +func testChmod(t *testing.T) { + dirMode := os.FileMode(0o755) + fileMode := os.FileMode(0o644) + cases := []struct { + chmod string + chmodDirs, chmodFiles *os.FileMode + startMode, endMode os.FileMode + isDir bool + err error + }{ + {chmod: "u=rw", startMode: 0o400, endMode: 0o600}, + {chmod: "u=rwX", startMode: 0o400, endMode: 0o600}, + {chmod: "u=rw", startMode: 0o400 | fs.ModeDir, endMode: 0o600 | fs.ModeDir, isDir: true}, + {chmod: "u=rwX", startMode: 0o400 | fs.ModeDir, endMode: 0o700 | fs.ModeDir, isDir: true}, + {chmod: "u=rwXabc", err: mode.ErrSyntax}, + {chmod: "go=", startMode: 0o777, endMode: 0o700}, + {chmod: "644", startMode: 0o753, endMode: 0o644}, + {chmod: "", startMode: 0o753, endMode: 0o753}, + {chmodDirs: &dirMode, startMode: 0o500 | fs.ModeDir, endMode: 0o755 | fs.ModeDir, isDir: true}, + {chmodDirs: &dirMode, startMode: 0o400, endMode: 0o400}, + {chmodFiles: &fileMode, startMode: 0o500 | fs.ModeDir, endMode: 0o500 | fs.ModeDir, isDir: true}, + {chmodFiles: &fileMode, startMode: 0o400, endMode: 0o644}, + {chmod: "u=rwX,go=", chmodFiles: &fileMode, startMode: 0o444, endMode: 0o600}, + {chmod: "u=rwX,go=", chmodDirs: &dirMode, startMode: 0o555 | fs.ModeDir, endMode: 0o700 | fs.ModeDir, isDir: true}, + } + + name := "test" + uidMap := []idtools.IDMap{{HostID: os.Getuid(), ContainerID: 0, Size: 1}} + gidMap := []idtools.IDMap{{HostID: os.Getgid(), ContainerID: 0, Size: 1}} + + for _, v := range cases { + t.Run(fmt.Sprintf("get chmod=%v,chmodDirs=%v,chmodFiles=%v,startMode=%O,endMode=%O,isDir=%v", v.chmod, v.chmodDirs, v.chmodFiles, v.startMode, v.endMode, v.isDir), func(t *testing.T) { + testDir := t.TempDir() + fn := filepath.Join(testDir, name) + var err error + if v.isDir { + err = os.Mkdir(fn, 0) + } else { + _, err = os.Create(fn) + } + if err != nil { + t.Fatalf("failed to create %s: %v", name, err) + } + if err = os.Chmod(fn, v.startMode); err != nil { + t.Fatalf("failed to chmod %s: %v", name, err) + } + + pipeReader, pipeWriter := io.Pipe() + var wg sync.WaitGroup + wg.Add(1) + go func() { + opts := GetOptions{Chmod: v.chmod, ChmodDirs: v.chmodDirs, ChmodFiles: v.chmodFiles} + err = Get(testDir, "", opts, []string{name}, pipeWriter) + pipeWriter.Close() + wg.Done() + }() + tr := tar.NewReader(pipeReader) + hdr, tarErr := tr.Next() + for tarErr == nil { + assert.Equal(t, int64(v.endMode), hdr.Mode) + hdr, tarErr = tr.Next() + } + assert.ErrorIs(t, tarErr, io.EOF, "expected EOF at end of archive") + wg.Wait() + if v.err != nil { + require.ErrorContains(t, err, v.err.Error()) + } + pipeReader.Close() + }) + + t.Run(fmt.Sprintf("put chmod=%v,chmodDirs=%v,chmodFiles=%v,startMode=%O,endMode=%O,isDir=%v", v.chmod, v.chmodDirs, v.chmodFiles, v.startMode, v.endMode, v.isDir), func(t *testing.T) { + testDir := t.TempDir() + typeFlag := byte(tar.TypeReg) + if v.isDir { + typeFlag = tar.TypeDir + } + archive := makeArchiveSlice([]tar.Header{ + {Name: name, Typeflag: typeFlag, Size: 0, Mode: int64(v.startMode), ModTime: testDate}, + }) + opts := PutOptions{ + UIDMap: uidMap, + GIDMap: gidMap, + Chmod: v.chmod, + ChmodDirs: v.chmodDirs, + ChmodFiles: v.chmodFiles, + } + err := Put(testDir, "", opts, bytes.NewReader(archive)) + if err != nil { + require.ErrorContains(t, err, v.err.Error()) + return + } + require.ErrorIs(t, err, v.err) + fn := filepath.Join(testDir, name) + fi, err := os.Stat(fn) + if err != nil { + t.Fatalf("failed to stat %s: %v", fn, err) + } + assert.Equal(t, v.endMode, fi.Mode()) + }) + } +} diff --git a/docs/buildah-add.1.md b/docs/buildah-add.1.md index 0b99e03d83f..d8d4c5396fd 100644 --- a/docs/buildah-add.1.md +++ b/docs/buildah-add.1.md @@ -47,7 +47,8 @@ container digest string. Only supported for HTTP sources. **--chmod** *permissions* -Sets the access permissions of the destination content. Accepts the numerical format. +Sets the access permissions of the destination content. Accepts numerical or +symbolic format. **--chown** *owner*:*group* diff --git a/docs/buildah-copy.1.md b/docs/buildah-copy.1.md index 62f92cb9b3e..da0f004302b 100644 --- a/docs/buildah-copy.1.md +++ b/docs/buildah-copy.1.md @@ -44,8 +44,8 @@ container digest string. Only supported for HTTP sources. **--chmod** *permissions* -Sets the access permissions of the destination content. Accepts the numerical -format. If `--from` is not used, defaults to `0755`. +Sets the access permissions of the destination content. Accepts numerical or +symbolic format. If `--from` is not used, defaults to `0755`. **--chown** *owner*:*group* diff --git a/go.mod b/go.mod index d7596f32a3c..88c8844eb9c 100644 --- a/go.mod +++ b/go.mod @@ -33,6 +33,7 @@ require ( github.com/spf13/cobra v1.10.2 github.com/spf13/pflag v1.0.10 github.com/stretchr/testify v1.11.1 + github.com/tonistiigi/dchapes-mode v0.0.0-20250318174251-73d941a28323 go.etcd.io/bbolt v1.4.3 go.podman.io/common v0.67.2-0.20260423135811-cbaa5f41e643 go.podman.io/image/v5 v5.39.3-0.20260423135811-cbaa5f41e643 diff --git a/go.sum b/go.sum index 577f7fed325..234f084b796 100644 --- a/go.sum +++ b/go.sum @@ -248,6 +248,8 @@ github.com/sylabs/sif/v2 v2.24.0 h1:1wB5uMDUQYjk8AckTySaDcP9YnpMb1LyDRr1Jt9A10w= github.com/sylabs/sif/v2 v2.24.0/go.mod h1:DbXWqWZ1hdLSU+K9ipdds5AmZeHWsyxCOj/oQakBa88= github.com/tchap/go-patricia/v2 v2.3.3 h1:xfNEsODumaEcCcY3gI0hYPZ/PcpVv5ju6RMAhgwZDDc= github.com/tchap/go-patricia/v2 v2.3.3/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k= +github.com/tonistiigi/dchapes-mode v0.0.0-20250318174251-73d941a28323 h1:r0p7fK56l8WPequOaR3i9LBqfPtEdXIQbUTzT55iqT4= +github.com/tonistiigi/dchapes-mode v0.0.0-20250318174251-73d941a28323/go.mod h1:3Iuxbr0P7D3zUzBMAZB+ois3h/et0shEz0qApgHYGpY= github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY= github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/vbatts/tar-split v0.12.2 h1:w/Y6tjxpeiFMR47yzZPlPj/FcPLpXbTUi/9H7d3CPa4= diff --git a/tests/add.bats b/tests/add.bats index 3c261be7431..3ff45fe3276 100644 --- a/tests/add.bats +++ b/tests/add.bats @@ -167,10 +167,30 @@ load helpers createrandom ${TEST_SCRATCH_DIR}/randomfile run_buildah from --quiet $WITH_POLICY_JSON busybox cid=$output + run_buildah add --chmod 777 $cid ${TEST_SCRATCH_DIR}/randomfile /tmp/random run_buildah run $cid ls -l /tmp/random - expect_output --substring rwxrwxrwx + + run_buildah add --chmod 0755 $cid ${TEST_SCRATCH_DIR}/randomfile /tmp/random2 + run_buildah run $cid ls -l /tmp/random2 + expect_output --substring rwxr-xr-x + + run_buildah add --chmod u=rX,go= $cid ${TEST_SCRATCH_DIR}/randomfile /tmp/random3 + run_buildah run $cid ls -l /tmp/random3 + expect_output --substring r-------- + + run_buildah add --chmod u=rwx,go=u-w $cid ${TEST_SCRATCH_DIR}/randomfile /tmp/random4 + run_buildah run $cid ls -l /tmp/random4 + expect_output --substring rwxr-xr-x + + mkdir -p ${TEST_SCRATCH_DIR}/dir1/dir2 + createrandom ${TEST_SCRATCH_DIR}/dir1/dir2/randomfile + run_buildah add --chmod u=rwX,go=X $cid ${TEST_SCRATCH_DIR}/dir1 /tmp/dir1 + run_buildah run $cid ls -l /tmp/dir1 + expect_output --substring drwx--x--x + run_buildah run $cid ls -l /tmp/dir1/dir2 + expect_output --substring rw------- } @test "add url" { diff --git a/tests/copy.bats b/tests/copy.bats index 1e748ac82a5..a113d0992d2 100644 --- a/tests/copy.bats +++ b/tests/copy.bats @@ -196,12 +196,13 @@ load helpers @test "copy --chmod" { mkdir -p ${TEST_SCRATCH_DIR}/subdir - mkdir -p ${TEST_SCRATCH_DIR}/other-subdir + mkdir -p ${TEST_SCRATCH_DIR}/subdir2 + mkdir -p ${TEST_SCRATCH_DIR}/subdir3 createrandom ${TEST_SCRATCH_DIR}/subdir/randomfile createrandom ${TEST_SCRATCH_DIR}/subdir/other-randomfile createrandom ${TEST_SCRATCH_DIR}/randomfile - createrandom ${TEST_SCRATCH_DIR}/other-subdir/randomfile - createrandom ${TEST_SCRATCH_DIR}/other-subdir/other-randomfile + createrandom ${TEST_SCRATCH_DIR}/subdir2/randomfile + createrandom ${TEST_SCRATCH_DIR}/subdir2/other-randomfile _prefetch alpine run_buildah from --quiet $WITH_POLICY_JSON alpine @@ -211,6 +212,10 @@ load helpers run_buildah copy --chmod 700 $cid ${TEST_SCRATCH_DIR}/randomfile /randomfile2 run_buildah copy --chmod 755 $cid ${TEST_SCRATCH_DIR}/randomfile /randomfile3 run_buildah copy --chmod 660 $cid ${TEST_SCRATCH_DIR}/subdir /subdir + run_buildah copy --chmod u=rX,go= $cid ${TEST_SCRATCH_DIR}/randomfile /randomfile4 + run_buildah copy --chmod u=rwx,go=u-w $cid ${TEST_SCRATCH_DIR}/randomfile /randomfile5 + run_buildah copy --chmod u=rwX,go=X $cid ${TEST_SCRATCH_DIR}/subdir3 /extradir/subdir3 + run_buildah copy --chmod 0755 $cid ${TEST_SCRATCH_DIR}/randomfile /randomfile6 run_buildah run $cid ls -l /randomfile expect_output --substring rwxrwxrwx @@ -229,11 +234,23 @@ load helpers run_buildah run $cid ls -l /subdir expect_output --substring rw-rw---- - run_buildah copy --chmod 600 $cid ${TEST_SCRATCH_DIR}/other-subdir /subdir + run_buildah copy --chmod 600 $cid ${TEST_SCRATCH_DIR}/subdir2 /subdir for i in randomfile other-randomfile ; do run_buildah run $cid ls -l /subdir/$i expect_output --substring rw------- done + + run_buildah run $cid ls -l /randomfile4 + expect_output --substring r-------- + + run_buildah run $cid ls -l /randomfile5 + expect_output --substring rwxr-xr-x + + run_buildah run $cid ls -l /randomfile6 + expect_output --substring rwxr-xr-x + + run_buildah run $cid ls -l /extradir + expect_output --substring drwxr-xr-x } @test "copy-symlink" { diff --git a/vendor/github.com/tonistiigi/dchapes-mode/.hgignore b/vendor/github.com/tonistiigi/dchapes-mode/.hgignore new file mode 100644 index 00000000000..be8f61dd154 --- /dev/null +++ b/vendor/github.com/tonistiigi/dchapes-mode/.hgignore @@ -0,0 +1,5 @@ +syntax: glob +bench*.out* +cmode +coverage.out +coverage.txt diff --git a/vendor/github.com/tonistiigi/dchapes-mode/LICENSE b/vendor/github.com/tonistiigi/dchapes-mode/LICENSE new file mode 100644 index 00000000000..a8743fb2d8b --- /dev/null +++ b/vendor/github.com/tonistiigi/dchapes-mode/LICENSE @@ -0,0 +1,22 @@ +Copyright © 2016-2018, Dave Chapeskie +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/tonistiigi/dchapes-mode/README.md b/vendor/github.com/tonistiigi/dchapes-mode/README.md new file mode 100644 index 00000000000..dca34f9fcc2 --- /dev/null +++ b/vendor/github.com/tonistiigi/dchapes-mode/README.md @@ -0,0 +1,26 @@ +Mode +======== + +This is a fork of [hg.sr.ht/~dchapes/mode](https://hg.sr.ht/~dchapes/mode) with minimal patches and basic CI. + +[Mode](https://hg.sr.ht/~dchapes/mode) +is a [Go](http://golang.org/) package that provides +a native Go implementation of BSD's +[`setmode`](https://www.freebsd.org/cgi/man.cgi?query=setmode&sektion=3) +and `getmode` which can be used to modify the mode bits of +an [`os.FileMode`](https://golang.org/pkg/os#FileMode) value +based on a symbolic value as described by the +Unix [`chmod`](https://www.freebsd.org/cgi/man.cgi?query=chmod&sektion=1) command. + +[![Go Reference](https://pkg.go.dev/badge/hg.sr.ht/~dchapes/mode.svg)](https://pkg.go.dev/hg.sr.ht/~dchapes/mode) + +Online package documentation is available via +[pkg.go.dev](https://pkg.go.dev/hg.sr.ht/~dchapes/mode). + +To install: + + go get hg.sr.ht/~dchapes/mode + +or `go build` any Go code that imports it: + + import "hg.sr.ht/~dchapes/mode" diff --git a/vendor/github.com/tonistiigi/dchapes-mode/bits.go b/vendor/github.com/tonistiigi/dchapes-mode/bits.go new file mode 100644 index 00000000000..4dbb08ad786 --- /dev/null +++ b/vendor/github.com/tonistiigi/dchapes-mode/bits.go @@ -0,0 +1,76 @@ +package mode + +import "os" + +type modet uint16 + +// Although many of these can be found in the syscall package +// we don't use those to avoid the dependency, add some more +// values, use non-exported Go names, and use octal for better clarity. +// +// Note that Go only uses the the nine least significant bits as "Unix +// permission bits" (os.ModePerm == 0777). We use chmod(1)'s octal +// definitions that include three further bits: isUID, isGID, and +// isTXT (07000). Go has os.ModeSetuid=1<<23, os.ModeSetgid=1<<22, +// and os.ModeSticy=1<<20 for these. We do this so that absolute +// octal values can include those bits as defined by chmod(1). +const ( + //ifDir = 040000 // directory + isUID = 04000 // set user id on execution + isGID = 02000 // set group id on execution + isTXT = 01000 // sticky bit + iRWXU = 00700 // RWX mask for owner + iRUser = 00400 // R for owner + iWUser = 00200 // W for owner + iXUser = 00100 // X for owner + iRWXG = 00070 // RWX mask for group + iRGroup = 00040 // R for group + iWGroup = 00020 // W for group + iXGroup = 00010 // X for group + iRWXO = 00007 // RWX mask for other + iROther = 00004 // R for other + iWOther = 00002 // W for other + iXOther = 00001 // X for other + + standardBits = isUID | isGID | iRWXU | iRWXG | iRWXO + + // os.FileMode bits we touch + fmBits = os.ModeSetuid | os.ModeSetgid | os.ModeSticky | os.ModePerm +) + +func fileModeToBits(fm os.FileMode) modet { + m := modet(fm.Perm()) + /* + if fm&os.ModeSetuid != 0 { + m |= isUID + } + if fm&os.ModeSetgid != 0 { + m |= isGID + } + if fm&os.ModeSticky != 0 { + m |= isTXT + } + */ + m |= modet(fm & (os.ModeSetuid | os.ModeSetgid) >> 12) + m |= modet(fm & os.ModeSticky >> 11) + return m +} + +func bitsToFileMode(old os.FileMode, m modet) os.FileMode { + fm := old &^ fmBits + fm |= os.FileMode(m) & os.ModePerm + /* + if m&isUID != 0 { + fm |= os.ModeSetuid + } + if m&isGID != 0 { + fm |= os.ModeSetgid + } + if m&isTXT != 0 { + fm |= os.ModeSticky + } + */ + fm |= os.FileMode(m&(isUID|isGID)) << 12 + fm |= os.FileMode(m&isTXT) << 11 + return fm +} diff --git a/vendor/github.com/tonistiigi/dchapes-mode/docker-bake.hcl b/vendor/github.com/tonistiigi/dchapes-mode/docker-bake.hcl new file mode 100644 index 00000000000..1220e909696 --- /dev/null +++ b/vendor/github.com/tonistiigi/dchapes-mode/docker-bake.hcl @@ -0,0 +1,24 @@ +variable "GO_VERSION" { + default = null +} + +group "default" { + targets = ["build"] +} + +target "build" { + args = { + GO_VERSION = GO_VERSION + } + output = ["type=cacheonly"] +} + +target "test" { + inherits = ["build"] + target = "test" +} + +target "cross" { + inherits = ["build"] + platforms = ["linux/amd64", "linux/386", "linux/arm64", "linux/arm", "linux/ppc64le", "linux/s390x", "darwin/amd64", "darwin/arm64", "windows/amd64", "windows/arm64", "freebsd/amd64", "freebsd/arm64"] +} \ No newline at end of file diff --git a/vendor/github.com/tonistiigi/dchapes-mode/mode.go b/vendor/github.com/tonistiigi/dchapes-mode/mode.go new file mode 100644 index 00000000000..a53aa7d0f10 --- /dev/null +++ b/vendor/github.com/tonistiigi/dchapes-mode/mode.go @@ -0,0 +1,548 @@ +/* + +Parts of this file are a heavily modified C to Go +translation of BSD's /usr/src/lib/libc/gen/setmode.c +that contains the following copyright notice: + + * Copyright (c) 1989, 1993, 1994 + * The Regents of the University of California. All rights reserved. + * + * This code is derived from software contributed to Berkeley by + * Dave Borman at Cray Research, Inc. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * 4. Neither the name of the University nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + +*/ + +// Package mode provides a native Go implementation of BSD's setmode and getmode +// which can be used to modify the mode bits of an os.FileMode value based on +// a symbolic value as described by the Unix chmod command. +// +// For a full description of the mode string see chmod(1). +// Some examples include: +// +// 644 make a file readable by anyone and writable by the owner +// only. +// +// go-w deny write permission to group and others. +// +// =rw,+X set the read and write permissions to the usual defaults, +// but retain any execute permissions that are currently set. +// +// +X make a directory or file searchable/executable by everyone +// if it is already searchable/executable by anyone. +// +// 755 +// u=rwx,go=rx +// u=rwx,go=u-w make a file readable/executable by everyone and writable by +// the owner only. +// +// go= clear all mode bits for group and others. +// +// go=u-w set the group bits equal to the user bits, but clear the +// group write bit. +// +// See Also: +// +// setmode(3): https://www.freebsd.org/cgi/man.cgi?query=setmode&sektion=3 +// chmod(1): https://www.freebsd.org/cgi/man.cgi?query=chmod&sektion=1 +package mode + +import ( + "errors" + "fmt" + "os" + "strconv" + "strings" +) + +// Set is a set of changes to apply to an os.FileMode. +// Changes include setting or clearing specific bits, copying bits from one +// user class to another (e.g. "u=go" sets the user permissions to a copy of +// the group and other permsissions), etc. +type Set struct { + cmds []bitcmd +} + +type bitcmd struct { + cmd byte + cmd2 byte + bits modet +} + +const ( + cmd2Clear byte = 1 << iota + cmd2Set + cmd2GBits + cmd2OBits + cmd2UBits +) + +func (c bitcmd) String() string { + c2 := "" + if c.cmd2 != 0 { + c2 = " cmd2:" + if c.cmd2&cmd2Clear != 0 { + c2 += " CLR" + } + if c.cmd2&cmd2Set != 0 { + c2 += " SET" + } + if c.cmd2&cmd2UBits != 0 { + c2 += " UBITS" + } + if c.cmd2&cmd2GBits != 0 { + c2 += " GBITS" + } + if c.cmd2&cmd2OBits != 0 { + c2 += " OBITS" + } + } + return fmt.Sprintf("cmd: %q bits %#05o%s", c.cmd, c.bits, c2) +} + +// The String method will likely only be useful when testing. +func (s Set) String() string { + var buf strings.Builder + buf.Grow(21*len(s.cmds) + 10) + _, _ = buf.WriteString("set: {\n") + for _, c := range s.cmds { + _, _ = buf.WriteString(c.String()) + _ = buf.WriteByte('\n') + } + _, _ = buf.WriteString("}") + return buf.String() +} + +// ErrSyntax indicates an argument does not represent a valid mode. +var ErrSyntax = errors.New("invalid syntax") + +// Apply changes the provided os.FileMode based on the given umask and +// absolute or symbolic mode value. +// +// Apply is a convience to calling ParseWithUmask followed by Apply. +// Since it needs to parse the mode value string on each call it +// should only be used when mode value string will not be reapplied. +func Apply(s string, perm os.FileMode, umask uint) (os.FileMode, error) { + set, err := ParseWithUmask(s, umask) + if err != nil { + return 0, err + } + return set.Apply(perm), nil +} + +// Parse takes an absolute (octal) or symbolic mode value, +// as described in chmod(1), as an argument and returns +// the set of bit operations representing the mode value +// that can be applied to specific os.FileMode values. +// +// Same as ParseWithUmask(s, 0). +func Parse(s string) (Set, error) { + return ParseWithUmask(s, 0) +} + +// TODO(dchapes): A Set.Parse method that reuses existing memory. + +// TODO(dchapes): Only call syscall.Umask when abosolutely necessary and +// provide a Set method to query if set is umask dependant (and perhaps +// the umask that was in effect when parsed). + +// ParseWithUmask is like Parse but uses the provided +// file creation mask instead of calling syscall.Umask. +func ParseWithUmask(s string, umask uint) (Set, error) { + var m Set + if s == "" { + return m, ErrSyntax + } + + // If an absolute number, get it and return; + // disallow non-octal digits or illegal bits. + if d := s[0]; '0' <= d && d <= '9' { + v, err := strconv.ParseInt(s, 8, 16) + if err != nil { + return m, err + } + if v&^(standardBits|isTXT) != 0 { + return m, ErrSyntax + } + // We know this takes exactly two bitcmds. + m.cmds = make([]bitcmd, 0, 2) + m.addcmd('=', standardBits|isTXT, modet(v), 0) + return m, nil + } + + // Get a copy of the mask for the permissions that are mask relative. + // Flip the bits, we want what's not set. + var mask modet = ^modet(umask) + + // Pre-allocate room for several commands. + //m.cmds = make([]bitcmd, 0, 8) + + // Build list of bitcmd structs to set/clear/copy bits as described by + // each clause of the symbolic mode. + equalOpDone := false + for { + // First, find out which bits might be modified. + var who modet + whoLoop: + for { + if len(s) == 0 { + return Set{}, ErrSyntax + } + switch s[0] { + case 'a': + who |= standardBits + case 'u': + who |= isUID | iRWXU + case 'g': + who |= isGID | iRWXG + case 'o': + who |= iRWXO + default: + break whoLoop + } + s = s[1:] + } + + var op byte + getop: + op, s = s[0], s[1:] + switch op { + case '+', '-': + // Nothing. + case '=': + equalOpDone = false + default: + return Set{}, ErrSyntax + } + + who &^= isTXT + permLoop: + for perm, permX := modet(0), modet(0); ; s = s[1:] { + var b byte + if len(s) > 0 { + b = s[0] + } + switch b { + case 'r': + perm |= iRUser | iRGroup | iROther + case 's': + // If only "other" bits ignore set-id. + if who == 0 || who&^iRWXO != 0 { + perm |= isUID | isGID + } + case 't': + // If only "other bits ignore sticky. + if who == 0 || who&^iRWXO != 0 { + who |= isTXT + perm |= isTXT + } + case 'w': + perm |= iWUser | iWGroup | iWOther + case 'X': + if op != '-' { + permX = iXUser | iXGroup | iXOther + } else { + perm |= iXUser | iXGroup | iXOther + } + case 'x': + perm |= iXUser | iXGroup | iXOther + case 'u', 'g', 'o': + // Whenever we hit 'u', 'g', or 'o', we have + // to flush out any partial mode that we have, + // and then do the copying of the mode bits. + if perm != 0 { + m.addcmd(op, who, perm, mask) + perm = 0 + } + if op == '=' { + equalOpDone = true + } + if permX != 0 { + m.addcmd('X', who, permX, mask) + permX = 0 + } + m.addcmd(b, who, modet(op), mask) + default: + // Add any permissions that we haven't alread done. + if perm != 0 || op == '=' && !equalOpDone { + if op == '=' { + equalOpDone = true + } + m.addcmd(op, who, perm, mask) + //perm = 0 + } + if permX != 0 { + m.addcmd('X', who, permX, mask) + //permX = 0 + } + break permLoop + } + } + + if s == "" { + break + } + if s[0] != ',' { + goto getop + } + s = s[1:] + } + + m.compress() + return m, nil +} + +// Apply returns the os.FileMode after applying the set of changes. +func (s Set) Apply(perm os.FileMode) os.FileMode { + omode := fileModeToBits(perm) + newmode := omode + + // When copying the user, group or other bits around, we "know" + // where the bits are in the mode so that we can do shifts to + // copy them around. If we don't use shifts, it gets real + // grundgy with lots of single bit checks and bit sets. + common := func(c bitcmd, value modet) { + if c.cmd2&cmd2Clear != 0 { + var clrval modet + if c.cmd2&cmd2Set != 0 { + clrval = iRWXO + } else { + clrval = value + } + if c.cmd2&cmd2UBits != 0 { + newmode &^= clrval << 6 & c.bits + } + if c.cmd2&cmd2GBits != 0 { + newmode &^= clrval << 3 & c.bits + } + if c.cmd2&cmd2OBits != 0 { + newmode &^= clrval & c.bits + } + } + if c.cmd2&cmd2Set != 0 { + if c.cmd2&cmd2UBits != 0 { + newmode |= value << 6 & c.bits + } + if c.cmd2&cmd2GBits != 0 { + newmode |= value << 3 & c.bits + } + if c.cmd2&cmd2OBits != 0 { + newmode |= value & c.bits + } + } + } + + for _, c := range s.cmds { + switch c.cmd { + case 'u': + common(c, newmode&iRWXU>>6) + case 'g': + common(c, newmode&iRWXG>>3) + case 'o': + common(c, newmode&iRWXO) + + case '+': + newmode |= c.bits + case '-': + newmode &^= c.bits + + case 'X': + if omode&(iXUser|iXGroup|iXOther) != 0 || perm.IsDir() { + newmode |= c.bits + } + } + } + + return bitsToFileMode(perm, newmode) +} + +// Chmod is a convience routine that applies the changes in +// Set to the named file. To avoid some race conditions, +// it opens the file and uses os.File.Stat and +// os.File.Chmod rather than os.Stat and os.Chmod if possible. +func (s *Set) Chmod(name string) (old, new os.FileMode, err error) { + if f, err := os.Open(name); err == nil { // nolint: vetshadow + defer f.Close() // nolint: errcheck + return s.ChmodFile(f) + } + // Fallback to os.Stat and os.Chmod if we + // don't have permission to open the file. + fi, err := os.Stat(name) + if err != nil { + return 0, 0, err + } + old = fi.Mode() + new = s.Apply(old) + if new != old { + err = os.Chmod(name, new) + } + return old, new, err + +} + +// ChmodFile is a convience routine that applies +// the changes in Set to the open file f. +func (s *Set) ChmodFile(f *os.File) (old, new os.FileMode, err error) { + fi, err := f.Stat() + if err != nil { + return 0, 0, err + } + old = fi.Mode() + new = s.Apply(old) + if new != old { + err = f.Chmod(new) + } + return old, new, err +} + +func (s *Set) addcmd(op byte, who, oparg, mask modet) { + c := bitcmd{} + switch op { + case '=': + c.cmd = '-' + if who != 0 { + c.bits = who + } else { + c.bits = standardBits + } + + s.cmds = append(s.cmds, c) + //c = bitcmd{} // reset, not actually needed + op = '+' + fallthrough + case '+', '-', 'X': + c.cmd = op + if who != 0 { + c.bits = who & oparg + } else { + c.bits = mask & oparg + } + + case 'u', 'g', 'o': + c.cmd = op + if who != 0 { + if who&iRUser != 0 { + c.cmd2 |= cmd2UBits + } + if who&iRGroup != 0 { + c.cmd2 |= cmd2GBits + } + if who&iROther != 0 { + c.cmd2 |= cmd2OBits + } + c.bits = ^modet(0) + } else { + c.cmd2 = cmd2UBits | cmd2GBits | cmd2OBits + c.bits = mask + } + + switch oparg { + case '+': + c.cmd2 |= cmd2Set + case '-': + c.cmd2 |= cmd2Clear + case '=': + c.cmd2 |= cmd2Set | cmd2Clear + } + default: + panic("unreachable") + } + s.cmds = append(s.cmds, c) +} + +// compress by compacting consecutive '+', '-' and 'X' +// commands into at most 3 commands, one of each. The 'u', +// 'g' and 'o' commands continue to be separate. They could +// probably be compacted, but it's not worth the effort. +func (s *Set) compress() { + //log.Println("before:", *m) + //log.Println("Start compress:") + j := 0 + for i := 0; i < len(s.cmds); i++ { + c := s.cmds[i] + //log.Println(" read", i, c) + if strings.IndexByte("+-X", c.cmd) < 0 { + // Copy over any 'u', 'g', and 'o' commands. + if i != j { + s.cmds[j] = c + } + //log.Println(" wrote", j, "from", i) + j++ + continue + } + var setbits, clrbits, Xbits modet + for ; i < len(s.cmds); i++ { + c = s.cmds[i] + //log.Println(" scan", i, c) + switch c.cmd { + case '-': + clrbits |= c.bits + setbits &^= c.bits + Xbits &^= c.bits + continue + case '+': + setbits |= c.bits + clrbits &^= c.bits + Xbits &^= c.bits + continue + case 'X': + Xbits |= c.bits &^ setbits + continue + default: + i-- + } + break + } + if clrbits != 0 { + s.cmds[j].cmd = '-' + s.cmds[j].cmd2 = 0 + s.cmds[j].bits = clrbits + //log.Println(" wrote", j, "clrbits") + j++ + } + if setbits != 0 { + s.cmds[j].cmd = '+' + s.cmds[j].cmd2 = 0 + s.cmds[j].bits = setbits + //log.Println(" wrote", j, "setbits") + j++ + } + if Xbits != 0 { + s.cmds[j].cmd = 'X' + s.cmds[j].cmd2 = 0 + s.cmds[j].bits = Xbits + //log.Println(" wrote", j, "Xbits") + j++ + } + } + /* + if len(m.cmds) != j { + log.Println("compressed", len(m.cmds), "down to", j) + } + */ + s.cmds = s.cmds[:j] + //log.Println("after:", *m) +} diff --git a/vendor/modules.txt b/vendor/modules.txt index cf583838825..8487643118c 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -395,6 +395,9 @@ github.com/sylabs/sif/v2/pkg/sif # github.com/tchap/go-patricia/v2 v2.3.3 ## explicit; go 1.16 github.com/tchap/go-patricia/v2/patricia +# github.com/tonistiigi/dchapes-mode v0.0.0-20250318174251-73d941a28323 +## explicit; go 1.21 +github.com/tonistiigi/dchapes-mode # github.com/ulikunitz/xz v0.5.15 ## explicit; go 1.12 github.com/ulikunitz/xz