#!/usr/bin/env python """ # matrix.py generate a matrix of all readers characteristics # Copyright (C) 2009-2011 Ludovic Rousseau # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """ # $Id$ import glob import os import ConfigParser #import pprint import templayer import time import re #pp = pprint.PrettyPrinter(indent=4) html_escape_table = { "&": "&", '"': """, ">": ">", "<": "<", } CCID_CLASS_CHARACTER = 0x00000000 CCID_CLASS_TPDU = 0x00010000 CCID_CLASS_SHORT_APDU = 0x00020000 CCID_CLASS_EXTENDED_APDU = 0x00040000 CCID_CLASS_EXCHANGE_MASK = 0x00070000 def html_escape(text): """Produce entities within text.""" L = [] for c in text: L.append(html_escape_table.get(c, c)) return "".join(L) def parse_interface(lines): """ parse a reader CCID interface and return a dictionnary """ reader_dict = {} reader_dict['features'] = list() reader_dict['limitations'] = list() for line in lines: # remove the \n line = line[0:-1] l = line.strip(" ").split(':') if (len(l) > 1): reader_dict[l[0]] = l[1].strip(" ") dwFeatures = int(reader_dict['dwFeatures'], 16) if (dwFeatures & CCID_CLASS_EXCHANGE_MASK) == CCID_CLASS_SHORT_APDU: reader_dict['limitations'].append("No extended APDU") return reader_dict def parse_reader(path, reader): """ parse a reader CCID descriptor and return a list of interfaces """ lines = open(path + reader).readlines() # split the .txt file per interface interfaces = list() interface = list() for line in lines: if line.split(':')[0] == ' idVendor': interface = list() interfaces.append(interface) interface.append(line) return interfaces def parse_all(path, reader_list): """ parse each reader from list return a dictionnary """ readers = {} for reader in reader_list: # split the .txt file per interface interfaces = parse_reader(path, reader) for interface in interfaces: p = parse_interface(interface) if reader in readers: # second interface readers[reader]['features'].append("Multi interface reader") p['features'].append("Second interface") readers[reader]['interface'] = p else: readers[reader] = p return readers def parse_ini(path, section): """ parse a foobar.ini file to extract all informations """ config = ConfigParser.ConfigParser() # do not use the default case insensitive transform for key value config.optionxform = str config.read(section + ".ini") reader_list = config.sections() readers = parse_all(path, reader_list) for r in readers.keys(): readers[r]['section'] = section for o in config.options(r): if o == 'features': # for the features we use a list readers[r][o].append(config.get(r, o)) else: readers[r][o] = config.get(r, o) bPINSupport = int(readers[r]['bPINSupport'], 16) if bPINSupport & 1: readers[r]['features'].append("PIN Verification") if bPINSupport & 2: readers[r]['features'].append("PIN Modification") if int(readers[r]['dwFeatures'], 16) & 0x0800: readers[r]['features'].append("ICCD") if 'interface' in readers[r]: second_interface = readers[r]['interface'] first_interface = readers[r] second_interface['section'] = section for elt in first_interface['features']: second_interface['features'].append(elt) if 'image' in first_interface: second_interface['image'] = first_interface['image'] #pp.pprint(readers["GemPCPinpad.txt"]) return readers def check_list(path, reader_list): """ Check that all .txt files are listed """ cwd = os.getcwd() os.chdir(path) real_list = glob.glob("*.txt") os.chdir(cwd) # check that each reader file is listed #print real_list for r in reader_list: #print "remove ", r try: real_list.remove(r) except ValueError: print "reader %s not yet listed" % r # also remove the non-reader supported_readers.txt file real_list.remove("supported_readers.txt") # some USB descriptor are not listed in readers.txt? if len(real_list) > 0: print "Reader(s) not listed in any .ini file:" print "\n".join(real_list) print "" def check_supported(path, all_readers): """ Check that all .txt files are mentionned in supported_readers.txt """ supported_readers = file(path + "supported_readers.txt").readlines() # convert in a long string and in uppercase s = "".join(supported_readers).upper() unlisted = list() for r in all_readers.keys(): pattern = "%s:%s" % (all_readers[r]['idVendor'], all_readers[r]['idProduct']) if not pattern.upper() in s: unlisted.append(pattern + " " + r) if len(unlisted) > 0: print "Reader(s) not in supported_readers.txt" print "\n".join(unlisted) print "" #pp.pprint(supported_readers) def check_descriptions(path, all_readers): """ Check that all readers mentionned in supported_readers.txt have a .txt descriptor """ supported_readers = file(path + "supported_readers.txt").readlines() unlisted = list() for line in supported_readers: # skip comments if line.startswith("#"): # but get commented readers if line.startswith("#0"): # remove the leading # line = line[1:] else: continue # remove newline line = line.rstrip() # skip empty lines if line is "": continue (vendor, product, name) = line.split(":") vendor = int(vendor, 16) product = int(product, 16) found = False for r in all_readers.keys(): reader = all_readers[r] if vendor == int(reader['idVendor'], 16) and product == int(reader['idProduct'], 16): found = True if not found: unlisted.append(line) if len(unlisted) > 0: print "Reader(s) without a .txt description" print "\n".join(unlisted) print "" #pp.pprint(supported_readers) def get_driver_version(readers): """ set the 'release' field for each reader """ changelog = get_changelog() for reader in readers.keys(): rev = get_driver_revision(reader, changelog) readers[reader]['release'] = driver_revision_to_version(rev) if 'interface' in readers[reader]: readers[reader]['interface']['release'] = readers[reader]['release'] def driver_revision_to_version(rev): """ convert a SVN revision in the first release containing this revision """ history = [ #[ SVN revision, CCID release ] [273, "0.1.0"], [342, "0.2.0"], [423, "0.3.0"], [467, "0.3.1"], [552, "0.3.2"], [697, "0.4.0"], [703, "0.4.1"], [1015, "0.9.0"], [1018, "0.9.1"], [1186, "0.9.2"], [1400, "0.9.3"], [1761, "0.9.4"], [1911, "1.0.0"], [2020, "1.0.1"], [2135, "1.1.0"], [2345, "1.2.0"], [2363, "1.2.1"], [2522, "1.3.0"], [2692, "1.3.1"], [2755, "1.3.2"], [2796, "1.3.3"], [2809, "1.3.4"], [2842, "1.3.5"], [2924, "1.3.6"], [2985, "1.3.7"], [3033, "1.3.8"], [3208, "1.3.9"], [3338, "1.3.10"], [4347, "1.3.11"], [4931, "1.3.12"], [4979, "1.3.13"], [5108, "1.4.0"], [5430, "1.4.1"], [5626, "1.4.2"], [5688, "1.4.3"], [5742, "1.4.4"]] for h in history: if rev <= h[0]: return h[1] return "SVN" def get_changelog(): """ read a complete svn2cl Changelog file and merge commits on one line """ lines = open("ccid/readers/ChangeLog").readlines() changelog = list() p = list() for line in lines: # the lines starts with a year (2000-2099) if line.startswith('20'): changelog.append("".join(p).replace('\n', '')) p = [line] else: p.append(line) # add the last line changelog.append("".join(p).replace('\n', '')) return changelog def get_driver_revision(reader, changelog): """ search a log line containing the reader string """ found = None for line in changelog: if reader in line: found = line if found: result = re.search('\\* \\[r(\d*)\\]', found) if result: # revision is the inner matching pattern return int(result.group(1)) else: print "reader %s not found in ChangeLog" % reader # fake SVN revision number high enough to be considered as # unreleased return 999999999 def get_by_manufacturer(readers): """ return a dict of the readers grouped by manufacturer d['manufacturer'] is a list of the manufacturer's readers """ d = {} for r in readers.keys(): d.setdefault(readers[r]['iManufacturer'], []).append(r) return d def generate_page(section, title, comment, readers): """ generate a web page for the corresponding section """ # sort the readers by manufacturers manufacturer_readers = get_by_manufacturer(readers) manufacturers = list(manufacturer_readers) # sort the manufacturers list alphabetically manufacturers.sort(key=str.lower) template = templayer.HTMLTemplate("webpage.template") file_writer = template.start_file(file=file("ccid/" + section + ".html", "w")) main_layer = file_writer.open(date=time.asctime(), title=title, comment=comment, section=section) # for each manufacturer for m in manufacturers: main_layer.write_layer('manufacturer', manufacturer=m) # for each reader for r in sorted(manufacturer_readers[m]): note_layer = main_layer.open_layer('reader', manufacturer=m, product=readers[r]['iProduct'], idVendor=readers[r]['idVendor'], idProduct=readers[r]['idProduct'], image="img/" + readers[r].get('image', "no_image.png")) note_layer.write_layer('descriptor', descriptor="readers/%s" % r) url = readers[r].get('url', "") if url: note_layer.write_layer('url', url=url, manufacturer=m, product=readers[r]['iProduct']) features = ", ".join(readers[r]['features']) if features: note_layer.write_layer('features', features=features) limitations = ", ".join(readers[r]['limitations']) if limitations: note_layer.write_layer('limitations', limitations=limitations) note = readers[r].get('note', "").split('\n') for n in note: note_layer.write_layer('note', contents=n) note_layer.write_layer('release', release=readers[r]['release']) file_writer.close() def add_line(file_desc, num, reader, fields): """ Add a line to the matrix """ # define color of the line if num % 2: # even line number color = reader['section'] else: # odd line number color = reader['section'] + '_odd' file_desc.write('' % color) file_desc.write("%d" % num) for f in fields: if f == 'iProduct': file_desc.write("%s" % (reader['section'], reader['idVendor'], reader['idProduct'], reader[f])) elif f == 'image': file_desc.write('image' % ("img/" + reader.get('image', "no_image.png"))) elif f == 'features': file_desc.write("%s" % ", ".join(reader[f])) else: file_desc.write("%s" % html_escape(reader.get(f, ""))) file_desc.write('\n') def generate_table(readers, field, index, fields): """ generate a web page with all the reader attributes readers are in the order given by index """ header = """ %s """ footer = """

