| 1 |
#!/usr/bin/env python
|
| 2 |
|
| 3 |
# matrix.py generate a matrix of all readers characteristics
|
| 4 |
# Copyright (C) 2009 Ludovic Rousseau
|
| 5 |
#
|
| 6 |
# This program is free software; you can redistribute it and/or modify
|
| 7 |
# it under the terms of the GNU General Public License as published by
|
| 8 |
# the Free Software Foundation; either version 2 of the License, or
|
| 9 |
# (at your option) any later version.
|
| 10 |
#
|
| 11 |
# This program is distributed in the hope that it will be useful,
|
| 12 |
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
| 13 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
| 14 |
# GNU General Public License for more details.
|
| 15 |
#
|
| 16 |
# You should have received a copy of the GNU General Public License along
|
| 17 |
# with this program; if not, write to the Free Software Foundation, Inc.,
|
| 18 |
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
| 19 |
|
| 20 |
import glob
|
| 21 |
import os
|
| 22 |
import ConfigParser
|
| 23 |
import pprint
|
| 24 |
import templayer
|
| 25 |
import time
|
| 26 |
import re
|
| 27 |
|
| 28 |
pp = pprint.PrettyPrinter(indent=4)
|
| 29 |
|
| 30 |
html_escape_table = {
|
| 31 |
"&": "&",
|
| 32 |
'"': """,
|
| 33 |
"'": "'",
|
| 34 |
">": ">",
|
| 35 |
"<": "<",
|
| 36 |
}
|
| 37 |
|
| 38 |
|
| 39 |
def html_escape(text):
|
| 40 |
"""Produce entities within text."""
|
| 41 |
L = []
|
| 42 |
for c in text:
|
| 43 |
L.append(html_escape_table.get(c, c))
|
| 44 |
return "".join(L)
|
| 45 |
|
| 46 |
|
| 47 |
def parse_reader(path, reader):
|
| 48 |
"""
|
| 49 |
parse a reader CCID descriptor and return a dictionnary
|
| 50 |
"""
|
| 51 |
reader_dict = {}
|
| 52 |
reader_file = open(path + reader)
|
| 53 |
for line in reader_file.readlines():
|
| 54 |
line = line[0:-1]
|
| 55 |
l = line.strip(" ").split(':')
|
| 56 |
if (len(l) > 1):
|
| 57 |
reader_dict[l[0]] = l[1].strip(" ")
|
| 58 |
reader_file.close()
|
| 59 |
return reader_dict
|
| 60 |
|
| 61 |
|
| 62 |
def parse_all(path, reader_list):
|
| 63 |
"""
|
| 64 |
parse each reader from list
|
| 65 |
return a dictionnary
|
| 66 |
"""
|
| 67 |
readers = {}
|
| 68 |
for reader in reader_list:
|
| 69 |
p = parse_reader(path, reader)
|
| 70 |
readers[reader] = p
|
| 71 |
|
| 72 |
return readers
|
| 73 |
|
| 74 |
|
| 75 |
def parse_ini(path, section):
|
| 76 |
"""
|
| 77 |
parse a foobar.ini file to extract all informations
|
| 78 |
"""
|
| 79 |
config = ConfigParser.ConfigParser()
|
| 80 |
# do not use the default case insensitive transform for key value
|
| 81 |
config.optionxform = str
|
| 82 |
config.read(section + ".ini")
|
| 83 |
reader_list = config.sections()
|
| 84 |
|
| 85 |
readers = parse_all(path, reader_list)
|
| 86 |
for r in readers.keys():
|
| 87 |
readers[r]['section'] = section
|
| 88 |
readers[r]['iManufacturer'] = html_escape(readers[r]['iManufacturer'])
|
| 89 |
for o in config.options(r):
|
| 90 |
if o == 'features':
|
| 91 |
# for the features we use a list
|
| 92 |
readers[r][o] = [config.get(r, o)]
|
| 93 |
else:
|
| 94 |
readers[r][o] = config.get(r, o)
|
| 95 |
|
| 96 |
bPINSupport = int(readers[r]['bPINSupport'], 16)
|
| 97 |
if bPINSupport != 0 and not 'features' in readers[r]:
|
| 98 |
readers[r]['features'] = list()
|
| 99 |
if bPINSupport & 1:
|
| 100 |
readers[r]['features'].append("PIN Verification")
|
| 101 |
if bPINSupport & 2:
|
| 102 |
readers[r]['features'].append("PIN Modification")
|
| 103 |
if int(readers[r]['dwFeatures'], 16) & 0x0800:
|
| 104 |
readers[r].setdefault('features', ["ICCD"])
|
| 105 |
|
| 106 |
#pp.pprint(readers["GemPCPinpad.txt"])
|
| 107 |
return readers
|
| 108 |
|
| 109 |
|
| 110 |
def check_list(path, reader_list):
|
| 111 |
"""
|
| 112 |
Check that all .txt files are listed
|
| 113 |
"""
|
| 114 |
cwd = os.getcwd()
|
| 115 |
os.chdir(path)
|
| 116 |
real_list = glob.glob("*.txt")
|
| 117 |
os.chdir(cwd)
|
| 118 |
|
| 119 |
# check that each reader file is listed
|
| 120 |
#print real_list
|
| 121 |
for r in reader_list:
|
| 122 |
#print "remove ", r
|
| 123 |
try:
|
| 124 |
real_list.remove(r)
|
| 125 |
except:
|
| 126 |
print "reader %s not yet listed" % r
|
| 127 |
|
| 128 |
# also remove the non-reader supported_readers.txt file
|
| 129 |
real_list.remove("supported_readers.txt")
|
| 130 |
|
| 131 |
# some USB descriptor are not listed in readers.txt?
|
| 132 |
if len(real_list) > 0:
|
| 133 |
print "Reader(s) not listed in any .ini file:"
|
| 134 |
print "\n".join(real_list)
|
| 135 |
print ""
|
| 136 |
|
| 137 |
|
| 138 |
def check_supported(path, all_readers):
|
| 139 |
"""
|
| 140 |
Check that all .txt files are mentionned in supported_readers.txt
|
| 141 |
"""
|
| 142 |
supported_readers = file(path + "supported_readers.txt").readlines()
|
| 143 |
# convert in a long string and in uppercase
|
| 144 |
s = "".join(supported_readers).upper()
|
| 145 |
|
| 146 |
unlisted = list()
|
| 147 |
for r in all_readers.keys():
|
| 148 |
pattern = "%s:%s" % (all_readers[r]['idVendor'], all_readers[r]['idProduct'])
|
| 149 |
if not pattern.upper() in s:
|
| 150 |
unlisted.append(pattern + " " + r)
|
| 151 |
|
| 152 |
if len(unlisted) > 0:
|
| 153 |
print "Reader(s) not in supported_readers.txt"
|
| 154 |
print "\n".join(unlisted)
|
| 155 |
print ""
|
| 156 |
#pp.pprint(supported_readers)
|
| 157 |
|
| 158 |
|
| 159 |
def get_driver_version(readers):
|
| 160 |
"""
|
| 161 |
set the 'release' field for each reader
|
| 162 |
"""
|
| 163 |
changelog = get_changelog()
|
| 164 |
|
| 165 |
for reader in readers.keys():
|
| 166 |
rev = get_driver_revision(reader, changelog)
|
| 167 |
readers[reader]['release'] = driver_revision_to_version(rev)
|
| 168 |
|
| 169 |
|
| 170 |
def driver_revision_to_version(rev):
|
| 171 |
"""
|
| 172 |
convert a SVN revision in the first release containing this revision
|
| 173 |
"""
|
| 174 |
history = [
|
| 175 |
#[ SVN revision, CCID release ]
|
| 176 |
[273, "0.1.0"],
|
| 177 |
[342, "0.2.0"],
|
| 178 |
[423, "0.3.0"],
|
| 179 |
[467, "0.3.1"],
|
| 180 |
[552, "0.3.2"],
|
| 181 |
[697, "0.4.0"],
|
| 182 |
[703, "0.4.1"],
|
| 183 |
[1015, "0.9.0"],
|
| 184 |
[1018, "0.9.1"],
|
| 185 |
[1186, "0.9.2"],
|
| 186 |
[1400, "0.9.3"],
|
| 187 |
[1761, "0.9.4"],
|
| 188 |
[1911, "1.0.0"],
|
| 189 |
[2020, "1.0.1"],
|
| 190 |
[2135, "1.1.0"],
|
| 191 |
[2345, "1.2.0"],
|
| 192 |
[2363, "1.2.1"],
|
| 193 |
[2522, "1.3.0"],
|
| 194 |
[2692, "1.3.1"],
|
| 195 |
[2755, "1.3.2"],
|
| 196 |
[2796, "1.3.3"],
|
| 197 |
[2809, "1.3.4"],
|
| 198 |
[2842, "1.3.5"],
|
| 199 |
[2924, "1.3.6"],
|
| 200 |
[2985, "1.3.7"],
|
| 201 |
[3033, "1.3.8"],
|
| 202 |
[3208, "1.3.9"],
|
| 203 |
[3338, "1.3.10"],
|
| 204 |
[4347, "1.3.11"],
|
| 205 |
[4931, "1.3.12"],
|
| 206 |
[4979, "1.3.13"],
|
| 207 |
[5108, "1.4.0"]]
|
| 208 |
for h in history:
|
| 209 |
if rev <= h[0]:
|
| 210 |
return h[1]
|
| 211 |
return "SVN"
|
| 212 |
|
| 213 |
|
| 214 |
def get_changelog():
|
| 215 |
"""
|
| 216 |
read a complete svn2cl Changelog file and merge commits on one line
|
| 217 |
"""
|
| 218 |
lines = open("readers/ChangeLog").readlines()
|
| 219 |
changelog = list()
|
| 220 |
p = list()
|
| 221 |
|
| 222 |
for line in lines:
|
| 223 |
# the lines starts with a year (2000-2099)
|
| 224 |
if line.startswith('20'):
|
| 225 |
changelog.append("".join(p).replace('\n', ''))
|
| 226 |
p = [line]
|
| 227 |
else:
|
| 228 |
p.append(line)
|
| 229 |
|
| 230 |
# add the last line
|
| 231 |
changelog.append("".join(p).replace('\n', ''))
|
| 232 |
|
| 233 |
return changelog
|
| 234 |
|
| 235 |
|
| 236 |
def get_driver_revision(reader, changelog):
|
| 237 |
"""
|
| 238 |
search a log line containing the reader string
|
| 239 |
"""
|
| 240 |
found = None
|
| 241 |
for line in changelog:
|
| 242 |
if reader in line:
|
| 243 |
found = line
|
| 244 |
|
| 245 |
if found:
|
| 246 |
result = re.search('\\* \\[r(\d*)\\]', found)
|
| 247 |
if result:
|
| 248 |
# revision is the inner matching pattern
|
| 249 |
return int(result.group(1))
|
| 250 |
else:
|
| 251 |
print "reader %s not found in ChangeLog" % reader
|
| 252 |
# fake SVN revision number high enough to be considered as
|
| 253 |
# unreleased
|
| 254 |
return 999999999
|
| 255 |
|
| 256 |
|
| 257 |
def get_by_manufacturer(readers):
|
| 258 |
"""
|
| 259 |
return a dict of the readers grouped by manufacturer
|
| 260 |
d['manufacturer'] is a list of the manufacturer's readers
|
| 261 |
"""
|
| 262 |
d = {}
|
| 263 |
for r in readers.keys():
|
| 264 |
d.setdefault(readers[r]['iManufacturer'], []).append(r)
|
| 265 |
return d
|
| 266 |
|
| 267 |
|
| 268 |
def generate_page(section, title, comment, readers):
|
| 269 |
"""
|
| 270 |
generate a web page for the corresponding section
|
| 271 |
"""
|
| 272 |
# sort the readers by manufacturers
|
| 273 |
manufacturer_readers = get_by_manufacturer(readers)
|
| 274 |
manufacturers = list(manufacturer_readers)
|
| 275 |
# sort the manufacturers list alphabetically
|
| 276 |
manufacturers.sort(key=str.lower)
|
| 277 |
|
| 278 |
template = templayer.HTMLTemplate("webpage.template")
|
| 279 |
file_writer = template.start_file(file=file(section + ".html", "w"))
|
| 280 |
main_layer = file_writer.open(date=time.asctime(),
|
| 281 |
title=title, comment=comment, section=section)
|
| 282 |
|
| 283 |
# for each manufacturer
|
| 284 |
for m in manufacturers:
|
| 285 |
main_layer.write_layer('manufacturer', manufacturer=m)
|
| 286 |
|
| 287 |
# for each reader
|
| 288 |
for r in sorted(manufacturer_readers[m]):
|
| 289 |
note_layer = main_layer.open_layer('reader',
|
| 290 |
manufacturer=m,
|
| 291 |
product=readers[r]['iProduct'],
|
| 292 |
idVendor=readers[r]['idVendor'],
|
| 293 |
idProduct=readers[r]['idProduct'],
|
| 294 |
image="img/" + readers[r].get('image', "no_image.png"))
|
| 295 |
|
| 296 |
note_layer.write_layer('descriptor', descriptor="readers/%s" % r)
|
| 297 |
|
| 298 |
url = readers[r].get('url', "")
|
| 299 |
if url:
|
| 300 |
note_layer.write_layer('url',
|
| 301 |
url=url,
|
| 302 |
manufacturer=m,
|
| 303 |
product=readers[r]['iProduct'])
|
| 304 |
|
| 305 |
features = ", ".join(readers[r].get('features', ""))
|
| 306 |
if features:
|
| 307 |
note_layer.write_layer('features', features=features)
|
| 308 |
|
| 309 |
note = readers[r].get('note', "").split('\n')
|
| 310 |
for n in note:
|
| 311 |
note_layer.write_layer('note', contents=n)
|
| 312 |
|
| 313 |
note_layer.write_layer('release',
|
| 314 |
release=readers[r]['release'])
|
| 315 |
|
| 316 |
file_writer.close()
|
| 317 |
|
| 318 |
|
| 319 |
def generate_table(readers, field, index, fields):
|
| 320 |
"""
|
| 321 |
generate a web page with all the reader attributes
|
| 322 |
readers are in the order given by index
|
| 323 |
"""
|
| 324 |
|
| 325 |
header = """<?xml version="1.0" encoding="UTF-8"?>
|
| 326 |
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
|
| 327 |
<html>
|
| 328 |
<head>
|
| 329 |
<title>%s</title>
|
| 330 |
<link rel="stylesheet" type="text/css" href="default.css">
|
| 331 |
<link rel="stylesheet" type="text/css" href="matrix.css">
|
| 332 |
</head>
|
| 333 |
<body>"""
|
| 334 |
|
| 335 |
footer = """
|
| 336 |
<script type="text/javascript">
|
| 337 |
var gaJsHost = (("https:" == document.location.protocol) ? "https://ssl." : "http://www.");
|
| 338 |
document.write(unescape("%3Cscript src='" + gaJsHost + "google-analytics.com/ga.js' type='text/javascript'%3E%3C/script%3E"));
|
| 339 |
</script>
|
| 340 |
<script type="text/javascript">
|
| 341 |
try {
|
| 342 |
var pageTracker = _gat._getTracker("UA-2404298-2");
|
| 343 |
pageTracker._trackPageview();
|
| 344 |
} catch(err) {}</script>
|
| 345 |
<p>Ludovic Rousseau</p>
|
| 346 |
</body>
|
| 347 |
</html>"""
|
| 348 |
|
| 349 |
documentation = """
|
| 350 |
<p>Click on the column header to sort by that column.</p>
|
| 351 |
<p>The background color indicates the section of the reader:</p>
|
| 352 |
<table border="1" summary="color codes">
|
| 353 |
<tr>
|
| 354 |
<td class="supported">supported</td>
|
| 355 |
<td class="shouldwork">should work</td>
|
| 356 |
<td class="unsupported">unsupported</td>
|
| 357 |
</tr></table>
|
| 358 |
"""
|
| 359 |
|
| 360 |
file = open(field + ".html", "w")
|
| 361 |
title = "Readers sorted by '%s' field" % field
|
| 362 |
file.write(header % title)
|
| 363 |
|
| 364 |
file.write("<h1>" + title + "</h1>")
|
| 365 |
file.write(documentation)
|
| 366 |
|
| 367 |
file.write('<table border="1" summary="">\n')
|
| 368 |
|
| 369 |
file.write('<tr>')
|
| 370 |
file.write("<th>#</th>")
|
| 371 |
for f in fields:
|
| 372 |
file.write("<th><a href='%s'>%s</a></th>" % (f + ".html", f))
|
| 373 |
file.write('</tr>\n')
|
| 374 |
|
| 375 |
num = 0
|
| 376 |
for r in index:
|
| 377 |
num += 1
|
| 378 |
file.write('<tr class="%s">' % readers[r]['section'])
|
| 379 |
file.write("<td>%d</td>" % num)
|
| 380 |
for f in fields:
|
| 381 |
if f == 'iProduct':
|
| 382 |
file.write("<td><a href='%s.html#%s%s'>%s</a></td>" % (readers[r]['section'], readers[r]['idVendor'], readers[r]['idProduct'], readers[r][f]))
|
| 383 |
elif f == 'iManufacturer':
|
| 384 |
file.write('<td><a onmouseover="">%s<img src="%s" alt="image"></a></td>' % (readers[r][f], "img/" + readers[r].get('image', "no_image.png")))
|
| 385 |
elif f == 'image':
|
| 386 |
file.write('<td><img src="%s" height="100" alt="image"></td>' % ("img/" + readers[r].get('image', "no_image.png")))
|
| 387 |
else:
|
| 388 |
file.write("<td>%s</td>" % readers[r].get(f, ""))
|
| 389 |
file.write('</tr>\n')
|
| 390 |
|
| 391 |
file.write('</table>\n')
|
| 392 |
|
| 393 |
file.write("<hr><p>Generated: %s</p>" % time.asctime())
|
| 394 |
|
| 395 |
file.write(footer)
|
| 396 |
file.close()
|
| 397 |
|
| 398 |
|
| 399 |
def generate_tables(readers):
|
| 400 |
"""
|
| 401 |
generate all the web page tables with all the fields values
|
| 402 |
"""
|
| 403 |
fields = ['section', 'iManufacturer', 'iProduct', 'image', 'idVendor',
|
| 404 |
'idProduct', 'bNumEndpoints', 'bInterfaceClass', 'bcdCCID',
|
| 405 |
'bMaxSlotIndex', 'bVoltageSupport', 'dwProtocols',
|
| 406 |
'dwDefaultClock', 'dwMaximumClock', 'dwDataRate',
|
| 407 |
'dwMaxDataRate', 'dwMaxIFSD', 'dwSynchProtocols',
|
| 408 |
'dwMechanical', 'dwFeatures', 'dwMaxCCIDMessageLength',
|
| 409 |
'bClassGetResponse', 'bClassEnveloppe', 'wLcdLayout',
|
| 410 |
'bPINSupport', 'bMaxCCIDBusySlots', 'features', 'note',
|
| 411 |
'release']
|
| 412 |
|
| 413 |
for f in fields:
|
| 414 |
index = list()
|
| 415 |
for r in readers.keys():
|
| 416 |
index.append([readers[r].get(f, ""), r])
|
| 417 |
if f == 'section':
|
| 418 |
# hack to sort in the order supported, shouldwork, unsupported
|
| 419 |
index = [(s.replace('supported', 'asupported'), r) for s, r in index]
|
| 420 |
if f in ['dwDefaultClock', 'dwMaximumClock', 'dwDataRate',
|
| 421 |
'dwMaxDataRate', 'dwMaxIFSD', 'dwMaxCCIDMessageLength']:
|
| 422 |
# convert from text to decimal
|
| 423 |
index = [(float(s.split(' ')[0]) * 1000, r) for s, r in index]
|
| 424 |
if f == 'release':
|
| 425 |
index = [(release2int(s), r) for s, r in index]
|
| 426 |
index.sort()
|
| 427 |
sorted_fields = list(fields)
|
| 428 |
sorted_fields.remove(f)
|
| 429 |
sorted_fields.insert(0, f)
|
| 430 |
generate_table(readers, f, [r for f, r in index], sorted_fields)
|
| 431 |
|
| 432 |
|
| 433 |
def release2int(r):
|
| 434 |
rr = r.split('.')
|
| 435 |
if len(rr) > 1:
|
| 436 |
return int(rr[2]) + int(rr[1]) * 1000 + int(rr[0]) * 1000 * 1000
|
| 437 |
else:
|
| 438 |
# SVN version
|
| 439 |
return 99*1000*1000
|
| 440 |
|
| 441 |
if __name__ == "__main__":
|
| 442 |
path = "../trunk/Drivers/ccid/readers/"
|
| 443 |
|
| 444 |
supported_readers = parse_ini(path, "supported")
|
| 445 |
shouldwork_readers = parse_ini(path, "shouldwork")
|
| 446 |
unsupported_readers = parse_ini(path, "unsupported")
|
| 447 |
|
| 448 |
# all_readers contain the union of the 3 lists
|
| 449 |
all_readers = dict(supported_readers)
|
| 450 |
all_readers.update(shouldwork_readers)
|
| 451 |
all_readers.update(unsupported_readers)
|
| 452 |
|
| 453 |
check_list(path, all_readers.keys())
|
| 454 |
check_supported(path, all_readers)
|
| 455 |
|
| 456 |
get_driver_version(supported_readers)
|
| 457 |
get_driver_version(shouldwork_readers)
|
| 458 |
get_driver_version(unsupported_readers)
|
| 459 |
|
| 460 |
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)
|
| 461 |
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)
|
| 462 |
generate_page("unsupported", "Unsupported or partly supported CCID readers", "These readers have problems or serious limitations.", unsupported_readers)
|
| 463 |
|
| 464 |
generate_tables(all_readers)
|