1###
2# Copyright (c) 2002-2005, Jeremiah Fincher
3# Copyright (c) 2008, James McCoy
4# All rights reserved.
5#
6# Redistribution and use in source and binary forms, with or without
7# modification, are permitted provided that the following conditions are met:
8#
9#   * Redistributions of source code must retain the above copyright notice,
10#     this list of conditions, and the following disclaimer.
11#   * Redistributions in binary form must reproduce the above copyright notice,
12#     this list of conditions, and the following disclaimer in the
13#     documentation and/or other materials provided with the distribution.
14#   * Neither the name of the author of this software nor the name of
15#     contributors to this software may be used to endorse or promote products
16#     derived from this software without specific prior written consent.
17#
18# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
21# ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
22# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
23# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
24# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
25# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
26# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
27# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
28# POSSIBILITY OF SUCH DAMAGE.
29###
30
31from __future__ import print_function
32
33import os
34import sys
35import ast
36import textwrap
37import warnings
38import functools
39import traceback
40import collections
41
42
43from . import crypt
44from .str import format
45from .file import mktemp
46from . import minisix
47from . import internationalization as _
48
49def warn_non_constant_time(f):
50    @functools.wraps(f)
51    def newf(*args, **kwargs):
52        # This method takes linear time whereas the subclass could probably
53        # do it in constant time.
54        warnings.warn('subclass of IterableMap does provide an efficient '
55                      'implementation of %s' % f.__name__,
56                      DeprecationWarning)
57        return f(*args, **kwargs)
58    return newf
59
60
61def abbrev(strings, d=None):
62    """Returns a dictionary mapping unambiguous abbreviations to full forms."""
63    def eachSubstring(s):
64        for i in range(1, len(s)+1):
65            yield s[:i]
66    if len(strings) != len(set(strings)):
67        raise ValueError(
68              'strings given to utils.abbrev have duplicates: %r' % strings)
69    if d is None:
70        d = {}
71    for s in strings:
72        for abbreviation in eachSubstring(s):
73            if abbreviation not in d:
74                d[abbreviation] = s
75            else:
76                if abbreviation not in strings:
77                    d[abbreviation] = None
78    removals = []
79    for key in d:
80        if d[key] is None:
81            removals.append(key)
82    for key in removals:
83        del d[key]
84    return d
85
86def timeElapsed(elapsed, short=False, leadingZeroes=False, years=True,
87                weeks=True, days=True, hours=True, minutes=True, seconds=True):
88    """Given <elapsed> seconds, returns a string with an English description of
89    the amount of time passed.  leadingZeroes determines whether 0 days, 0
90    hours, etc. will be printed; the others determine what larger time periods
91    should be used.
92    """
93    ret = []
94    before = False
95    def Format(s, i):
96        if i or leadingZeroes or ret:
97            if short:
98                ret.append('%s%s' % (i, s[0]))
99            else:
100                ret.append(format('%n', (i, s)))
101    elapsed = int(elapsed)
102
103    # Handle negative times
104    if elapsed < 0:
105        before = True
106        elapsed = -elapsed
107
108    assert years or weeks or days or \
109           hours or minutes or seconds, 'One flag must be True'
110    if years:
111        (yrs, elapsed) = (elapsed // 31536000, elapsed % 31536000)
112        Format(_('year'), yrs)
113    if weeks:
114        (wks, elapsed) = (elapsed // 604800, elapsed % 604800)
115        Format(_('week'), wks)
116    if days:
117        (ds, elapsed) = (elapsed // 86400, elapsed % 86400)
118        Format(_('day'), ds)
119    if hours:
120        (hrs, elapsed) = (elapsed // 3600, elapsed % 3600)
121        Format(_('hour'), hrs)
122    if minutes or seconds:
123        (mins, secs) = (elapsed // 60, elapsed % 60)
124        if leadingZeroes or mins:
125            Format(_('minute'), mins)
126        if seconds:
127            leadingZeroes = True
128            Format(_('second'), secs)
129    if not ret:
130        raise ValueError('Time difference not great enough to be noted.')
131    result = ''
132    if short:
133        result = ' '.join(ret)
134    else:
135        result = format('%L', ret)
136    if before:
137        result = _('%s ago') % result
138    return result
139
140def findBinaryInPath(s):
141    """Return full path of a binary if it's in PATH, otherwise return None."""
142    cmdLine = None
143    for dir in os.getenv('PATH').split(':'):
144        filename = os.path.join(dir, s)
145        if os.path.exists(filename):
146            cmdLine = filename
147            break
148    return cmdLine
149
150def sortBy(f, L):
151    """Uses the decorate-sort-undecorate pattern to sort L by function f."""
152    for (i, elt) in enumerate(L):
153        L[i] = (f(elt), i, elt)
154    L.sort()
155    for (i, elt) in enumerate(L):
156        L[i] = L[i][2]
157
158def saltHash(password, salt=None, hash='sha'):
159    if salt is None:
160        salt = mktemp()[:8]
161    if hash == 'sha':
162        hasher = crypt.sha
163    elif hash == 'md5':
164        hasher = crypt.md5
165    return '|'.join([salt, hasher((salt + password).encode('utf8')).hexdigest()])
166
167_astStr2 = ast.Str if minisix.PY2 else ast.Bytes
168def safeEval(s, namespace=None):
169    """Evaluates s, safely.  Useful for turning strings into tuples/lists/etc.
170    without unsafely using eval()."""
171    try:
172        node = ast.parse(s, mode='eval').body
173    except SyntaxError as e:
174        raise ValueError('Invalid string: %s.' % e)
175    def checkNode(node):
176        if node.__class__ is ast.Expr:
177            node = node.value
178        if node.__class__ in (ast.Num,
179                              ast.Str,
180                              _astStr2):
181            return True
182        elif node.__class__ in (ast.List,
183                              ast.Tuple):
184            return all([checkNode(x) for x in node.elts])
185        elif node.__class__ is ast.Dict:
186            return all([checkNode(x) for x in node.values]) and \
187                    all([checkNode(x) for x in node.values])
188        elif node.__class__ is ast.Name:
189            if namespace is None and node.id in ('True', 'False', 'None'):
190                # For Python < 3.4, which does not have NameConstant.
191                return True
192            elif namespace is not None and node.id in namespace:
193                return True
194            else:
195                return False
196        elif sys.version_info[0:2] >= (3, 4) and \
197                node.__class__ is ast.NameConstant:
198            return True
199        elif sys.version_info[0:2] >= (3, 8) and \
200                node.__class__ is ast.Constant:
201            return True
202        else:
203            return False
204    if checkNode(node):
205        if namespace is None:
206            return eval(s, namespace, namespace)
207        else:
208            # Probably equivalent to eval() because checkNode(node) is True,
209            # but it's an extra security.
210            return ast.literal_eval(node)
211    else:
212        raise ValueError(format('Unsafe string: %q', s))
213
214def exnToString(e):
215    """Turns a simple exception instance into a string (better than str(e))"""
216    strE = str(e)
217    if strE:
218        return '%s: %s' % (e.__class__.__name__, strE)
219    else:
220        return e.__class__.__name__
221
222class IterableMap(object):
223    """Define .items() in a class and subclass this to get the other iters.
224    """
225    def items(self):
226        if minisix.PY3 and hasattr(self, 'iteritems'):
227            # For old plugins
228            return self.iteritems() # avoid 2to3
229        else:
230            raise NotImplementedError()
231    __iter__ = items
232
233    def keys(self):
234        for (key, __) in self.items():
235            yield key
236
237    def values(self):
238        for (__, value) in self.items():
239            yield value
240
241
242    @warn_non_constant_time
243    def __len__(self):
244        ret = 0
245        for __ in self.items():
246            ret += 1
247        return ret
248
249    @warn_non_constant_time
250    def __bool__(self):
251        for __ in self.items():
252            return True
253        return False
254    __nonzero__ = __bool__
255
256
257class InsensitivePreservingDict(collections.MutableMapping):
258    def key(self, s):
259        """Override this if you wish."""
260        if s is not None:
261            s = s.lower()
262        return s
263
264    def __init__(self, dict=None, key=None):
265        if key is not None:
266            self.key = key
267        self.data = {}
268        if dict is not None:
269            self.update(dict)
270
271    def __repr__(self):
272        return '%s(%r)' % (self.__class__.__name__, self.data)
273
274    def fromkeys(cls, keys, s=None, dict=None, key=None):
275        d = cls(dict=dict, key=key)
276        for key in keys:
277            d[key] = s
278        return d
279    fromkeys = classmethod(fromkeys)
280
281    def __getitem__(self, k):
282        return self.data[self.key(k)][1]
283
284    def __setitem__(self, k, v):
285        self.data[self.key(k)] = (k, v)
286
287    def __delitem__(self, k):
288        del self.data[self.key(k)]
289
290    def __iter__(self):
291        return iter(self.data)
292
293    def __len__(self):
294        return len(self.data)
295
296    def items(self):
297        return self.data.values()
298
299    def items(self):
300        return self.data.values()
301
302    def keys(self):
303        L = []
304        for (k, __) in self.items():
305            L.append(k)
306        return L
307
308    def __reduce__(self):
309        return (self.__class__, (dict(self.data.values()),))
310
311
312class NormalizingSet(set):
313    def __init__(self, iterable=()):
314        iterable = list(map(self.normalize, iterable))
315        super(NormalizingSet, self).__init__(iterable)
316
317    def normalize(self, x):
318        return x
319
320    def add(self, x):
321        return super(NormalizingSet, self).add(self.normalize(x))
322
323    def remove(self, x):
324        return super(NormalizingSet, self).remove(self.normalize(x))
325
326    def discard(self, x):
327        return super(NormalizingSet, self).discard(self.normalize(x))
328
329    def __contains__(self, x):
330        return super(NormalizingSet, self).__contains__(self.normalize(x))
331    has_key = __contains__
332
333def stackTrace(frame=None, compact=True):
334    if frame is None:
335        frame = sys._getframe()
336    if compact:
337        L = []
338        while frame:
339            lineno = frame.f_lineno
340            funcname = frame.f_code.co_name
341            filename = os.path.basename(frame.f_code.co_filename)
342            L.append('[%s|%s|%s]' % (filename, funcname, lineno))
343            frame = frame.f_back
344        return textwrap.fill(' '.join(L))
345    else:
346        return traceback.format_stack(frame)
347
348def callTracer(fd=None, basename=True):
349    if fd is None:
350        fd = sys.stdout
351    def tracer(frame, event, __):
352        if event == 'call':
353            code = frame.f_code
354            lineno = frame.f_lineno
355            funcname = code.co_name
356            filename = code.co_filename
357            if basename:
358                filename = os.path.basename(filename)
359            print('%s: %s(%s)' % (filename, funcname, lineno), file=fd)
360    return tracer
361
362# vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79:
363