#!/usr/bin/env 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-unsafe symbol when _USE_TIME_BITS64 is not defined.
#
# Example usage:
# $ cppcheck --dump path-to-src/test.c
# $ y2038.py path-to-src/test.c.dump
#
# y2038.py will walk the source tree for .dump files.
from __future__ import print_function

import cppcheckdata
import sys
import os
import re


# --------------------------------------------
# #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-unsafe identifiers
# --------------------------------

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

id_Y2038 = {
    # Y2038-unsafe types by definition
    'time_t'
    # Types using Y2038-unsafe types
    'lastlog',
    'msqid_ds',
    'semid_ds',
    'timeb',
    'timespec',
    'timeval',
    'utimbuf',
    'itimerspec',
    'stat',
    'clnt_ops',
    'elf_prstatus',
    'itimerval',
    'ntptimeval',
    'rusage',
    'timex',
    'utmp',
    'utmpx',
    # APIs using 2038-unsafe types
    'ctime',
    'ctime_r',
    'difftime',
    'gmtime',
    'gmtime_r',
    'localtime',
    'localtime_r',
    'mktime',
    'stime',
    'timegm',
    'timelocal',
    'time',
    'msgctl',
    'ftime',
    'aio_suspend',
    'clock_getres',
    'clock_gettime',
    'clock_nanosleep',
    'clock_settime',
    'futimens',
    'mq_timedreceive',
    'mq_timedsend',
    'nanosleep',
    'pselect',
    'pthread_cond_timedwait',
    'pthread_mutex_timedlock',
    'pthread_rwlock_timedrdlock',
    'pthread_rwlock_timedwrlock',
    'sched_rr_get_interval',
    'sem_timedwait',
    'sigtimedwait',
    'timespec_get',
    'utimensat',
    'adjtime',
    'pmap_rmtcall',
    'clntudp_bufcreate',
    'clntudp_create',
    'futimes',
    'gettimeofday',
    'lutimes',
    'select',
    'settimeofday',
    'utimes',
    'utime',
    'timerfd_gettime',
    'timerfd_settime',
    'timer_gettime',
    'timer_settime',
    'fstatat',
    'fstat',
    '__fxstatat',
    '__fxstat',
    'lstat',
    '__lxstat',
    'stat',
    '__xstat',
    'struct itimerval',
    'setitimer',
    'getitimer',
    'ntp_gettime',
    'getrusage',
    'wait3',
    'wait4',
    'adjtimex',
    'ntp_adjtime',
    'getutent_r',
    'getutent',
    'getutid_r',
    'getutid',
    'getutline_r',
    'getutline',
    'login',
    'pututline',
    'updwtmp',
    'getutxent',
    'getutxid',
    'getutxline',
    'pututxline'
}


def check_y2038_safe(dumpfile, quiet=False):
    # 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)

    # Convert dump file path to source file in format generated by cppcheck.
    # For example after the following call:
    # cppcheck ./src/my-src.c --dump
    # We got 'src/my-src.c' value for 'file' field in cppcheckdata.
    srcfile = dumpfile.rstrip('.dump')
    srcfile = os.path.expanduser(srcfile)
    srcfile = os.path.normpath(srcfile)

    # go through each configuration
    for cfg in data.configurations:
        if not quiet:
            print('Checking ' + srcfile + ', config "' + cfg.name + '"...')
        safe_ranges = []
        safe = -1
        time_bits_defined = False
        srclinenr = '0'

        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):
                cppcheckdata.reportError(directive, 'error',
                                         '_TIME_BITS must be defined equal to 64',
                                         'y2038',
                                         'type-bits-not-64')
                time_bits_defined = False
                y2038safe = 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 not time_bits_defined:
                    cppcheckdata.reportError(directive, 'warning',
                                             '_USE_TIME_BITS64 is defined but _TIME_BITS was not',
                                             'y2038',
                                             'type-bits-undef')
            elif re_undef_use_time_bits64.match(directive.str):
                unsafe = int(srclinenr)
                # do we have a safe..unsafe area?
                if unsafe > safe > 0:
                    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 unsafe > safe > 0:
                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):
                    cppcheckdata.reportError(token, 'warning',
                                             token.str + ' is Y2038-unsafe',
                                             'y2038',
                                             'unsafe-call')
                    y2038safe = False
            token = token.next

    return y2038safe


def get_args():
    parser = cppcheckdata.ArgumentParser()
    parser.add_argument("dumpfile", nargs='*', help="Path of dump file from cppcheck")
    parser.add_argument('-q', '--quiet', action='store_true',
                        help='do not print "Checking ..." lines')
    parser.add_argument('--cli', help='Addon is executed from Cppcheck', action='store_true')
    return parser.parse_args()


if __name__ == '__main__':
    args = get_args()

    exit_code = 0
    quiet = not any((args.quiet, args.cli))

    if args.dumpfile:
        for dumpfile in args.dumpfile:
            if not os.path.isfile(dumpfile):
                print("Error: File not found: %s" % dumpfile)
                sys.exit(127)
            if not os.access(dumpfile, os.R_OK):
                print("Error: Permission denied: %s" % dumpfile)
                sys.exit(13)
            if not args.quiet:
                print('Checking ' + dumpfile + '...')

            y2038safe = check_y2038_safe(dumpfile, quiet)
            if not y2038safe and exit_code == 0:
                exit_code = 1

    sys.exit(exit_code)