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

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