Skip to content

Commit

Permalink
Implement os:chmod.
Browse files Browse the repository at this point in the history
This supersedes #1730.
  • Loading branch information
xiaq committed Jan 3, 2024
1 parent 266e01d commit cf9ec15
Show file tree
Hide file tree
Showing 6 changed files with 138 additions and 24 deletions.
10 changes: 6 additions & 4 deletions pkg/eval/vals/conversion.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@ import (
// Conversion between "Go values" (those expected by native Go functions) and
// "Elvish values" (those participating in the Elvish runtime).
//
// Among the conversion functions, ScanToGo and FromGo implement the implicit
// conversion used when calling native Go functions from Elvish. The API is
// asymmetric; this has to do with two characteristics of Elvish's type system:
// Among the conversion functions, [ScanToGo] and [FromGo] implement the
// implicit conversion used when calling native Go functions from Elvish. The
// API is asymmetric; this has to do with two characteristics of Elvish's type
// system:
//
// - Elvish doesn't have a dedicated rune type and uses strings to represent
// them.
Expand All @@ -31,7 +32,8 @@ import (
// the destination type is, and the process may fail. Thus ScanToGo takes the
// pointer to the destination as an argument, and returns an error.
//
// The rest of the conversion functions need to explicitly invoked.
// The rest of the conversion functions are exported for use in more
// sophisticated binding code, and need to explicitly invoked.

// WrongType is returned by ScanToGo if the source value doesn't have a
// compatible type.
Expand Down
27 changes: 27 additions & 0 deletions pkg/mods/os/os.d.elv
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,33 @@ fn is-dir {|&follow-symlink=$false path| }
# See also [`os:is-dir`]().
fn is-regular {|&follow-symlink=$false path| }

# Changes the mode of the file at `$path` to have permission bits set to `$perm`
# and special modes set to `$special-modes`.
#
# The permission bits follow the [numeric notation of Unix
# permission](https://en.wikipedia.org/wiki/File-system_permissions#Numeric_notation),
# but note Elvish requires a `0o` prefix for octal numbers (unprefixed numbers
# like `444` are interpreted as decimal instead). On Windows, only the `0o200`
# bit (owner writable) is used; clearing it makes the file read-only. All other
# bits are ignored.
#
# The special modes should be specified as a list, with elements being one of
# `setuid`, `setgid` or `sticky`.
#
# If the file is a symbolic link, this command always on the link's target.
#
# Example:
#
# ```elvish-transcript
# ~> touch file
# ~> printf "%o %v\n" (os:stat file)[perm special-modes]
# 644 []
# ~> os:chmod &special-modes=[sticky] 0o600 file
# ~> printf "%o %v\n" (os:stat file)[perm special-modes]
# 600 [sticky]
# ```
fn chmod {|&special-modes=[] perm path| }

# Creates a new directory and outputs its name.
#
# The &dir option determines where the directory will be created; if it is an
Expand Down
25 changes: 25 additions & 0 deletions pkg/mods/os/os.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ package os

import (
_ "embed"
"io/fs"
"os"
"path/filepath"
"strconv"

"src.elv.sh/pkg/eval"
"src.elv.sh/pkg/eval/errs"
Expand All @@ -25,6 +27,7 @@ var Ns = eval.BuildNsNamed("os").
"mkdir": mkdir,
"remove": remove,
"remove-all": removeAll,
"chmod": chmod,

"eval-symlinks": filepath.EvalSymlinks,

Expand Down Expand Up @@ -89,6 +92,28 @@ func removeAll(path string) error {
return os.RemoveAll(path)
}

type chmodOpts struct {
SpecialModes any
}

func (*chmodOpts) SetDefaultOptions() {}

func chmod(opts chmodOpts, perm int, path string) error {
if perm < 0 || perm > 0x777 {
return errs.OutOfRange{What: "permission bits",
ValidLow: "0", ValidHigh: "0o777", Actual: strconv.Itoa(perm)}
}
mode := fs.FileMode(perm)
if opts.SpecialModes != nil {
special, err := specialModesFromIterable(opts.SpecialModes)
if err != nil {
return err
}
mode |= special
}
return os.Chmod(path, mode)
}

type statOpts struct{ FollowSymlink bool }

func (opts *statOpts) SetDefaultOptions() {}
Expand Down
17 changes: 17 additions & 0 deletions pkg/mods/os/os_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (

"src.elv.sh/pkg/eval"
"src.elv.sh/pkg/eval/errs"
"src.elv.sh/pkg/eval/vals"
"src.elv.sh/pkg/mods/file"
osmod "src.elv.sh/pkg/mods/os"
"src.elv.sh/pkg/testutil"
Expand Down Expand Up @@ -43,6 +44,22 @@ func TestFSModifications(t *testing.T) {
Puts(false),
That(`os:remove-all d`).DoesNothing(),
That(`os:remove-all ""`).Throws(osmod.ErrEmptyPath),

// chmod
That(`os:mkdir d; os:chmod 0o400 d; put (os:stat d)[perm]`).Puts(0o400),
That(`os:mkdir d; os:chmod &special-modes=[setuid setgid sticky] 0o400 d; put (os:stat d)[special-modes]`).
Puts(vals.MakeList("setuid", "setgid", "sticky")),
// chmod errors
That(`os:chmod -1 d`).
Throws(errs.OutOfRange{What: "permission bits",
ValidLow: "0", ValidHigh: "0o777", Actual: "-1"}),
// TODO: This error should be more informative and point out that it is
// the special modes that should be iterable
That(`os:chmod &special-modes=(num 0) 0 d`).
Throws(ErrorWithMessage("cannot iterate number")),
That(`os:chmod &special-modes=[bad] 0 d`).
Throws(errs.BadValue{What: "special mode",
Valid: "setuid, setgid or sticky", Actual: "bad"}),
)
}

Expand Down
61 changes: 61 additions & 0 deletions pkg/mods/os/special_modes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package os

import (
"io/fs"

"src.elv.sh/pkg/eval/errs"
"src.elv.sh/pkg/eval/vals"
)

// Conversion between an Elvish list representation of special modes and Go's
// bit flag representation. These are used from different places, but since they
// are symmetrical, keeping them in the same file makes it easier to ensure they
// are consistent.
//
// A special mode is one of the bits in [fs.FileMode] that is not part of
// [fs.ModeType] or [fs.ModePerm]. We omit [fs.ModeAppend], [fs.ModeExclusive]
// and [fs.ModeTemporary] since they are only used on Plan 9, which Elvish
// doesn't support (yet) so we can't test them.
//
// TODO: Use a set as the Elvish representation when Elvish has lists.

func specialModesFromIterable(v any) (fs.FileMode, error) {
var mode fs.FileMode
var errElem error
errIterate := vals.Iterate(v, func(elem any) bool {
switch elem {
case "setuid":
mode |= fs.ModeSetuid
case "setgid":
mode |= fs.ModeSetgid
case "sticky":
mode |= fs.ModeSticky
default:
errElem = errs.BadValue{What: "special mode",
Valid: "setuid, setgid or sticky", Actual: vals.ToString(elem)}
return false
}
return true
})
if errIterate != nil {
return 0, errIterate
}
if errElem != nil {
return 0, errElem
}
return mode, nil
}

func specialModesToList(mode fs.FileMode) vals.List {
l := vals.EmptyList
if mode&fs.ModeSetuid != 0 {
l = l.Conj("setuid")
}
if mode&fs.ModeSetgid != 0 {
l = l.Conj("setgid")
}
if mode&fs.ModeSticky != 0 {
l = l.Conj("sticky")
}
return l
}
22 changes: 2 additions & 20 deletions pkg/mods/os/stat.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,6 @@ var typeNames = map[fs.FileMode]string{
fs.ModeIrregular: "irregular",
}

var specialModeNames = [...]struct {
bit fs.FileMode
name string
}{
// fs.ModeAppend, fs.ModeExclusive and fs.ModeTemporary are only used on
// Plan 9, which Elvish doesn't support (yet).
{fs.ModeSetuid, "setuid"},
{fs.ModeSetgid, "setgid"},
{fs.ModeSticky, "sticky"},
}

// Implementation of the stat function itself is in os.go.

func statMap(fi fs.FileInfo) vals.Map {
Expand All @@ -39,19 +28,12 @@ func statMap(fi fs.FileInfo) vals.Map {
// information.
typeName = fmt.Sprintf("unknown %d", mode.Type())
}
// TODO: Make this a set when Elvish has a set type.
specialModes := vals.EmptyList
for _, special := range specialModeNames {
if mode&special.bit != 0 {
specialModes = specialModes.Conj(special.name)
}
}
return vals.MakeMap(
"name", fi.Name(),
"size", vals.Int64ToNum(fi.Size()),
"type", typeName,
"perm", int(fi.Mode()&fs.ModePerm),
"special-modes", specialModes,
"perm", int(mode&fs.ModePerm),
"special-modes", specialModesToList(mode),
"sys", statSysMap(fi.Sys()))
// TODO: ModTime
}

0 comments on commit cf9ec15

Please sign in to comment.