#!/usr/bin/python3
# vim:expandtab:autoindent:tabstop=4:shiftwidth=4:filetype=python:tw=0

  #############################################################################
  #
  # Copyright (c) 2005 Dell Computer Corporation
  # Dual Licenced under GNU GPL and OSL
  # Contributor: (c) 2019 Marcin Mielniczuk <marmistrz dot dev at zoho dot eu>
  #
  #############################################################################
"""smbios-token-ctl"""



# import arranged alphabetically
import csv
import gettext
import locale
import fnmatch
from optparse import OptionParser
import os
import io
import string
import sys
import traceback

# the following vars are all substituted on install
# this bin isnt byte-compiled, so this is ok
pythondir="/usr/lib/python3.12/site-packages"
clidir="/usr/share/smbios-utils"
# end vars

# import all local modules after this.
sys.path.insert(0,pythondir)
sys.path.insert(0,clidir)

import cli
from libsmbios_c import smbios_token, localedir, GETTEXT_PACKAGE, pkgdatadir
from libsmbios_c.trace_decorator import traceLog, getLog
from libsmbios_c import system_info as sysinfo

__VERSION__="2.4.3"

locale.setlocale(locale.LC_ALL, '')
gettext.install(GETTEXT_PACKAGE, localedir)

moduleLog = getLog()
verboseLog = getLog(prefix="verbose.")

TOKENLIST_CSV=os.path.join(pkgdatadir, "token_list.csv")
TOKENBLACKLIST_CSV=os.path.join(pkgdatadir, "token_blacklist.csv")

class CmdlineError(Exception): pass
class BlacklistedToken(Exception): pass

def command_parse():
    parser = cli.OptionParser(usage=__doc__, version=__VERSION__)

    parser.add_option("-d", '--dump-tokens', action="store_const", dest="action",
                      help= _("Action: dump token table"), const="dump-tokens", default="dump-tokens")
    parser.add_option('--dump-tokens-csv', action="store_const", dest="action",
                      help= _("Action: dump token table in CSV format"), const="dump-tokens-csv")

    parser.add_option('--import-token-settings-csv', action="store_const", dest="action",
                      help= _("Action: Restore token settings from CSV table dump"), const="import-tokens-csv")

    parser.add_option('--is-string', action="store_const", dest="action",
                      help=_("Action: return true if specified token is a string."),
                      const="is-string")

    parser.add_option('--is-bool', action="store_const", dest="action",
                      help=_("Action: return true if specified token is a bool."),
                      const="is-bool")

    parser.add_option('--is-active', action="store_const", dest="action",
                      help=_("Action: return true if specified bool token is active."),
                      const="is-active")

    parser.add_option('--get-string', action="store_const", dest="action",
                      help=_("Action: get string token value."),
                      const="get-string")

    parser.add_option('--activate', action="store_const", dest="action",
                      help=_("Action: activate specified bool token."),
                      const="activate")

    parser.add_option('--set-string', action="store_const", dest="action",
                      help=_("Action: set string token value."),
                      const="set-string")

    parser.add_option("-i", '--token-id', "--token", action="store", dest="token_id",
                      help=_("Filter only token with ID."), default=None)
    parser.add_option("-n", '--token-name', action="store", dest="token_name",
                      help=_("Filter only token with name."), default=None)
    parser.add_option("-s", '--token-setting', action="store", dest="token_setting",
                      help=_("Filter only token with setting."), default=None)

    parser.add_option('--token-csv', action="append", type="string", dest="token_csv",
                      help=_("path to the token.csv file."), default=[TOKENLIST_CSV,])
    parser.add_option('--token-blacklist-csv', action="append", dest="token_blacklist_csv",
                      help=_("Path to token blacklist file."), default=[TOKENBLACKLIST_CSV])

    cli.addStdOptions(parser)
    return parser.parse_args()

class translatedToken(object):
    id = ""
    name = _("unknown")
    setting = _("unknown")
    description = _("unknown")
    spec = _("unknown")
    def __init__(self, tokenObj):
        self.id = tokenObj.getId()

