Skip to content

Instantly share code, notes, and snippets.

@marwatk
Created February 26, 2020 23:23
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save marwatk/4f9c59b112fd4dd5c360a5a29ddbfccf to your computer and use it in GitHub Desktop.
Save marwatk/4f9c59b112fd4dd5c360a5a29ddbfccf to your computer and use it in GitHub Desktop.
go-chmod with tests
package chmod
import (
"fmt"
"os"
"regexp"
"strconv"
"strings"
)
const setuid = 4
const setgid = 2
const sticky = 1
const read = 4
const write = 2
const execute = 1
const specialMask = 07000
const ownerMask = 0700
const groupMask = 070
const otherMask = 07
func applyNumericPerms(curMode uint32, modifier string, perms uint32) (uint32, error) {
if modifier == "" {
modifier = "="
}
switch modifier {
case "=":
return perms, nil
case "+":
return curMode | perms, nil
case "-":
return curMode &^ perms, nil
default:
return curMode, fmt.Errorf("Unknown permissions modifier [%s]", modifier)
}
}
func applySymbolicPerms(curMode uint32, isFolder bool, appliesTo string, perms string) (uint32, error) {
if appliesTo == "" {
/*
This is the case where you `chmod +rw <file>` (emtpy specifier for where to apply)
I can't find a way to only fetch the umask in golang so am not going to implement
it (I can't imagine needing it, anyway). It's supposed to (from the man page):
If none of these are given, the effect is as if (a) were given, but bits that are
set in the umask are not affected
*/
return 0, fmt.Errorf("Empty appliesTo is not supported")
}
appliesTo = strings.ReplaceAll(appliesTo, "a", "ugo")
//We put the ugo before the rwxXst so that it doesn't use the empty string instead of continuing to ugo
for _, parts := range regexp.MustCompile(`([+=-])([ugo]|[rwxXst]*)`).FindAllStringSubmatch(perms, -1) {
modifier := parts[1]
perms := parts[2]
for _, whoTo := range appliesTo {
switch perms {
case "u":
fallthrough
case "g":
fallthrough
case "o":
srcPerms, err := getPerms(curMode, perms)
if err != nil {
return 0, err
}
curMode, err = setPerms(curMode, modifier, string(whoTo), srcPerms)
if err != nil {
return 0, err
}
default:
specialBits, sectionBits, err := calculateBits(string(whoTo), isFolder, curMode, perms)
if err != nil {
return 0, err
}
curMode, err = setPerms(curMode, modifier, string(whoTo), sectionBits)
if err != nil {
return 0, fmt.Errorf("Error calling setPerms for %v in applySymbolicPerms default case: %v", whoTo, err)
}
curMode, err = setPerms(curMode, modifier, "s", specialBits)
if err != nil {
return 0, fmt.Errorf("Error calling setPerms for 's' section in applySymbolicPerms default case: %v", err)
}
}
}
}
return curMode, nil
}
func calculateBits(whoTo string, isFolder bool, curMode uint32, bits string) (uint8, uint8, error) {
specialBits := uint8(0)
sectionBits := uint8(0)
for _, bit := range bits {
switch string(bit) {
case "r":
sectionBits |= read
case "w":
sectionBits |= write
case "x":
sectionBits |= execute
case "X":
existingPerms, err := getPerms(curMode, string(whoTo))
if err != nil {
return 0, 0, err
}
if isFolder || existingPerms&execute != 0 {
sectionBits |= execute
}
case "s":
if string(whoTo) == "g" {
specialBits |= setgid
} else if string(whoTo) == "u" {
specialBits |= setuid
}
case "t":
if string(whoTo) == "o" { // This filter is not document in chmod man page
specialBits |= sticky
}
}
}
return specialBits, sectionBits, nil
}
func calculateFileMode(curMode uint32, isFolder bool, applyMode string) (uint32, error) {
// From chmod man page:
// Each MODE is of the form '[ugoa]*([-+=]([rwxXst]*|[ugo]))+|[-+=][0-7]+'.
newMode := uint32(curMode)
numericRe := regexp.MustCompile(`^([+=-]?)([0-7]{1,4})$`)
//We put the ugo before the rwxXst so that it doesn't use the empty string instead of continuing to ugo
symbolicRe := regexp.MustCompile(`^([ugoa]*)(([+=-]([ugo]|[rwxXst]*))+)$`)
for _, mode := range strings.Split(applyMode, ",") {
if numericRe.MatchString(mode) {
parts := numericRe.FindStringSubmatch(mode)
thisMode, err := strconv.ParseInt(parts[2], 8, 32)
if err != nil {
return 0, err
}
modifier := parts[1]
newMode, err = applyNumericPerms(newMode, modifier, uint32(thisMode))
if err != nil {
return 0, err
}
} else if symbolicRe.MatchString(mode) {
parts := symbolicRe.FindStringSubmatch(mode)
var err error
newMode, err = applySymbolicPerms(newMode, isFolder, parts[1], parts[2])
if err != nil {
return 0, err
}
} else {
return curMode, fmt.Errorf("Mode %q is not valid", mode)
}
}
return newMode, nil
}
// Chmod allows for any mode specified in the linux chmod command.
func Chmod(name string, mode string) error {
curMode, isDir, err := getFileMode(name)
if err != nil {
return err
}
newMode, err := calculateFileMode(curMode, isDir, mode)
if err != nil {
return err
}
if newMode != curMode {
err = setFileMode(name, newMode)
}
return err
}
func getFileMode(name string) (uint32, bool, error) {
stat, err := os.Stat(name)
if err != nil {
return 0, false, err
}
goMode := stat.Mode()
unixMode := uint32(goMode & 0777) //Strip non permission metadata (like folder flag)
if goMode&os.ModeSetgid != 0 {
unixMode, err = setPerms(unixMode, "+", "s", setgid)
if err != nil {
return 0, false, err
}
}
if goMode&os.ModeSetuid != 0 {
unixMode, err = setPerms(unixMode, "+", "s", setuid)
if err != nil {
return 0, false, err
}
}
if goMode&os.ModeSticky != 0 {
unixMode, err = setPerms(unixMode, "+", "s", sticky)
if err != nil {
return 0, false, err
}
}
return unixMode, stat.IsDir(), nil
}
func getPerms(mode uint32, who string) (uint8, error) {
switch who {
case "s":
return uint8((mode & specialMask) >> 9), nil
case "u":
return uint8((mode & ownerMask) >> 6), nil
case "g":
return uint8((mode & groupMask) >> 3), nil
case "o":
return uint8(mode & otherMask), nil
default:
return 0, fmt.Errorf("Invalid source specification [%s]", who)
}
}
func setFileMode(name string, mode uint32) error {
fileMode, err := uint32ToFileMode(mode)
err = os.Chmod(name, fileMode)
if err != nil {
return err
}
return nil
}
func setPerms(mode uint32, modifier string, who string, perms uint8) (uint32, error) {
newMode := uint32(perms)
mask := uint32(0)
if who == "a" {
var err error
mode, err = setPerms(mode, modifier, "u", perms)
if err != nil {
return 0, err
}
mode, err = setPerms(mode, modifier, "g", perms)
if err != nil {
return 0, err
}
mode, err = setPerms(mode, modifier, "o", perms)
if err != nil {
return 0, err
}
return mode, nil
}
switch who {
case "s":
newMode <<= 9
mask = specialMask
case "u":
newMode <<= 6
mask = ownerMask
case "g":
newMode <<= 3
mask = groupMask
case "o":
mask = otherMask
default:
return mode, fmt.Errorf("Invalid who specification on setPerms [%s]", who)
}
switch modifier {
case "+":
mode |= newMode
case "-":
mode &^= newMode
case "=":
mode &^= mask
mode |= newMode
}
return mode, nil
}
// UnixModeToFileMode converts a string representation of a unix file mode (755)
// to a golang os.FileMode.
func UnixModeToFileMode(unixMode string) (os.FileMode, error) {
intVal, err := strconv.ParseInt(unixMode, 8, 32)
if err != nil {
return 0, err
}
return uint32ToFileMode(uint32(intVal))
}
func uint32ToFileMode(mode uint32) (os.FileMode, error) {
fileMode := os.FileMode(mode)
perms, err := getPerms(mode, "s")
if err != nil {
return 0, err
}
if perms&sticky != 0 {
fileMode |= os.ModeSticky
}
if perms&setgid != 0 {
fileMode |= os.ModeSetgid
}
if perms&setuid != 0 {
fileMode |= os.ModeSetuid
}
return fileMode, nil
}
package chmod
import (
"io/ioutil"
"os"
"os/exec"
"testing"
)
func testGetPerms(t *testing.T, mode uint32, who string, expected uint8, wantError bool) {
actual, err := getPerms(mode, who)
if wantError {
if err == nil {
t.Errorf("getPerms(%o, %q) should have errored but didn't", mode, who)
}
} else {
if err != nil {
t.Errorf("getPerms(%o, %q) errored %v", mode, who, err)
}
if actual != expected {
t.Errorf("getPerms(%o, %q) = %d; expected %d", mode, who, actual, expected)
}
}
}
func TestGetPerms(t *testing.T) {
testGetPerms(t, 01754, "o", 4, false)
testGetPerms(t, 01754, "g", 5, false)
testGetPerms(t, 01754, "u", 7, false)
testGetPerms(t, 01754, "s", 1, false)
testGetPerms(t, 01754, "p", 1, true)
}
func testSetPerms(t *testing.T, mode uint32, modifier string, who string, perms uint8, expected uint32, wantError bool) {
actual, err := setPerms(mode, modifier, who, perms)
if wantError {
if err == nil {
t.Errorf("setPerms(%o, %q, %q, %o) should have errored but didn't", mode, modifier, who, perms)
}
} else {
if err != nil {
t.Errorf("setPerms(%o, %q, %q, %o) errored %v", mode, modifier, who, perms, err)
}
if actual != expected {
t.Errorf("setPerms(%o, %q, %q, %o) = %o; expected %o", mode, modifier, who, perms, actual, expected)
}
}
}
func TestSetPerms(t *testing.T) {
testSetPerms(t, 0777, "-", "o", 1, 0776, false)
testSetPerms(t, 0777, "-", "o", 2, 0775, false)
testSetPerms(t, 0777, "-", "o", 7, 0770, false)
testSetPerms(t, 0770, "-", "o", 1, 0770, false)
testSetPerms(t, 0770, "-", "o", 2, 0770, false)
testSetPerms(t, 0770, "-", "o", 7, 0770, false)
testSetPerms(t, 0000, "-", "o", 1, 0000, false)
testSetPerms(t, 0000, "-", "o", 2, 0000, false)
testSetPerms(t, 0000, "-", "o", 7, 0000, false)
testSetPerms(t, 0777, "-", "g", 1, 0767, false)
testSetPerms(t, 0777, "-", "g", 2, 0757, false)
testSetPerms(t, 0777, "-", "g", 7, 0707, false)
testSetPerms(t, 0777, "-", "u", 1, 0677, false)
testSetPerms(t, 0777, "-", "u", 2, 0577, false)
testSetPerms(t, 0777, "-", "u", 7, 0077, false)
testSetPerms(t, 0777, "+", "o", 1, 0777, false)
testSetPerms(t, 0777, "+", "o", 2, 0777, false)
testSetPerms(t, 0777, "+", "o", 7, 0777, false)
testSetPerms(t, 0770, "+", "o", 1, 0771, false)
testSetPerms(t, 0770, "+", "o", 2, 0772, false)
testSetPerms(t, 0770, "+", "o", 7, 0777, false)
testSetPerms(t, 0000, "+", "o", 1, 0001, false)
testSetPerms(t, 0000, "+", "o", 2, 0002, false)
testSetPerms(t, 0000, "+", "o", 7, 0007, false)
testSetPerms(t, 0000, "+", "g", 1, 0010, false)
testSetPerms(t, 0000, "+", "g", 2, 0020, false)
testSetPerms(t, 0000, "+", "g", 7, 0070, false)
testSetPerms(t, 0000, "+", "u", 1, 0100, false)
testSetPerms(t, 0000, "+", "u", 2, 0200, false)
testSetPerms(t, 0000, "+", "u", 7, 0700, false)
testSetPerms(t, 0777, "=", "o", 1, 0771, false)
testSetPerms(t, 0777, "=", "o", 2, 0772, false)
testSetPerms(t, 0777, "=", "o", 7, 0777, false)
testSetPerms(t, 0770, "=", "o", 1, 0771, false)
testSetPerms(t, 0770, "=", "o", 2, 0772, false)
testSetPerms(t, 0770, "=", "o", 7, 0777, false)
testSetPerms(t, 0000, "=", "o", 1, 0001, false)
testSetPerms(t, 0000, "=", "o", 2, 0002, false)
testSetPerms(t, 0000, "=", "o", 7, 0007, false)
testSetPerms(t, 0000, "=", "g", 1, 0010, false)
testSetPerms(t, 0000, "=", "g", 2, 0020, false)
testSetPerms(t, 0000, "=", "g", 7, 0070, false)
testSetPerms(t, 0000, "=", "u", 1, 0100, false)
testSetPerms(t, 0000, "=", "u", 2, 0200, false)
testSetPerms(t, 0000, "=", "u", 7, 0700, false)
testSetPerms(t, 0000, "=", "s", 1, 01000, false)
testSetPerms(t, 0000, "=", "s", 2, 02000, false)
testSetPerms(t, 0000, "=", "s", 7, 07000, false)
testSetPerms(t, 0000, "+", "s", 1, 01000, false)
testSetPerms(t, 0000, "+", "s", 2, 02000, false)
testSetPerms(t, 0000, "+", "s", 7, 07000, false)
testSetPerms(t, 07777, "-", "s", 1, 06777, false)
testSetPerms(t, 07777, "-", "s", 2, 05777, false)
testSetPerms(t, 07777, "-", "s", 7, 00777, false)
testSetPerms(t, 0000, "+", "a", 1, 0111, false)
testSetPerms(t, 0000, "+", "a", 2, 0222, false)
testSetPerms(t, 0000, "+", "a", 7, 0777, false)
testSetPerms(t, 07777, "-", "a", 1, 07666, false)
testSetPerms(t, 07777, "-", "a", 2, 07555, false)
testSetPerms(t, 07777, "-", "a", 7, 07000, false)
testSetPerms(t, 07777, "-", "a", 0, 07777, false)
testSetPerms(t, 07777, "-", "a", 0, 07777, false)
testSetPerms(t, 07777, "-", "a", 0, 07777, false)
testSetPerms(t, 07777, "=", "a", 0, 07000, false)
testSetPerms(t, 07777, "=", "a", 0, 07000, false)
testSetPerms(t, 07777, "=", "a", 0, 07000, false)
testSetPerms(t, 07777, "+", "a", 0, 07777, false)
testSetPerms(t, 07777, "+", "a", 0, 07777, false)
testSetPerms(t, 07777, "+", "a", 0, 07777, false)
testSetPerms(t, 07777, "-", "o", 0, 07777, false)
testSetPerms(t, 07777, "-", "o", 0, 07777, false)
testSetPerms(t, 07777, "-", "o", 0, 07777, false)
testSetPerms(t, 07777, "=", "o", 0, 07770, false)
testSetPerms(t, 07777, "=", "o", 0, 07770, false)
testSetPerms(t, 07777, "=", "o", 0, 07770, false)
testSetPerms(t, 07777, "+", "o", 0, 07777, false)
testSetPerms(t, 07777, "+", "o", 0, 07777, false)
testSetPerms(t, 07777, "+", "o", 0, 07777, false)
testSetPerms(t, 0777, "-", "o", 0, 0777, false)
testSetPerms(t, 0777, "-", "o", 0, 0777, false)
testSetPerms(t, 0777, "-", "o", 0, 0777, false)
testSetPerms(t, 0777, "=", "o", 0, 0770, false)
testSetPerms(t, 0777, "=", "o", 0, 0770, false)
testSetPerms(t, 0777, "=", "o", 0, 0770, false)
testSetPerms(t, 0777, "+", "o", 0, 0777, false)
testSetPerms(t, 0777, "+", "o", 0, 0777, false)
testSetPerms(t, 0777, "+", "o", 0, 0777, false)
testSetPerms(t, 0777, "-", "p", 1, 0677, true)
}
func confirmRealChmodMatch(t *testing.T, curMode uint32, isFolder bool, applyMode string, expected uint32) {
path, err := exec.LookPath("chmod")
if err != nil {
//No chmod in path, can't do integration test
return
}
var temp string
if isFolder {
temp, err = ioutil.TempDir("", "")
if err != nil {
t.Errorf("Error making temp folder %v", err)
return
}
} else {
tempFile, err := ioutil.TempFile("", "")
if err != nil {
t.Errorf("Error making temp file %v", err)
return
}
temp = tempFile.Name()
tempFile.Close()
}
defer os.Remove(temp)
setFileMode(temp, curMode)
err = exec.Command(path, applyMode, temp).Run()
if err != nil {
t.Errorf("Error running [chmod %q %q] %v", applyMode, temp, err)
return
}
actual, _, err := getFileMode(temp)
if err != nil {
t.Errorf("Error `stat`ing file %q: %v", temp, err)
return
}
if actual != expected {
t.Errorf("The test is a lie! Real chmod says (folder: %t) starting with %o having [chmod %q] applied should end at %o, not %o as coded!", isFolder, curMode, applyMode, actual, expected)
}
}
func testCalculateFileMode(t *testing.T, curMode uint32, isFolder bool, applyMode string, expected uint32, wantError bool) {
if !wantError {
confirmRealChmodMatch(t, curMode, isFolder, applyMode, expected)
}
actual, err := calculateFileMode(curMode, isFolder, applyMode)
if wantError {
if err == nil {
t.Errorf("calculateFileMode(%o, %t, %q) should have errored but didn't", curMode, isFolder, applyMode)
}
} else {
if err != nil {
t.Errorf("calculateFileMode(%o, %t, %q) errored %v", curMode, isFolder, applyMode, err)
}
if actual != expected {
t.Errorf("calculateFileMode(%o, %t, %q) = %o; expected %o", curMode, isFolder, applyMode, actual, expected)
}
}
}
func TestCalculateFileMode(t *testing.T) {
testCalculateFileMode(t, 0755, false, "644", 0644, false)
testCalculateFileMode(t, 0755, false, "+020", 0775, false)
testCalculateFileMode(t, 0755, false, "-111", 0644, false)
testCalculateFileMode(t, 0666, false, "go+x", 0677, false)
testCalculateFileMode(t, 0666, false, "go=+x", 0611, false)
testCalculateFileMode(t, 0666, false, "go=+", 0600, false)
testCalculateFileMode(t, 0666, false, "go+x", 0677, false)
testCalculateFileMode(t, 0666, false, "go+", 0666, false)
testCalculateFileMode(t, 0666, false, "go=-w", 0600, false)
testCalculateFileMode(t, 0666, false, "a=-w", 0000, false)
testCalculateFileMode(t, 0666, false, "go-w", 0644, false)
testCalculateFileMode(t, 0666, false, "a-w", 0444, false)
testCalculateFileMode(t, 0666, false, "go=-w+x", 0611, false)
testCalculateFileMode(t, 0666, false, "go-w+x", 0655, false)
testCalculateFileMode(t, 0700, false, "g=u", 0770, false)
testCalculateFileMode(t, 0700, false, "a=u", 0777, false)
testCalculateFileMode(t, 0700, false, "go=u", 0777, false)
testCalculateFileMode(t, 0755, false, "go=u", 0777, false)
testCalculateFileMode(t, 0644, false, "g=u", 0664, false)
testCalculateFileMode(t, 0644, false, "a=u", 0666, false)
testCalculateFileMode(t, 0700, false, "go=u-w", 0755, false)
testCalculateFileMode(t, 0644, false, "ug+rwX", 0664, false)
testCalculateFileMode(t, 0644, true, "ug+rwX", 0774, false)
testCalculateFileMode(t, 0644, true, "ug+rwX,o+rX", 0775, false)
testCalculateFileMode(t, 0644, false, "a+rwX", 0666, false)
testCalculateFileMode(t, 0644, true, "a+rwX", 0777, false)
testCalculateFileMode(t, 0644, true, "a+X", 0755, false)
testCalculateFileMode(t, 0755, true, "a=X", 0111, false)
testCalculateFileMode(t, 0755, false, "a=X", 0111, false)
testCalculateFileMode(t, 0755, true, "go-X", 0744, false)
testCalculateFileMode(t, 0755, false, "go-X", 0744, false)
testCalculateFileMode(t, 0000, false, "+755,-111,g+w", 0664, false)
testCalculateFileMode(t, 0000, false, "+755,a-x,g+w", 0664, false)
testCalculateFileMode(t, 0000, false, "+755,,", 0664, true)
// Test various order of operations
testCalculateFileMode(t, 0751, false, "oa=g", 0555, false)
testCalculateFileMode(t, 0751, false, "ao=g", 0555, false)
testCalculateFileMode(t, 0751, false, "ao=g", 0555, false)
testCalculateFileMode(t, 0751, false, "ug=g", 0551, false)
testCalculateFileMode(t, 0751, false, "ug=g,o=u", 0555, false)
// special bits
testCalculateFileMode(t, 0751, false, "g+s", 02751, false)
testCalculateFileMode(t, 0751, false, "g-s", 0751, false)
testCalculateFileMode(t, 01751, false, "g-s", 01751, false)
testCalculateFileMode(t, 02751, false, "g-s", 0751, false)
testCalculateFileMode(t, 07751, false, "g-s", 05751, false)
testCalculateFileMode(t, 0751, false, "u+s", 04751, false)
testCalculateFileMode(t, 0751, false, "u-s", 0751, false)
testCalculateFileMode(t, 01751, false, "u-s", 01751, false)
testCalculateFileMode(t, 04751, false, "u-s", 0751, false)
testCalculateFileMode(t, 07751, false, "u-s", 03751, false)
testCalculateFileMode(t, 0751, false, "u+t", 0751, false)
testCalculateFileMode(t, 0751, false, "g+t", 0751, false)
testCalculateFileMode(t, 0751, false, "o+t", 01751, false)
testCalculateFileMode(t, 0751, false, "a+t", 01751, false)
testCalculateFileMode(t, 01751, false, "u-t", 01751, false)
testCalculateFileMode(t, 0751, false, "u-t", 0751, false)
testCalculateFileMode(t, 03751, false, "u-t", 03751, false)
testCalculateFileMode(t, 07751, false, "u-t", 07751, false)
testCalculateFileMode(t, 01751, false, "o-t", 0751, false)
testCalculateFileMode(t, 0751, false, "o-t", 0751, false)
testCalculateFileMode(t, 03751, false, "o-t", 02751, false)
testCalculateFileMode(t, 07751, false, "o-t", 06751, false)
testCalculateFileMode(t, 01751, false, "a-t", 0751, false)
testCalculateFileMode(t, 0751, false, "a-t", 0751, false)
testCalculateFileMode(t, 03751, false, "a-t", 02751, false)
testCalculateFileMode(t, 07751, false, "a-t", 06751, false)
testCalculateFileMode(t, 0751, true, "g+s", 02751, false)
testCalculateFileMode(t, 0751, true, "g-s", 0751, false)
testCalculateFileMode(t, 01751, true, "g-s", 01751, false)
testCalculateFileMode(t, 02751, true, "g-s", 0751, false)
testCalculateFileMode(t, 07751, true, "g-s", 05751, false)
testCalculateFileMode(t, 0751, true, "u+s", 04751, false)
testCalculateFileMode(t, 0751, true, "u-s", 0751, false)
testCalculateFileMode(t, 01751, true, "u-s", 01751, false)
testCalculateFileMode(t, 04751, true, "u-s", 0751, false)
testCalculateFileMode(t, 07751, true, "u-s", 03751, false)
testCalculateFileMode(t, 0751, true, "u+t", 0751, false)
testCalculateFileMode(t, 0751, true, "g+t", 0751, false)
testCalculateFileMode(t, 0751, true, "o+t", 01751, false)
testCalculateFileMode(t, 0751, true, "a+t", 01751, false)
testCalculateFileMode(t, 01751, true, "u-t", 01751, false)
testCalculateFileMode(t, 0751, true, "u-t", 0751, false)
testCalculateFileMode(t, 03751, true, "u-t", 03751, false)
testCalculateFileMode(t, 07751, true, "u-t", 07751, false)
testCalculateFileMode(t, 01751, true, "o-t", 0751, false)
testCalculateFileMode(t, 0751, true, "o-t", 0751, false)
testCalculateFileMode(t, 03751, true, "o-t", 02751, false)
testCalculateFileMode(t, 07751, true, "o-t", 06751, false)
testCalculateFileMode(t, 01751, true, "a-t", 0751, false)
testCalculateFileMode(t, 0751, true, "a-t", 0751, false)
testCalculateFileMode(t, 03751, true, "a-t", 02751, false)
testCalculateFileMode(t, 07751, true, "a-t", 06751, false)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment