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

287 lines, 186 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
200def _ParseClause(mask, initial_mask, clause):
201 # type: (int, int, str) -> Tuple[bool, int]
202 if len(clause) == 0:
203 # TODO: location highlighting would be nice
204 print_stderr(
205 "oils warning: symbolic mode operator cannot be empty")
206 return False, 0
207
208 parser = SymbolicClauseParser(clause)
209 wholist = parser.ParseWholist()
210 if parser.AtEnd():
211 print_stderr("oils warning: actionlist is required")
212 return False, 0
213
214 while True:
215 ok, mask = parser.ParseNextAction(wholist, mask, initial_mask)
216 if not ok:
217 return False, 0
218 elif parser.AtEnd():
219 return True, mask
220
221
222def _ParseClauseList(initial_mask, clause_list):
223 # type: (int, List[str]) -> Tuple[bool, int]
224 mask = initial_mask
225 for clause in clause_list:
226 ok, mask = _ParseClause(mask, initial_mask, clause)
227 if not ok:
228 return False, 0
229
230 return True, mask
231
232
233class Umask(vm._Builtin):
234
235 def __init__(self):
236 # type: () -> None
237 """Dummy constructor for mycpp."""
238 pass
239
240 def Run(self, cmd_val):
241 # type: (cmd_value.Argv) -> int
242 # see https://pubs.opengroup.org/onlinepubs/009696899/utilities/chmod.html for more details
243
244 attrs, arg_r = flag_util.ParseCmdVal('umask', cmd_val)
245
246 if arg_r.AtEnd(): # no args
247 # umask() has a dumb API: to get the umask, we must modify it
248 # see: https://man7.org/linux/man-pages/man2/umask.2.html
249 # NOTE: dash disables interrupts around the two umask() calls, but that
250 # shouldn't be a concern for us. Signal handlers won't call umask().
251 mask = posix.umask(0)
252 posix.umask(mask)
253 print('0%03o' % mask) # octal format
254 return 0
255
256 first_arg, first_loc = arg_r.ReadRequired2('expected an argument')
257 arg_r.Done() # only one arg
258
259 if first_arg[0].isdigit():
260 try:
261 new_mask = int(first_arg, 8)
262 except ValueError:
263 # NOTE: This also happens when we have '8' or '9' in the input.
264 print_stderr("oils warning: `%s` is not an octal number" %
265 first_arg)
266 return 1
267
268 posix.umask(new_mask)
269 return 0
270
271 # NOTE: it's possible to avoid this extra syscall in cases where we don't care about
272 # the initial value (ex: umask ...,a=rwx) although it's non-trivial to determine
273 # when, so it's probably not worth it
274 initial_mask = posix.umask(0)
275 try:
276 ok, new_mask = _ParseClauseList(initial_mask, first_arg.split(","))
277 if not ok:
278 posix.umask(initial_mask)
279 return 1
280
281 posix.umask(new_mask)
282 return 0
283
284 except Exception as e:
285 # this guard protects the umask value against any accidental exceptions
286 posix.umask(initial_mask)
287 raise