1# -*- coding: utf-8 -*-
2"Common utility functions"
3# SPDX-License-Identifier: BSD-2-Clause
4
5from __future__ import print_function, division
6
7
8import collections
9import os
10import re
11import shutil
12import socket
13import string
14import sys
15import time
16import ntp.ntpc
17import ntp.magic
18import ntp.control
19
20
21# Old CTL_PST defines for version 2.
22OLD_CTL_PST_CONFIG = 0x80
23OLD_CTL_PST_AUTHENABLE = 0x40
24OLD_CTL_PST_AUTHENTIC = 0x20
25OLD_CTL_PST_REACH = 0x10
26OLD_CTL_PST_SANE = 0x08
27OLD_CTL_PST_DISP = 0x04
28
29OLD_CTL_PST_SEL_REJECT = 0
30OLD_CTL_PST_SEL_SELCAND = 1
31OLD_CTL_PST_SEL_SYNCCAND = 2
32OLD_CTL_PST_SEL_SYSPEER = 3
33
34
35# Units for formatting
36UNIT_NS = "ns"       # nano second
37UNIT_US = u"µs"      # micro second
38UNIT_MS = "ms"       # milli second
39UNIT_S = "s"         # second
40UNIT_KS = "ks"       # kilo seconds
41UNITS_SEC = [UNIT_NS, UNIT_US, UNIT_MS, UNIT_S, UNIT_KS]
42UNIT_PPT = "ppt"     # parts per trillion
43UNIT_PPB = "ppb"     # parts per billion
44UNIT_PPM = "ppm"     # parts per million
45UNIT_PPK = u"‰"      # parts per thousand
46UNITS_PPX = [UNIT_PPT, UNIT_PPB, UNIT_PPM, UNIT_PPK]
47unitgroups = (UNITS_SEC, UNITS_PPX)
48
49
50# These two functions are not tested because they will muck up the module
51# for everything else, and they are simple.
52
53def check_unicode():  # pragma: no cover
54    if "UTF-8" != sys.stdout.encoding:
55        deunicode_units()
56        return True  # needed by ntpmon
57    return False
58
59
60def deunicode_units():  # pragma: no cover
61    """Under certain conditions it is not possible to force unicode output,
62    this overwrites units that contain unicode with safe versions"""
63    global UNIT_US
64    global UNIT_PPK
65    # Replacement units
66    new_us = "us"
67    new_ppk = "ppk"
68    # Replace units in unit groups
69    UNITS_SEC[UNITS_SEC.index(UNIT_US)] = new_us
70    UNITS_PPX[UNITS_PPX.index(UNIT_PPK)] = new_ppk
71    # Replace the units themselves
72    UNIT_US = new_us
73    UNIT_PPK = new_ppk
74
75
76# Variables that have units
77S_VARS = ("tai", "poll")
78MS_VARS = ("rootdelay", "rootdisp", "rootdist", "offset", "sys_jitter",
79           "clk_jitter", "leapsmearoffset", "authdelay", "koffset", "kmaxerr",
80           "kesterr", "kprecis", "kppsjitter", "fuzz", "clk_wander_threshold",
81           "tick", "in", "out", "bias", "delay", "jitter", "dispersion",
82           "fudgetime1", "fudgetime2")
83PPM_VARS = ("frequency", "clk_wander")
84
85
86def dolog(logfp, text, debug, threshold):
87    """debug is the current debug value
88    threshold is the trigger for the current log"""
89    if logfp is None:
90        return  # can turn off logging by supplying a None file descriptor
91    text = rfc3339(time.time()) + " " + text + "\n"
92    if debug >= threshold:
93        logfp.write(text)
94        logfp.flush()  # we don't want to lose an important log to a crash
95
96
97def safeargcast(arg, castfunc, errtext, usage):
98    """Attempts to typecast an argument, prints and dies on failure.
99    errtext must contain a %s for splicing in the argument, and be
100    newline terminated."""
101    try:
102        casted = castfunc(arg)
103    except ValueError:
104        sys.stderr.write(errtext % arg)
105        sys.stderr.write(usage)
106        raise SystemExit(1)
107    return casted
108
109
110def stdversion():
111    "Returns the NTPsec version string in a standard format"
112    return "ntpsec-%s" % "@NTPSEC_VERSION_EXTENDED@"
113
114
115def rfc3339(t):
116    "RFC 3339 string from Unix time, including fractional second."
117    rep = time.strftime("%Y-%m-%dT%H:%M:%S", time.gmtime(t))
118    t = str(t)
119    if "." in t:
120        subsec = t.split(".", 1)[1]
121        if int(subsec) > 0:
122            rep += "." + subsec
123    rep += "Z"
124    return rep
125
126
127def deformatNTPTime(txt):
128    txt = txt[2:]  # Strip '0x'
129    txt = "".join(txt.split("."))  # Strip '.'
130    value = ntp.util.hexstr2octets(txt)
131    return value
132
133
134def hexstr2octets(hexstr):
135    if (len(hexstr) % 2) != 0:
136        hexstr = hexstr[:-1]  # slice off the last char
137    values = []
138    for index in range(0, len(hexstr), 2):
139        values.append(chr(int(hexstr[index:index+2], 16)))
140    return "".join(values)
141
142
143def slicedata(data, slicepoint):
144    "Breaks a sequence into two pieces at the slice point"
145    return data[:slicepoint], data[slicepoint:]
146
147
148def portsplit(hostname):
149    portsuffix = ""
150    if hostname.count(":") == 1:                # IPv4 with appended port
151        (hostname, portsuffix) = hostname.split(":")
152        portsuffix = ":" + portsuffix
153    elif ']' in hostname:                       # IPv6
154        rbrak = hostname.rindex("]")
155        if ":" in hostname[rbrak:]:
156            portsep = hostname.rindex(":")
157            portsuffix = hostname[portsep:]
158            hostname = hostname[:portsep]
159            hostname = hostname[1:-1]   # Strip brackets
160    return (hostname, portsuffix)
161
162
163def parseConf(text):
164    inQuote = False
165    quoteStarter = ""
166    lines = []
167    tokens = []
168    current = []
169
170    def pushToken():
171        token = "".join(current)
172        if not inQuote:  # Attempt type conversion
173            try:
174                token = int(token)
175            except ValueError:
176                try:
177                    token = float(token)
178                except ValueError:
179                    pass
180        wrapper = (inQuote, token)
181        tokens.append(wrapper)
182        current[:] = []
183
184    def pushLine():
185        if current:
186            pushToken()
187        if tokens:
188            lines.append(tokens[:])
189            tokens[:] = []
190
191    i = 0
192    tlen = len(text)
193    while i < tlen:
194        if inQuote:
195            if text[i] == quoteStarter:  # Ending a text string
196                pushToken()
197                quoteStarter = ""
198                inQuote = False
199            elif text[i] == "\\":  # Starting an escape sequence
200                i += 1
201                if text[i] in "'\"n\\":
202                    current.append(eval("\'\\" + text[i] + "\'"))
203            else:
204                current.append(text[i])
205        else:
206            if text[i] == "#":  # Comment
207                while (i < tlen) and (text[i] != "\n"):
208                    i += 1  # Advance to end of line...
209                i -= 1  # ...and back up so we don't skip the newline
210            elif text[i] in "'\"":  # Starting a text string
211                inQuote = True
212                quoteStarter = text[i]
213                if current:
214                    pushToken()
215            elif text[i] == "\\":  # Linebreak escape
216                i += 1
217                if text[i] != "\n":
218                    raise SyntaxError
219            elif text[i] == "\n":  # EOL: break the lines
220                pushLine()
221            elif text[i] in string.whitespace:
222                if current:
223                    pushToken()
224            else:
225                current.append(text[i])
226        i += 1
227    pushLine()
228    return lines
229
230
231def stringfilt(data):
232    "Pretty print string of space separated numbers"
233    parts = data.split()
234
235    cooked = []
236    for part in parts:
237        # These are expected to fit on a single 80-char line.
238        # Accounting for other factors this leaves each number with
239        # 7 chars + a space.
240        fitted = fitinfield(part, 7)
241        cooked.append(fitted)
242    rendered = " ".join(cooked)
243    return rendered
244
245
246def stringfiltcooker(data):
247    "Cooks a filt* string of space separated numbers, expects milliseconds"
248    parts = data.split()
249    oomcount = {}
250    minscale = -100000  # Keep track of the maxdownscale for each value
251    # Find out what the 'natural' unit of each value is
252    for part in parts:
253        # Only care about OOMs, the real scaling happens later
254        value, oom = scalestring(part)
255        # Track the highest maxdownscale so we do not invent precision
256        ds = maxdownscale(part)
257        minscale = max(ds, minscale)
258        oomcount[oom] = oomcount.get(oom, 0) + 1
259    # Find the most common unit
260    mostcommon = 0
261    highestcount = 0
262    for key in oomcount.keys():
263        if key < minscale:
264            continue  # skip any scale that would result in making up data
265        count = oomcount[key]
266        if count > highestcount:
267            mostcommon = key
268            highestcount = count
269    # Shift all values to the new unit
270    cooked = []
271    for part in parts:
272        part = rescalestring(part, mostcommon)
273        fitted = fitinfield(part, 7)
274        cooked.append(fitted)
275    rendered = " ".join(cooked) + " " + UNITS_SEC[mostcommon +
276                                                  UNITS_SEC.index(UNIT_MS)]
277    return rendered
278
279
280def getunitgroup(unit):
281    "Returns the unit group which contains a given unit"
282    for group in unitgroups:
283        if unit in group:
284            return group
285
286
287def oomsbetweenunits(a, b):
288    "Calculates how many orders of magnitude separate two units"
289    group = getunitgroup(a)
290    if b is None:  # Caller is asking for the distance from the base unit
291        return group.index(a) * 3
292    elif b in group:
293        ia = group.index(a)
294        ib = group.index(b)
295        return abs((ia - ib) * 3)
296    return None
297
298
299def breaknumberstring(value):
300    "Breaks a number string into (aboveDecimal, belowDecimal, isNegative?)"
301    if value[0] == "-":
302        value = value[1:]
303        negative = True
304    else:
305        negative = False
306    if "." in value:
307        above, below = value.split(".")
308    else:
309        above = value
310        below = ""
311    return (above, below, negative)
312
313
314def gluenumberstring(above, below, isnegative):
315    "Glues together parts of a number string"
316    if above == "":
317        above = "0"
318    if below:
319        newvalue = ".".join((above, below))
320    else:
321        newvalue = above
322    if isnegative:
323        newvalue = "-" + newvalue
324    return newvalue
325
326
327def maxdownscale(value):
328    "Maximum units a value can be scaled down without inventing data"
329    if "." in value:
330        digitcount = len(value.split(".")[1])
331        # Return a negative so it can be fed directly to a scaling function
332        return -(digitcount // 3)
333    else:
334        # No decimals, the value is already at the maximum down-scale
335        return 0
336
337
338def rescalestring(value, unitsscaled):
339    "Rescale a number string by a given number of units"
340    whole, dec, negative = breaknumberstring(value)
341    if unitsscaled == 0:
342        # This may seem redundant, but glue forces certain formatting details
343        value = gluenumberstring(whole, dec, negative)
344        return value
345    hilen = len(whole)
346    lolen = len(dec)
347    digitsmoved = abs(unitsscaled * 3)
348    if unitsscaled > 0:  # Scale to a larger unit, move decimal left
349        if hilen < digitsmoved:
350            # Scaling beyond the digits, pad it out. We can pad here
351            # without making up digits that don't exist
352            padcount = digitsmoved - hilen
353            newwhole = ""
354            newdec = ("0" * padcount) + whole + dec
355        else:  # Scaling in the digits, no need to pad
356            choppoint = -digitsmoved
357            newdec = whole[choppoint:] + dec
358            newwhole = whole[:choppoint]
359    elif unitsscaled < 0:  # scale to a smaller unit, move decimal right
360        if lolen < digitsmoved:
361            # Scaling beyond the digits would force us to make up data
362            # that doesn't exist. So fail.
363            # The caller should have already caught this with maxdownscale()
364            return None
365        else:
366            newwhole = whole + dec[:digitsmoved]
367            newdec = dec[digitsmoved:]
368    newwhole = newwhole.lstrip("0")
369    newvalue = gluenumberstring(newwhole, newdec, negative)
370    return newvalue
371
372
373def formatzero(value):
374    "Scale a zero value for the unit with the highest available precision"
375    scale = maxdownscale(value)
376    newvalue = rescalestring(value, scale).lstrip("-")
377    return (newvalue, scale)
378
379
380def scalestring(value):
381    "Scales a number string to fit in the range 1.0-999.9"
382    if isstringzero(value):
383        return formatzero(value)
384    whole, dec, negative = breaknumberstring(value)
385    hilen = len(whole)
386    if (hilen == 0) or isstringzero(whole):  # Need to shift to smaller units
387        i = 0
388        lolen = len(dec)
389        while i < lolen:  # need to find the actual digits
390            if dec[i] != "0":
391                break
392            i += 1
393        lounits = (i // 3) + 1  # always need to shift one more unit
394        movechars = lounits * 3
395        if lolen < movechars:
396            # Not enough digits to scale all the way down. Inventing
397            # digits is unacceptable, so scale down as much as we can.
398            lounits = (i // 3)  # "always", unless out of digits
399            movechars = lounits * 3
400        newwhole = dec[:movechars].lstrip("0")
401        newdec = dec[movechars:]
402        unitsmoved = -lounits
403    else:  # Shift to larger units
404        hiunits = hilen // 3  # How many we have, not how many to move
405        hidigits = hilen % 3
406        if hidigits == 0:  # full unit above the decimal
407            hiunits -= 1  # the unit above the decimal doesn't count
408            hidigits = 3
409        newwhole = whole[:hidigits]
410        newdec = whole[hidigits:] + dec
411        unitsmoved = hiunits
412    newvalue = gluenumberstring(newwhole, newdec, negative)
413    return (newvalue, unitsmoved)
414
415
416def fitinfield(value, fieldsize):
417    "Attempt to fit value into a field, preserving as much data as possible"
418    vallen = len(value)
419    if fieldsize is None:
420        newvalue = value
421    elif vallen == fieldsize:  # Goldilocks!
422        newvalue = value
423    elif vallen < fieldsize:  # Extra room, pad it out
424        pad = " " * (fieldsize - vallen)
425        newvalue = pad + value
426    else:  # Insufficient room, round as few digits as possible
427        if "." in value:  # Ok, we *do* have decimals to crop
428            diff = vallen - fieldsize
429            declen = len(value.split(".")[1])  # length of decimals
430            croplen = min(declen, diff)  # Never round above the decimal point
431            roundlen = declen - croplen  # How many digits we round to
432            newvalue = str(round(float(value), roundlen))
433            splitted = newvalue.split(".")  # This should never fail
434            declen = len(splitted[1])
435            if roundlen == 0:  # if rounding all the decimals don't display .0
436                # but do display the point, to show that there is more beyond
437                newvalue = splitted[0] + "."
438            elif roundlen > declen:  # some zeros have been cropped, fix that
439                padcount = roundlen - declen
440                newvalue = newvalue + ("0" * padcount)
441        else:  # No decimals, nothing we can crop
442            newvalue = value
443    return newvalue
444
445
446def cropprecision(value, ooms):
447    "Crops digits below the maximum precision"
448    if "." not in value:  # No decimals, nothing to crop
449        return value
450    if ooms == 0:  # We are at the baseunit, crop it all
451        return value.split(".")[0]
452    dstart = value.find(".") + 1
453    dsize = len(value) - dstart
454    precision = min(ooms, dsize)
455    cropcount = dsize - precision
456    if cropcount > 0:
457        value = value[:-cropcount]
458    return value
459
460
461def isstringzero(value):
462    "Detects whether a string is equal to zero"
463    for i in value:
464        if i not in ("-", ".", "0"):
465            return False
466    return True
467
468
469def unitrelativeto(unit, move):
470    "Returns a unit at a different scale from the input unit"
471    for group in unitgroups:
472        if unit in group:
473            if move is None:  # asking for the base unit
474                return group[0]
475            else:
476                index = group.index(unit)
477                index += move  # index of the new unit
478                if 0 <= index < len(group):  # found the new unit
479                    return group[index]
480                else:  # not in range
481                    return None
482    return None  # couldn't find anything
483
484
485def unitifyvar(value, varname, baseunit=None, width=8, unitSpace=False):
486    "Call unitify() with the correct units for varname"
487    if varname in S_VARS:
488        start = UNIT_S
489    elif varname in MS_VARS:
490        start = UNIT_MS
491    elif varname in PPM_VARS:
492        start = UNIT_PPM
493    else:
494        return value
495    return unitify(value, start, baseunit, width, unitSpace)
496
497
498def unitify(value, startingunit, baseunit=None, width=8, unitSpace=False):
499    "Formats a numberstring with relevant units. Attempts to fit in width."
500    if baseunit is None:
501        baseunit = getunitgroup(startingunit)[0]
502    ooms = oomsbetweenunits(startingunit, baseunit)
503    if isstringzero(value):
504        newvalue, unitsmoved = formatzero(value)
505    else:
506        newvalue = cropprecision(value, ooms)
507        newvalue, unitsmoved = scalestring(newvalue)
508    unitget = unitrelativeto(startingunit, unitsmoved)
509    if unitSpace:
510        spaceWidthAdjustment = 1
511        spacer = " "
512    else:
513        spaceWidthAdjustment = 0
514        spacer = ""
515    if unitget is not None:  # We have a unit
516        if width is None:
517            realwidth = None
518        else:
519            realwidth = width - (len(unitget) + spaceWidthAdjustment)
520        newvalue = fitinfield(newvalue, realwidth) + spacer + unitget
521    else:  # don't have a replacement unit, use original
522        newvalue = value + spacer + startingunit
523    if width is None:
524        newvalue = newvalue.strip()
525    return newvalue
526
527
528def f8dot4(f):
529    "Scaled floating point formatting to fit in 8 characters"
530
531    if isinstance(f, str):
532        # a string? pass it on as a signal
533        return "%8s" % f
534    if not isinstance(f, (int, float)):
535        # huh?
536        return "       X"
537    if str(float(f)).lower() == 'nan':
538        # yes, this is a better test than math.isnan()
539        # it also catches None, strings, etc.
540        return "     nan"
541
542    fmt = "%8d"          # xxxxxxxx or -xxxxxxx
543    if f >= 0:
544        if f < 1000.0:
545            fmt = "%8.4f"    # xxx.xxxx  normal case
546        elif f < 10000.0:
547            fmt = "%8.3f"    # xxxx.xxx
548        elif f < 100000.0:
549            fmt = "%8.2f"    # xxxxx.xx
550        elif f < 1000000.0:
551            fmt = "%8.1f"    # xxxxxx.x
552    else:
553        # negative number, account for minus sign
554        if f > -100.0:
555            fmt = "%8.4f"      # -xx.xxxx  normal case
556        elif f > -1000.0:
557            fmt = "%8.3f"      # -xxx.xxx
558        elif f > -10000.0:
559            fmt = "%8.2f"      # -xxxx.xx
560        elif f > -100000.0:
561            fmt = "%8.1f"      # -xxxxx.x
562
563    return fmt % f
564
565
566def f8dot3(f):
567    "Scaled floating point formatting to fit in 8 characters"
568    if isinstance(f, str):
569        # a string? pass it on as a signal
570        return "%8s" % f
571    if not isinstance(f, (int, float)):
572        # huh?
573        return "       X"
574    if str(float(f)).lower() == 'nan':
575        # yes, this is a better test than math.isnan()
576        # it also catches None, strings, etc.
577        return "     nan"
578
579    fmt = "%8d"          # xxxxxxxx or -xxxxxxx
580    if f >= 0:
581        if f < 10000.0:
582            fmt = "%8.3f"    # xxxx.xxx  normal case
583        elif f < 100000.0:
584            fmt = "%8.2f"    # xxxxx.xx
585        elif f < 1000000.0:
586            fmt = "%8.1f"    # xxxxxx.x
587    else:
588        # negative number, account for minus sign
589        if f > -1000.0:
590            fmt = "%8.3f"    # -xxx.xxx  normal case
591        elif f > -10000.0:
592            fmt = "%8.2f"    # -xxxx.xx
593        elif f > -100000.0:
594            fmt = "%8.1f"    # -xxxxx.x
595
596    return fmt % f
597
598
599def monoclock():
600    "Try to get a monotonic clock value unaffected by NTP stepping."
601    try:
602        # Available in Python 3.3 and up.
603        return time.monotonic()
604    except AttributeError:
605        return time.time()
606
607
608class Cache:
609    "Simple time-based cache"
610
611    def __init__(self, defaultTimeout=300):  # 5 min default TTL
612        self.defaultTimeout = defaultTimeout
613        self._cache = {}
614
615    def get(self, key):
616        if key in self._cache:
617            value, settime, ttl = self._cache[key]
618            if settime >= monoclock() - ttl:
619                return value
620            else:  # key expired, delete it
621                del self._cache[key]
622                return None
623        else:
624            return None
625
626    def set(self, key, value, customTTL=None):
627        ttl = customTTL if customTTL is not None else self.defaultTimeout
628        self._cache[key] = (value, monoclock(), ttl)
629
630
631# A hack to avoid repeatedly hammering on DNS when ntpmon runs.
632canonicalization_cache = Cache()
633
634
635def canonicalize_dns(inhost, family=socket.AF_UNSPEC):
636    "Canonicalize a hostname or numeric IP address."
637    resname = canonicalization_cache.get(inhost)
638    if resname is not None:
639        return resname
640    # Catch garbaged hostnames in corrupted Mode 6 responses
641    m = re.match("([:.[\]]|\w)*", inhost)
642    if not m:
643        raise TypeError
644    (hostname, portsuffix) = portsplit(inhost)
645    try:
646        ai = socket.getaddrinfo(hostname, None, family, 0, 0,
647                                socket.AI_CANONNAME)
648    except socket.gaierror:
649        return "DNSFAIL:%s" % hostname
650    (family, socktype, proto, canonname, sockaddr) = ai[0]
651    try:
652        name = socket.getnameinfo(sockaddr, socket.NI_NAMEREQD)
653        result = name[0].lower() + portsuffix
654    except socket.gaierror:
655        # On OS X, canonname is empty for hosts without rDNS.
656        # Fall back to the hostname.
657        canonicalized = canonname or hostname
658        result = canonicalized.lower() + portsuffix
659    canonicalization_cache.set(inhost, result)
660    return result
661
662
663TermSize = collections.namedtuple("TermSize", ["width", "height"])
664
665
666# Python 2.x does not have the shutil.get_terminal_size function.
667# This conditional import is only needed by termsize() and should be kept
668# near it. It is not inside the function because the unit tests need to be
669# able to splice in a jig.
670if str is bytes:  # We are on python 2.x
671    import fcntl
672    import termios
673    import struct
674
675
676def termsize():  # pragma: no cover
677    "Return the current terminal size."
678    # Alternatives at http://stackoverflow.com/questions/566746
679    # The way this is used makes it not a big deal if the default is wrong.
680    size = (80, 24)
681    if os.isatty(1):
682        if str is not bytes:
683            # str is bytes means we are >py3.0, but this will still fail
684            # on versions <3.3. We do not support those anyway.
685            (w, h) = shutil.get_terminal_size((80, 24))
686            size = (w, h)
687        else:
688            try:
689                # OK, Python version < 3.3, cope
690                h, w, hp, wp = struct.unpack(
691                    'HHHH',
692                    fcntl.ioctl(2, termios.TIOCGWINSZ,
693                                struct.pack('HHHH', 0, 0, 0, 0)))
694                size = (w, h)
695            except IOError:
696                pass
697    return TermSize(*size)
698
699
700class PeerStatusWord:
701    "A peer status word from readstats(), dissected for display"
702
703    def __init__(self, status, pktversion=ntp.magic.NTP_VERSION):
704        # Event
705        self.event = ntp.control.CTL_PEER_EVENT(status)
706        # Event count
707        self.event_count = ntp.control.CTL_PEER_NEVNT(status)
708        statval = ntp.control.CTL_PEER_STATVAL(status)
709        # Config
710        if statval & ntp.control.CTL_PST_CONFIG:
711            self.conf = "yes"
712        else:
713            self.conf = "no"
714        # Reach
715        if statval & ntp.control.CTL_PST_BCAST:
716            self.reach = "none"
717        elif statval & ntp.control.CTL_PST_REACH:
718            self.reach = "yes"
719        else:
720            self.reach = "no"
721        # Auth
722        if (statval & ntp.control.CTL_PST_AUTHENABLE) == 0:
723            self.auth = "none"
724        elif statval & ntp.control.CTL_PST_AUTHENTIC:
725            self.auth = "ok "
726        else:
727            self.auth = "bad"
728        # Condition
729        if pktversion > ntp.magic.NTP_OLDVERSION:
730            seldict = {
731                ntp.control.CTL_PST_SEL_REJECT: "reject",
732                ntp.control.CTL_PST_SEL_SANE: "falsetick",
733                ntp.control.CTL_PST_SEL_CORRECT: "excess",
734                ntp.control.CTL_PST_SEL_SELCAND: "outlier",
735                ntp.control.CTL_PST_SEL_SYNCCAND: "candidate",
736                ntp.control.CTL_PST_SEL_EXCESS: "backup",
737                ntp.control.CTL_PST_SEL_SYSPEER: "sys.peer",
738                ntp.control.CTL_PST_SEL_PPS: "pps.peer",
739                }
740            self.condition = seldict[statval & 0x7]
741        else:
742            if (statval & 0x3) == OLD_CTL_PST_SEL_REJECT:
743                if (statval & OLD_CTL_PST_SANE) == 0:
744                    self.condition = "insane"
745                elif (statval & OLD_CTL_PST_DISP) == 0:
746                    self.condition = "hi_disp"
747                else:
748                    self.condition = ""
749            elif (statval & 0x3) == OLD_CTL_PST_SEL_SELCAND:
750                self.condition = "sel_cand"
751            elif (statval & 0x3) == OLD_CTL_PST_SEL_SYNCCAND:
752                self.condition = "sync_cand"
753            elif (statval & 0x3) == OLD_CTL_PST_SEL_SYSPEER:
754                self.condition = "sys_peer"
755        # Last Event
756        event_dict = {
757            ntp.magic.PEVNT_MOBIL: "mobilize",
758            ntp.magic.PEVNT_DEMOBIL: "demobilize",
759            ntp.magic.PEVNT_UNREACH: "unreachable",
760            ntp.magic.PEVNT_REACH: "reachable",
761            ntp.magic.PEVNT_RESTART: "restart",
762            ntp.magic.PEVNT_REPLY: "no_reply",
763            ntp.magic.PEVNT_RATE: "rate_exceeded",
764            ntp.magic.PEVNT_DENY: "access_denied",
765            ntp.magic.PEVNT_ARMED: "leap_armed",
766            ntp.magic.PEVNT_NEWPEER: "sys_peer",
767            ntp.magic.PEVNT_CLOCK: "clock_alarm",
768            }
769        self.last_event = event_dict.get(ntp.magic.PEER_EVENT | self.event, "")
770
771    def __str__(self):
772        return ("conf=%(conf)s, reach=%(reach)s, auth=%(auth)s, "
773                "cond=%(condition)s, event=%(last_event)s ec=%(event_count)s"
774                % self.__dict__)
775
776
777def cook(variables, showunits=False, sep=", "):
778    "Cooked-mode variable display."
779    width = ntp.util.termsize().width - 2
780    text = ""
781    specials = ("filtdelay", "filtoffset", "filtdisp", "filterror")
782    longestspecial = len(max(specials, key=len))
783    for (name, (value, rawvalue)) in variables.items():
784        if name in specials:  # need special formatting for column alignment
785            formatter = "%" + str(longestspecial) + "s ="
786            item = formatter % name
787        else:
788            item = "%s=" % name
789        if name in ("reftime", "clock", "org", "rec", "xmt"):
790            item += ntp.ntpc.prettydate(value)
791        elif name in ("srcadr", "peeradr", "dstadr", "refid"):
792            # C ntpq cooked these in obscure ways.  Since they
793            # came up from the daemon as human-readable
794            # strings this was probably a bad idea, but we'll
795            # leave this case separated in case somebody thinks
796            # re-cooking them is a good idea.
797            item += value
798        elif name == "leap":
799            item += ("00", "01", "10", "11")[value]
800        elif name == "reach":
801            item += "%03lo" % value
802        elif name in specials:
803            if showunits:
804                item += stringfiltcooker(value)
805            else:
806                item += "\t".join(value.split())
807        elif name == "flash":
808            item += "%02x " % value
809            if value == 0:
810                item += "ok "
811            else:
812                # flasher bits
813                tstflagnames = (
814                    "pkt_dup",          # BOGON1
815                    "pkt_bogus",        # BOGON2
816                    "pkt_unsync",       # BOGON3
817                    "pkt_denied",       # BOGON4
818                    "pkt_auth",         # BOGON5
819                    "pkt_stratum",      # BOGON6
820                    "pkt_header",       # BOGON7
821                    "pkt_autokey",      # BOGON8
822                    "pkt_crypto",       # BOGON9
823                    "peer_stratum",     # BOGON10
824                    "peer_dist",        # BOGON11
825                    "peer_loop",        # BOGON12
826                    "peer_unreach"      # BOGON13
827                )
828                for (i, n) in enumerate(tstflagnames):
829                    if (1 << i) & value:
830                        item += tstflagnames[i] + " "
831            item = item[:-1]
832        elif name in MS_VARS:
833            #  Note that this is *not* complete, there are definitely
834            #   missing variables here.
835            #  Completion cannot occur until all units are tracked down.
836            if showunits:
837                item += unitify(rawvalue, UNIT_MS, UNIT_NS, width=None)
838            else:
839                item += repr(value)
840        elif name in S_VARS:
841            if showunits:
842                item += unitify(rawvalue, UNIT_S, UNIT_NS, width=None)
843            else:
844                item += repr(value)
845        elif name in PPM_VARS:
846            if showunits:
847                item += unitify(rawvalue, UNIT_PPM, width=None)
848            else:
849                item += repr(value)
850        else:
851            item += repr(value)
852        # add field separator
853        item += sep
854        # add newline so we don't overflow screen
855        lastcount = 0
856        for c in text:
857            if c == '\n':
858                lastcount = 0
859            else:
860                lastcount += 1
861        if lastcount + len(item) > width:
862            text = text[:-1] + "\n"
863        text += item
864    text = text[:-2] + "\n"
865    return text
866
867
868class PeerSummary:
869    "Reusable report generator for peer statistics"
870
871    def __init__(self, displaymode, pktversion, showhostnames,
872                 wideremote, showunits=False, termwidth=None,
873                 debug=0, logfp=sys.stderr):
874        self.displaymode = displaymode          # peers/apeers/opeers
875        self.pktversion = pktversion            # interpretation of flash bits
876        self.showhostnames = showhostnames      # If false, display numeric IPs
877        self.showunits = showunits              # If False show old style float
878        self.wideremote = wideremote            # show wide remote names?
879        self.debug = debug
880        self.logfp = logfp
881        self.termwidth = termwidth
882        # By default, the peer spreadsheet layout is designed so lines just
883        # fit in 80 characters. This tells us how much extra horizontal space
884        # we have available on a wider terminal emulator.
885        self.horizontal_slack = min((termwidth or 80) - 80, 24)
886        # Peer spreadsheet column widths. The reason we cap extra
887        # width used at 24 is that on very wide displays, slamming the
888        # non-hostname fields all the way to the right produces a huge
889        # river that makes the entries difficult to read as wholes.
890        # This choice caps the peername field width at that of the longest
891        # possible IPV6 numeric address.
892        self.namewidth = 15 + self.horizontal_slack
893        self.refidwidth = 15
894        # Compute peer spreadsheet headers
895        self.__remote = "     remote    ".ljust(self.namewidth)
896        self.__common = "st t when poll reach   delay   offset   "
897        self.__header = None
898        self.polls = []
899
900    @staticmethod
901    def prettyinterval(diff):
902        "Print an interval in natural time units."
903        if not isinstance(diff, int) or diff <= 0:
904            return '-'
905        if diff <= 2048:
906            return str(diff)
907        diff = (diff + 29) / 60
908        if diff <= 300:
909            return "%dm" % diff
910        diff = (diff + 29) / 60
911        if diff <= 96:
912            return "%dh" % diff
913        diff = (diff + 11) / 24
914        return "%dd" % diff
915
916    @staticmethod
917    def high_truncate(hostname, maxlen):
918        "Truncate on the left using leading _ to indicate 'more'."
919        # Used for local IPv6 addresses, best distinguished by low bits
920        if len(hostname) <= maxlen:
921            return hostname
922        else:
923            return '-' + hostname[-maxlen+1:]
924
925    @staticmethod
926    def is_clock(variables):
927        "Does a set of variables look like it returned from a clock?"
928        return "srchost" in variables and '(' in variables["srchost"][0]
929
930    def header(self):
931        "Column headers for peer display"
932        if self.displaymode == "apeers":
933            self.__header = self.__remote + \
934                "   refid   assid  ".ljust(self.refidwidth) + \
935                self.__common + "jitter"
936        elif self.displaymode == "opeers":
937            self.__header = self.__remote + \
938                "       local      ".ljust(self.refidwidth) + \
939                self.__common + "  disp"
940        elif self.displaymode == 'rpeers':
941            self.__header = ' st t when poll reach   delay   ' + \
942                            'offset   jitter refid           T remote'
943        else:
944            self.__header = self.__remote + \
945                "       refid      ".ljust(self.refidwidth) + \
946                self.__common + "jitter"
947        return self.__header
948
949    def width(self):
950        "Width of display"
951        return 79 + self.horizontal_slack
952
953    def summary(self, rstatus, variables, associd):
954        "Peer status summary line."
955        clock_name = ''
956        dstadr_refid = ""
957        dstport = 0
958        estdelay = '.'
959        estdisp = '.'
960        estjitter = '.'
961        estoffset = '.'
962        filtdelay = 0.0
963        filtdisp = 0.0
964        filtoffset = 0.0
965        flash = 0
966        have_jitter = False
967        headway = 0
968        hmode = 0
969        hpoll = 0
970        keyid = 0
971        last_sync = None
972        leap = 0
973        pmode = 0
974        ppoll = 0
975        precision = 0
976        ptype = '?'
977        reach = 0
978        rec = None
979        reftime = None
980        rootdelay = 0.0
981        saw6 = False        # x.6 floats for delay and friends
982        srcadr = None
983        srchost = None
984        srcport = 0
985        stratum = 20
986        mode = 0
987        unreach = 0
988        xmt = 0
989        ntscookies = -1
990
991        now = time.time()
992
993        for item in variables.items():
994            if 2 != len(item) or 2 != len(item[1]):
995                # bad item
996                continue
997            (name, (value, rawvalue)) = item
998            if name == "delay":
999                estdelay = rawvalue if self.showunits else value
1000                if len(rawvalue) > 6 and rawvalue[-7] == ".":
1001                    saw6 = True
1002            elif name == "dstadr":
1003                # The C code tried to get a fallback ptype from this in case
1004                # the hmode field was not included
1005                if "local" in self.__header:
1006                    dstadr_refid = rawvalue
1007            elif name == "dstport":
1008                # FIXME, dstport never used.
1009                dstport = value
1010            elif name == "filtdelay":
1011                # FIXME, filtdelay never used.
1012                filtdelay = value
1013            elif name == "filtdisp":
1014                # FIXME, filtdisp never used.
1015                filtdisp = value
1016            elif name == "filtoffset":
1017                # FIXME, filtoffset never used.
1018                filtoffset = value
1019            elif name == "flash":
1020                # FIXME, flash never used.
1021                flash = value
1022            elif name == "headway":
1023                # FIXME, headway never used.
1024                headway = value
1025            elif name == "hmode":
1026                hmode = value
1027            elif name == "hpoll":
1028                hpoll = value
1029                if hpoll < 0:
1030                    hpoll = ntp.magic.NTP_MINPOLL
1031            elif name == "jitter":
1032                if "jitter" in self.__header:
1033                    estjitter = rawvalue if self.showunits else value
1034                    have_jitter = True
1035            elif name == "keyid":
1036                # FIXME, keyid never used.
1037                keyid = value
1038            elif name == "leap":
1039                # FIXME, leap never used.
1040                leap = value
1041            elif name == "offset":
1042                estoffset = rawvalue if self.showunits else value
1043            elif name == "pmode":
1044                # FIXME, pmode never used.
1045                pmode = value
1046            elif name == "ppoll":
1047                ppoll = value
1048                if ppoll < 0:
1049                    ppoll = ntp.magic.NTP_MINPOLL
1050            elif name == "precision":
1051                # FIXME, precision never used.
1052                precision = value
1053            elif name == "reach":
1054                # Shipped as hex, displayed in octal
1055                reach = value
1056            elif name == "refid":
1057                # The C code for this looked crazily overelaborate.  Best
1058                # guess is that it was designed to deal with formats that
1059                # no longer occur in this field.
1060                if "refid" in self.__header:
1061                    dstadr_refid = rawvalue
1062            elif name == "rec":
1063                rec = value         # l_fp timestamp
1064                last_sync = int(now - ntp.ntpc.lfptofloat(rec))
1065            elif name == "reftime":
1066                reftime = value     # l_fp timestamp
1067                last_sync = int(now - ntp.ntpc.lfptofloat(reftime))
1068            elif name == "rootdelay":
1069                # FIXME, rootdelay never used.
1070                rootdelay = value   # l_fp timestamp
1071            elif name == "rootdisp" or name == "dispersion":
1072                estdisp = rawvalue if self.showunits else value
1073            elif name in ("srcadr", "peeradr"):
1074                srcadr = value
1075            elif name == "srchost":
1076                srchost = value
1077            elif name == "srcport" or name == "peerport":
1078                # FIXME, srcport never used.
1079                srcport = value
1080            elif name == "stratum":
1081                stratum = value
1082            elif name == "mode":
1083                # FIXME, mode never used.
1084                mode = value
1085            elif name == "unreach":
1086                # FIXME, unreach never used.
1087                unreach = value
1088            elif name == "xmt":
1089                # FIXME, xmt never used.
1090                xmt = value
1091            elif name == "ntscookies":
1092                ntscookies = value
1093            else:
1094                # unknown name?
1095                # line = " name=%s " % (name)    # debug
1096                # return line                    # debug
1097                continue
1098        if hmode == ntp.magic.MODE_BCLIENTX:
1099            # broadcastclient or multicastclient
1100            ptype = 'b'
1101        elif hmode == ntp.magic.MODE_BROADCASTx:
1102            # broadcast or multicast server
1103            if srcadr.startswith("224."):       # IANA multicast address prefix
1104                ptype = 'M'
1105            else:
1106                ptype = 'B'
1107        elif hmode == ntp.magic.MODE_CLIENT:
1108            if PeerSummary.is_clock(variables):
1109                ptype = 'l'     # local refclock
1110            elif dstadr_refid == "POOL":
1111                ptype = 'p'     # pool
1112            elif srcadr.startswith("224."):
1113                ptype = 'a'     # manycastclient (compatibility with Classic)
1114            elif ntscookies > -1:
1115                # FIXME: Will foo up if there are ever more than 9 cookies
1116                ptype = chr(ntscookies + ord('0'))
1117            else:
1118                ptype = 'u'     # unicast
1119        elif hmode == ntp.magic.MODE_ACTIVEx:
1120            ptype = 's'         # symmetric active
1121        elif hmode == ntp.magic.MODE_PASSIVEx:
1122            ptype = 'S'         # symmetric passive
1123
1124        #
1125        # Got everything, format the line
1126        #
1127        line = ""
1128        poll_sec = 1 << min(ppoll, hpoll)
1129        self.polls.append(poll_sec)
1130        if self.pktversion > ntp.magic.NTP_OLDVERSION:
1131            c = " x.-+#*o"[ntp.control.CTL_PEER_STATVAL(rstatus) & 0x7]
1132        else:
1133            c = " .+*"[ntp.control.CTL_PEER_STATVAL(rstatus) & 0x3]
1134        # Source host or clockname or poolname or servername
1135        # After new DNS, 2017-Apr-17
1136        # servers setup via numerical IP Address have only srcadr
1137        # servers setup via DNS have both srcadr and srchost
1138        # refclocks have both srcadr and srchost
1139        # pool has "0.0.0.0" (or "::") and srchost
1140        # slots setup via pool have only srcadr
1141        if srcadr is not None \
1142                and srcadr != "0.0.0.0" \
1143                and not srcadr.startswith("127.127") \
1144                and srcadr != "::":
1145            if self.showhostnames & 2 and 'srchost' in locals() and srchost:
1146                clock_name = srchost
1147            elif self.showhostnames & 1:
1148                try:
1149                    if self.debug:
1150                        self.logfp.write("DNS lookup begins...\n")
1151                    clock_name = canonicalize_dns(srcadr)
1152                    if self.debug:
1153                        self.logfp.write("DNS lookup ends.\n")
1154                except TypeError:  # pragma: no cover
1155                    return ''
1156            else:
1157                clock_name = srcadr
1158        else:
1159            clock_name = srchost
1160        if clock_name is None:
1161            if srcadr:
1162                clock_name = srcadr
1163            else:
1164                clock_name = ""
1165        if self.displaymode != "rpeers":
1166            if self.wideremote and len(clock_name) > self.namewidth:
1167                line += ("%c%s\n" % (c, clock_name))
1168                line += (" " * (self.namewidth + 2))
1169            else:
1170                line += ("%c%-*.*s " % (c, self.namewidth, self.namewidth,
1171                                        clock_name[:self.namewidth]))
1172        # Destination address, assoc ID or refid.
1173        assocwidth = 7 if self.displaymode == "apeers" else 0
1174        if "." not in dstadr_refid and ":" not in dstadr_refid:
1175            dstadr_refid = "." + dstadr_refid + "."
1176        if assocwidth and len(dstadr_refid) >= self.refidwidth - assocwidth:
1177            visible = "..."
1178        else:
1179            visible = dstadr_refid
1180        if self.displaymode != "rpeers":
1181            line += self.high_truncate(visible, self.refidwidth)
1182            if self.displaymode == "apeers":
1183                line += (" " * (self.refidwidth - len(visible) - assocwidth + 1))
1184                line += ("%-6d" % (associd))
1185            else:
1186                line += (" " * (self.refidwidth - len(visible)))
1187        # The rest of the story
1188        if last_sync is None:
1189            last_sync = now
1190        jd = estjitter if have_jitter else estdisp
1191        if self.showunits:
1192            fini = lambda x : unitify(x, UNIT_MS)
1193        elif saw6:
1194            fini = lambda x : f8dot4(x)
1195        else:
1196            fini = lambda x : f8dot3(x)
1197        line += (
1198            " %2ld %c %4.4s %4.4s  %3lo %s %s %s"
1199            % (stratum, ptype,
1200               PeerSummary.prettyinterval(last_sync),
1201               PeerSummary.prettyinterval(poll_sec), reach,
1202               fini(estdelay), fini(estoffset), fini(jd)))
1203        line += "\n"
1204        # for debugging both case
1205        # if srcadr != None and srchost != None:
1206        #   line += "srcadr: %s, srchost: %s\n" % (srcadr, srchost)
1207        return line
1208
1209    def intervals(self):
1210        "Return and flush the list of actual poll intervals."
1211        res = self.polls[:]
1212        self.polls = []
1213        return res
1214
1215
1216class MRUSummary:
1217    "Reusable class for MRU entry summary generation."
1218
1219    def __init__(self, showhostnames, wideremote=False,
1220                 debug=0, logfp=sys.stderr):
1221        self.debug = debug
1222        self.logfp = logfp
1223        self.now = None
1224        self.showhostnames = showhostnames  # if & 1, display names
1225        self.wideremote = wideremote
1226
1227    header = " lstint avgint rstr r m v  count    score   drop rport remote address"
1228
1229    def summary(self, entry):
1230        first = ntp.ntpc.lfptofloat(entry.first)
1231        last = ntp.ntpc.lfptofloat(entry.last)
1232        active = float(last - first)
1233        count = int(entry.ct)
1234        if self.now:
1235            lstint = int(self.now - last + 0.5)
1236            stats = "%7d" % lstint
1237            if count == 1:
1238                favgint = 0
1239            else:
1240                favgint = active / (count-1)
1241            avgint = int(favgint + 0.5)
1242            if 5.0 < favgint or 1 == count:
1243                stats += " %6d" % avgint
1244            elif 1.0 <= favgint:
1245                stats += " %6.2f" % favgint
1246            else:
1247                stats += " %6.3f" % favgint
1248        else:
1249            MJD_1970 = 40587     # MJD for 1 Jan 1970, Unix epoch
1250            days, lstint = divmod(int(last), 86400)
1251            stats = "%5d %5d %6d" % (days + MJD_1970, lstint, active)
1252        if entry.rs & ntp.magic.RES_KOD:
1253            rscode = 'K'
1254        elif entry.rs & ntp.magic.RES_LIMITED:
1255            rscode = 'L'
1256        else:
1257            rscode = '.'
1258        (ip, port) = portsplit(entry.addr)
1259        try:
1260            if not self.showhostnames & 1:  # if not & 1 display numeric IPs
1261                dns = ip
1262            else:
1263                dns = canonicalize_dns(ip)
1264                # Forward-confirm the returned DNS
1265                confirmed = canonicalization_cache.get(dns)
1266                if confirmed is None:
1267                    confirmed = False
1268                    try:
1269                        ai = socket.getaddrinfo(dns, None)
1270                        for (_, _, _, _, sockaddr) in ai:
1271                            if sockaddr and sockaddr[0] == ip:
1272                                confirmed = True
1273                                break
1274                    except socket.gaierror:
1275                        pass
1276                    canonicalization_cache.set(dns, confirmed)
1277                if not confirmed:
1278                    dns = "%s (%s)" % (ip, dns)
1279            if not self.wideremote:
1280                # truncate for narrow display
1281                dns = dns[:40]
1282            if entry.sc:
1283                score = float(entry.sc)
1284                if score > 100000.0:
1285                  score = "%8.1f" % score
1286                elif score > 10000.0:
1287                  score = "%8.2f" % score
1288                else:
1289                  score = "%8.3f" % score
1290            else:
1291                score = "-"
1292            if entry.dr!= None:     # 0 is valid
1293                drop = "%4d" % entry.dr
1294            else:
1295                drop = "-"
1296            stats += " %4hx %c %d %d %6d %8s %6s %5s %s" % \
1297                     (entry.rs, rscode,
1298                      ntp.magic.PKT_MODE(entry.mv),
1299                      ntp.magic.PKT_VERSION(entry.mv),
1300                      entry.ct, score, drop, port[1:], dns)
1301            return stats
1302        except ValueError:
1303            # This can happen when ntpd ships a corrupt varlist
1304            return ''
1305
1306
1307class ReslistSummary:
1308    "Reusable class for reslist entry summary generation."
1309    header = """\
1310   hits    addr/prefix or addr mask
1311           restrictions
1312"""
1313    width = 72
1314
1315    @staticmethod
1316    def __getPrefix(mask):
1317        if not mask:
1318            prefix = ''
1319        if ':' in mask:
1320            sep = ':'
1321            base = 16
1322        else:
1323            sep = '.'
1324            base = 10
1325        prefix = sum([bin(int(x, base)).count('1')
1326                      for x in mask.split(sep) if x])
1327        return '/' + str(prefix)
1328
1329    def summary(self, variables):
1330        hits = variables.get("hits", "?")
1331        address = variables.get("addr", "?")
1332        mask = variables.get("mask", "?")
1333        if address == '?' or mask == '?':
1334            return ''
1335        address += ReslistSummary.__getPrefix(mask)
1336        flags = variables.get("flags", "?")
1337        # reslist responses are often corrupted
1338        s = "%10s %s\n           %s\n" % (hits, address, flags)
1339        # Throw away corrupted entries.  This is a shim - we really
1340        # want to make ntpd stop generating garbage
1341        for c in s:
1342            if not c.isalnum() and c not in "/.: \n":
1343                return ''
1344        return s
1345
1346
1347class IfstatsSummary:
1348    "Reusable class for ifstats entry summary generation."
1349    header = """\
1350    interface name                                  send
1351 #  address/broadcast     drop flag received sent failed peers   uptime
1352 """
1353    width = 74
1354    # Numbers are the fieldsize
1355    fields = {'name':  '%-24.24s',
1356              'flags': '%4x',
1357              'rx':    '%6d',
1358              'tx':    '%6d',
1359              'txerr': '%6d',
1360              'pc':    '%5d',
1361              'up':    '%8d'}
1362
1363    def summary(self, i, variables):
1364        formatted = {}
1365        try:
1366            # Format the fields
1367            for name in self.fields.keys():
1368                value = variables.get(name, "?")
1369                if value == "?":
1370                    fmt = value
1371                else:
1372                    fmt = self.fields[name] % value
1373                formatted[name] = fmt
1374            enFlag = '.' if variables.get('en', False) else 'D'
1375            address = variables.get("addr", "?")
1376            bcast = variables.get("bcast")
1377            # Assemble the fields into a line
1378            s = ("%3u %s %s %s %s %s %s %s %s\n    %s\n"
1379                 % (i,
1380                    formatted['name'],
1381                    enFlag,
1382                    formatted['flags'],
1383                    formatted['rx'],
1384                    formatted['tx'],
1385                    formatted['txerr'],
1386                    formatted['pc'],
1387                    formatted['up'],
1388                    address))
1389            if bcast:
1390                s += "    %s\n" % bcast
1391        except TypeError:  # pragma: no cover
1392            # Can happen when ntpd ships a corrupted response
1393            return ''
1394
1395        # FIXME, a brutal and slow way to check for invalid chars..
1396        # maybe just strip non-printing chars?
1397        for c in s:
1398            if not c.isalnum() and c not in "/.:[] \%\n":
1399                return ''
1400        return s
1401
1402
1403try:
1404    from collections import OrderedDict
1405except ImportError:  # pragma: no cover
1406    class OrderedDict(dict):
1407        "A stupid simple implementation in order to be back-portable to 2.6"
1408
1409        # This can be simple because it doesn't need to be fast.
1410        # The programs that use it only have to run at human speed,
1411        # and the collections are small.
1412        def __init__(self, items=None):
1413            dict.__init__(self)
1414            self.__keys = []
1415            if items:
1416                for (k, v) in items:
1417                    self[k] = v
1418
1419        def __setitem__(self, key, val):
1420            dict.__setitem__(self, key, val)
1421            self.__keys.append(key)
1422
1423        def __delitem__(self, key):
1424            dict.__delitem__(self, key)
1425            self.__keys.remove(key)
1426
1427        def keys(self):
1428            return self.__keys
1429
1430        def items(self):
1431            return tuple([(k, self[k]) for k in self.__keys])
1432
1433        def __iter__(self):
1434            for key in self.__keys:
1435                yield key
1436
1437
1438def prettyuptime(uptime):
1439    result = ''
1440    if uptime >= 86400:
1441        result += '%dD ' % (uptime // 86400)
1442    result += '%02d:%02d:%02d' % ((uptime % 86400) //
1443                                  3600, (uptime % 3600) // 60, uptime % 60)
1444    return result
1445# end
1446