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

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