OILS / builtin / umask_osh.py View on Github | oils.pub

284 lines, 184 significant
1#!/usr/bin/env python2
2"""
3umask_osh.py - Implements the umask builtin, including parsing
4"""
5from __future__ import print_function
6
7from _devbuild.gen.runtime_asdl import cmd_value
8from core import vm
9from frontend import flag_util
10from mycpp.mylib import print_stderr
11
12import posix_ as posix
13
14from typing import List, Tuple
15
16_WHO = "ugoa"
17_OP = "+-="
18_PERM_U_PERMCOPY = "rwxXstugo"
19
20# NOTE: bitsets are a great way to store fixed width sets & add / remove
21# items easily. Thus, we use different bitsets for $wholist, $permlist,
22# $perm, and $mask.
23
24
25def _WhoCharToBitset(who_ch):
26 # type: (str) -> int
27 if who_ch == "u":
28 return 0o4
29 elif who_ch == "g":
30 return 0o2
31 elif who_ch == "o":
32 return 0o1
33 elif who_ch == "a":
34 return 0o7
35 else:
36 assert False, "unreachable"
37 return 0o0
38
39
40def _PermlistCharToBitset(permlist_ch):
41 # type: (str) -> int
42 if permlist_ch == "r":
43 return 0o400
44 elif permlist_ch == "w":
45 return 0o200
46 elif permlist_ch == "x":
47 return 0o100
48 elif permlist_ch == "X":
49 return 0o040
50 elif permlist_ch == "s":
51 return 0o020
52 elif permlist_ch == "t":
53 return 0o010
54 elif permlist_ch == "u":
55 return 0o004
56 elif permlist_ch == "g":
57 return 0o002
58 elif permlist_ch == "o":
59 return 0o001
60 else:
61 assert False, "unreachable"
62 return 0o0
63
64
65# perm = [rwx][Xst][ugo]
66def _PermlistToBits(permlist, initial_mask):
67 # type: (int, int) -> int
68 perm = 0o0
69 if (permlist & 0o400) != 0:
70 perm |= 0o4 # r
71 if (permlist & 0o200) != 0:
72 perm |= 0o2 # w
73 if (permlist & 0o100) != 0:
74 perm |= 0o1 # x
75 if (permlist & 0o040) != 0 and (initial_mask & 0o111) != 0:
76 # X == x iff one of the execute bits are set on the mask
77 perm |= 0o1 # X
78 if (permlist & 0o020) != 0:
79 # does nothing b/c umask ignores the set-on-execution bits
80 perm |= 0o0 # s
81 if (permlist & 0o010) != 0:
82 # also does nothing
83 perm |= 0o0 # t
84 if (permlist & 0o4) != 0:
85 perm |= (~initial_mask & 0o700) >> 6 # u
86 if (permlist & 0o2) != 0:
87 perm |= (~initial_mask & 0o070) >> 3 # g
88 if (permlist & 0o1) != 0:
89 perm |= (~initial_mask & 0o007) >> 0 # o
90 return perm
91
92
93def _SetMask(wholist, perm, mask):
94 # type: (int, int, int) -> int
95 if (wholist & 0o4) != 0:
96 mask |= perm << 6
97 if (wholist & 0o2) != 0:
98 mask |= perm << 3
99 if (wholist & 0o1) != 0:
100 mask |= perm << 0
101 return mask
102
103
104# can these be done with |= ?
105def _ClearMask(wholist, perm, mask):
106 # type: (int, int, int) -> int
107 if (wholist & 0o4) != 0:
108 mask &= 0o777 - (perm << 6)
109 if (wholist & 0o2) != 0:
110 mask &= 0o777 - (perm << 3)
111 if (wholist & 0o1) != 0:
112 mask &= 0o777 - (perm << 0)
113 return mask
114
115
116class SymbolicClauseParser:
117
118 def __init__(self, clause):
119 # type: (str) -> None
120 self.clause = clause
121 self.i = 0
122
123 def AtEnd(self):
124 # type: () -> bool
125 return self.i >= len(self.clause)
126
127 def Ch(self):
128 # type: () -> str
129 return self.clause[self.i]
130
131 def ParseWholist(self):
132 # type: () -> int
133 if self.Ch() not in _WHO:
134 return 0o7
135
136 wholist = 0o0
137 while not self.AtEnd():
138 if self.Ch() not in _WHO:
139 break
140
141 wholist |= _WhoCharToBitset(self.Ch())
142 self.i += 1
143
144 return wholist
145
146 # An actionlist is a sequence of actions. An action always starts with an op.
147 # returns success
148 def ParseNextAction(self, wholist, mask, initial_mask):
149 # type: (int, int, int) -> Tuple[bool, int]
150
151 op = self.Ch()
152 if op not in _OP:
153 print_stderr(
154 "oils warning: expected one of `%s` at start of action instead of `%s`"
155 % (_OP, op))
156 return False, 0
157
158 self.i += 1
159
160 if op == "=":
161 mask = _SetMask(wholist, 0o7, mask)
162
163 if self.AtEnd() or self.Ch() not in _PERM_U_PERMCOPY:
164 if op == "+" or op == "=":
165 return True, mask
166 elif op == "-":
167 return True, mask
168
169 # perm represents the bits [rwx] for a single permission
170 perm = 0o0
171
172 # permlist = [rwx][Xst][ugo]
173 permlist = 0o000
174 while not (self.AtEnd() or self.Ch() in _OP):
175 # While a list of permcopy chars mixed with permlist is not posix, both dash and mksh
176 # support it.
177 if self.Ch() not in _PERM_U_PERMCOPY:
178 print_stderr(
179 "oil warning: expected one of `%s` in permlist instead of `%s`"
180 % (_PERM_U_PERMCOPY, self.Ch()))
181 return False, 0
182
183 permlist |= _PermlistCharToBitset(self.Ch())
184 self.i += 1
185
186 perm = _PermlistToBits(permlist, initial_mask)
187
188 if op == "+" or op == "=":
189 return True, _ClearMask(wholist, perm, mask)
190 elif op == "-":
191 return True, _SetMask(wholist, perm, mask)
192
193 assert False, "unreachable"
194 return False, 0
195
196
197def _ParseClause(mask, initial_mask, clause):
198 # type: (int, int, str) -> Tuple[bool, int]
199 if len(clause) == 0:
200 # TODO: location highlighting would be nice
201 print_stderr(
202 "oils warning: symbolic mode operator cannot be empty")
203 return False, 0
204
205 parser = SymbolicClauseParser(clause)
206 wholist = parser.ParseWholist()
207 if parser.AtEnd():
208 print_stderr("oils warning: actionlist is required")
209 return False, 0
210
211 while True:
212 ok, mask = parser.ParseNextAction(wholist, mask, initial_mask)
213 if not ok:
214 return False, 0
215 elif parser.AtEnd():
216 return True, mask
217
218
219def _ParseClauseList(initial_mask, clause_list):
220 # type: (int, List[str]) -> Tuple[bool, int]
221 mask = initial_mask
222 for clause in clause_list:
223 ok, mask = _ParseClause(mask, initial_mask, clause)
224 if not ok:
225 return False, 0
226
227 return True, mask
228
229
230class Umask(vm._Builtin):
231
232 def __init__(self):
233 # type: () -> None
234 """Dummy constructor for mycpp."""
235 pass
236
237 def Run(self, cmd_val):
238 # type: (cmd_value.Argv) -> int
239 # see https://pubs.opengroup.org/onlinepubs/009696899/utilities/chmod.html for more details
240
241 attrs, arg_r = flag_util.ParseCmdVal('umask', cmd_val)
242
243 if arg_r.AtEnd(): # no args
244 # umask() has a dumb API: to get the umask, we must modify it
245 # see: https://man7.org/linux/man-pages/man2/umask.2.html
246 # NOTE: dash disables interrupts around the two umask() calls, but that
247 # shouldn't be a concern for us. Signal handlers won't call umask().
248 mask = posix.umask(0)
249 posix.umask(mask)
250 print('0%03o' % mask) # octal format
251 return 0
252
253 first_arg, first_loc = arg_r.ReadRequired2('expected an argument')
254 arg_r.Done() # only one arg
255
256 if first_arg[0].isdigit():
257 try:
258 new_mask = int(first_arg, 8)
259 except ValueError:
260 # NOTE: This also happens when we have '8' or '9' in the input.
261 print_stderr("oils warning: `%s` is not an octal number" %
262 first_arg)
263 return 1
264
265 posix.umask(new_mask)
266 return 0
267
268 # NOTE: it's possible to avoid this extra syscall in cases where we don't care about
269 # the initial value (ex: umask ...,a=rwx) although it's non-trivial to determine
270 # when, so it's probably not worth it
271 initial_mask = posix.umask(0)
272 try:
273 ok, new_mask = _ParseClauseList(initial_mask, first_arg.split(","))
274 if not ok:
275 posix.umask(initial_mask)
276 return 1
277
278 posix.umask(new_mask)
279 return 0
280
281 except Exception as e:
282 # this guard protects the umask value against any accidental exceptions
283 posix.umask(initial_mask)
284 raise