class TokenTranslator(object):
    def __init__(self, *args, **kargs):
        self.tokens={}
        self.blacklist_tokens={}
        self.honorBlacklist=kargs.get('honorBlacklist',1)
        self.addToList(csv.DictReader(filter(lambda row: row[0]!='#', io.StringIO(INTERNAL_BLACKLIST))), self.blacklist_tokens)

        for csvfile in kargs.get('csvblacklist',[]):
            try:
                csvdict = csv.DictReader(filter(lambda row: row[0]!='#', open(csvfile, "r")))
                self.addToList(csvdict, self.blacklist_tokens)
            except IOError as e:
                pass

    def __call__(self, tokenObj):
        retval = translatedToken(tokenObj)
        if self.tokens.get(retval.id) is not None:
            retval.name = self.tokens[retval.id]["Attribute Name"]
            retval.setting = self.tokens[retval.id]["Attribute Setting"]
            retval.description = self.tokens[retval.id]["Description"]
            retval.spec = self.tokens[retval.id]["Spec"]
        if self.blacklist_tokens.get(retval.id) and self.honorBlacklist:
            raise BlacklistedToken(self.blacklist_tokens[retval.id]['Reason'])
        return retval

    def addToList(self, csvdict, whichlist):
        for line in csvdict:
            try:
                val = int(line["Token Value"],16)
                line["Token Value"] = "0x%04x" % val
                whichlist[val] = line
            except Exception as e:
                sys.stderr.write("="*79 + "\n")
                sys.stderr.write(_("Ignoring parsing error in CSV file:") )
                traceback.print_exc()
                sys.stderr.write("="*79 + "\n")

class TokenTranslatorCsv(TokenTranslator):
    def utf8_decode_gen(self, dictiter):
        for d in dictiter:
            for k,v in d.items():
                if v is not None and isinstance(v, str):
                    d[k] = str(v, 'utf-8')
                yield d

    def __init__(self, *args, **kargs):
        super(TokenTranslatorCsv,self).__init__(self, *args, **kargs)
        for csvfile in kargs.get('csvlist', []):
            try:
                csvdict = csv.DictReader(
                    filter(lambda row: row[0]!='#', open(csvfile, "r")))
                self.addToList(csvdict, self.tokens)
            except IOError as e:
                sys.stderr.write(_("Skipping '%s' due to IOError.\n") % cvsfile)
                sys.stderr.write(_("The error message was: %s") % e)
                continue


def tokenInfo(tokenObj, action):
    exit_code=1

    type = _("<weird unknown type>")
    value = _("<unknown value>")
    if tokenObj.isBool():
        if action == "is-bool": exit_code = 0
        type="bool"
        value="false"
        if tokenObj.isActive():
            if action == "is-active": exit_code = 0
            value="true"

    elif tokenObj.isString():
        if action == "is-string": exit_code = 0
        type = "string"
        value = tokenObj.getString()

    return (exit_code, type, value)

def shouldSkipToken(token, options):
    skip = 0

    if options.token_id is not None:
        skip = 1
        if token.id == options.token_id:
            skip=0

    if options.token_name is not None:
        skip = 1
        if fnmatch.fnmatch(token.name.lower(), options.token_name.lower()):
            skip=0

    if options.token_setting is not None:
        skip = 1
        if token.setting == options.token_setting:
            skip=0

    return skip

def dumpTokens(tokenTable, tokenXlator, options):
     for token in tokenTable:
        try:
            translatedToken = tokenXlator(token)
        except BlacklistedToken as e:
            #sys.stdout.write( "="*80 + "\n" )
            #print "Not dumping blacklisted token 0x%04x. Reason: %s" % (token.getId(), str(e))
            continue

        if shouldSkipToken(translatedToken, options):
            continue

        sys.stdout.write( "="*80 + "\n" )
        print(_("  Token: 0x%04x - %s (%s)") % (translatedToken.id, translatedToken.name, translatedToken.setting))

        try:
            (e, type, value) = tokenInfo(token, "dump-tokens")
            print(_("  value: %s = %s") % (type, cli.makePrintable(value)))
        except RuntimeError as e:
            pass
        except smbios_token.TokenManipulationFailure as e:
            print(f"  value: token query failed: {e}")

        desc = _("   Desc: ")
        sys.stdout.write(desc)
        cli.wrap(translatedToken.description, indent=len(desc), first_line_start=len(desc))
        print()


