Jonathan A. Sternberg 8fb1157b5f
vendor: github.com/moby/buildkit v0.21.0-rc1
Signed-off-by: Jonathan A. Sternberg <jonathan.sternberg@docker.com>
2025-04-09 10:28:03 -05:00

549 lines
13 KiB
Go

/*
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)
}