Valid HTML 4.01! Valid CSS!

Ludovic Rousseau

""" documentation = """ """ file_desc = open("ccid/" + field + ".html", "w") title = "Readers sorted by '%s' field" % field file_desc.write(header % title) file_desc.write("

" + title + "

") file_desc.write(documentation) file_desc.write('\n') file_desc.write('') file_desc.write("") for f in fields: file_desc.write("" % (f + ".html", f)) file_desc.write('\n') num = 0 for r in index: num += 1 add_line(file_desc, num, readers[r], fields) if 'interface' in readers[r]: num += 1 add_line(file_desc, num, readers[r]['interface'], fields) file_desc.write('
#%s
\n') file_desc.write("

Generated: %s

" % time.asctime()) file_desc.write(footer) file_desc.close() def generate_tables(readers): """ generate all the web page tables with all the fields values """ fields = ['section', 'iManufacturer', 'iProduct', 'image', 'idVendor', 'idProduct', 'iInterface', 'bNumEndpoints', 'bInterfaceClass', 'bcdCCID', 'bMaxSlotIndex', 'bVoltageSupport', 'dwProtocols', 'dwDefaultClock', 'dwMaximumClock', 'dwDataRate', 'dwMaxDataRate', 'dwMaxIFSD', 'dwSynchProtocols', 'dwMechanical', 'dwFeatures', 'dwMaxCCIDMessageLength', 'bClassGetResponse', 'bClassEnveloppe', 'wLcdLayout', 'bPINSupport', 'bMaxCCIDBusySlots', 'features', 'limitations', 'note', 'release'] for f in fields: # create a list of tuples [field value, reader] index = list() for r in readers.keys(): index.append([readers[r].get(f, ""), r]) if f == 'section': # hack to sort in the order supported, shouldwork, unsupported, disabled index = [(s.replace('supported', 'asupported'), r) for s, r in index] index = [(s.replace('disabled', 'zdisabled'), r) for s, r in index] if f in ['dwDefaultClock', 'dwMaximumClock', 'dwDataRate', 'dwMaxDataRate', 'dwMaxIFSD', 'dwMaxCCIDMessageLength']: # convert from text to decimal index = [(float(s.split(' ')[0]) * 1000, r) for s, r in index] if f == 'release': index = [(release2int(s), r) for s, r in index] # sort the index according to field value index.sort() # put the main field in column 0 sorted_fields = list(fields) sorted_fields.remove(f) sorted_fields.insert(0, f) # generate the table generate_table(readers, f, [r for f, r in index], sorted_fields) def release2int(r): """ Convert a release value "a.b.c" in c + b * 1000 + a * 1000 * 1000 """ rr = r.split('.') if len(rr) > 1: return int(rr[2]) + int(rr[1]) * 1000 + int(rr[0]) * 1000 * 1000 else: # SVN version return 99 * 1000 * 1000 if __name__ == "__main__": path = "ccid/readers/" supported_readers = parse_ini(path, "supported") shouldwork_readers = parse_ini(path, "shouldwork") unsupported_readers = parse_ini(path, "unsupported") disabled_readers = parse_ini(path, "disabled") # all_readers contain the union of the 3 lists all_readers = dict(supported_readers) all_readers.update(shouldwork_readers) all_readers.update(unsupported_readers) all_readers.update(disabled_readers) check_list(path, all_readers.keys()) check_supported(path, all_readers) check_descriptions(path, all_readers) get_driver_version(supported_readers) get_driver_version(shouldwork_readers) get_driver_version(unsupported_readers) get_driver_version(disabled_readers) generate_page("supported", "Supported CCID readers/ICCD tokens", "If you are a reader manufacturer and your reader is not listed here then contact me at ludovic.rousseau@free.fr", supported_readers) generate_page("shouldwork", "Should work but untested by me", "The CCID readers and ICCD tokens listed bellow should work with the driver but have not be validated by me. I would like to get these readers to perform test and validation and move them in the supported list above. If you are one of the manufacturers, please, contact me at ludovic.rousseau@free.fr.", shouldwork_readers) generate_page("unsupported", "Unsupported or partly supported CCID readers", "These readers have problems or serious limitations.", unsupported_readers) generate_page("disabled", "Disabled CCID readers", "These readers are not even recognized by the driver because they are too bogus or because they are supported by another driver.", disabled_readers) generate_tables(all_readers)