def dumpTokensCsv(tokenTable, tokenXlator, options):
    # make sure not to localize the string below:
    sys.stdout.write("ID,Type,Value,Name,Setting\n")
    w = csv.writer( sys.stdout )
    for token in tokenTable:
        try:
            translatedToken = tokenXlator(token)
        except BlacklistedToken as e:
            #sys.stdout.write( "="*80 + "\n" )
            #print "Not dumping blacklisted token 0x%04x. Reason: %s" % (token.getId(), str(e))
            continue

        if shouldSkipToken(translatedToken, options):
            continue

        (e, type, value) = tokenInfo(token, "dump-tokens")
        w.writerow(("0x%04x" % translatedToken.id,type,cli.makePrintable(value),translatedToken.name, translatedToken.setting))
    sys.stdout.flush()

def importTokensCsv(tokenTable, tokenXlator, options, args):
    retval=True
    print(_("Importing token values from '%s'") % args[0])
    csvdict = csv.DictReader(CommentedFile(open(args[0], "rb")))
    for line in csvdict:
        try:
            id = int(line["ID"], 0)
            type=line["Type"]
            value=line["Value"]
        except KeyError as e:
            print(_("File format error. The first line should list the column names. Could not find the %s column") % str(e))
            print(_("Cannot continue, exiting."))
            retval=False
            break

        try:
            token = tokenTable[id]
            translatedToken = tokenXlator(token)
            if shouldSkipToken(translatedToken, options):
                continue

            if translatedToken.name == _("unknown") or translatedToken.setting == _("unknown"):
                print(_("Not importing token that is unknown: 0x%04x") % id)
                retval=False
                continue

            if translatedToken.name != line["Name"]:
                print(_("INFO: token 0x%04x settings file name does not match my DB. Applying setting anyways.") % id)
                print(_("\tsettings file: %s") % line["Name"])
                print(_("\tDB name      : %s") % translatedToken.name)

            if translatedToken.setting != line["Setting"]:
                print(_("INFO: token 0x%04x settings file setting-name does not match my DB. Applying setting anyways.") % id)
                print(_("\tsettings file: %s") % line["Setting"])
                print(_("\tDB name      : %s") % translatedToken.setting)

            if type == "bool":
                if not token.isBool():
                    print(_("SKIPPING: TYPE MISMATCH. Settings file says token 0x%04x should be bool, but it doesnt pass bool check.") % (id))
                    retval=False
                    continue
                elif value == "true":
                    if not token.isActive():
                        print(_("Importing setting for token (active): 0x%04x") % id)
                        token.activate()
                    else:
                        print(_("Token already correct (bool, active): 0x%04x") % id)
                elif value == "false":
                    if token.isActive():
                        print(_("Info: Cannot de-activate tokens, can only activate the contrapositive. (0x%04x == false)") % id)
                    else:
                        print(_("Token already correct (bool, inactive): 0x%04x") % id)
                else:
                    print(_("UNEXPECTED VALUE: Bool token should only ever have values of 'true' or 'false', but token 0x%04x tries to set value '%s'") % (id, value))
                    continue

            elif type == "string":
                if not token.isString():
                    print(_("SKIPPING: TYPE MISMATCH. Settings file says token 0x%04x should be string, but it doesnt pass string check.") % (id))
                    retval=False
                    continue
                if token.getString() != value:
                    print(_("Importing setting for token (string): 0x%04x") % id)
                    token.setString(value)
                else:
                    print(_("Token already correct (string=='%s'): 0x%04x") % (value, id))

        except IndexError as e:
            print(_("Not importing token which is inapplicable to this system: 0x%04x") % (id))
            retval=False
            continue

        except BlacklistedToken as e:
            print(_("Not importing blacklisted token 0x%04x. Reason: %s") % (token.getId(), str(e)))
            retval=False
            continue

        except Exception as e:
            sys.stderr.write("="*79 + "\n")
            sys.stderr.write(_("Ignoring parsing error in CSV file:") )
            traceback.print_exc()
            sys.stderr.write("="*79 + "\n")
            retval=False
            continue
    return retval

def getTokenObj(tokenTable, tokenXlator, options):
    # optimization
    if options.token_id:
        return tokenTable[options.token_id]

    if options.token_name is None or options.token_setting is None:
        raise CmdlineError(_("require token-id or token name/setting pair."))

    for token in tokenTable:
        try:
            translatedToken = tokenXlator(token)
            if shouldSkipToken(translatedToken, options):
                continue
            return token
        except BlacklistedToken as e:
            pass

    raise CmdlineError(_("token not found. Name(%s) Setting(%s).") % (
            options.token_name, options.token_setting))

