#! /usr/bin/python
#
# cppcheck addon for Y2038 safeness detection
#
# Detects:
#
# 1. _TIME_BITS being defined to something else than 64 bits
# 2. _USE_TIME_BITS64 being defined when _TIME_BITS is not
# 3. Any Y2038-sensitive symbol when _USE_TIME_BITS64 is not defined.
#
# Example usage:
# $ cppcheck --dump path-to-src/
# $ y2038.py path-to-src/
#
# y2038.py will walk the source tree for .dump files.

import cppcheckdata
import sys
import os
import re
import argparse

#---------------
# Error handling
#---------------

diagnostics = {}


def reportDiagnostic(template, configuration, file, line, severity, message):
    # collect diagnostics by configuration
    if not configuration in diagnostics:
        diagnostics[configuration] = []
    # add error to this configuration
    diagnostics[configuration].append(
        cppcheckdata.reportError(template, [[file, line]], severity, message))


def printDiagnostics():
    for cfg in diagnostics:
        sys.stderr.write('# Configuration "' + cfg + '":\n')
        for diag in diagnostics[cfg]:
            sys.stderr.write(diag + '\n')


def reportDirDiag(template, cfg, filename, linenr, directive, severity, msg):
    reportDiagnostic(template, cfg.name,
                     directive.file, directive.linenr,
                     severity, msg)
    if (filename != directive.file) or (linenr != directive.linenr):
        reportDiagnostic(template, cfg.name,
                         filename, linenr, 'information',
                         directive.file + ' was included from here')


def reportTokDiag(template, cfg, token, severity, msg):
    reportDiagnostic(template, cfg.name,
                     token.file, token.linenr,
                     severity, msg)

#---------------------------------------------
# #define/#undef detection regular expressions
#---------------------------------------------

# test for '#define _TIME_BITS 64'
re_define_time_bits_64 = re.compile(r'^\s*#\s*define\s+_TIME_BITS\s+64\s*$')

# test for '#define _TIME_BITS ...' (combine w/ above to test for 'not 64')
re_define_time_bits = re.compile(r'^\s*#\s*define\s+_TIME_BITS\s+.*$')

# test for '#undef _TIME_BITS' (if it ever happens)
re_undef_time_bits = re.compile(r'^\s*#\s*undef\s+_TIME_BITS\s*$')

# test for '#define _USE_TIME_BITS64'
re_define_use_time_bits64 = re.compile(r'^\s*#\s*define\s+_USE_TIME_BITS64\s*$')

# test for '#undef _USE_TIME_BITS64' (if it ever happens)
re_undef_use_time_bits64 = re.compile(r'^\s*#\s*undef\s+_USE_TIME_BITS64\s*$')

#------------------------------------
# List of Y2038-sensitive identifiers
#------------------------------------

# This is WIP. Eventually it should contain all identifiers (types
# and functions) which would be affected by the Y2038 bug.

id_Y2038 = [
    'time_t',
    'timespec',
    'clock_gettime'
]

# return all files ending in .dump among or under the given paths

def find_dump_files(paths):
    dumpfiles = []
    for path in paths:
        if path.endswith('.dump'):
            if not path in dumpfiles:
                dumpfiles.append(path)
        else:
            for (top, subdirs, files) in os.walk(path):
                for file in files:
                    if file.endswith('.dump'):
                        f = top+'/'+file
                        if not f in dumpfiles:
                            dumpfiles.append(f)
    dumpfiles.sort()
    return dumpfiles

#------------------
# Let's get to work
#------------------

# extend cppcheck parser with our own options

parser = cppcheckdata.ArgumentParser()
parser.add_argument('-q', '--quiet', action='store_true',
    help='do not print "Checking ..." lines')
parser.add_argument('paths', nargs='+', metavar='path',
    help='path to dump file or directory')

# parse command line

args = parser.parse_args()

# now operate on each file in turn

dumpfiles = find_dump_files(args.paths)

for dumpfile in dumpfiles:
    if not args.quiet:
        print('Checking ' + dumpfile + '...')
    srcfile = dumpfile.rstrip('.dump')
    # at the start of the check, we don't know if code is Y2038 safe
    y2038safe = False
    # load XML from .dump file
    data = cppcheckdata.parsedump(dumpfile)
    # go through each configuration
    for cfg in data.configurations:
        if not args.quiet:
            print('Checking ' + dumpfile + ', config "' + cfg.name + '"...')
        safe_ranges = []
        safe = -1
        time_bits_defined = False
        for directive in cfg.directives:
            # track source line number
            if directive.file == srcfile:
                srclinenr = directive.linenr
            # check for correct _TIME_BITS if present
            if re_define_time_bits_64.match(directive.str):
                time_bits_defined = True
            elif re_define_time_bits.match(directive.str):
                reportDirDiag(args.template, cfg, srcfile, srclinenr,
                              directive, 'error',
                              '_TIME_BITS must be defined equal to 64')
                time_bits_defined = False
            elif re_undef_time_bits.match(directive.str):
                time_bits_defined = False
            # check for _USE_TIME_BITS64 (un)definition
            if re_define_use_time_bits64.match(directive.str):
                safe = int(srclinenr)
                # warn about _TIME_BITS not being defined
                if time_bits_defined == False:
                    reportDirDiag(args.template, 
                        cfg, srcfile, srclinenr, directive, 'warning',
                                  '_USE_TIME_BITS64 is defined but _TIME_BITS was not')
            elif re_undef_use_time_bits64.match(directive.str):
                unsafe = int(srclinenr)
                # do we have a safe..unsafe area?
                if safe > 0 and unsafe > safe:
                    safe_ranges.append((safe, unsafe))
                    safe = -1
        # check end of source beyond last directive
        if len(cfg.tokenlist) > 0:
            unsafe = int(cfg.tokenlist[-1].linenr)
            if safe > 0 and unsafe > safe:
                safe_ranges.append((safe, unsafe))
        # go through all tokens
        for token in cfg.tokenlist:
            if token.str in id_Y2038:
                if not any(lower <= int(token.linenr) <= upper
                           for (lower, upper) in safe_ranges):
                    reportTokDiag(args.template, cfg, token, 'warning',
                                  token.str + ' might be Y2038-sensitive')
            token = token.next

printDiagnostics()
