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

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