def main():
    exit_code = 0
    (options, args) = command_parse()
    cli.setup_std_options(options)
    if options.token_id:
        options.token_id = int(options.token_id, 0)

    try:
        tokenXlator = TokenTranslatorCsv(csvlist=options.token_csv, csvblacklist=options.token_blacklist_csv)

        tokenTable = smbios_token.TokenTable()

        if options.action == "dump-tokens":
            dumpTokens(tokenTable, tokenXlator, options)
            return exit_code

        if options.action == "dump-tokens-csv":
            dumpTokensCsv(tokenTable, tokenXlator, options)
            return exit_code

        if options.action == "import-tokens-csv":
            exit_code = not importTokensCsv(tokenTable, tokenXlator, options, args)
            return exit_code

        tokenObj = getTokenObj(tokenTable, tokenXlator, options)
        tokenObj.tryPassword(options.password_ascii, options.password_scancode)

        if options.action in ("set-string", "activate"):
            (exit_code, type, value) = tokenInfo(tokenObj, options.action)
            print(_("Original Value"))
            print(_("       token: 0x%04x") % tokenObj.getId())
            print(_("        type: %s") % type)
            print(_("       value: %s") % cli.makePrintable(value))

        if options.action == "set-string":
            (exit_code, type, value) = tokenInfo(tokenObj, "dump-tokens")
            print(_("Setting string value ..."))
            tokenObj.setString(args[0])
            print(_("New value"))

        if options.action == "activate":
            (exit_code, type, value) = tokenInfo(tokenObj, "dump-tokens")
            print(_("Activating token..."))
            tokenObj.activate()
            print(_("New value"))

        if options.action in ("set-string", "activate", "is-bool", "is-string", "is-active", "get-string"):
            (exit_code, type, value) = tokenInfo(tokenObj, options.action)
            print(_("        type: %s") % type)
            print(_("       value: %s") % cli.makePrintable(value))

    except (smbios_token.TokenTableParseError, ) as e:
        exit_code=3
        moduleLog.info( _("ERROR: Could not parse system SMBIOS table.") )
        verboseLog.info( _("The smbios library returned this error:") )
        verboseLog.info( str(e) )
        moduleLog.info( cli.standardFailMessage )
    except (smbios_token.TokenManipulationFailure,) as e:
        exit_code=4
        moduleLog.info( _("ERROR: Could not manipulate system token.") )
        verboseLog.info( _("The token library returned this error:") )
        verboseLog.info( str(e) )
        moduleLog.info( cli.standardFailMessage )
    except StopIteration:
        pass

    return exit_code

INTERNAL_BLACKLIST = \
"""\
"Token Value","Reason"
# blacklist:
#   - raid shadow copy (0x00CD, 0x00CE, 0x00CF, 0x00D0)
0x00CD,"Manufacturing use."
0x00CE,"Manufacturing use."
0x00CF,"Manufacturing use."
0x00D0,"Manufacturing use."

#   - sata controller shadow copy ( 0x013a 0x013b 0x013c 0x013d 0x01FF)
0x013A,"Manufacturing use."
0x013B,"Manufacturing use."
0x013C,"Manufacturing use."
0x013D,"Manufacturing use."
0x01FF,"Manufacturing use."

#   - management driver  (0x0058, 0x0059)
0x0058,"Management driver use."
0x0059,"Management driver use."
0x8004,"dangerous - hard system power down."

#   - absolute security rom (0x0175, 0x0176)
0x0175,"dangerous - permanent write once"
0x0176,"dangerous - permanent write once"

#   - manufacturing mode (0x4026, 0x4027)
0x4026,"Manufacturing mode."
0x4027,"Manufacturing mode."

#   - cmos location for post (0x9000, 0x9001)
0x9000,"Manufacturing use."
0x9001,"Manufacturing use."

#   - TPM os enable/disable (0xa0002, 0xa003)
0xa002,"Manufacturing use."
0xa003,"Manufacturing use."
"""

if __name__ == "__main__":
    sys.exit( main() )
